├── .coveragerc
├── .flake8
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── issue.yml
├── PULL_REQUEST_TEMPLATE.md
├── labeler
│ └── subpackages.yml
└── workflows
│ ├── lint_test.yml
│ └── triaging.yml
├── .gitignore
├── .isort.cfg
├── CONTRIBUTING.md
├── LICENSE
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── docs
├── mkdocs.yml
├── requirements.txt
├── source
│ ├── css
│ │ └── _extra.css
│ ├── index.md
│ ├── reference
│ │ ├── interactions
│ │ │ └── commands.md
│ │ └── utils.md
│ ├── setup
│ │ ├── authenticating.md
│ │ ├── creating-the-bot.md
│ │ ├── images
│ │ │ ├── authenticating
│ │ │ │ ├── bot-overview.png
│ │ │ │ ├── general-information.png
│ │ │ │ └── oauth2-page.png
│ │ │ ├── creating-the-bot
│ │ │ │ ├── bot-confirmation.png
│ │ │ │ ├── bot-invite-flow.png
│ │ │ │ ├── bot-joined.png
│ │ │ │ ├── bot-overview.png
│ │ │ │ ├── build-a-bot.png
│ │ │ │ ├── developer-portal.png
│ │ │ │ ├── general-information.png
│ │ │ │ ├── name-prompt.png
│ │ │ │ └── oauth2-tab.png
│ │ │ └── running-the-bot
│ │ │ │ └── interaction-url.png
│ │ ├── index.md
│ │ └── running-the-bot.md
│ └── tutorial
│ │ └── extensions
│ │ ├── advanced-usage.md
│ │ ├── extensions-faq.md
│ │ └── simple-example.md
└── templates
│ └── python
│ └── material
│ ├── method.html
│ └── parameters.html
├── library
├── wumpy-bot
│ ├── README.md
│ ├── pyproject.toml
│ └── wumpy
│ │ └── bot
│ │ ├── __init__.py
│ │ ├── _bot.py
│ │ ├── _dispatch.py
│ │ ├── _errors.py
│ │ ├── _extension.py
│ │ ├── _utils.py
│ │ ├── events
│ │ ├── __init__.py
│ │ ├── _channel.py
│ │ ├── _gateway.py
│ │ ├── _guild.py
│ │ └── _message.py
│ │ └── py.typed
├── wumpy-cache
│ ├── README.md
│ ├── pyproject.toml
│ └── wumpy
│ │ └── cache
│ │ ├── __init__.py
│ │ ├── _protocol.py
│ │ ├── in_memory
│ │ ├── __init__.py
│ │ ├── _base.py
│ │ ├── _channel.py
│ │ ├── _guild.py
│ │ └── _user.py
│ │ └── py.typed
├── wumpy-gateway
│ ├── README.md
│ ├── pyproject.toml
│ └── wumpy
│ │ └── gateway
│ │ ├── __init__.py
│ │ ├── _errors.py
│ │ ├── _shard.py
│ │ ├── _utils.py
│ │ └── py.typed
├── wumpy-interactions
│ ├── README.md
│ ├── pyproject.toml
│ └── wumpy
│ │ └── interactions
│ │ ├── __init__.py
│ │ ├── _app.py
│ │ ├── _compat.py
│ │ ├── _errors.py
│ │ ├── _middleware.py
│ │ ├── _models.py
│ │ ├── _utils.py
│ │ ├── commands
│ │ ├── __init__.py
│ │ ├── _base.py
│ │ ├── _checks.py
│ │ ├── _context.py
│ │ ├── _option.py
│ │ ├── _registrar.py
│ │ └── _slash.py
│ │ ├── components
│ │ ├── __init__.py
│ │ └── _handler.py
│ │ └── py.typed
├── wumpy-models
│ ├── README.md
│ ├── pyproject.toml
│ └── wumpy
│ │ └── models
│ │ ├── __init__.py
│ │ ├── _raw
│ │ ├── __init__.py
│ │ ├── _channels.py
│ │ ├── _components.py
│ │ ├── _embed.py
│ │ ├── _emoji.py
│ │ ├── _flags.py
│ │ ├── _guild.py
│ │ ├── _integrations.py
│ │ ├── _interactions.py
│ │ ├── _invite.py
│ │ ├── _member.py
│ │ ├── _message.py
│ │ ├── _permissions.py
│ │ ├── _role.py
│ │ ├── _sticker.py
│ │ └── _user.py
│ │ ├── _stateful
│ │ ├── __init__.py
│ │ ├── _asset.py
│ │ ├── _channels.py
│ │ ├── _emoji.py
│ │ ├── _guild.py
│ │ ├── _integrations.py
│ │ ├── _interactions.py
│ │ ├── _invite.py
│ │ ├── _member.py
│ │ ├── _message.py
│ │ ├── _role.py
│ │ ├── _sticker.py
│ │ └── _user.py
│ │ ├── _utils.py
│ │ └── py.typed
└── wumpy-rest
│ ├── README.md
│ ├── pyproject.toml
│ └── wumpy
│ └── rest
│ ├── __init__.py
│ ├── _config.py
│ ├── _errors.py
│ ├── _impl.py
│ ├── _ratelimiter.py
│ ├── _requester.py
│ ├── _route.py
│ ├── _utils.py
│ ├── endpoints
│ ├── __init__.py
│ ├── _channel.py
│ ├── _commands.py
│ ├── _gateway.py
│ ├── _guild.py
│ ├── _guild_template.py
│ ├── _interactions.py
│ ├── _sticker.py
│ ├── _user.py
│ └── _webhook.py
│ └── py.typed
├── readthedocs.yml
├── setup.py
└── tests
├── README.md
├── wumpy-bot
├── extensions
│ ├── data_passed.py
│ ├── empty.py
│ ├── non_callable.py
│ ├── raises.py
│ ├── raises_load.py
│ └── raises_unload.py
├── test_dispatcher.py
└── test_extension.py
├── wumpy-gateway
└── test_limiter.py
├── wumpy-interactions
└── commands
│ ├── test_context.py
│ ├── test_decorators.py
│ ├── test_option.py
│ └── test_slash.py
├── wumpy-models
├── test_base.py
├── test_flags.py
├── test_message.py
└── test_permissions.py
└── wumpy-rest
└── test_ratelimiter.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | exclude_lines =
3 | pragma: not covered
4 | @overload
5 | if TYPE_CHECKING:
6 | raise NotImplementedError
7 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | # Lines should be between 79 to 95, but the hard-stop is 105
3 | max-line-length = 105
4 |
5 | per-file-ignores =
6 | # Allow any __init__.py file to have unused imports
7 | **/__init__.py: F401, F403, F405
8 |
9 | ignore =
10 | # Line break before binary operator (Flake8 has one for before- and after
11 | # linebreaks. From my understanding you're meant to disable one of them).
12 | W503
13 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Read the Documentation
4 | url: https://wumpy.readthedocs.io/
5 | about: Refer to the documentation before asking for help
6 |
7 | - name: Questions or Support
8 | url: https://github.com/wumpyproject/wumpy/discussions/new?category=questions-and-support
9 | about: Ask the community for help with a question or report a problem with the library
10 |
11 | - name: Discord API Feature Request
12 | url: https://github.com/discord/discord-api-docs/discussions/new
13 | about: Suggest a new feature to the Discord API
14 |
15 | - name: Library Feature Request
16 | url: https://github.com/wumpyproject/wumpy/discussions/new?category=feature-requests
17 | about: Discuss a new feature to be implemented into Wumpy
18 |
19 | - name: Development Discussion
20 | url: https://github.com/wumpyproject/wumpy/discussions/new?category=development
21 | about: Open a discussion about development of the library
22 |
23 | - name: Join the Discord server
24 | url: https://discord.gg/ZWpjYdKKTF
25 | about: There is a development server for IRC-style discussion
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue.yml:
--------------------------------------------------------------------------------
1 | name: Open an Issue
2 | description: Open a new issue with the library. Consider picking the other options first
3 |
4 | body:
5 | - type: markdown
6 | # '>' means that single newlines are turned into spaces - like with Markdown
7 | attributes:
8 | # We use double-newlines here so that they appear as empty newlines in
9 | # the final markdown that GitHub renders.
10 | value: >
11 | **Use [GitHub Discussions][discussions] for miscellaneous support and development.**
12 |
13 |
14 | Thanks for taking the time to open an issue. Issues are used to track verified bugs,
15 | feature requests that should be worked on, and to track API support.
16 |
17 |
18 | All issues and subsequent pull requests get tracked in the
19 | [organization project][org-project] so that they are triaged and one can follow its
20 | progress. This means that support queries should not go into the issue tracker - for
21 | that [GitHub Discussions][discussions] are used!
22 |
23 |
24 | If you're opening an issue to discuss and scope out a change or addition use
25 | [GitHub Discussions][discussions] or join the [development Discord server][discord].
26 |
27 |
28 | This text is not included in the final issue, please fill out the form below:
29 |
30 | [discussions]: https://github.com/wumpyproject/wumpy/discussions
31 | [org-project]: https://github.com/orgs/wumpyproject/projects/1
32 | [discord]: https://discord.gg/ZWpjYdKKTF
33 |
34 | ---
35 |
36 | - type: textarea
37 | attributes:
38 | label: Summary
39 | description: >
40 | A Tl;Dr-style summary of why the issue is opened. Please keep this short and use one of
41 | the other text areas below to elaborate.
42 | validations:
43 | required: true
44 |
45 | - type: textarea
46 | attributes:
47 | label: Description
48 | description: >
49 | Longer description and full explanation of the issue. If this is a feature request
50 | explain the underlying problem - or if it is an API feature explain it more in detail
51 | and how it could be implemented into the library.
52 |
53 | - type: textarea
54 | attributes:
55 | label: Reproduction steps
56 | description: Instructions for how to locally run into the issue. How did you run into it?
57 |
58 | - type: textarea
59 | attributes:
60 | label: Tasks
61 | description: >
62 | A list of things to do before closing this issue. Please use checkboxes as shown in the
63 | placeholder below, these can be marked as completed individually.
64 | placeholder: '- [ ] ...'
65 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Summary
2 |
3 |
4 |
5 |
6 | ## Semantic Versioning
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.github/labeler/subpackages.yml:
--------------------------------------------------------------------------------
1 | wumpy-bot:
2 | - 'library/wumpy-bot/**/*'
3 |
4 | wumpy-cache:
5 | - 'library/wumpy-cache/**/*'
6 |
7 | wumpy-gateway:
8 | - 'library/wumpy-gateway/**/*'
9 |
10 | wumpy-interactions:
11 | - 'library/wumpy-interactions/**/*'
12 |
13 | wumpy-models:
14 | - 'library/wumpy-models/**/*'
15 |
16 | wumpy-rest:
17 | - 'library/wumpy-rest/**/*'
18 |
--------------------------------------------------------------------------------
/.github/workflows/triaging.yml:
--------------------------------------------------------------------------------
1 | name: Triage issues and PRs
2 |
3 | on:
4 | issues:
5 | types:
6 | - opened
7 | pull_request:
8 | types:
9 | - opened
10 |
11 | jobs:
12 | greet:
13 | name: Greet first-time contributors
14 |
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/first-interaction@v1
18 | with:
19 | repo-token: ${{ secrets.GITHUB_TOKEN }}
20 |
21 | issue-message: >
22 | ### Thank you for opening your first issue! 🎉
23 |
24 |
25 | Issues are used to track verified bugs, feature requests that should be worked on,
26 | and keep tabs on API support. This issue will be added to the
27 | [organization project][org-project] that contains all issues and PRs in various
28 | stages.
29 |
30 |
31 | You do not need to create an issue for each code-change you want to make, but it
32 | can be good to ensure that you don't work on the same thing as another person or
33 | put effort into something that ultimately gets rejected.
34 |
35 |
36 | ---
37 |
38 |
39 | The next step is opening a pull request, if you want to. For more help, take a look
40 | at the [Contribution guide][contrib-guide] and join the
41 | [development Discord server][discord] for steps and useful tips.
42 |
43 | [org-project]: https://github.com/orgs/wumpyproject/projects/1
44 | [contrib-guide]: https://github.com/wumpyproject/wumpy/blob/main/CONTRIBUTING.md
45 | [discord]: https://discord.gg/ZWpjYdKKTF
46 |
47 | pr-message: >
48 | ### Congratulations on your first PR
49 |
50 |
51 | Contributions - like yours here - is what makes open source projects work. All
52 | changes are greatly appreciated: whether that is smaller cosmetic changes or bigger
53 | features - thank you for putting the time to contribute. ❤️
54 |
55 |
56 | This PR will be added to the [organization project][org-project] to be triaged. You
57 | should receive a review within at least a few days depending on when you opened the
58 | pull request. The review may ask for some more changes before merging, which you
59 | should push to the same branch in your fork.
60 |
61 |
62 | Eventually your PR will be approved and merged into the repository. Your changes
63 | will then be include in the next release of the subpackage it was apart of!
64 |
65 |
66 | ---
67 |
68 |
69 | Consider joining the [development Discord server][discord] to ask for help with
70 | finishing your PR or!
71 |
72 | [org-project]: https://github.com/orgs/wumpyproject/projects/1
73 | [discord]: https://discord.gg/ZWpjYdKKTF
74 |
75 | add-labels:
76 | name: Label subpackages for PRs
77 |
78 | runs-on: ubuntu-latest
79 | steps:
80 | - if: github.event_name == 'pull_request'
81 | uses: actions/labeler@v4
82 | with:
83 | repo-token: ${{ secrets.GITHUB_TOKEN }}
84 | configuration-path: '.github/labeler/subpackages.yml'
85 | sync-labels: true
86 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | wumpy/*.html
8 | *.c
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # Installer logs
31 | pip-log.txt
32 | pip-delete-this-directory.txt
33 |
34 | # Unit test / coverage reports
35 | htmlcov/
36 | .tox/
37 | .nox/
38 | .coverage
39 | .coverage.*
40 | .cache
41 | nosetests.xml
42 | coverage.xml
43 | *.cover
44 | .hypothesis/
45 | .pytest_cache/
46 |
47 | # Sphinx documentation
48 | docs/_build/
49 |
50 | # PyBuilder
51 | target/
52 |
53 | # Jupyter Notebook
54 | .ipynb_checkpoints
55 |
56 | # IPython
57 | profile_default/
58 | ipython_config.py
59 |
60 | # pyenv
61 | .python-version
62 |
63 | # Environments
64 | .env
65 | .venv
66 | env/
67 | venv/
68 | ENV/
69 | env.bak/
70 | venv.bak/
71 |
72 | # Spyder project settings
73 | .spyderproject
74 | .spyproject
75 |
76 | # Rope project settings
77 | .ropeproject
78 |
79 | # Visual Studio code settings
80 | .vscode
81 |
82 | # IntelliJ project settings
83 | .idea
84 |
85 | # mkdocs documentation
86 | /site
87 |
88 | # mypy
89 | .mypy_cache/
90 | .dmypy.json
91 | dmypy.json
92 |
93 | # Pyre type checker
94 | .pyre/
95 |
--------------------------------------------------------------------------------
/.isort.cfg:
--------------------------------------------------------------------------------
1 | [settings]
2 | multi_line_output=5
3 | combine_as_imports=true
4 |
5 | known_third_party=wumpy
6 |
7 | skip_glob=**/__init__.py
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for your interest in contributing to this project.
4 |
5 | ## Setting up tools
6 |
7 | Static type checkers are encouraged to be used. There are plans to set up a
8 | CI that run PyRight or MyPy on the project to ensure it passes type checking.
9 |
10 | This project uses [flake8](https://github.com/PyCQA/flake8) for linting as a
11 | laid-back linter, mainly it enforces line length. Preferably lines should be
12 | between 79 and 95 characters long, but flake8 will not complain until a line
13 | reaches 105 characters which is the hard limit.
14 |
15 | Imports should be ordered according to the rules of
16 | [isort](https://github.com/PyCQA/isort).
17 |
18 | ### Building the documentation
19 |
20 | Because of how [Read the Docs](https://readthedocs.org/) builds our documentation from
21 | the root of our project that means that files and folders have to be configured
22 | from that origin.
23 |
24 | This means that building and serving the documentation needs to be done from
25 | *the root of the project* as well with the following commands:
26 |
27 | ```bash
28 | # Locally serve the documentation
29 | mkdocs serve --config-file docs/mkdocs.yml
30 |
31 | # For building use the equivalent:
32 | mkdocs build --config-file docs/mkdocs.yml
33 | ```
34 |
35 | ## Making commits
36 |
37 | Once you have made your changes it is time to commit it to Git. These commits
38 | should be imperative and clearly explain what was changed. The commit body
39 | can be used to explain why it was changed.
40 |
41 | If you are unsure what this means,
42 | [read this blog post](https://chris.beams.io/posts/git-commit/). The blog post
43 | can be summarized to having your commit messages fit into this sentence:
44 |
45 | > If applied, this commit will ...
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This software is made available under the terms of *either* of the licenses
2 | found in LICENSE-MIT or LICENSE-APACHE. Contributions to this software are
3 | made under the terms of *both* these licenses.
4 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any person obtaining a copy
2 | of this software and associated documentation files (the "Software"), to deal
3 | in the Software without restriction, including without limitation the rights
4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5 | copies of the Software, and to permit persons to whom the Software is
6 | furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all
9 | copies or substantial portions of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17 | SOFTWARE.
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wumpy
2 |
3 | A Discord API wrapper for Python. Wumpy aims to be easy enough for Wumpus, and
4 | extensible enough for Clyde!
5 |
6 | ## Community
7 |
8 | There is a [development Discord server](https://discord.gg/ZWpjYdKKTF) you can
9 | join. GitHub Discussions is used for support.
10 |
11 | ## Usage
12 |
13 | If you are new to Wumpy consider [reading the documentation](https://wumpy.rtfd.io).
14 |
15 | A lot of the functionality of Wumpy is separated into *multiple subpackages*,
16 | for example [`wumpy-gateway`](library/wumpy-gateway/README.md) (imported as
17 | `wumpy.gateway`) is a subpackage that only contains the code for interacting
18 | with the gateway.
19 |
20 | If you are building another library, consider using some of these subpackages
21 | instead of re-implementing the same functionality - more times than not it is
22 | only the highest level (the one that the end-user interacts with) that differs.
23 | For Wumpy the highest level of abstraction is
24 | [`wumpy-client`](library/wumpy-client/README.md) which is built on top of all
25 | other subpackages.
26 |
27 | ## Support
28 |
29 | If you have a problem please open a discussion through GitHub. This is the best
30 | place to ask for help as only bug reports or detailed feature requests should
31 | go in the issue tracker.
32 |
33 | ⭐ Please consider starring the repository, it really helps! ⭐
34 |
35 | ## Future plans
36 |
37 | The highest priority right now is to get full API coverage, while keeping the
38 | standards of quality that Wumpy stands for.
39 |
40 | Development is triaged through
41 | [this GitHub project](https://github.com/orgs/wumpyproject/projects/1) if you
42 | get curious about the current state of the project, this brings together all
43 | issues and todos from all subpackage.
44 |
45 | ## Contributing
46 |
47 | The library is only first now starting to get a good structure. Take a look
48 | at [CONTRIBUTING.md](CONTRIBUTING.md) and
49 | [the Wiki](https://github.com/wumpyproject/wumpy/wiki) for developer notes and
50 | different design decision write-ups.
51 |
52 | The library is licensed under one of MIT or Apache 2.0 (up to you). Please
53 | see [LICENSE](./LICENSE) for more information.
54 |
55 | ## Version guarantees
56 |
57 | The Wumpy project's official subpackages all follow
58 | [Semantic Versioning 2.0](https://semver.org/):
59 |
60 | - Major version bumps hold no guarantees at all.
61 |
62 | - Minor version bumps are backwards compatible but not forwards compatible.
63 |
64 | - Patch versions are *both* backwards compatible and forwards compatible.
65 |
66 | To simplify the above, this is how it will affect you (these are imaginary
67 | example versions):
68 |
69 | - You can upgrade from `v1.0.6` to `v1.0.8`, or downgrade to `v1.0.3` without
70 | any code changes.
71 |
72 | - You can upgrade from `v1.5.0` to `v1.6.0` without any code changes, but you
73 | can't downgrade to `v1.4.0`.
74 |
75 | - You cannot upgrade from `v4.0.0` to `v5.0.0` or downgrade to `v3.0.0`. Major
76 | versions have no guarantee of compatability.
77 |
78 | ## Public API
79 |
80 | The public API is documented in the API reference hosted on Read the docs,
81 | which can be found [here](https://wumpy.rtfd.io/).
82 |
--------------------------------------------------------------------------------
/docs/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Wumpy
2 | site_description: Documentation for the Discord API wrapper Wumpy
3 | site_author: Bluenix
4 |
5 | repo_url: https://github.com/wumpyproject/wumpy
6 | repo_name: GitHub
7 | edit_uri: tree/main/docs
8 |
9 | docs_dir: 'source'
10 |
11 | theme:
12 | name: material
13 | palette:
14 | scheme: discord # Custom scheme
15 | primary: blurple
16 | accent: blurple
17 | features:
18 | - content.code.annotate
19 | - navigation.indexes
20 | - navigation.tabs
21 | - navigation.top
22 | extra_css:
23 | - css/_extra.css
24 | markdown_extensions:
25 | - admonition
26 | - pymdownx.highlight
27 | - pymdownx.superfences
28 | - pymdownx.tabbed
29 |
30 | nav:
31 | - Start page:
32 | - index.md
33 |
34 | - Setting up the bot:
35 | - setup/index.md
36 | - Creating the Bot: setup/creating-the-bot.md
37 | - Authenticatin with Discord: setup/authenticating.md
38 | - Running the Bot: setup/running-the-bot.md
39 |
40 | - Basic Tutorial:
41 | - Extensions:
42 | - Extensions FAQ: tutorial/extensions/extensions-faq.md
43 | - Getting started: tutorial/extensions/simple-example.md
44 | - Advanced usage: tutorial/extensions/advanced-usage.md
45 |
46 | - Reference:
47 | - Application commands: reference/interactions/commands.md
48 | - Utils: reference/utils.md
49 |
50 | plugins:
51 | - search: {}
52 | - mkdocstrings:
53 | custom_templates: docs/templates/
54 | enable_inventory: true
55 | handlers:
56 | python:
57 | selection:
58 | new_path_syntax: true
59 | inherited_members: true
60 | rendering:
61 | show_source: false
62 | show_root_heading: true
63 | show_root_full_path: false
64 | show_root_members_full_path: false
65 | members_order: source
66 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs==1.2.2
2 | mkdocs-material==7.3.0
3 | mkdocstrings==0.16.1
4 |
--------------------------------------------------------------------------------
/docs/source/css/_extra.css:
--------------------------------------------------------------------------------
1 | /* This is the custom Discord color scheme */
2 | [data-md-color-scheme=discord] {
3 |
4 | /* Default color shades */
5 | --md-default-fg-color: #dcddde;
6 | --md-default-fg-color--light: #e3e5e8;
7 | --md-default-fg-color--lighter: #f5f6f7;
8 | --md-default-fg-color--lightest: #ffffff;
9 | --md-default-bg-color: #36393f;
10 | --md-default-bg-color--light: #4F545C;
11 | --md-default-bg-color--lighter: #72767D;
12 | --md-default-bg-color--lightest: #72767D;
13 |
14 | /* Primary color shades */
15 | --md-primary-fg-color: #5865f2;
16 | --md-primary-fg-color--light: #5865f2;
17 | --md-primary-fg-color--dark: #5865f2;
18 | --md-primary-bg-color: hsla(0, 0%, 100%, 1);
19 | --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);
20 |
21 | /* Accent color shades */
22 | --md-accent-fg-color: #5865f2;
23 | --md-accent-fg-color--transparent: hsla(#5865f2, 0.1);
24 | --md-accent-bg-color: hsla(0, 0%, 100%, 1);
25 | --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7);
26 |
27 | /* Code color shades */
28 | --md-code-fg-color: hsla(var(--md-hue),18%,86%,1);
29 | --md-code-bg-color: #18191c;
30 | --md-code-bg-border: #23272a;
31 |
32 | /* Code highlighting color shades */
33 | --md-code-hl-color: rgba(66,135,255,0.15);
34 | --md-code-hl-number-color: #e6695b;
35 | --md-code-hl-special-color: #f06090;
36 | --md-code-hl-function-color: #c973d9;
37 | --md-code-hl-constant-color: #9383e2;
38 | --md-code-hl-keyword-color: #6791e0;
39 | --md-code-hl-comment-color: #4f545c;
40 | --md-code-hl-string-color: #2fb170;
41 | --md-code-hl-docstring-color: var(--md-code-hl-string-color);
42 | --md-code-hl-name-color: var(--md-code-fg-color);
43 | --md-code-hl-operator-color: var(--md-default-fg-color--light);
44 | --md-code-hl-punctuation-color: var(--md-default-fg-color--light);
45 | --md-code-hl-generic-color: var(--md-default-fg-color--light);
46 | --md-code-hl-variable-color: var(--md-default-fg-color--light);
47 |
48 | /* Typeset color shades */
49 | --md-typeset-color: #b9bbbe;
50 |
51 | /* Typeset `a` color shades */
52 | --md-typeset-a-color: var(--md-default-fg-color--lightest);
53 |
54 | /* Typeset `mark` color shades */
55 | --md-typeset-mark-color: rgba(255,255,0,0.5);
56 |
57 | /* Typeset `del` and `ins` color shades */
58 | --md-typeset-del-color: hsla(6, 90%, 60%, 0.15);
59 | --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15);
60 |
61 | /* Typeset `kbd` color shades */
62 | --md-typeset-kbd-color: hsla(0, 0%, 98%, 1);
63 | --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1);
64 | --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1);
65 |
66 | /* Typeset `table` color shades */
67 | --md-typeset-table-color: hsla(0, 0%, 0%, 0.12);
68 |
69 | /* Admonition color shades */
70 | --md-admonition-fg-color: var(--md-default-fg-color);
71 | --md-admonition-bg-color: var(--md-default-bg-color);
72 |
73 | /* Footer color shades */
74 | --md-footer-fg-color: hsla(0, 0%, 100%, 1);
75 | --md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7);
76 | --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.3);
77 | --md-footer-bg-color: hsla(0, 0%, 0%, 0.87);
78 | --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32);
79 |
80 | /* Fonts similar to Discord's */
81 | --md-text-font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
82 | --md-code-font-family: monospace;
83 | }
84 |
85 | /* Make headers clearer */
86 | .md-typeset h1, .md-typeset h2, .md-typeset h3 {
87 | color: var(--md-default-fg-color--lightest);
88 | }
89 |
90 | /* Give codeblocks borders */
91 | .md-typeset code {
92 | outline-width: 1px;
93 | border: 1px solid var(--md-code-bg-border);
94 | border-radius: 4px;
95 | }
96 |
97 | /* Center images */
98 | .md-typeset img, .md-typeset svg {
99 | position: relative;
100 | left: 50%;
101 | transform: translate(-50%);
102 | }
103 |
104 | /* Make docstrings have a separate color */
105 | .highlight .sd {
106 | color: var(--md-code-hl-docstring-color);
107 | }
108 |
109 | /*
110 | * The navigation links use the lighter colors by default but in the discord
111 | * theme the lighter colors mean more white and accentuated
112 | */
113 | .md-nav__link[data-md-state=blur] {
114 | color: var(--md-typeset-color);
115 | }
116 |
117 | .md-nav__link:focus, .md-nav__link:hover {
118 | color: var(--md-primary-fg-color);
119 | }
120 |
121 | .md-nav__item .md-nav__link--active {
122 | color: var(--md-default-fg-color--lightest);
123 | }
124 |
125 | .doc-heading code {
126 | /*display: flex;*/
127 | border: none; /* Remove the border from earlier */
128 | }
129 |
130 | /* Indentation. */
131 | div.doc-contents:not(.first) {
132 | padding-left: 25px;
133 | }
134 |
135 | /* Avoid breaking parameters name, etc. in table cells. */
136 | td code {
137 | word-break: normal !important;
138 | }
139 |
--------------------------------------------------------------------------------
/docs/source/index.md:
--------------------------------------------------------------------------------
1 | # Wumpy
2 |
3 | Imagine an efficient and extensible Discord API wrapper.
4 |
--------------------------------------------------------------------------------
/docs/source/reference/interactions/commands.md:
--------------------------------------------------------------------------------
1 | ---
2 | hide:
3 | - toc
4 | ---
5 |
6 | # Application commands API Reference
7 |
8 | This file contains the API reference for the application commands part of the
9 | `wumpy.interactions` package.
10 |
11 | ::: wumpy.interactions.commands:CommandRegistrar
12 |
13 | ::: wumpy.interactions.commands:Option
14 |
15 | ::: wumpy.interactions.commands.option:OptionClass
16 |
17 | ::: wumpy.interactions.commands:option
18 |
19 | ::: wumpy.interactions.commands:SlashCommand
20 |
21 | ::: wumpy.interactions.commands:SubcommandGroup
22 |
23 | ::: wumpy.interactions.commands:Subcommand
24 |
25 | ::: wumpy.interactions.commands:MessageCommand
26 |
27 | ::: wumpy.interactions.commands:UserCommand
28 |
--------------------------------------------------------------------------------
/docs/source/reference/utils.md:
--------------------------------------------------------------------------------
1 | ---
2 | hide:
3 | - toc
4 | ---
5 |
6 | # Wumpy utils API Reference
7 |
8 | ::: wumpy.utils:Event
9 |
10 | ::: wumpy.utils:EventDispatcher
11 |
--------------------------------------------------------------------------------
/docs/source/setup/creating-the-bot.md:
--------------------------------------------------------------------------------
1 | # Creating the bot
2 |
3 | The first step of all Discord tutorials is to create the bot account, this is how you
4 | authenticate and talk to the Discord API.
5 |
6 | ## Creating an application
7 |
8 | Discord's API consist of applications. These applications are used for games, "Log in with
9 | Discord", and of course - bots!
10 |
11 | Start by navigating to the [Discord Developer Portal](https://discord.com/developers/). The
12 | page should look like this:
13 |
14 | 
15 |
16 | Now click the "New Application" button at the top right. This will prompt you for a name for
17 | your application, like this:
18 |
19 | 
20 |
21 | Wumpus decided to call its bot "Wumpbot", but you are free to name yours as you wish.
22 |
23 | Now click "Create", you should see the following page:
24 |
25 | 
26 |
27 | This step is completely optional, but you can now add a description and picture to your
28 | application if you want to.
29 |
30 | ## Adding a bot to the mix
31 |
32 | It is now time to add a bot to the application, click the "Bot" tab in the sidebar. It should
33 | look like this:
34 |
35 | 
36 |
37 | Click "Add Bot" in the top right and then "Yes, do it!" in the following dialog box to confirm:
38 |
39 | 
40 |
41 | There is now a bot attached to your application. You should see the following page:
42 |
43 | 
44 |
45 | ## Inviting the bot to your server
46 |
47 | The very last part of creating a Discord bot is inviting it to your server.
48 |
49 | In the left sidebar go to "OAuth2" and pick "bot" as well as "application.commands" in the
50 | OAuth2 URL Generator at the bottom of the page:
51 |
52 | 
53 |
54 | Now copy the link generated and open it in your browser. It should look like this:
55 |
56 | 
57 |
58 | Select the server you want to add the bot to and click "Authorize". The bot will now appear
59 | in your server with the power of technology:
60 |
61 | 
62 |
--------------------------------------------------------------------------------
/docs/source/setup/images/authenticating/bot-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/authenticating/bot-overview.png
--------------------------------------------------------------------------------
/docs/source/setup/images/authenticating/general-information.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/authenticating/general-information.png
--------------------------------------------------------------------------------
/docs/source/setup/images/authenticating/oauth2-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/authenticating/oauth2-page.png
--------------------------------------------------------------------------------
/docs/source/setup/images/creating-the-bot/bot-confirmation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/creating-the-bot/bot-confirmation.png
--------------------------------------------------------------------------------
/docs/source/setup/images/creating-the-bot/bot-invite-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/creating-the-bot/bot-invite-flow.png
--------------------------------------------------------------------------------
/docs/source/setup/images/creating-the-bot/bot-joined.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/creating-the-bot/bot-joined.png
--------------------------------------------------------------------------------
/docs/source/setup/images/creating-the-bot/bot-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/creating-the-bot/bot-overview.png
--------------------------------------------------------------------------------
/docs/source/setup/images/creating-the-bot/build-a-bot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/creating-the-bot/build-a-bot.png
--------------------------------------------------------------------------------
/docs/source/setup/images/creating-the-bot/developer-portal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/creating-the-bot/developer-portal.png
--------------------------------------------------------------------------------
/docs/source/setup/images/creating-the-bot/general-information.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/creating-the-bot/general-information.png
--------------------------------------------------------------------------------
/docs/source/setup/images/creating-the-bot/name-prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/creating-the-bot/name-prompt.png
--------------------------------------------------------------------------------
/docs/source/setup/images/creating-the-bot/oauth2-tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/creating-the-bot/oauth2-tab.png
--------------------------------------------------------------------------------
/docs/source/setup/images/running-the-bot/interaction-url.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/docs/source/setup/images/running-the-bot/interaction-url.png
--------------------------------------------------------------------------------
/docs/source/setup/index.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | The purpose of this guide is to walk you through setting it up a bot and getting it to run.
4 |
5 | These steps will not be repeated anywhere else in the documentation, and all documentation is
6 | based around the fact that you have read this.
7 |
--------------------------------------------------------------------------------
/docs/source/setup/running-the-bot.md:
--------------------------------------------------------------------------------
1 | # Running the bot
2 |
3 | Discord will not host bots for you, it is your responsibility to be running the bot.
4 | This page will go through tips for running the bot and testing locally.
5 |
6 | ## Interaction server
7 |
8 | The interaction server is just an HTTP server, like a website!
9 |
10 | Wumpy's `InteractionApp` implements the ASGI specification, meaning that you can run
11 | your bot with any ASGI server. The following is a non-exhaustive list of ASGI servers:
12 |
13 | - [Uvicorn](https://github.com/encode/uvicorn)
14 | - [Hypercorn](https://gitlab.com/pgjones/hypercorn)
15 |
16 | Follow the respective guide for the ASGI server you pick.
17 |
18 | To use an interaction server, make sure the the server is running then log into the
19 | [Discord Developer Portal](https://discord.com/developers) and configure the interaction
20 | server URL to tell Discord where to send the interactions:
21 |
22 | 
23 |
24 | ### Running it locally
25 |
26 | The big drawback with using an interaction server is that Discord requires the usage of HTTPS
27 | which can have a very complicated setup.
28 |
29 | This means that you need to port-forward the server and configure certificates.
30 |
31 | There are tools for this though, because you are not alone in this inconvenience.
32 |
33 | [Ngrok](https://ngrok.com) will open a tunnel to your computer, and allow you to
34 | test the interaction server locally over HTTPS.
35 |
36 | !!! info
37 | Ngrok will not give you the same URL everytime you run it unless you are using a Pro plan.
38 | This means that you need to update the interaction URL everytime you restart Ngrok.
39 |
40 | ## Gateway
41 |
42 | The Discord gateway uses WebSockets and requires no further setup.
43 |
44 | Start the bot by importing `anyio` and use `anyio.run()` with the backend you want to use:
45 |
46 | === "Asyncio"
47 |
48 | ```python
49 | import anyio
50 | import wumpy
51 |
52 | bot = wumpy.Client(...)
53 |
54 | if __name__ == '__main__':
55 | anyio.run(bot.start, backend='asyncio')
56 | ```
57 |
58 | !!! note
59 | `if __name__ == '__main__':` is a way to ensure that code only runs when the file is
60 | ran directly, otherwise you might start the bot when attempting to import it.
61 |
62 | === "Trio"
63 |
64 | ```python
65 | import anyio
66 | import wumpy
67 |
68 | bot = wumpy.Client(...)
69 |
70 | if __name__ == '__main__':
71 | anyio.run(bot.start, backend='trio')
72 | ```
73 |
74 | !!! note
75 | `if __name__ == '__main__':` is a way to ensure that code only runs when the file is
76 | ran directly, otherwise you might start the bot when attempting to import it.
77 |
78 | !!! warning
79 | The downside with a long-living WebSocket is that many free hosters like
80 | [Replit](https://replit.com) or [Heroku](https://heroku.com) might kill the bot.
81 |
--------------------------------------------------------------------------------
/docs/source/tutorial/extensions/advanced-usage.md:
--------------------------------------------------------------------------------
1 | # More advanced extensions usages
2 |
3 | There are times when you want to run some code upon loading. This is fully
4 | supported in Wumpy's extension system.
5 |
6 | ## Partial example
7 |
8 | Most of the time you just want to some code when loading but still load
9 | commands and listeners. This can be accomplished as follows:
10 |
11 | ```python
12 | from wumpy import Extension
13 |
14 |
15 | ext = Extension()
16 |
17 |
18 | ... # Here is some code that adds commands and listeners
19 |
20 |
21 | def load(target, data):
22 | print('Loading extension...')
23 |
24 | # Let the extension take over
25 | return ext.load(target, data)
26 | ```
27 |
28 | ## Underlying extension API
29 |
30 | The underlying extension API is very ambiguous and only consists of two
31 | callables.
32 |
33 | The first callable should take two positional arguments - the target that
34 | the items (like commands and listeners) should be added to and a dictionary of
35 | values passed.
36 |
37 | The second argument allows passing values into an extension as it is loaded,
38 | which can be useful for configuration.
39 |
40 | At last the first callable should return another callable which only takes one
41 | argument - the target from before. The point of this callable is to undo what
42 | the first callable did (unload).
43 |
44 | ## Full usage
45 |
46 | A fully managed extension loading where you set some attribute can look like
47 | this as an example:
48 |
49 | ```python
50 | def unloader(target):
51 | del target.loaded
52 |
53 | def load(target, data):
54 | target.loaded = data.get('value', True)
55 | # Return the unloader that Wumpy will call when this extension is
56 | # unloaded for cleanup.
57 | return unloader
58 | ```
59 |
--------------------------------------------------------------------------------
/docs/source/tutorial/extensions/extensions-faq.md:
--------------------------------------------------------------------------------
1 | # Why use extensions?
2 |
3 | As you follow this tutorial you will eventually feel the need to split your
4 | Discord bot into multiple files to make your code mode readable and easier to
5 | manage. This is exactly what extensions are for.
6 |
7 | ## What are extensions?
8 |
9 | Extensions allow you to define some commands and listeners elsewhere, then load
10 | them onto your main bot or application.
11 |
12 | This way you can put some commands having to do with a certain feature in one
13 | specific file reducing the length of your main file.
14 |
15 | ## Why are extensions loaded using strings?
16 |
17 | There's another big benefit to extensions; they are dynamically loaded!
18 |
19 | For you as a user this means that you can reload an extension to apply changes
20 | without restarting your bot. If the extensions are imported directly using
21 | `from ... import ...` that complicates things greatly.
22 |
23 | ## What are the downsides of extensions?
24 |
25 | When extensions are reloaded they are also fully executed again so that new
26 | changes can be applied. This means that existing code could be holding outdated
27 | references.
28 |
29 | For example, if you define a class inside an extension and have other code
30 | that imports this and uses it with `isinstance(...)` you will get unexpected
31 | behavior when reloading an extension. The code that has the `isinstance(...)`
32 | *could* be holding a reference to the old class (from Python's point of view
33 | that is a different class) and `isinstance(...)` will return False.
34 |
35 | This is very uncommon if you structure your code correctly though, but could be
36 | worth noting when designing your bot.
37 |
--------------------------------------------------------------------------------
/docs/source/tutorial/extensions/simple-example.md:
--------------------------------------------------------------------------------
1 | # Simple extension example
2 |
3 | For most code you can easily change `InteractionApp` or `GatewayBot` to
4 | `Extension` and it will work as expected.
5 |
6 | Here's the code-block from [the Hello World](#hello-world.md) changed to be an
7 | extension:
8 |
9 | ```python
10 | from wumpy import Extension, interactions
11 |
12 |
13 | ext = Extension()
14 |
15 |
16 | @ext.command()
17 | async def hello(interaction: interactions.CommandInteraction) -> None:
18 | """Greet the bot and say hello."""
19 | await interaction.respond('Hello')
20 | ```
21 |
22 | Inside of your main file you can now load this extension like this:
23 |
24 | ```python
25 | from wumpy import interactions
26 |
27 |
28 | app = interactions.InteractionApp(...)
29 |
30 |
31 | # Load the extension, which is inside `hello_world.py`. Then after the ':' we
32 | # tell Wumpy that the actual extension variable is called `ext`.
33 | app.load_extension('hello_world:ext')
34 | ```
35 |
--------------------------------------------------------------------------------
/docs/templates/python/material/method.html:
--------------------------------------------------------------------------------
1 | {{ log.debug() }}
2 | {% if config.show_if_no_docstring or method.has_contents %}
3 |
4 |
5 | {% with html_id = method.path %}
6 |
7 | {% if not root or config.show_root_heading %}
8 |
9 | {% if root %}
10 | {% set show_full_path = config.show_root_full_path %}
11 | {% set root_members = True %}
12 | {% elif root_members %}
13 | {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %}
14 | {% set root_members = False %}
15 | {% else %}
16 | {% set show_full_path = config.show_object_full_path %}
17 | {% endif %}
18 |
19 | {% filter heading(heading_level,
20 | role="method",
21 | id=html_id,
22 | class="doc doc-heading",
23 | toc_label=method.name ~ "()") %}
24 |
25 |
26 | {% with properties = method.properties %}
27 | {% include "properties.html" with context %}
28 | {% endwith %}
29 |
30 | {% filter highlight(language="python", inline=True) %}
31 | {% if show_full_path %}{{ method.path }}{% else %}{{ method.name }}{% endif %}
32 | {% with signature = method.signature %}{% include "signature.html" with context %}{% endwith %}
33 | {% endfilter %}
34 |
35 | {% endfilter %}
36 |
37 | {% else %}
38 | {% if config.show_root_toc_entry %}
39 | {% filter heading(heading_level,
40 | role="method",
41 | id=html_id,
42 | toc_label=method.path,
43 | hidden=True) %}
44 | {% endfilter %}
45 | {% endif %}
46 | {% set heading_level = heading_level - 1 %}
47 | {% endif %}
48 |
49 |
50 | {% with docstring_sections = method.docstring_sections %}
51 | {% include "docstring.html" with context %}
52 | {% endwith %}
53 |
54 | {% if config.show_source and method.source %}
55 |
56 | Source code in {{ method.relative_file_path }}
57 | {{ method.source.code|highlight(language="python", linestart=method.source.line_start, linenums=False) }}
58 |
59 | {% endif %}
60 |
61 |
62 | {% endwith %}
63 |
64 |
65 | {% endif %}
--------------------------------------------------------------------------------
/docs/templates/python/material/parameters.html:
--------------------------------------------------------------------------------
1 | {{ log.debug() }}
2 | Parameters:
3 |
4 |
5 |
6 | Name |
7 | Type |
8 | Description |
9 | Default |
10 |
11 |
12 |
13 | {% for parameter in parameters %}
14 |
15 | {{ parameter.name }} |
16 | {% if parameter.annotation %}{{ parameter.annotation }} {% endif %} |
17 | {{ parameter.description|convert_markdown(heading_level, html_id) }} |
18 |
19 |
20 | {% if parameter.default %}
21 | {% if parameter.default == '' %}
22 | optional
23 | {% else %}
24 | {{ parameter.default }}
25 | {% endif %}
26 | {% else %}
27 | required
28 | {% endif %}
29 | |
30 |
31 | {% endfor %}
32 |
33 |
--------------------------------------------------------------------------------
/library/wumpy-bot/README.md:
--------------------------------------------------------------------------------
1 | # Wumpy-bot
2 |
3 | Easy to use high abstraction over other Wumpy subpackages.
4 |
5 | This is the most beginner-friendly way to use Wumpy, and provides all features
6 | of the Discord API. This is the primary package installed from PyPI when you
7 | only specify [`wumpy`](https://pypi.org/project/wumpy/).
8 |
9 | ## Getting started
10 |
11 | All official Wumpy projects prioritise both asyncio and Trio support so you can
12 | run the bot under either:
13 |
14 | ```python
15 | import anyio
16 | from wumpy.bot import Bot
17 |
18 |
19 | bot = Bot('ABC123.XYZ789') # Replace with your token and keep it safe!
20 |
21 | # This runs the bot with Trio as the event loop (recommended),
22 | # use backend='asyncio' to run it under asyncio.
23 | anyio.run(bot.run, backend='trio')
24 | ```
25 |
26 | ### Registering listeners
27 |
28 | Continuing from the previous code, you can register listeners for Discord
29 | events using Wumpy's rich event listeners:
30 |
31 | ```python
32 | import anyio
33 | from wumpy.bot import Bot
34 | from wumpy.bot.events import MessageDeleteEvent
35 |
36 |
37 | bot = Bot('ABC123.XYZ789')
38 |
39 |
40 | @bot.listener()
41 | async def log_deleted_messages(event: MessageDeleteEvent):
42 | print(f'Message {event.message_id} in {event.channel_id} was deleted')
43 |
44 |
45 | anyio.run(bot.run, backend='trio')
46 | ```
47 |
48 | The listener is registered with the `@bot.listener()` decorator, which tells
49 | Wumpy to read the annotation of the first parameter (name does not matter, but
50 | here it is called `event`) and register that function for the type of event
51 | that it is typehinted as.
52 |
--------------------------------------------------------------------------------
/library/wumpy-bot/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "wumpy-bot"
3 | version = "0.1.0"
4 | description = "Highly abstracted and easy to use wrapper over the Wumpy project"
5 | readme = {file = "README.md", content-type="text/markdown"}
6 |
7 | requires-python = ">=3.7"
8 |
9 | license = {text = "Dual-licensed under the Apache License 2.0 and the MIT License"}
10 | authors = [{name = "Bluenix", email = "bluenixdev@gmail.com"}]
11 |
12 | keywords = [
13 | "wumpy", "wumpus", "wrapper",
14 | "discord", "discord-api", "discord-bot", "discord-api-wrapper",
15 | "python-3"
16 | ]
17 | classifiers = [
18 | "Development Status :: 2 - Pre-Alpha",
19 | "Framework :: AnyIO",
20 | "Intended Audience :: Developers",
21 | "License :: OSI Approved :: MIT License",
22 | "License :: OSI Approved :: Apache Software License",
23 | "Natural Language :: English",
24 | "Operating System :: OS Independent",
25 | "Programming Language :: Python",
26 | "Programming Language :: Python :: 3",
27 | "Programming Language :: Python :: 3 :: Only",
28 | "Programming Language :: Python :: 3.7",
29 | "Programming Language :: Python :: 3.8",
30 | "Programming Language :: Python :: 3.9",
31 | "Programming Language :: Python :: 3.10",
32 | "Topic :: Internet",
33 | "Topic :: Internet :: WWW/HTTP",
34 | "Topic :: Software Development :: Libraries",
35 | "Topic :: Software Development :: Libraries :: Python Modules",
36 | "Typing :: Typed",
37 | ]
38 |
39 | dependencies = [
40 | "anyio >= 3.3.4, <4",
41 | "typing_extensions >= 4.3, <5",
42 | "attrs >= 21.3, <= 24",
43 | "wumpy-gateway >= 0.3, <1",
44 | "wumpy-rest >= 0.3, <1",
45 | "wumpy-models >= 0.1, <1",
46 | "wumpy-interactions >= 0.1, <1",
47 | "wumpy-cache >= 0.1, <1",
48 | ]
49 |
50 | [project.urls]
51 | Homepage = "https://github.com/wumpyproject/wumpy"
52 | Repository = "https://github.com/wumpyproject/wumpy/tree/main/library/wumpy-bot"
53 | Documentation = "https://wumpy.rtfd.io"
54 |
55 | [build-system]
56 | requires = ["flit_core >=3.5, <4"]
57 | build-backend = "flit_core.buildapi"
58 |
59 | [tool.flit.module]
60 | # This is a subpackage under the wumpy namespace package,
61 | # we need to tell flit this otherwise it tries to make the
62 | # import wumpy-client rather than wumpy.client
63 | name = "wumpy.bot"
64 |
--------------------------------------------------------------------------------
/library/wumpy-bot/wumpy/bot/__init__.py:
--------------------------------------------------------------------------------
1 | from ._bot import (
2 | Bot,
3 | get_bot,
4 | )
5 | from ._dispatch import (
6 | ErrorContext,
7 | Event,
8 | EventDispatcher,
9 | ErrorHandlerMixin,
10 | )
11 | from ._errors import (
12 | WumpyException,
13 | ConnectionClosed,
14 | CommandException,
15 | ExtensionFailure,
16 | )
17 | from ._extension import (
18 | Extension,
19 | ExtensionLoader,
20 | )
21 |
22 | __all__ = (
23 | 'Bot',
24 | 'get_bot',
25 | 'ErrorContext',
26 | 'Event',
27 | 'EventDispatcher',
28 | 'ErrorHandlerMixin',
29 | 'WumpyException',
30 | 'ConnectionClosed',
31 | 'CommandException',
32 | 'ExtensionFailure',
33 | )
34 |
--------------------------------------------------------------------------------
/library/wumpy-bot/wumpy/bot/_utils.py:
--------------------------------------------------------------------------------
1 | import functools
2 | from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar
3 |
4 | # Internal, but it would be pointless to copy it over.
5 | from wumpy.models._utils import \
6 | _get_as_snowflake as _get_as_snowflake # noqa: F401
7 |
8 | __all__ = (
9 |
10 | )
11 |
12 |
13 | T = TypeVar('T')
14 |
15 |
16 | def _eval_annotations(obj: 'Callable[..., object]') -> Dict[str, Any]:
17 | """Eval a callable's annotations.
18 |
19 | This is primarily a backport of Python 3.10's `get_annotations()`
20 | method implemented by Larry Hastings:
21 | https://github.com/python/cpython/commit/74613a46fc79cacc88d3eae4105b12691cd4ba20
22 |
23 | Parameters:
24 | obj: The received callable to evaluate
25 |
26 | Returns:
27 | A dictionary of parameter name to its annotation.
28 | """
29 | unwrapped = obj
30 | while True:
31 | if hasattr(unwrapped, '__wrapped__'):
32 | unwrapped = unwrapped.__wrapped__
33 | continue
34 | if isinstance(unwrapped, functools.partial):
35 | unwrapped = unwrapped.func
36 | continue
37 | break
38 |
39 | annotations = getattr(unwrapped, '__annotations__', None)
40 | eval_globals = getattr(unwrapped, '__globals__', None)
41 |
42 | if annotations is None or not annotations:
43 | return {}
44 |
45 | if not isinstance(annotations, dict):
46 | raise ValueError(f'{unwrapped!r}.__annotations__ is neither a dict nor None')
47 |
48 | try:
49 | return {
50 | key: value if not isinstance(value, str) else eval(value, eval_globals)
51 | for key, value in annotations.items()
52 | }
53 | except (NameError, SyntaxError) as e:
54 | raise ValueError(f'Could not evaluate the annotations of {unwrapped!r}') from e
55 |
56 |
57 | class RuntimeVar(Generic[T]):
58 | """Descriptor for attributes that are set during runtime.
59 |
60 | This descriptor raises a `RuntimeError` if the attribute is accessed before
61 | it has been set - the benefit of which is that you can have attributes that
62 | should usualy use `Optional[T]` but without the unnecessary runtime checks
63 | to pass type hinting.
64 |
65 | This is a generic for the type of the underlying value.
66 | """
67 |
68 | name: Optional[str]
69 | value: T
70 |
71 | def __init__(self, name: Optional[str] = None) -> None:
72 | self.name = name
73 |
74 | def __set_name__(self, owner: Type[object], name: str) -> None:
75 | if self.name is not None:
76 | self.name = name
77 |
78 | def __get__(self, instance: Optional[object], cls: Type[object]) -> T:
79 | if instance is None:
80 | raise AttributeError(f'{cls.__name__!r} object has no attribute {self.name!r}')
81 |
82 | if not hasattr(self, 'value'):
83 | raise RuntimeError(
84 | f'Cannot access runtime variable {self.name!r} before bot is running'
85 | )
86 |
87 | return self.value
88 |
89 | def __set__(self, instance: object, value: T) -> None:
90 | self.value = value
91 |
--------------------------------------------------------------------------------
/library/wumpy-bot/wumpy/bot/events/__init__.py:
--------------------------------------------------------------------------------
1 | from ._channel import (
2 | ChannelCreateEvent,
3 | ChannelUpdateEvent,
4 | ChannelDeleteEvent,
5 | TypingEvent,
6 | ThreadCreateEvent,
7 | ThreadUpdateEvent,
8 | ThreadDeleteEvent,
9 | ThreadListSyncEvent,
10 | ChannelPinsUpdateEvent,
11 | )
12 | from ._gateway import (
13 | HelloEvent,
14 | ResumedEvent,
15 | ReadyEvent,
16 | )
17 | from ._guild import (
18 | GuildDeleteEvent,
19 | BanAddEvent,
20 | BanRemoveEvent,
21 | GuildEmojisUpdateEvent,
22 | GuildStickersUpdateEvent,
23 | MemberJoinEvent,
24 | MemberRemoveEvent,
25 | MemberUpdateEvent,
26 | RoleCreateEvent,
27 | RoleUpdateEvent,
28 | RoleDeleteEvent,
29 | )
30 | from ._message import (
31 | MessageCreateEvent,
32 | MessageUpdateEvent,
33 | MessageDeleteEvent,
34 | BulkMessageDeleteEvent,
35 | ReactionAddEvent,
36 | ReactionRemoveEvent,
37 | ReactionClearEvent,
38 | ReactionEmojiClearEvent,
39 | )
40 |
41 | __all__ = (
42 | 'ChannelCreateEvent',
43 | 'ChannelUpdateEvent',
44 | 'ChannelDeleteEvent',
45 | 'TypingEvent',
46 | 'ThreadCreateEvent',
47 | 'ThreadUpdateEvent',
48 | 'ThreadDeleteEvent',
49 | 'ThreadListSyncEvent',
50 | 'ChannelPinsUpdateEvent',
51 | 'HelloEvent',
52 | 'ReadyEvent',
53 | 'ResumedEvent',
54 | 'GuildDeleteEvent',
55 | 'BanAddEvent',
56 | 'BanRemoveEvent',
57 | 'GuildEmojisUpdateEvent',
58 | 'GuildStickersUpdateEvent',
59 | 'MemberJoinEvent',
60 | 'MemberRemoveEvent',
61 | 'MemberUpdateEvent',
62 | 'RoleCreateEvent',
63 | 'RoleUpdateEvent',
64 | 'RoleDeleteEvent',
65 | 'MessageCreateEvent',
66 | 'MessageUpdateEvent',
67 | 'MessageDeleteEvent',
68 | 'BulkMessageDeleteEvent',
69 | 'ReactionAddEvent',
70 | 'ReactionRemoveEvent',
71 | 'ReactionClearEvent',
72 | 'ReactionEmojiClearEvent',
73 | )
74 |
--------------------------------------------------------------------------------
/library/wumpy-bot/wumpy/bot/events/_gateway.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, FrozenSet, Mapping, Optional, Tuple
2 |
3 | import attrs
4 | from discord_typings import ReadyData
5 | from typing_extensions import Self
6 | from wumpy.models import ApplicationFlags, Snowflake, User
7 |
8 | from .._dispatch import Event
9 |
10 | __all__ = (
11 | 'HelloEvent',
12 | 'ResumedEvent',
13 | 'ReadyEvent',
14 | )
15 |
16 |
17 | @attrs.define()
18 | class HelloEvent(Event):
19 |
20 | NAME: ClassVar[str] = 'HELLO'
21 |
22 | @classmethod
23 | def from_payload(cls, payload: Mapping[str, Any], cached: None = None) -> Self:
24 | return cls()
25 |
26 |
27 | @attrs.define()
28 | class ResumedEvent(Event):
29 |
30 | NAME: ClassVar[str] = 'RESUMED'
31 |
32 | @classmethod
33 | def from_payload(cls, payload: Mapping[str, Any], cached: None = None) -> Optional[Self]:
34 | return cls()
35 |
36 |
37 | @attrs.define(kw_only=True)
38 | class ReadyEvent(Event):
39 | user: User
40 | guilds: FrozenSet[Snowflake]
41 | shard: Optional[Tuple[int, int]]
42 |
43 | flags: ApplicationFlags
44 |
45 | NAME: ClassVar[str] = 'READY'
46 |
47 | @classmethod
48 | def from_payload(cls, payload: ReadyData, cached: None = None) -> Self:
49 | return cls(
50 | user=User.from_data(payload['user']),
51 | guilds=frozenset([Snowflake(int(guild['id'])) for guild in payload['guilds']]),
52 | shard=tuple(payload['shard']) if 'shard' in payload else None,
53 | flags=ApplicationFlags(int(payload['application']['flags']))
54 | )
55 |
--------------------------------------------------------------------------------
/library/wumpy-bot/wumpy/bot/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/library/wumpy-bot/wumpy/bot/py.typed
--------------------------------------------------------------------------------
/library/wumpy-cache/README.md:
--------------------------------------------------------------------------------
1 | # Wumpy-cache
2 |
3 | Simple memory cache for the gateway.
4 |
5 | The default implementation in `wumpy-cache` is rather simple dictionaries, but
6 | it can be overriden by other implementations as long as they follow the typing
7 | `Cache` Protocol.
8 |
9 | This means that you can use alternate implementations of data structures for
10 | more efficient retrieval for your use-case or offload caching to something like
11 | Redis or even store it in Postgres.
12 |
--------------------------------------------------------------------------------
/library/wumpy-cache/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "wumpy-cache"
3 | version = "0.1.0"
4 | description = "Simple and plain caching for Discord API libraries."
5 | readme = {file = "README.md", content-type="text/markdown"}
6 |
7 | requires-python = ">=3.7"
8 |
9 | license = {text = "Dual-licensed under the Apache License 2.0 and the MIT License"}
10 | authors = [{name = "Bluenix", email = "bluenixdev@gmail.com"}]
11 |
12 | keywords = [
13 | "wumpy", "wumpus", "wrapper",
14 | "discord", "discord-api", "discord-bot", "discord-api-wrapper",
15 | "python-3"
16 | ]
17 | classifiers = [
18 | "Development Status :: 1 - Planning",
19 | "Framework :: AnyIO",
20 | "Intended Audience :: Developers",
21 | "License :: OSI Approved :: MIT License",
22 | "License :: OSI Approved :: Apache Software License",
23 | "Natural Language :: English",
24 | "Operating System :: OS Independent",
25 | "Programming Language :: Python",
26 | "Programming Language :: Python :: 3",
27 | "Programming Language :: Python :: 3 :: Only",
28 | "Programming Language :: Python :: 3.7",
29 | "Programming Language :: Python :: 3.8",
30 | "Programming Language :: Python :: 3.9",
31 | "Programming Language :: Python :: 3.10",
32 | "Topic :: Internet",
33 | "Topic :: Internet :: WWW/HTTP",
34 | "Topic :: Software Development :: Libraries",
35 | "Topic :: Software Development :: Libraries :: Python Modules",
36 | "Typing :: Typed",
37 | ]
38 |
39 | dependencies = [
40 | "anyio >= 3.3.4, <4",
41 | "typing_extensions >= 4, <5",
42 | "discord-typings >= 0.4.0, <1",
43 | "wumpy-models >= 0.1.0, <1"
44 | ]
45 |
46 | [project.urls]
47 | Homepage = "https://github.com/wumpyproject/wumpy"
48 | Repository = "https://github.com/wumpyproject/wumpy/tree/main/library/wumpy-cache"
49 | documentation = "https://wumpy.rtfd.io"
50 |
51 | [build-system]
52 | requires = ["flit_core >=3.5, <4"]
53 | build-backend = "flit_core.buildapi"
54 |
55 | [tool.flit.module]
56 | # This is a subpackage under the wumpy namespace package,
57 | # we need to tell flit this otherwise it tries to make the
58 | # import wumpy-cache rather than wumpy.cache
59 | name = "wumpy.cache"
60 |
--------------------------------------------------------------------------------
/library/wumpy-cache/wumpy/cache/__init__.py:
--------------------------------------------------------------------------------
1 | from ._protocol import (
2 | Cache,
3 | )
4 |
5 | __all__ = (
6 | 'Cache',
7 | )
8 |
--------------------------------------------------------------------------------
/library/wumpy-cache/wumpy/cache/in_memory/__init__.py:
--------------------------------------------------------------------------------
1 | from ._channel import (
2 | ChannelMemoryCache,
3 | MessageMemoryCache,
4 | )
5 | from ._guild import (
6 | EmojiMemoryCache,
7 | GuildMemoryCache,
8 | RoleMemoryCache,
9 | )
10 | from ._user import (
11 | MemberMemoryCache,
12 | UserMemoryCache,
13 | )
14 |
15 |
16 | __all__ = (
17 | 'ChannelMemoryCache',
18 | 'MessageMemoryCache',
19 | 'EmojiMemoryCache',
20 | 'GuildMemoryCache',
21 | 'RoleMemoryCache',
22 | 'MemberMemoryCache',
23 | 'UserMemoryCache',
24 | 'InMemoryCache',
25 | )
26 |
27 |
28 | class InMemoryCache(ChannelMemoryCache, EmojiMemoryCache, GuildMemoryCache, MemberMemoryCache,
29 | MessageMemoryCache, RoleMemoryCache, UserMemoryCache):
30 | """Cache implementation storing data in local memory."""
31 |
32 | __slots__ = ()
33 |
--------------------------------------------------------------------------------
/library/wumpy-cache/wumpy/cache/in_memory/_base.py:
--------------------------------------------------------------------------------
1 | from types import TracebackType
2 | from typing import Any, Callable, Mapping, Optional, SupportsInt, Tuple, Type
3 |
4 | import anyio.lowlevel
5 | from discord_typings import UserData
6 | from typing_extensions import Self
7 | from wumpy.models import (
8 | Category, Emoji, Guild, Invite, Member, Message, Role, Sticker, Thread,
9 | User
10 | )
11 |
12 | from .._protocol import Cache, Channel
13 |
14 | __all__ = (
15 |
16 | )
17 |
18 |
19 | EventProcessor = Callable[[Mapping[str, Any]], Tuple[Optional[Any], Any]]
20 |
21 |
22 | class BaseMemoryCache(Cache):
23 | """The base for all in-memory caches.
24 |
25 | The point of this is to house the base implementation of the required
26 | `update()` method, which propogates to the matching processor, allowing for
27 | one O(1) dictionary lookup on the attributes.
28 | """
29 |
30 | __slots__ = ()
31 |
32 | async def __aenter__(self) -> Self:
33 | return self
34 |
35 | async def __aexit__(
36 | self,
37 | exc_type: Optional[Type[BaseException]],
38 | exc_val: Optional[BaseException],
39 | traceback: Optional[TracebackType]
40 | ) -> None:
41 | pass
42 |
43 | async def update(self, payload: Mapping[str, Any], *, return_old: bool = True) -> Any:
44 | """Propogate the `update()` to a processor.
45 |
46 | Processors needs to follow: `_process_discord_event` naming; starting
47 | with `_process_` and then the event from Discord but lowercased.
48 |
49 | This method will simply lookup the naming above in the attributes and
50 | call it.
51 |
52 | Parameters:
53 | payload: The payload to propogate.
54 |
55 | Returns:
56 | Whatever the processor returned; if no processor is found, None.
57 | """
58 | if 't' not in payload:
59 | # If the type key is missing, that means that this is not a
60 | # dispatch event, so there is nothing for us to handle.
61 | await anyio.lowlevel.checkpoint()
62 | return None
63 |
64 | # Because this is a coroutine-function we should await somewhere, but
65 | # it's best to run the in-memory cache stuff as soon as possible
66 | # (so that other tasks can't modify the cache). This can't happen after
67 | # we have successfully updated the cache though as we don't want to get
68 | # cancelled causing us to both succeed and fail at the same time.
69 | # Therefore the yielding is split into multiple steps.
70 | await anyio.lowlevel.checkpoint_if_cancelled()
71 |
72 | try:
73 | processor: EventProcessor = getattr(self, f"_process_{payload['t'].lower()}")
74 | except AttributeError:
75 | return None
76 | else:
77 | return processor(payload['d'])
78 | finally:
79 | await anyio.lowlevel.cancel_shielded_checkpoint()
80 |
81 | # If we don't define these methods the type checker will complain because
82 | # we don't implement the protocol
83 |
84 | async def get_guild(self, guild: SupportsInt) -> Optional[Guild]:
85 | ...
86 |
87 | async def get_role(self, role: SupportsInt) -> Optional[Role]:
88 | ...
89 |
90 | async def get_channel(self, channel: SupportsInt) -> Optional[Channel]:
91 | ...
92 |
93 | async def get_thread(self, thread: SupportsInt) -> Optional[Thread]:
94 | ...
95 |
96 | async def get_category(self, category: SupportsInt) -> Optional[Category]:
97 | ...
98 |
99 | async def get_message(
100 | self,
101 | channel: Optional[SupportsInt],
102 | message: SupportsInt
103 | ) -> Optional[Message]:
104 | ...
105 |
106 | async def get_emoji(self, emoji: SupportsInt) -> Optional[Emoji]:
107 | ...
108 |
109 | async def get_sticker(self, sticker: SupportsInt) -> Optional[Sticker]:
110 | ...
111 |
112 | async def get_invite(self, invite: SupportsInt) -> Optional[Invite]:
113 | ...
114 |
115 | async def get_member(self, guild: SupportsInt, user: SupportsInt) -> Optional[Member]:
116 | ...
117 |
118 | async def get_user(self, user: SupportsInt) -> Optional[User]:
119 | ...
120 |
121 | # Certain caches depend on others to store things for them, we need to
122 | # define these fallbacks so that they can be safely called without worrying
123 | # about AttributeErrors trying to access the method.
124 |
125 | def _store_user(self, data: UserData) -> User:
126 | # This method is overriden by UserMemoryCache and adds the user to the
127 | # cache - still, in the fallback we can create a user without actually
128 | # storing it for later.
129 | return User.from_data(data)
130 |
--------------------------------------------------------------------------------
/library/wumpy-cache/wumpy/cache/in_memory/_channel.py:
--------------------------------------------------------------------------------
1 | import collections
2 | from typing import (
3 | Any, Deque, Dict, FrozenSet, Mapping, Optional, SupportsInt, Type, Union
4 | )
5 |
6 | from discord_typings import (
7 | ChannelData, MessageCreateData, MessageDeleteBulkData, MessageDeleteData,
8 | MessageUpdateData
9 | )
10 | from wumpy.models import Category, Message, TextChannel, Thread, VoiceChannel
11 |
12 | from ._base import BaseMemoryCache, Channel
13 |
14 | __all__ = (
15 | 'ChannelMemoryCache',
16 | 'MessageMemoryCache',
17 | )
18 |
19 |
20 | AllChannels = Union[Channel, Category, Thread]
21 |
22 |
23 | class ChannelMemoryCache(BaseMemoryCache):
24 | _channels: Dict[int, AllChannels]
25 |
26 | CHANNELS: Mapping[int, Type[AllChannels]] = {
27 | 0: TextChannel,
28 | 2: VoiceChannel,
29 | 4: Category,
30 | 5: TextChannel,
31 | 10: Thread,
32 | 11: Thread,
33 | 12: Thread,
34 | 13: VoiceChannel,
35 | }
36 |
37 | def __init__(self, *args: Any, **kwargs: Any) -> None:
38 | super().__init__(*args, **kwargs)
39 |
40 | self._channels = {}
41 |
42 | def _process_create_channel(self, data: ChannelData, *, return_old: bool = True) -> None:
43 | cls = self.CHANNELS[data['type']]
44 | # The type checker doesn't understand the relation between data['type']
45 | # and the class returned so it thinks that any channel data may be
46 | # passed to any channel class.
47 | channel = cls.from_data(data) # type: ignore
48 | self._channels[channel.id] = channel
49 | return
50 |
51 | def _process_channel_update(
52 | self,
53 | data: ChannelData,
54 | *,
55 | return_old: bool = True
56 | ) -> Optional[AllChannels]:
57 | old = self._process_channel_delete(data, return_old=return_old)
58 | self._process_create_channel(data, return_old=return_old)
59 | return old
60 |
61 | def _process_channel_delete(
62 | self,
63 | data: ChannelData,
64 | *,
65 | return_old: bool = True
66 | ) -> Optional[AllChannels]:
67 | return self._channels.pop(int(data['id']), None)
68 |
69 | async def get_channel(self, channel: SupportsInt) -> Optional[AllChannels]:
70 | return self._channels.get(int(channel))
71 |
72 | async def get_thread(self, thread: SupportsInt) -> Optional[Thread]:
73 | channel = self.get_channel(thread)
74 | if isinstance(channel, Thread):
75 | return channel
76 |
77 | return None
78 |
79 | async def get_category(self, category: SupportsInt) -> Optional[Category]:
80 | channel = await self.get_channel(category)
81 | if isinstance(channel, Category):
82 | return channel
83 |
84 | return None
85 |
86 |
87 | class MessageMemoryCache(BaseMemoryCache):
88 | _messages: Deque[Message]
89 |
90 | def __init__(self, *args: Any, max_messages: int, **kwargs: Any) -> None:
91 | super().__init__(*args, **kwargs)
92 |
93 | self._messages = collections.deque(maxlen=max_messages)
94 |
95 | def _process_message_create(
96 | self,
97 | data: MessageCreateData,
98 | *,
99 | return_old: bool = True
100 | ) -> None:
101 | m = Message.from_data(data)
102 | self._messages.append(m)
103 |
104 | def _process_message_update(
105 | self,
106 | data: MessageUpdateData,
107 | *,
108 | return_old: bool = True
109 | ) -> Optional[Message]:
110 | old = self._process_message_delete(data, return_old=return_old)
111 | self._process_message_create(data, return_old=return_old)
112 | return old
113 |
114 | def _process_message_delete(
115 | self,
116 | data: MessageDeleteData,
117 | *,
118 | return_old: bool = True
119 | ) -> Optional[Message]:
120 | if return_old:
121 | id_ = int(data['id'])
122 |
123 | found = [msg for msg in self._messages if msg == id_]
124 | if found:
125 | return found[0]
126 |
127 | return None
128 |
129 | def _process_message_delete_bulk(
130 | self,
131 | data: MessageDeleteBulkData,
132 | *,
133 | return_old: bool = True
134 | ) -> FrozenSet[Message]:
135 | if return_old:
136 | ids = set([int(id_) for id_ in data['ids']])
137 |
138 | found = [msg for msg in self._messages if msg.id in ids]
139 | return frozenset(found)
140 |
141 | return frozenset()
142 |
143 | async def get_message(
144 | self,
145 | channel: Optional[SupportsInt],
146 | message: SupportsInt
147 | ) -> Optional[Message]:
148 | id_ = int(message)
149 | found = [msg for msg in self._messages if msg == id_]
150 | if found:
151 | return found[0]
152 |
153 | return None
154 |
--------------------------------------------------------------------------------
/library/wumpy-cache/wumpy/cache/in_memory/_user.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Optional, SupportsInt, Tuple
2 | from weakref import WeakValueDictionary
3 |
4 | from discord_typings import (
5 | GuildMemberAddData, GuildMemberRemoveData, GuildMemberUpdateData, UserData
6 | )
7 | from wumpy.models import Member, User
8 |
9 | from ._base import BaseMemoryCache
10 |
11 | __all__ = (
12 | 'UserMemoryCache',
13 | 'MemberMemoryCache',
14 | )
15 |
16 |
17 | class UserMemoryCache(BaseMemoryCache):
18 | _users: 'WeakValueDictionary[int, User]'
19 |
20 | def __init__(self, *args: Any, **kwargs: Any) -> None:
21 | super().__init__(*args, **kwargs)
22 |
23 | self._users = WeakValueDictionary()
24 |
25 | def _store_user(self, data: UserData) -> User:
26 | # This is slower than simply constructing another User model, but for
27 | # memory purposes we want to share it between objects like Members as
28 | # much as possible.
29 | user = self._users.get(int(data['id']))
30 | if (
31 | user and user.id == int(data['id'])
32 | and user.name == data['username']
33 | and user.discriminator == data['discriminator']
34 | and user.public_flags == data.get('public_flags')
35 | ):
36 | return user # The existing user is up-to-date
37 |
38 | user = User.from_data(data)
39 | self._users[int(data['id'])] = user
40 |
41 | return user
42 |
43 |
44 | class MemberMemoryCache(BaseMemoryCache):
45 | _members: Dict[Tuple[int, int], Member]
46 |
47 | def __init__(self, *args: Any, **kwargs: Any) -> None:
48 | super().__init__(*args, **kwargs)
49 |
50 | self._members = {}
51 |
52 | def _process_guild_member_add(
53 | self,
54 | data: GuildMemberAddData,
55 | *,
56 | return_old: bool = True
57 | ) -> None:
58 | guild_id = int(data['guild_id'])
59 | user = self._store_user(data['user'])
60 |
61 | member = Member.from_user(user, data)
62 | self._members[(guild_id, member.id)] = member
63 |
64 | def _process_guild_member_update(
65 | self,
66 | data: GuildMemberUpdateData,
67 | *,
68 | return_old: bool = True
69 | ) -> Optional[Member]:
70 | old = self._process_guild_member_remove(data, return_old=return_old)
71 | self._process_guild_member_add(data, return_old=return_old)
72 | return old
73 |
74 | def _process_guild_member_remove(
75 | self,
76 | data: GuildMemberRemoveData,
77 | *,
78 | return_old: bool = True
79 | ) -> Optional[Member]:
80 | return self._members.pop((int(data['guild_id']), int(data['user']['id'])), None)
81 |
82 | async def get_member(self, guild: SupportsInt, user: SupportsInt) -> Optional[Member]:
83 | return self._members.get((int(guild), int(user)))
84 |
--------------------------------------------------------------------------------
/library/wumpy-cache/wumpy/cache/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/library/wumpy-cache/wumpy/cache/py.typed
--------------------------------------------------------------------------------
/library/wumpy-gateway/README.md:
--------------------------------------------------------------------------------
1 | # Wumpy-gateway
2 |
3 | Gateway implementation for the Wumpy project.
4 |
5 | ## Installation
6 |
7 | Wumpy-gateway is available as a package on PyPI. Just use your favourite
8 | package manager such as pip or Poetry:
9 |
10 | ```bash
11 | # Installing from PyPI using pip:
12 | pip install wumpy-gateway
13 | ```
14 |
15 | ```bash
16 | # Alternatively, using Poetry:
17 | poetry add wumpy-gateway
18 | ```
19 |
20 | ## Quickstart
21 |
22 | The API of wumpy-gateway is very simple:
23 |
24 | ```python
25 | from wumpy.gateway import Shard
26 |
27 |
28 | INTENTS = 65535
29 | TOKEN = 'ABC123.XYZ789'
30 |
31 |
32 | async def main():
33 | # Connect to the URI wss://gateway.discord.gg/ with the token
34 | # ABC123.XYZ8789 and all intents.
35 | async with Shard('wss://gateway.discord.gg/', TOKEN, INTENTS) as ws:
36 | async for event in ws:
37 | print(event) # The deserialized JSON event payload
38 | ```
39 |
--------------------------------------------------------------------------------
/library/wumpy-gateway/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "wumpy-gateway"
3 | version = "0.3.0"
4 | description = "Lowlevel but easy-to-use wrapper over the Discord gateway"
5 | readme = {file = "README.md", content-type="text/markdown"}
6 |
7 | # Both discord-gateway and anyio have 3.6.2 as the minimum requirement
8 | # but @contextlib.asynccontextmanager was added in Python 3.7
9 | requires-python = ">=3.7"
10 |
11 | license = {text = "Dual-licensed under the Apache License 2.0 and the MIT License"}
12 | authors = [{name = "Bluenix", email = "bluenixdev@gmail.com"}]
13 |
14 | keywords = [
15 | "wumpy", "wumpus", "wrapper",
16 | "discord", "discord-api", "discord-bot", "discord-api-wrapper",
17 | "python-3"
18 | ]
19 | classifiers = [
20 | "Development Status :: 2 - Pre-Alpha",
21 | "Framework :: AnyIO",
22 | "Intended Audience :: Developers",
23 | "License :: OSI Approved :: MIT License",
24 | "License :: OSI Approved :: Apache Software License",
25 | "Natural Language :: English",
26 | "Operating System :: OS Independent",
27 | "Programming Language :: Python",
28 | "Programming Language :: Python :: 3",
29 | "Programming Language :: Python :: 3 :: Only",
30 | "Programming Language :: Python :: 3.7",
31 | "Programming Language :: Python :: 3.8",
32 | "Programming Language :: Python :: 3.9",
33 | "Programming Language :: Python :: 3.10",
34 | "Topic :: Internet",
35 | "Topic :: Internet :: WWW/HTTP",
36 | "Topic :: Software Development :: Libraries",
37 | "Topic :: Software Development :: Libraries :: Python Modules",
38 | "Typing :: Typed",
39 | ]
40 |
41 | dependencies = ["discord-gateway >=0.4.0, <1", "anyio >= 3.3.4, <4"]
42 |
43 | [project.urls]
44 | Homepage = "https://github.com/wumpyproject/wumpy"
45 | Repository = "https://github.com/wumpyproject/wumpy/tree/main/library/wumpy-gateway"
46 | Documentation = "https://wumpy.rtfd.io"
47 |
48 | [build-system]
49 | requires = ["flit_core >=3.5, <4"]
50 | build-backend = "flit_core.buildapi"
51 |
52 | [tool.flit.module]
53 | # This is a subpackage under the wumpy namespace package,
54 | # we need to tell flit this otherwise it tries to make the
55 | # import wumpy-gateway rather than wumpy.gateway
56 | name = "wumpy.gateway"
57 |
--------------------------------------------------------------------------------
/library/wumpy-gateway/wumpy/gateway/__init__.py:
--------------------------------------------------------------------------------
1 | """Gateway implementation for the Wumpy project.
2 |
3 | This module contains a main `Shard` class, to get started import it and use the
4 | `connect()` classmethod as an asynchronous context manager:
5 |
6 | ```python
7 | from wumpy.gateway import Shard
8 |
9 |
10 | TOKEN = 'ABC123.XYZ789'
11 | INTENTS = 65535
12 |
13 |
14 | async def main():
15 | # Connect to the URI wss://gateway.discord.gg/ with the token
16 | # ABC123.XYZ8789 and all intents.
17 | async with Shard.connect('wss://gateway.discord.gg/', TOKEN, INTENTS) as ws:
18 | async for event in ws:
19 | print(event) # The deserialized JSON event payload
20 | ```
21 | """
22 |
23 | from ._errors import (
24 | ConnectionClosed,
25 | )
26 | from ._shard import (
27 | Shard,
28 | )
29 | from ._utils import (
30 | GatewayLimiter,
31 | DefaultGatewayLimiter,
32 | )
33 |
34 | __all__ = (
35 | 'ConnectionClosed',
36 | 'Shard',
37 | 'GatewayLimiter',
38 | 'DefaultGatewayLimiter',
39 | )
40 |
--------------------------------------------------------------------------------
/library/wumpy-gateway/wumpy/gateway/_errors.py:
--------------------------------------------------------------------------------
1 | __all__ = (
2 | 'ConnectionClosed',
3 | )
4 |
5 |
6 | class ConnectionClosed(Exception):
7 | """Exception indicating that the connection to Discord fully closed.
8 |
9 | This is raised when the connection cannot naturally reconnect and the
10 | program should exit - which happens if Discord unexpectedly closes the
11 | socket during the crucial bootstrapping process.
12 |
13 | Additionally, it may also be raised if Discord responded with a special
14 | error code in the 4000-range - which signals that the connection absolutely
15 | cannot reconnect such as sending an improper token or intents.
16 | """
17 | pass
18 |
--------------------------------------------------------------------------------
/library/wumpy-gateway/wumpy/gateway/_utils.py:
--------------------------------------------------------------------------------
1 | import time
2 | from contextlib import asynccontextmanager
3 | from functools import partial
4 | from types import TracebackType
5 | from typing import (
6 | Any, AsyncContextManager, AsyncGenerator, Awaitable, Callable, Optional,
7 | Type
8 | )
9 |
10 | import anyio
11 | from discord_gateway import Opcode
12 | from typing_extensions import Protocol, Self
13 |
14 | __all__ = (
15 | 'GatewayLimiter',
16 | 'DefaultGatewayLimiter',
17 | )
18 |
19 |
20 | class GatewayLimiter(Protocol):
21 | """Interface for a gateway ratelimiter.
22 |
23 | This protocol is almost the same as the following:
24 |
25 | ```python
26 | AsynContextManager[Callable[
27 | [int], AsyncContextManager[object]
28 | ]]
29 | ```
30 |
31 | The difference is the fact that the async context manager must also be
32 | callable (and therefore it should return `self`).
33 | """
34 | async def __aenter__(self) -> object:
35 | ...
36 |
37 | async def __aexit__(
38 | self,
39 | exc_type: Optional[Type[BaseException]],
40 | exc_val: Optional[BaseException],
41 | exc_tb: Optional[TracebackType]
42 | ) -> Optional[bool]:
43 | ...
44 |
45 | def __call__(self, __opcode: Opcode) -> AsyncContextManager[object]:
46 | ...
47 |
48 |
49 | class DefaultGatewayLimiter:
50 | _lock: anyio.Lock
51 | _reset: Optional[float]
52 | _value: int
53 |
54 | RATE = 120 - 3 # We leave a margin of 3 heartbeats per minute
55 | PER = 60
56 |
57 | __slots__ = ('_lock', '_reset', '_value')
58 |
59 | def __init__(self) -> None:
60 | self._reset = None
61 | self._value = self.RATE
62 |
63 | # self._lock is set in __aenter__()
64 |
65 | @asynccontextmanager
66 | async def __call__(self, opcode: Opcode) -> AsyncGenerator[None, None]:
67 | # Heartbeats are not ratelimited and we have left a margin for them.
68 | if opcode is not Opcode.HEARTBEAT:
69 | async with self._lock:
70 | if self._reset is None or self._reset < time.perf_counter():
71 | self._reset = time.perf_counter() + self.PER
72 | self._value = self.RATE - 1
73 |
74 | elif self._value <= 0:
75 | await anyio.sleep(self._reset - time.perf_counter())
76 | self._reset = time.perf_counter() + self.PER
77 | self._value = self.RATE - 1
78 | else:
79 | self._value -= 1
80 |
81 | # Yield once we have released the lock, it is only held for the
82 | # case that we're sleeping, because if one tasks sleep then all
83 | # following tasks will also need to sleep.
84 | yield
85 |
86 | async def __aenter__(self) -> Self:
87 | self._lock = anyio.Lock()
88 |
89 | return self
90 |
91 | async def __aexit__(
92 | self,
93 | exc_type: Optional[Type[BaseException]],
94 | exc_val: Optional[BaseException],
95 | exc_tb: Optional[TracebackType]
96 | ) -> None:
97 | pass
98 |
--------------------------------------------------------------------------------
/library/wumpy-gateway/wumpy/gateway/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/library/wumpy-gateway/wumpy/gateway/py.typed
--------------------------------------------------------------------------------
/library/wumpy-interactions/README.md:
--------------------------------------------------------------------------------
1 | # Wumpy-interactions
2 |
3 | Support for HTTP interactions with the Discord API
4 |
5 | Refer to the [documentation](https://wumpy.rtfd.io) for more.
6 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "wumpy-interactions"
3 | version = "0.1.0"
4 | description = "Support for HTTP interactions with the Discord API"
5 | readme = {file = "README.md", content-type="text/markdown"}
6 |
7 | requires-python = ">=3.7"
8 |
9 | license = {text = "Dual-licensed under the Apache License 2.0 and the MIT License"}
10 | authors = [{name = "Bluenix", email = "bluenixdev@gmail.com"}]
11 |
12 | keywords = [
13 | "wumpy", "wumpus", "wrapper",
14 | "discord", "discord-api", "discord-bot", "discord-api-wrapper", "discord-interaction",
15 | "python-3"
16 | ]
17 | classifiers = [
18 | "Development Status :: 2 - Pre-Alpha",
19 | "Framework :: AnyIO",
20 | "Intended Audience :: Developers",
21 | "License :: OSI Approved :: MIT License",
22 | "License :: OSI Approved :: Apache Software License",
23 | "Natural Language :: English",
24 | "Operating System :: OS Independent",
25 | "Programming Language :: Python",
26 | "Programming Language :: Python :: 3",
27 | "Programming Language :: Python :: 3 :: Only",
28 | "Programming Language :: Python :: 3.7",
29 | "Programming Language :: Python :: 3.8",
30 | "Programming Language :: Python :: 3.9",
31 | "Programming Language :: Python :: 3.10",
32 | "Topic :: Internet",
33 | "Topic :: Internet :: WWW/HTTP",
34 | "Topic :: Software Development :: Libraries",
35 | "Topic :: Software Development :: Libraries :: Python Modules",
36 | "Typing :: Typed",
37 | ]
38 |
39 | dependencies = [
40 | "typing_extensions >= 4, <5",
41 | "anyio >= 3.3.4, <4",
42 | "discord-typings >= 0.4.0, <1",
43 | "pynacl >= 1, <2",
44 | "wumpy-models >= 0.1.0, <1"
45 | ]
46 |
47 | [project.optional-dependencies]
48 | sanic = ["sanic >= 21.3, <22.3"]
49 |
50 | [project.urls]
51 | Homepage = "https://github.com/wumpyproject/wumpy"
52 | Repository = "https://github.com/wumpyproject/wumpy/tree/main/library/wumpy-interactions"
53 | Documentation = "https://wumpy.rtfd.io"
54 |
55 | [build-system]
56 | requires = ["flit_core >=3.5, <4"]
57 | build-backend = "flit_core.buildapi"
58 |
59 | [tool.flit.module]
60 | # This is a subpackage under the wumpy namespace package,
61 | # we need to tell flit this otherwise it tries to make the
62 | # import wumpy-interactions rather than wumpy.interactions
63 | name = "wumpy.interactions"
64 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/__init__.py:
--------------------------------------------------------------------------------
1 | from ._app import (
2 | InteractionAppRequester,
3 | InteractionApp,
4 | get_app,
5 | )
6 | from ._compat import (
7 | Request,
8 | ASGIRequest,
9 | SanicRequest,
10 | )
11 | from ._errors import (
12 | CommandSetupError,
13 | )
14 | from ._middleware import (
15 | ASGIMiddleware,
16 | SanicMiddleware,
17 | )
18 | from ._models import (
19 | AutocompleteInteraction,
20 | Interaction,
21 | CommandInteraction,
22 | ComponentInteraction,
23 | )
24 | from ._utils import (
25 | DiscordRequestVerifier,
26 | )
27 | from .components import (
28 | ComponentHandler,
29 | )
30 | from .commands import (
31 | MiddlewareDecorator,
32 | CheckFailure,
33 | check,
34 | BucketType,
35 | max_concurrency,
36 | cooldown,
37 | ContextMenuCommand,
38 | MessageCommand,
39 | UserCommand,
40 | MiddlewareCallback,
41 | CommandMiddlewareMixin,
42 | CommandType,
43 | Option,
44 | option,
45 | OptionClass,
46 | group,
47 | command,
48 | CommandRegistrar,
49 | Command,
50 | SubcommandGroup,
51 | command_payload,
52 | )
53 |
54 | __all__ = (
55 | 'InteractionAppRequester',
56 | 'InteractionApp',
57 | 'get_app',
58 | 'Request',
59 | 'ASGIRequest',
60 | 'SanicRequest',
61 | 'CommandSetupError',
62 | 'ASGIMiddleware',
63 | 'SanicMiddleware',
64 | 'AutocompleteInteraction',
65 | 'Interaction',
66 | 'CommandInteraction',
67 | 'ComponentInteraction',
68 | 'DiscordRequestVerifier',
69 | 'ComponentHandler',
70 | 'MiddlewareDecorator',
71 | 'CheckFailure',
72 | 'check',
73 | 'BucketType',
74 | 'max_concurrency',
75 | 'cooldown',
76 | 'ContextMenuCommand',
77 | 'MessageCommand',
78 | 'UserCommand',
79 | 'MiddlewareCallback',
80 | 'CommandMiddlewareMixin',
81 | 'CommandType',
82 | 'Option',
83 | 'option',
84 | 'OptionClass',
85 | 'group',
86 | 'command',
87 | 'CommandRegistrar',
88 | 'Command',
89 | 'SubcommandGroup',
90 | 'command_payload',
91 | )
92 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/_compat.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import (
3 | TYPE_CHECKING, Any, Awaitable, Callable, Dict, Mapping, Optional, overload
4 | )
5 |
6 | from typing_extensions import Protocol
7 |
8 | try:
9 | from sanic import HTTPResponse # type: ignore
10 | SANIC_AVAILABLE = True
11 | except ImportError:
12 | SANIC_AVAILABLE = False
13 |
14 | if TYPE_CHECKING:
15 | from sanic import Request as OriginalSanicRequest
16 |
17 | __all__ = (
18 | 'Request',
19 | 'ASGIRequest',
20 | 'SanicRequest',
21 | )
22 |
23 |
24 | class Request(Protocol):
25 |
26 | @overload
27 | async def respond(
28 | self,
29 | data: Optional[Mapping[str, Any]] = None,
30 | *,
31 | status: int = 200,
32 | ) -> None:
33 | ...
34 |
35 | @overload
36 | async def respond(
37 | self,
38 | *,
39 | status: int = 200,
40 | body: Optional[bytes] = None,
41 | content_type: Optional[str] = None,
42 | ) -> None:
43 | ...
44 |
45 |
46 | class ASGIRequest(Request):
47 | def __init__(
48 | self,
49 | scope: Dict[str, Any],
50 | receive: Callable[[], Awaitable[Dict[str, Any]]],
51 | send: Callable[[Dict[str, Any]], Awaitable[None]]
52 | ) -> None:
53 | self._responded = False
54 |
55 | self.scope = scope
56 | self.receive = receive
57 | self.send = send
58 |
59 | async def respond(
60 | self,
61 | data: Optional[Mapping[str, Any]] = None,
62 | *,
63 | status: int = 200,
64 | body: Optional[bytes] = None,
65 | content_type: Optional[str] = None,
66 | ) -> None:
67 | if (
68 | data is None and body is None
69 | or data is not None and body is not None
70 | ):
71 | raise TypeError("'respond()' has to be called with one of 'data' and 'body'")
72 |
73 | if self._responded:
74 | raise RuntimeError('Response has already been sent')
75 |
76 | if data is not None:
77 | body = json.dumps(data).encode('utf-8')
78 | content_type = 'application/json'
79 |
80 | if content_type is None:
81 | content_type = 'text/plain'
82 |
83 | await self.send({
84 | 'type': 'http.response.start', 'status': status,
85 | 'headers': [(b'content-type', content_type.encode('utf-8'))]
86 | })
87 | await self.send({'type': 'http.response.body', 'body': body})
88 |
89 |
90 | class SanicRequest(Request):
91 | def __init__(self, request: 'OriginalSanicRequest') -> None:
92 | self._responded = False
93 |
94 | self.request = request
95 |
96 | async def respond(
97 | self,
98 | data: Optional[Mapping[str, Any]] = None,
99 | *,
100 | status: int = 200,
101 | body: Optional[bytes] = None,
102 | content_type: Optional[str] = None,
103 | ) -> None:
104 | if (
105 | data is None and body is None
106 | or data is not None and body is not None
107 | ):
108 | raise TypeError("'respond()' has to be called with one of 'data' and 'body'")
109 |
110 | if self._responded:
111 | raise RuntimeError('Response has already been sent')
112 |
113 | if data is not None:
114 | body = json.dumps(data).encode('utf-8')
115 | content_type = 'application/json'
116 |
117 | if content_type is None:
118 | content_type = 'text/plain'
119 |
120 | await self.request.respond(HTTPResponse(
121 | body, status=status, content_type=content_type
122 | ))
123 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/_errors.py:
--------------------------------------------------------------------------------
1 | __all__ = (
2 | 'CommandSetupError',
3 | )
4 |
5 |
6 | class CommandSetupError(Exception):
7 | """Raised when local command setup does not align with interactions.
8 |
9 | Examples of this is incorrect types of local options compared to what
10 | Discord sends, or subcommand-groups not receiving subcommands.
11 | """
12 | pass
13 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/_utils.py:
--------------------------------------------------------------------------------
1 | import functools
2 | from typing import Any, Callable, Dict, Optional, Union
3 |
4 | from nacl.exceptions import BadSignatureError
5 | from nacl.signing import VerifyKey
6 |
7 | __all__ = (
8 | 'DiscordRequestVerifier',
9 | )
10 |
11 |
12 | class DiscordRequestVerifier:
13 | """Thin wrapper over PyNaCl's Ed25519 verifier.
14 |
15 | This is designed to verify Discord interactions requests. See the
16 | documentation for middlewares for specific libraries wrapping this class.
17 | """
18 |
19 | def __init__(self, public_key: str) -> None:
20 | self._verifier = VerifyKey(bytes.fromhex(public_key))
21 |
22 | def verify(
23 | self,
24 | signature: str,
25 | timestamp: Union[str, bytes],
26 | body: Union[str, bytes, bytearray]
27 | ) -> bool:
28 | """Verify the signature of a request.
29 |
30 | Parameters:
31 | signature: The `X-Signature-Ed25519` header signature.
32 | timestamp: The `X-Signature-Timestamp` header value.
33 | body:
34 | The request body data. This can be either a string, bytes or
35 | bytearray (although they are all converted into `bytes`).
36 |
37 | Returns:
38 | Whether the signature is valid. If this returns `False` you should
39 | respond with a 401 Unauthorized.
40 | """
41 | if isinstance(timestamp, str):
42 | timestamp = timestamp.encode('utf-8')
43 |
44 | if isinstance(body, str):
45 | body = body.encode('utf-8')
46 |
47 | message = bytes(timestamp + body)
48 |
49 | try:
50 | self._verifier.verify(message, signature=bytes.fromhex(signature))
51 | return True
52 | except BadSignatureError:
53 | return False
54 |
55 |
56 | def _eval_annotations(obj: Callable) -> Dict[str, Any]:
57 | """Eval a callable's annotations.
58 |
59 | This is primarily a backport of Python 3.10's `get_annotations()`
60 | method implemented by Larry Hastings:
61 | https://github.com/python/cpython/commit/74613a46fc79cacc88d3eae4105b12691cd4ba20
62 |
63 | Parameters:
64 | obj: The received callable to evaluate
65 |
66 | Returns:
67 | A dictionary of parameter name to its annotation.
68 | """
69 | unwrapped = obj
70 | while True:
71 | if hasattr(unwrapped, '__wrapped__'):
72 | unwrapped = unwrapped.__wrapped__
73 | continue
74 | if isinstance(unwrapped, functools.partial):
75 | unwrapped = unwrapped.func
76 | continue
77 | break
78 |
79 | annotations = getattr(unwrapped, '__annotations__', None)
80 | eval_globals = getattr(unwrapped, '__globals__', None)
81 |
82 | if annotations is None or not annotations:
83 | return {}
84 |
85 | if not isinstance(annotations, dict):
86 | raise ValueError(f'{unwrapped!r}.__annotations__ is neither a dict nor None')
87 |
88 | try:
89 | return {
90 | key: value if not isinstance(value, str) else eval(value, eval_globals)
91 | for key, value in annotations.items()
92 | }
93 | except (NameError, SyntaxError) as e:
94 | raise ValueError(f'Could not evaluate the annotations of {unwrapped!r}') from e
95 |
96 |
97 | class State:
98 | """An object which allows setting arbitrary attributes."""
99 |
100 | __state: Dict[str, Any]
101 |
102 | __slots__ = ('__state',)
103 |
104 | def __init__(self, state: Optional[Dict[str, Any]] = None) -> None:
105 | super().__setattr__('__state', state.copy() if state is not None else {})
106 |
107 | def __setattr__(self, key: str, value: Any) -> None:
108 | self.__state[key] = value
109 |
110 | def __getattr__(self, key: str) -> Any:
111 | try:
112 | return self.__state[key]
113 | except KeyError:
114 | raise AttributeError(
115 | f'{self.__class__.__name__!r} object has no attribute {key!r}'
116 | ) from None
117 |
118 | def __delattr__(self, key: str) -> None:
119 | try:
120 | del self.__state[key]
121 | except KeyError:
122 | raise AttributeError(key) from None
123 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/commands/__init__.py:
--------------------------------------------------------------------------------
1 | from ._checks import (
2 | MiddlewareDecorator,
3 | CheckFailure,
4 | check,
5 | BucketType,
6 | max_concurrency,
7 | cooldown,
8 | )
9 | from ._context import (
10 | ContextMenuCommand,
11 | MessageCommand,
12 | UserCommand,
13 | )
14 | from ._middleware import (
15 | MiddlewareCallback,
16 | CommandMiddlewareMixin,
17 | )
18 | from ._option import (
19 | CommandType,
20 | Option,
21 | option,
22 | OptionClass,
23 | )
24 | from ._registrar import (
25 | group,
26 | command,
27 | CommandRegistrar,
28 | )
29 | from ._slash import (
30 | Command,
31 | SubcommandGroup,
32 | command_payload,
33 | )
34 |
35 | __all__ = (
36 | 'MiddlewareDecorator',
37 | 'CheckFailure',
38 | 'check',
39 | 'BucketType',
40 | 'max_concurrency',
41 | 'cooldown',
42 | 'ContextMenuCommand',
43 | 'MessageCommand',
44 | 'UserCommand',
45 | 'MiddlewareCallback',
46 | 'CommandMiddlewareMixin',
47 | 'CommandType',
48 | 'Option',
49 | 'option',
50 | 'OptionClass',
51 | 'group',
52 | 'command',
53 | 'CommandRegistrar',
54 | 'Command',
55 | 'SubcommandGroup',
56 | 'command_payload',
57 | )
58 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/commands/_base.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from functools import update_wrapper
3 | from typing import Any, Awaitable, Callable, Generic, TypeVar
4 |
5 | from typing_extensions import ParamSpec
6 |
7 | from .._utils import _eval_annotations
8 |
9 | __all__ = (
10 |
11 | )
12 |
13 |
14 | P = ParamSpec('P')
15 | RT = TypeVar('RT', covariant=True)
16 |
17 |
18 | Callback = Callable[P, Awaitable[RT]]
19 |
20 |
21 | class CommandCallback(Generic[P, RT]):
22 | """Asynchronous callback wrapped with processing.
23 |
24 | This class is used to wrap, store, and process a callback.
25 |
26 | To use it, subclass and override the methods below. They are marked
27 | internal as to not leak out to the user since they are only meant to be
28 | called by this class.
29 | """
30 |
31 | _callback: Callback[P, RT]
32 |
33 | def __init__(self, callback: Callback[P, RT]) -> None:
34 | super().__init__()
35 |
36 | self.callback = callback
37 |
38 | async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> RT:
39 | return await self.callback(*args, **kwargs)
40 |
41 | @property
42 | def callback(self) -> Callback[P, RT]:
43 | """The callback this object wraps."""
44 | return self._callback
45 |
46 | @callback.setter
47 | def callback(self, function: Callback[P, RT]) -> None:
48 | self._callback = function
49 | self._process_callback(function)
50 |
51 | def _process_callback(self, callback: Callback[P, RT]) -> None:
52 | """Process a callback being set.
53 |
54 | This is called first out of all available methods and is used to call
55 | the other methods by default. If this method is overriden it is
56 | important to call the super method.
57 |
58 | Parameters:
59 | callback: The callback being set.
60 | """
61 | update_wrapper(self, callback)
62 |
63 | signature = inspect.signature(callback)
64 | annotations = _eval_annotations(callback)
65 |
66 | if not signature.parameters:
67 | self._process_no_params(signature)
68 | else:
69 | for i, param in enumerate(signature.parameters.values()):
70 | annotation = annotations.get(param.name, param.empty)
71 |
72 | self._process_param(i, param.replace(annotation=annotation))
73 |
74 | # We piggyback on inspect's Parameter.empty sentinel value
75 | return_type = annotations.get('return', inspect.Parameter.empty)
76 |
77 | if return_type is not inspect.Parameter.empty:
78 | self._process_return_type(return_type)
79 |
80 | def _process_param(self, index: int, param: inspect.Parameter) -> None:
81 | """Process a parameter of the set callback.
82 |
83 | This method is called for each parameter of the callback when being
84 | set, allowing for subclasses to hook into the process.
85 |
86 | Parameters:
87 | index: The index of the parameter.
88 | param:
89 | The parameter of the callback. Annotations have been resolved
90 | and replaced with the actual type.
91 | """
92 | ...
93 |
94 | def _process_no_params(self, signature: inspect.Signature) -> None:
95 | """Process a callback having no signature.
96 |
97 | This method is only called if `_process_param()` won't be called.
98 |
99 | Parameters:
100 | signature: The signature of the callback.
101 | """
102 | ...
103 |
104 | def _process_return_type(self, annotation: Any) -> None:
105 | """Process the extracted return type of the callback.
106 |
107 | This is only called if the callback has a return type. Consider
108 | overriding `_process_callback()` and processing the callback
109 | *after the `super()` call* to do more processing at the end.
110 |
111 | Parameters:
112 | annotation: The annotation of the function's return type.
113 | """
114 | ...
115 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/commands/_checks.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from typing import Awaitable, Callable, TypeVar, cast
3 | from typing_extensions import ParamSpec
4 |
5 | from .._models import CommandInteraction
6 |
7 | __all__ = (
8 | 'middleware',
9 | 'CheckFailure',
10 | 'check',
11 | )
12 |
13 |
14 | P = ParamSpec('P')
15 | RT = TypeVar('RT')
16 |
17 |
18 | def middleware(
19 | func: Callable[[CommandInteraction], Awaitable[object]],
20 | ) -> Callable[[Callable[P, Awaitable[RT]]], Callable[P, Awaitable[RT]]]:
21 | """Helper-decorator to apply a function to wrap a callback.
22 |
23 | The benefit of using this over a custom decorator is that it does not
24 | interfere with the command decorator.
25 |
26 | Parameters:
27 | func:
28 | Function to call before the final callback, which takes the
29 | interaction. The return type is ignored.
30 |
31 | Returns:
32 | A decorator which wraps the command callback.
33 | """
34 | def middleware_decorator(
35 | callback: Callable[P, Awaitable[RT]]
36 | ) -> Callable[P, Awaitable[RT]]:
37 | @wraps(callback)
38 | async def callback_wrapper(*args: P.args, **kwargs: P.kwargs) -> RT:
39 | await func(cast(CommandInteraction, args[0]))
40 | return await callback(*args, **kwargs)
41 | return callback_wrapper
42 | return middleware_decorator
43 |
44 |
45 | class CheckFailure(Exception):
46 | """Raised when a check for an application command fails.
47 |
48 | Checks are encouraged to raise their own subclass of this exception, but it
49 | will automatically be raised if the check only returned a falsely value.
50 | """
51 |
52 | interaction: CommandInteraction
53 | predicate: Callable[[CommandInteraction], Awaitable[bool]]
54 |
55 | def __init__(
56 | self,
57 | interaction: CommandInteraction,
58 | predicate: Callable[[CommandInteraction], Awaitable[bool]]
59 | ) -> None:
60 | super().__init__(f'Check {self.predicate} failed for interaction {interaction}')
61 |
62 | self.interaction = interaction
63 | self.predicate = predicate
64 |
65 |
66 | def check(
67 | predicate: Callable[[CommandInteraction], Awaitable[bool]],
68 | ) -> Callable[[Callable[P, Awaitable[RT]]], Callable[P, Awaitable[RT]]]:
69 | """Create a check for an application command.
70 |
71 | This is a helper decorator which only executes the command if the passed
72 | predicate returns a truthy value. The benefit of using this over a custom
73 | decorator is that it does not interfere with the command decorator.
74 |
75 | Examples:
76 |
77 | ```python
78 | from typing import Callable
79 |
80 | from wumpy import interactions
81 | from wumpy.interactions import InteractionApp, CommandInteraction
82 |
83 |
84 | async def on_version(v: int) -> Callable[[CommandInteraction], bool]:
85 | async def predicate(interaction: CommandInteraction) -> bool:
86 | return interaction.version == v
87 | return predicate
88 |
89 |
90 | app = InteractionApp(...)
91 |
92 |
93 | @app.command()
94 | @interactions.check(on_version(1))
95 | async def ping(interaction: CommandInteraction) -> None:
96 | \"\"\"Pong!\"\"\"
97 | await interaction.respond('Pong!')
98 | ```
99 |
100 | Parameters:
101 | predicate: A callable that takes the interaction and returns a boolean.
102 |
103 | Returns:
104 | A decorator to apply to the application command.
105 | """
106 | # While these functions are usually named 'decorator' and 'inner' or
107 | # 'wrapper', by using more descriptive names their repr:s (which show up
108 | # when printed) become more useful when debugging
109 | def check_decorator(callback: Callable[P, Awaitable[RT]]) -> Callable[P, Awaitable[RT]]:
110 | @wraps(callback)
111 | async def check_command_wrapper(*args: P.args, **kwargs: P.kwargs) -> RT:
112 | inter = cast(CommandInteraction, args[0])
113 |
114 | if await predicate(inter) is False:
115 | raise CheckFailure(inter, predicate)
116 |
117 | return await callback(*args, **kwargs)
118 | return check_command_wrapper
119 | return check_decorator
120 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/components/__init__.py:
--------------------------------------------------------------------------------
1 | from ._handler import (
2 | ComponentHandler,
3 | )
4 |
5 | __all__ = (
6 | 'ComponentHandler',
7 | )
8 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/components/_handler.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import Any, Callable, Coroutine, List, Tuple, Union
3 |
4 | import anyio
5 |
6 | from .._models import ComponentInteraction
7 |
8 | __all__ = (
9 | 'ComponentHandler',
10 | )
11 |
12 |
13 | ComponentCallback = Callable[
14 | [ComponentInteraction, 're.Match[str]'],
15 | Coroutine[Any, Any, object]
16 | ]
17 |
18 |
19 | class ComponentHandler:
20 | """Handler for components, dispatching waiting components.
21 |
22 | This is a mixin class keeping track of handlers for components setup.
23 | """
24 |
25 | _regex_components: List[Tuple['re.Pattern[str]', ComponentCallback]]
26 |
27 | def __init__(self, *args: Any, **kwargs: Any) -> None:
28 | super().__init__(*args, **kwargs)
29 |
30 | self._regex_components = []
31 |
32 | async def invoke_component(self, interaction: ComponentInteraction) -> None:
33 | for pattern, callback in self._regex_components:
34 | match = pattern.match(interaction.custom_id)
35 | if match:
36 | await callback(interaction, match)
37 | return
38 |
39 | def add_component(self, pattern: 're.Pattern[str]', func: ComponentCallback) -> None:
40 | """Add a callback to be dispatched when the pattern is matched.
41 |
42 | Parameters:
43 | pattern: The compiled regex pattern to match custom IDs with.
44 | func: The callback to be called when the pattern is matched.
45 | """
46 | self._regex_components.append((pattern, func))
47 |
48 | def remove_component(self, pattern: 're.Pattern[str]', func: ComponentCallback) -> None:
49 | """Remove a callback from the list of components.
50 |
51 | Parameters:
52 | pattern: The regex pattern used to add the callback.
53 | func: The callback to be removed.
54 |
55 | Raises:
56 | ValueError: The callback was not found.
57 | """
58 | try:
59 | self._regex_components.remove((pattern, func))
60 | except ValueError:
61 | raise ValueError(f'Callback {func} with pattern {pattern} not found.') from None
62 |
63 | def component(
64 | self,
65 | pattern: Union[str, 're.Pattern[str]']
66 | ) -> Callable[[ComponentCallback], ComponentCallback]:
67 | """Add a callback to be dispatched when the custom ID is matched.
68 |
69 | Examples:
70 |
71 | ```python
72 | @app.component(r'upvote-(?P)')
73 | async def process_upvote(interaction: ComponentInteraction, match: re.Match):
74 | message_id = match.group('message_id')
75 | await interaction.reply(
76 | f'You upvoted message {message_id}',
77 | ephemeral=True
78 | )
79 | ```
80 |
81 | Parameters:
82 | pattern: The regex pattern to match custom IDs with.
83 |
84 | Returns:
85 | A decorator that adds the callback to the list of components.
86 | """
87 | def decorator(func: ComponentCallback) -> ComponentCallback:
88 | nonlocal pattern
89 |
90 | if isinstance(pattern, str):
91 | pattern = re.compile(pattern)
92 |
93 | self.add_component(pattern, func)
94 | return func
95 | return decorator
96 |
--------------------------------------------------------------------------------
/library/wumpy-interactions/wumpy/interactions/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/library/wumpy-interactions/wumpy/interactions/py.typed
--------------------------------------------------------------------------------
/library/wumpy-models/README.md:
--------------------------------------------------------------------------------
1 | # Wumpy-models
2 |
3 | Fully typed, memory efficient Discord object representations.
4 |
5 | ```python
6 | from wumpy.models import User
7 |
8 |
9 | user = User.from_data({ ... })
10 |
11 | print(f'{user.name}#{user.discriminator}')
12 | ```
13 |
--------------------------------------------------------------------------------
/library/wumpy-models/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "wumpy-models"
3 | version = "0.1.0"
4 | description = "Fully typed, memory efficient Discord object representations"
5 | readme = {file = "README.md", content-type="text/markdown"}
6 |
7 | requires-python = ">=3.7"
8 |
9 | license = {text = "Dual-licensed under the Apache License 2.0 and the MIT License"}
10 | authors = [{name = "Bluenix", email = "bluenixdev@gmail.com"}]
11 |
12 | keywords = [
13 | "wumpy", "wumpus", "wrapper",
14 | "discord", "discord-api", "discord-bot", "discord-api-wrapper",
15 | "python-3"
16 | ]
17 | classifiers = [
18 | "Development Status :: 2 - Pre-Alpha",
19 | "Framework :: AnyIO",
20 | "Intended Audience :: Developers",
21 | "License :: OSI Approved :: MIT License",
22 | "License :: OSI Approved :: Apache Software License",
23 | "Natural Language :: English",
24 | "Operating System :: OS Independent",
25 | "Programming Language :: Python",
26 | "Programming Language :: Python :: 3",
27 | "Programming Language :: Python :: 3 :: Only",
28 | "Programming Language :: Python :: 3.7",
29 | "Programming Language :: Python :: 3.8",
30 | "Programming Language :: Python :: 3.9",
31 | "Programming Language :: Python :: 3.10",
32 | "Topic :: Internet",
33 | "Topic :: Internet :: WWW/HTTP",
34 | "Topic :: Software Development :: Libraries",
35 | "Topic :: Software Development :: Libraries :: Python Modules",
36 | "Typing :: Typed",
37 | ]
38 |
39 | dependencies = [
40 | "typing_extensions >= 4, <5",
41 | "discord-typings >= 0.4.0, <1",
42 | "attrs >= 21.3, <= 24",
43 | ]
44 |
45 | [project.urls]
46 | Homepage = "https://github.com/wumpyproject/wumpy"
47 | Repository = "https://github.com/wumpyproject/wumpy/tree/main/library/wumpy-models"
48 | Documentation = "https://wumpy.rtfd.io"
49 |
50 | [build-system]
51 | requires = ["flit_core >=3.5, <4"]
52 | build-backend = "flit_core.buildapi"
53 |
54 | [tool.flit.module]
55 | # This is a subpackage under the wumpy namespace package,
56 | # we need to tell flit this otherwise it tries to make the
57 | # import wumpy-models rather than wumpy.models
58 | name = "wumpy.models"
59 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/__init__.py:
--------------------------------------------------------------------------------
1 | from ._raw import (
2 | PartialChannel,
3 | ChannelMention,
4 | InteractionChannel,
5 | RawDMChannel,
6 | RawTextChannel,
7 | RawThreadMember,
8 | RawThread,
9 | RawVoiceChannel,
10 | RawCategory,
11 | ActionRow,
12 | Button,
13 | LinkButton,
14 | SelectMenu,
15 | SelectMenuOption,
16 | TextInput,
17 | component_data,
18 | EmbedThumbnail,
19 | EmbedImage,
20 | EmbedFooter,
21 | EmbedField,
22 | EmbedAuthor,
23 | Embed,
24 | EmbedBuilder,
25 | embed_data,
26 | RawEmoji,
27 | RawMessageReaction,
28 | ApplicationFlags,
29 | Intents,
30 | MessageFlags,
31 | UserFlags,
32 | RawGuild,
33 | IntegrationExpire,
34 | IntegrationType,
35 | IntegrationAccount,
36 | RawIntegrationApplication,
37 | RawBotIntegration,
38 | RawStreamIntegration,
39 | InteractionType,
40 | ComponentType,
41 | ApplicationCommandOption,
42 | RawResolvedInteractionData,
43 | CommandInteractionOption,
44 | RawInteraction,
45 | RawAutocompleteInteraction,
46 | RawCommandInteraction,
47 | RawComponentInteraction,
48 | SelectInteractionValue,
49 | RawInvite,
50 | RawMember,
51 | RawInteractionMember,
52 | AllowedMentions,
53 | RawAttachment,
54 | RawMessageMentions,
55 | MessageType,
56 | RawMessage,
57 | Permissions,
58 | PermissionTarget,
59 | PermissionOverwrite,
60 | RoleTags,
61 | RawRole,
62 | StickerType,
63 | StickerFormatType,
64 | RawStickerItem,
65 | RawSticker,
66 | RawUser,
67 | RawBotUser,
68 | )
69 | from ._stateful import (
70 | Asset,
71 | DMChannel,
72 | TextChannel,
73 | ThreadMember,
74 | Thread,
75 | VoiceChannel,
76 | Category,
77 | Emoji,
78 | MessageReaction,
79 | Guild,
80 | IntegrationApplication,
81 | BotIntegration,
82 | StreamIntegration,
83 | ResolvedInteractionData,
84 | Interaction,
85 | AutocompleteInteraction,
86 | CommandInteraction,
87 | ComponentInteraction,
88 | Invite,
89 | Member,
90 | InteractionMember,
91 | Attachment,
92 | MessageMentions,
93 | Message,
94 | Role,
95 | StickerItem,
96 | Sticker,
97 | User,
98 | BotUser,
99 | )
100 | from ._utils import (
101 | Model,
102 | Snowflake,
103 | )
104 |
105 |
106 | __all__ = (
107 | 'PartialChannel',
108 | 'ChannelMention',
109 | 'InteractionChannel',
110 | 'RawDMChannel',
111 | 'RawTextChannel',
112 | 'RawThreadMember',
113 | 'RawThread',
114 | 'RawVoiceChannel',
115 | 'RawCategory',
116 | 'ActionRow',
117 | 'Button',
118 | 'LinkButton',
119 | 'SelectMenu',
120 | 'SelectMenuOption',
121 | 'TextInput',
122 | 'component_data',
123 | 'EmbedThumbnail',
124 | 'EmbedImage',
125 | 'EmbedFooter',
126 | 'EmbedField',
127 | 'EmbedAuthor',
128 | 'Embed',
129 | 'EmbedBuilder',
130 | 'embed_data',
131 | 'RawEmoji',
132 | 'RawMessageReaction',
133 | 'ApplicationFlags',
134 | 'Intents',
135 | 'MessageFlags',
136 | 'UserFlags',
137 | 'RawGuild',
138 | 'IntegrationExpire',
139 | 'IntegrationType',
140 | 'IntegrationAccount',
141 | 'RawIntegrationApplication',
142 | 'RawBotIntegration',
143 | 'RawStreamIntegration',
144 | 'InteractionType',
145 | 'ComponentType',
146 | 'ApplicationCommandOption',
147 | 'RawResolvedInteractionData',
148 | 'CommandInteractionOption',
149 | 'RawInteraction',
150 | 'RawAutocompleteInteraction',
151 | 'RawCommandInteraction',
152 | 'RawComponentInteraction',
153 | 'SelectInteractionValue',
154 | 'RawInvite',
155 | 'RawMember',
156 | 'RawInteractionMember',
157 | 'AllowedMentions',
158 | 'RawAttachment',
159 | 'RawMessageMentions',
160 | 'MessageType',
161 | 'RawMessage',
162 | 'Permissions',
163 | 'PermissionTarget',
164 | 'PermissionOverwrite',
165 | 'RoleTags',
166 | 'RawRole',
167 | 'StickerType',
168 | 'StickerFormatType',
169 | 'RawStickerItem',
170 | 'RawSticker',
171 | 'RawUser',
172 | 'RawBotUser',
173 | 'Asset',
174 | 'DMChannel',
175 | 'TextChannel',
176 | 'ThreadMember',
177 | 'Thread',
178 | 'VoiceChannel',
179 | 'Category',
180 | 'Emoji',
181 | 'MessageReaction',
182 | 'Guild',
183 | 'IntegrationApplication',
184 | 'BotIntegration',
185 | 'StreamIntegration',
186 | 'ResolvedInteractionData',
187 | 'Interaction',
188 | 'AutocompleteInteraction',
189 | 'CommandInteraction',
190 | 'ComponentInteraction',
191 | 'Invite',
192 | 'Member',
193 | 'InteractionMember',
194 | 'Attachment',
195 | 'MessageMentions',
196 | 'Message',
197 | 'Role',
198 | 'StickerItem',
199 | 'Sticker',
200 | 'User',
201 | 'BotUser',
202 | 'Model',
203 | 'Snowflake',
204 | )
205 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_raw/__init__.py:
--------------------------------------------------------------------------------
1 | from ._channels import (
2 | PartialChannel,
3 | ChannelMention,
4 | InteractionChannel,
5 | RawDMChannel,
6 | RawTextChannel,
7 | RawThreadMember,
8 | RawThread,
9 | RawVoiceChannel,
10 | RawCategory,
11 | )
12 | from ._components import (
13 | ActionRow,
14 | Button,
15 | LinkButton,
16 | SelectMenu,
17 | SelectMenuOption,
18 | TextInput,
19 | component_data,
20 | )
21 | from ._embed import (
22 | EmbedThumbnail,
23 | EmbedImage,
24 | EmbedFooter,
25 | EmbedField,
26 | EmbedAuthor,
27 | Embed,
28 | EmbedBuilder,
29 | embed_data,
30 | )
31 | from ._emoji import (
32 | RawEmoji,
33 | RawMessageReaction,
34 | )
35 | from ._flags import (
36 | ApplicationFlags,
37 | Intents,
38 | MessageFlags,
39 | UserFlags,
40 | )
41 | from ._guild import (
42 | RawGuild,
43 | )
44 | from ._integrations import (
45 | IntegrationAccount,
46 | RawIntegrationApplication,
47 | RawBotIntegration,
48 | RawStreamIntegration,
49 | )
50 | from ._interactions import (
51 | RawResolvedInteractionData,
52 | CommandInteractionOption,
53 | RawInteraction,
54 | RawAutocompleteInteraction,
55 | RawCommandInteraction,
56 | RawComponentInteraction,
57 | SelectInteractionValue,
58 | )
59 | from ._invite import (
60 | RawInvite,
61 | )
62 | from ._member import (
63 | RawMember,
64 | RawInteractionMember,
65 | )
66 | from ._message import (
67 | AllowedMentions,
68 | RawAttachment,
69 | RawMessageMentions,
70 | RawMessage,
71 | )
72 | from ._permissions import (
73 | Permissions,
74 | PermissionOverwrite,
75 | )
76 | from ._role import (
77 | RoleTags,
78 | RawRole,
79 | )
80 | from ._sticker import (
81 | RawStickerItem,
82 | RawSticker,
83 | )
84 | from ._user import (
85 | RawUser,
86 | RawBotUser,
87 | )
88 |
89 | __all__ = (
90 | 'PartialChannel',
91 | 'ChannelMention',
92 | 'InteractionChannel',
93 | 'RawDMChannel',
94 | 'RawTextChannel',
95 | 'RawThreadMember',
96 | 'RawThread',
97 | 'RawVoiceChannel',
98 | 'RawCategory',
99 | 'ActionRow',
100 | 'Button',
101 | 'LinkButton',
102 | 'SelectMenu',
103 | 'SelectMenuOption',
104 | 'TextInput',
105 | 'component_data',
106 | 'EmbedThumbnail',
107 | 'EmbedImage',
108 | 'EmbedFooter',
109 | 'EmbedField',
110 | 'EmbedAuthor',
111 | 'Embed',
112 | 'EmbedBuilder',
113 | 'embed_data',
114 | 'RawEmoji',
115 | 'RawMessageReaction',
116 | 'ApplicationFlags',
117 | 'Intents',
118 | 'MessageFlags',
119 | 'UserFlags',
120 | 'RawGuild',
121 | 'IntegrationAccount',
122 | 'RawIntegrationApplication',
123 | 'RawBotIntegration',
124 | 'RawStreamIntegration',
125 | 'RawResolvedInteractionData',
126 | 'CommandInteractionOption',
127 | 'RawInteraction',
128 | 'RawAutocompleteInteraction',
129 | 'RawCommandInteraction',
130 | 'RawComponentInteraction',
131 | 'SelectInteractionValue',
132 | 'RawInvite',
133 | 'RawMember',
134 | 'RawInteractionMember',
135 | 'AllowedMentions',
136 | 'RawAttachment',
137 | 'RawMessageMentions',
138 | 'RawMessage',
139 | 'Permissions',
140 | 'PermissionOverwrite',
141 | 'RoleTags',
142 | 'RawRole',
143 | 'RawStickerItem',
144 | 'RawSticker',
145 | 'RawUser',
146 | 'RawBotUser',
147 | )
148 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_raw/_emoji.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import ClassVar, Optional, Tuple
3 |
4 | import attrs
5 | from discord_typings import EmojiData, MessageReactionData
6 | from typing_extensions import Self
7 |
8 | from .._utils import DISCORD_EPOCH, Model, Snowflake
9 | from ._user import RawUser
10 |
11 | __all__ = (
12 | 'RawEmoji',
13 | 'RawMessageReaction',
14 | )
15 |
16 |
17 | @attrs.define(eq=False, frozen=True, kw_only=True)
18 | class RawEmoji(Model):
19 |
20 | name: str
21 |
22 | roles: Tuple[Snowflake, ...] = ()
23 | user: Optional[RawUser] = None
24 |
25 | require_colons: bool = True
26 | managed: bool = False
27 | animated: bool = False
28 | available: bool = True
29 |
30 | REGEX: ClassVar['re.Pattern[str]'] = re.compile(
31 | r'(?Pa)?:?(?P[A-Za-z0-9\_]+):(?P[0-9]{13,20})>?'
32 | )
33 |
34 | @classmethod
35 | def from_data(cls, data: EmojiData) -> Self:
36 | user = data.get('user')
37 | if user is not None:
38 | user = RawUser.from_data(user)
39 |
40 | return cls(
41 | id=int(data['id'] or DISCORD_EPOCH << 22),
42 | name=data.get('name') or '_',
43 | roles=tuple(Snowflake(int(s)) for s in data.get('roles', [])),
44 | user=user,
45 | require_colons=data.get('require_colons', True),
46 | managed=data.get('managed', False),
47 | animated=data.get('animated', False),
48 | available=data.get('available', True),
49 | )
50 |
51 | @classmethod
52 | def from_string(cls, value: str) -> Self:
53 | match = cls.REGEX.match(value)
54 | if match:
55 | return cls(
56 | id=int(match.group('id')),
57 | name=match.group('name'),
58 | roles=(),
59 | user=None,
60 |
61 | require_colons=True,
62 | managed=False,
63 | animated=bool(match.group('animated')),
64 | available=True
65 | )
66 |
67 | # The regex didn't match, we'll just have to assume the user passed a
68 | # built-in unicode emoji
69 | return cls(
70 | id=DISCORD_EPOCH << 22,
71 | name=value,
72 | )
73 |
74 |
75 | @attrs.define(frozen=True)
76 | class RawMessageReaction:
77 | count: int
78 | emoji: RawEmoji
79 |
80 | me: bool = attrs.field(default=False, kw_only=True)
81 |
82 | @classmethod
83 | def from_data(cls, data: MessageReactionData) -> Self:
84 | return cls(
85 | count=data['count'],
86 | emoji=RawEmoji.from_data(data['emoji']),
87 |
88 | me=data['me'],
89 | )
90 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_raw/_guild.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional, Set, Union
2 |
3 | import attrs
4 | from discord_typings import GuildCreateData, GuildData, GuildUpdateData
5 | from typing_extensions import Literal, Self
6 |
7 | from .._utils import Model, Snowflake, _get_as_snowflake
8 |
9 | __all__ = (
10 | 'RawGuild',
11 | )
12 |
13 |
14 | @attrs.define(eq=False, frozen=True, kw_only=True)
15 | class RawGuild(Model):
16 | name: str
17 | owner_id: Snowflake
18 |
19 | icon: Optional[str]
20 | splash: Optional[str]
21 | discovery_splash: Optional[str]
22 |
23 | features: Set[str]
24 |
25 | afk_timeout: int
26 | afk_channel_id: Optional[Snowflake]
27 |
28 | verification_level: Literal[0, 1, 2, 3, 4]
29 | default_notifications: Literal[0, 1]
30 | explicit_content_filter: Literal[0, 1, 2]
31 | mfa_level: Literal[0, 1]
32 | premium_tier: Literal[0, 1, 2, 3]
33 | nsfw_level: Literal[0, 1, 2, 3]
34 |
35 | # This is an exception to the immutabily rule, as we want to keep the list
36 | # of these IDs up-to-date without needing to somehow re-create the entire
37 | # guild object each time.
38 |
39 | roles: List[Snowflake]
40 | emojis: List[Snowflake]
41 | members: List[Snowflake]
42 | channels: List[Snowflake]
43 |
44 | @classmethod
45 | def from_data(cls, data: Union[GuildData, GuildCreateData, GuildUpdateData]) -> Self:
46 | return cls(
47 | id=int(data['id']),
48 | name=data['name'],
49 | owner_id=Snowflake(int(data['owner_id'])),
50 |
51 | icon=data.get('icon'),
52 | splash=data.get('splash'),
53 | discovery_splash=data.get('discovery_splash'),
54 |
55 | features=set(data['features']),
56 |
57 | afk_timeout=data['afk_timeout'],
58 | afk_channel_id=_get_as_snowflake(data, 'afk_channel_id'),
59 |
60 | verification_level=data['verification_level'],
61 | default_notifications=data['default_message_notifications'],
62 | explicit_content_filter=data['explicit_content_filter'],
63 | mfa_level=data['mfa_level'],
64 | premium_tier=data['premium_tier'],
65 | nsfw_level=data['nsfw_level'],
66 |
67 | roles=[Snowflake(int(item['id'])) for item in data['roles']],
68 | emojis=[
69 | Snowflake(int(item['id'])) for item in data['emojis']
70 | if item['id'] is not None
71 | ],
72 | channels=[Snowflake(int(item['id'])) for item in data.get('channels', [])],
73 | members=[]
74 | )
75 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_raw/_integrations.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional
3 |
4 | import attrs
5 | from discord_typings import (
6 | DiscordIntegrationData, IntegrationAccountData, IntegrationApplicationData,
7 | StreamingIntegrationData
8 | )
9 | from typing_extensions import Self
10 |
11 | from .._utils import Model, Snowflake, _get_as_snowflake
12 | from ._user import RawUser
13 |
14 | __all__ = (
15 | 'IntegrationAccount',
16 | 'RawIntegrationApplication',
17 | 'RawBotIntegration',
18 | 'RawStreamIntegration',
19 | )
20 |
21 |
22 | @attrs.define(frozen=True)
23 | class IntegrationAccount:
24 | """Information about the account associated with an integration.
25 |
26 | Attributes:
27 | id: ID of the account.
28 | name: The name of the account.
29 | """
30 |
31 | id: str
32 | name: str
33 |
34 | @classmethod
35 | def from_data(cls, data: IntegrationAccountData) -> Self:
36 | return cls(data['id'], data['name'])
37 |
38 |
39 | @attrs.define(eq=False, frozen=True, kw_only=True)
40 | class RawIntegrationApplication(Model):
41 | """Information about a bot/OAuth2 application.
42 |
43 | Attributes:
44 | name: The name of the application.
45 | icon: Icon hash of the application.
46 | description: Description of the application.
47 | summary: A summary of the application.
48 | user: The user object associated with the application.
49 | """
50 |
51 | name: str
52 | icon: Optional[str]
53 | description: str
54 | summary: str
55 | user: Optional[RawUser] = None
56 |
57 | @classmethod
58 | def from_data(cls, data: IntegrationApplicationData) -> Self:
59 | user = data.get('bot')
60 | if user is not None:
61 | user = RawUser.from_data(user)
62 |
63 | return cls(
64 | id=int(data['id']),
65 | name=data['name'],
66 | icon=data.get('icon'),
67 | description=data['description'],
68 | summary=data['summary'],
69 | user=user
70 | )
71 |
72 |
73 | @attrs.define(eq=False, frozen=True, kw_only=True)
74 | class RawBotIntegration(Model):
75 | """Representation of a bot integration in a guild.
76 |
77 | Attributes:
78 | application: The application associated with the integration.
79 | """
80 |
81 | name: str
82 | type: str
83 | enabled: bool
84 | account: IntegrationAccount
85 |
86 | application: Optional[RawIntegrationApplication] = None
87 |
88 | @property
89 | def user(self) -> Optional[RawUser]:
90 | """The user associated with the integration."""
91 | return None if self.application is None else self.application.user
92 |
93 | @classmethod
94 | def from_data(cls, data: DiscordIntegrationData) -> Self:
95 | application = data.get('application')
96 | if application is not None:
97 | application = RawIntegrationApplication.from_data(application)
98 |
99 | return cls(
100 | id=int(data['id']),
101 | name=data['name'],
102 | type=data['type'],
103 | enabled=data['enabled'],
104 | account=IntegrationAccount.from_data(data['account']),
105 | application=application
106 | )
107 |
108 |
109 | @attrs.define(eq=False, frozen=True, kw_only=True)
110 | class RawStreamIntegration(Model):
111 | """Representation of a guild integration for Twitch or YouTube.
112 |
113 | Attributes:
114 | syncing: Whether the integration is synchronizing.
115 | role_id: The ID of the role associated with the integration.
116 | enable_emoticons:
117 | Whether emoticons should be synchronized (currently only for
118 | twitch integrations).
119 | expire_behavior: The behavior when the integration expires.
120 | expire_grace_period: The grace period before the integration expires.
121 | user: The user associated with the integration.
122 | synced_at: When the integration was last synchronized.
123 | subscriber_count: How many subscribers the integration has.
124 | revoked: Whether the integration has been revoked.
125 | """
126 |
127 | name: str
128 | type: str
129 | enabled: bool
130 | account: IntegrationAccount
131 |
132 | syncing: bool
133 | role_id: Optional[Snowflake]
134 | enable_emoticons: bool
135 | expire_behavior: int
136 | expire_grace_period: int
137 | user: RawUser
138 | synced_at: datetime
139 | subscriber_count: int
140 | revoked: bool
141 |
142 | @classmethod
143 | def from_data(cls, data: StreamingIntegrationData) -> Self:
144 | return cls(
145 | id=int(data['id']),
146 | name=data['name'],
147 | type=data['type'],
148 | enabled=data['enabled'],
149 | account=IntegrationAccount.from_data(data['account']),
150 | syncing=data['syncing'],
151 | role_id=_get_as_snowflake(data, 'role_id'),
152 | enable_emoticons=data['enable_emoticons'],
153 | expire_behavior=data['expire_behavior'],
154 | expire_grace_period=data['expire_grace_period'],
155 | user=RawUser.from_data(data['user']),
156 | synced_at=datetime.fromisoformat(data['synced_at']),
157 | subscriber_count=data['subscriber_count'],
158 | revoked=data['revoked'],
159 | )
160 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_raw/_invite.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 | from typing import Optional, Union
3 |
4 | import attrs
5 | from discord_typings import InviteCreateData, InviteData
6 | from typing_extensions import Self
7 |
8 | from ._channels import PartialChannel
9 | from ._user import RawUser
10 |
11 | __all__ = (
12 | 'RawInvite',
13 | )
14 |
15 |
16 | @attrs.define(frozen=True)
17 | class RawInvite:
18 | """Representation of a Discord invite."""
19 |
20 | code: str
21 | expires_at: Optional[datetime] = attrs.field(default=None, kw_only=True)
22 |
23 | inviter: Optional[RawUser] = attrs.field(default=None, kw_only=True)
24 | channel: Optional[PartialChannel] = attrs.field(default=None, kw_only=True)
25 |
26 | def __str__(self) -> str:
27 | return self.url
28 |
29 | @property
30 | def url(self) -> str:
31 | """Formatted invite URL for the invite code."""
32 | return f'https://discord.gg/{self.code}'
33 |
34 | @property
35 | def expired(self) -> bool:
36 | """Whether the invite has expired."""
37 | if self.expires_at is None:
38 | return False
39 |
40 | return self.expires_at < datetime.now(timezone.utc)
41 |
42 | @classmethod
43 | def from_data(cls, data: Union[InviteData, InviteCreateData]) -> Self:
44 | expires_at = data.get('expires_at')
45 | if expires_at is not None:
46 | expires_at = datetime.fromisoformat(expires_at)
47 |
48 | inviter = data.get('inviter')
49 | if inviter is not None:
50 | inviter = RawUser.from_data(inviter)
51 |
52 | channel = data.get('channel')
53 | if channel is not None:
54 | channel = PartialChannel.from_data(channel)
55 |
56 | return cls(
57 | code=data['code'],
58 | expires_at=expires_at,
59 | inviter=inviter,
60 | channel=channel
61 | )
62 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_raw/_member.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 | from typing import Optional, Tuple, Union
3 |
4 | import attrs
5 | from discord_typings import GuildMemberAddData, GuildMemberData, UserData
6 | from typing_extensions import Self
7 |
8 | from .._utils import Snowflake
9 | from ._flags import UserFlags
10 | from ._permissions import Permissions
11 | from ._user import RawUser
12 |
13 | __all__ = (
14 | 'RawMember',
15 | 'RawInteractionMember',
16 | )
17 |
18 |
19 | @attrs.define(eq=False, frozen=True, kw_only=True)
20 | class RawMember(RawUser):
21 | joined_at: datetime
22 | roles: Tuple[Snowflake, ...]
23 |
24 | nick: Optional[str] = None
25 | pending: bool = False
26 |
27 | premium_since: Optional[datetime] = None
28 | timed_out_until: Optional[datetime] = None
29 |
30 | @property
31 | def timed_out(self) -> bool:
32 | if self.timed_out_until is None:
33 | return False
34 |
35 | return self.timed_out_until > datetime.now(timezone.utc)
36 |
37 | @classmethod
38 | def from_data(
39 | cls,
40 | data: Union[GuildMemberData, GuildMemberAddData],
41 | user: Optional[UserData] = None
42 | ) -> Self:
43 | if user is None:
44 | if 'user' not in data:
45 | raise ValueError('Cannot create a member without a user.')
46 | else:
47 | user = data['user']
48 |
49 | premium_since = data.get('premium_since')
50 | if premium_since is not None:
51 | premium_since = datetime.fromisoformat(premium_since)
52 |
53 | timed_out_until = data.get('premium_since')
54 | if timed_out_until is not None:
55 | timed_out_until = datetime.fromisoformat(timed_out_until)
56 |
57 | return cls(
58 | id=int(user['id']),
59 |
60 | name=user['username'],
61 | discriminator=int(user['discriminator']),
62 | bot=user.get('bot', False),
63 | system=user.get('system', False),
64 | public_flags=UserFlags(user.get('public_flags', 0)),
65 |
66 | joined_at=datetime.fromisoformat(data['joined_at']),
67 | roles=tuple(Snowflake(int(s)) for s in data['roles']),
68 |
69 | nick=data.get('nick'),
70 | pending=data.get('pending', False),
71 |
72 | premium_since=premium_since,
73 | timed_out_until=timed_out_until,
74 | )
75 |
76 |
77 | @attrs.define(eq=False, frozen=True, kw_only=True)
78 | class RawInteractionMember(RawMember):
79 | permissions: Permissions = Permissions(0)
80 |
81 | @classmethod
82 | def from_data(cls, data: GuildMemberData, user: Optional[UserData] = None) -> Self:
83 | if user is None:
84 | if 'user' not in data:
85 | raise ValueError('Cannot create a member without a user.')
86 | else:
87 | user = data['user']
88 |
89 | premium_since = data.get('premium_since')
90 | if premium_since is not None:
91 | premium_since = datetime.fromisoformat(premium_since)
92 |
93 | timed_out_until = data.get('premium_since')
94 | if timed_out_until is not None:
95 | timed_out_until = datetime.fromisoformat(timed_out_until)
96 |
97 | return cls(
98 | id=int(user['id']),
99 |
100 | name=user['username'],
101 | discriminator=int(user['discriminator']),
102 | bot=user.get('bot', False),
103 | system=user.get('system', False),
104 | public_flags=UserFlags(user.get('public_flags', 0)),
105 |
106 | joined_at=datetime.fromisoformat(data['joined_at']),
107 | roles=tuple(Snowflake(int(s)) for s in data['roles']),
108 |
109 | nick=data.get('nick'),
110 | pending=data.get('pending', False),
111 |
112 | premium_since=premium_since,
113 | timed_out_until=timed_out_until,
114 |
115 | permissions=Permissions(int(data.get('permissions', 0))),
116 | )
117 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_raw/_role.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, SupportsInt, Union
2 |
3 | import attrs
4 | from discord_typings import (
5 | GuildRoleCreateData, GuildRoleUpdateData, RoleData, RoleTagsData
6 | )
7 | from typing_extensions import Self
8 |
9 | from .._utils import Model, Snowflake, _get_as_snowflake
10 | from ._permissions import Permissions
11 |
12 | __all__ = (
13 | 'RoleTags',
14 | 'RawRole',
15 | )
16 |
17 |
18 | @attrs.define(frozen=True, kw_only=True)
19 | class RoleTags:
20 | """Tags on a particular Discord role.
21 |
22 | This contains extra metadata about a role, such as what makes the role
23 | managed or whether it is the role members get when boosting.
24 | """
25 |
26 | bot_id: Optional[Snowflake] = None
27 | integration_id: Optional[Snowflake] = None
28 |
29 | premium_subscriber: bool = False
30 |
31 | @classmethod
32 | def from_data(cls, data: RoleTagsData) -> Self:
33 | bot_id = data.get('bot_id')
34 | if bot_id is not None:
35 | bot_id = Snowflake(int(bot_id))
36 |
37 | integration_id = data.get('integration_id')
38 | if integration_id is not None:
39 | integration_id = Snowflake(int(integration_id))
40 |
41 | return cls(
42 | bot_id=bot_id,
43 | integration_id=integration_id,
44 | premium_subscriber=data.get('premium_subscriber', False) is None
45 | )
46 |
47 |
48 | @attrs.define(eq=False, frozen=True, kw_only=True)
49 | class RawRole(Model):
50 | """Representation of a Discord role with permissions.
51 |
52 | A role represents a set of permissions attached to a group of members.
53 | `@everyone` is also a role like any other role expect for the fact that its
54 | ID is the same as the guild it is attached to.
55 |
56 | Attributes:
57 | name: The name of the role.
58 | color: The color of the role as an integer.
59 | position: The position of the role in the role list.
60 | permissions: The permissions that having this role gives.
61 | guild_id: Guild the role belongs to, if it was passed.
62 | hoist: Whether this role is pinned in the user list.
63 | managed: Whether this role is managed by an integration or bot.
64 | mentionable: Whether this role can be mentioned by anyone.
65 | tags:
66 | Extra metadata about the role - this attribute contains data about
67 | what is managing this role and whether it is the role that premium
68 | subscribers (also called "boosters") are given.
69 | """
70 |
71 | name: str
72 | color: int
73 | position: int
74 | permissions: Permissions = Permissions(0)
75 |
76 | guild_id: Optional[Snowflake] = None
77 |
78 | hoist: bool = False
79 | managed: bool = False
80 | mentionable: bool = False
81 | tags: RoleTags = RoleTags()
82 |
83 | @property
84 | def premium_subscriber(self) -> bool:
85 | """Whether this role is the role that premium subscribers get.
86 |
87 | Shortcut for accessing the `premium_subscriber` attribute of the
88 | RoleTags object.
89 | """
90 | return self.tags.premium_subscriber
91 |
92 | @classmethod
93 | def from_data(
94 | cls,
95 | data: Union[RoleData, GuildRoleCreateData, GuildRoleUpdateData],
96 | *,
97 | guild_id: Optional[SupportsInt] = None,
98 | ) -> Self:
99 | if guild_id is not None:
100 | guild_id = Snowflake(guild_id)
101 | else:
102 | guild_id = _get_as_snowflake(data, 'guild_id')
103 |
104 | if 'role' in data:
105 | data = data['role']
106 |
107 | return cls(
108 | id=int(data['id']),
109 | name=data['name'],
110 | color=data['color'],
111 | position=data['position'],
112 | permissions=Permissions(int(data['permissions'])),
113 | hoist=data['hoist'],
114 | managed=data['managed'],
115 | mentionable=data['mentionable'],
116 | tags=RoleTags.from_data(data.get('tags', {}))
117 | )
118 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_raw/_sticker.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import Optional
3 |
4 | import attrs
5 | from discord_typings import StickerData, StickerItemData
6 | from typing_extensions import Self
7 |
8 | from .._utils import Model, Snowflake, _get_as_snowflake
9 | from ._user import RawUser
10 |
11 | __all__ = (
12 | 'RawStickerItem',
13 | 'RawSticker',
14 | )
15 |
16 |
17 | @attrs.define(eq=False, frozen=True, kw_only=True)
18 | class RawStickerItem(Model):
19 | name: str
20 | format_type: int
21 |
22 | @classmethod
23 | def from_data(cls, data: StickerItemData) -> Self:
24 | return cls(
25 | id=int(data['id']),
26 | name=data['name'],
27 | format_type=int(data['format_type'])
28 | )
29 |
30 |
31 | @attrs.define(eq=False, frozen=True, kw_only=True)
32 | class RawSticker(RawStickerItem):
33 | type: int
34 |
35 | tags: str
36 | description: Optional[str] = None
37 |
38 | pack_id: Optional[Snowflake] = None
39 | sort_value: Optional[int] = None
40 |
41 | available: bool = True
42 | guild_id: Optional[Snowflake] = None
43 |
44 | user: Optional[RawUser] = None
45 |
46 | @classmethod
47 | def from_data(cls, data: StickerData) -> Self:
48 | user = data.get('user')
49 | if user is not None:
50 | user = RawUser.from_data(user)
51 |
52 | return cls(
53 | id=int(data['id']),
54 |
55 | name=data['name'],
56 | description=data.get('description'),
57 |
58 | pack_id=_get_as_snowflake(data, 'pack_id'),
59 | sort_value=data.get('sort_value'),
60 |
61 | tags=data['tags'],
62 | type=data['type'],
63 | format_type=data['format_type'],
64 |
65 | available=data.get('available', True),
66 | guild_id=_get_as_snowflake(data, 'guild_id'),
67 |
68 | user=user
69 | )
70 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_raw/_user.py:
--------------------------------------------------------------------------------
1 | import attrs
2 | from discord_typings import UserData
3 | from typing_extensions import Self
4 |
5 | from .._utils import Model
6 | from ._flags import UserFlags
7 |
8 | __all__ = (
9 | 'RawUser',
10 | 'RawBotUser',
11 | )
12 |
13 |
14 | @attrs.define(eq=False, frozen=True, kw_only=True)
15 | class RawUser(Model):
16 | name: str
17 | discriminator: int
18 |
19 | bot: bool = False
20 | system: bool = False
21 | public_flags: UserFlags = UserFlags.none()
22 |
23 | def __str__(self) -> str:
24 | return f'{self.name}#{self.discriminator}'
25 |
26 | @property
27 | def mention(self) -> str:
28 | return f'<@{self.id}>'
29 |
30 | @classmethod
31 | def from_data(cls, data: UserData) -> Self:
32 | return cls(
33 | id=int(data['id']),
34 | name=data['username'],
35 | discriminator=int(data['discriminator']),
36 | bot=data.get('bot', False),
37 | system=data.get('system', False),
38 | public_flags=UserFlags(data.get('public_flags', 0))
39 | )
40 |
41 |
42 | @attrs.define(eq=False, frozen=True, kw_only=True)
43 | class RawBotUser(RawUser):
44 | bot: bool = True # Update default
45 |
46 | locale: str
47 | mfa_enabled: bool
48 | verified: bool
49 |
50 | @classmethod
51 | def from_data(cls, data: UserData) -> Self:
52 | if (
53 | 'locale' not in data
54 | or 'mfa_enabled' not in data
55 | or 'verified' not in data
56 | ):
57 | raise ValueError('Cannot create a BotUser from data without extra user fields')
58 |
59 | return cls(
60 | id=int(data['id']),
61 | name=data['username'],
62 | discriminator=int(data['discriminator']),
63 |
64 | locale=data['locale'],
65 | mfa_enabled=data['mfa_enabled'],
66 | verified=data['verified'],
67 |
68 | bot=data.get('bot', True),
69 | system=data.get('system', False),
70 | public_flags=UserFlags(data.get('public_flags', 0)),
71 | )
72 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/__init__.py:
--------------------------------------------------------------------------------
1 | from ._asset import (
2 | Asset,
3 | )
4 | from ._channels import (
5 | DMChannel,
6 | TextChannel,
7 | ThreadMember,
8 | Thread,
9 | VoiceChannel,
10 | Category,
11 | )
12 | from ._emoji import (
13 | Emoji,
14 | MessageReaction,
15 | )
16 | from ._guild import (
17 | Guild,
18 | )
19 | from ._integrations import (
20 | IntegrationApplication,
21 | BotIntegration,
22 | StreamIntegration,
23 | )
24 | from ._interactions import (
25 | ResolvedInteractionData,
26 | Interaction,
27 | AutocompleteInteraction,
28 | CommandInteraction,
29 | ComponentInteraction,
30 | )
31 | from ._invite import (
32 | Invite,
33 | )
34 | from ._member import (
35 | Member,
36 | InteractionMember,
37 | )
38 | from ._message import (
39 | Attachment,
40 | MessageMentions,
41 | Message,
42 | )
43 | from ._role import (
44 | Role,
45 | )
46 | from ._sticker import (
47 | StickerItem,
48 | Sticker,
49 | )
50 | from ._user import (
51 | User,
52 | BotUser,
53 | )
54 |
55 | __all__ = (
56 | 'Asset',
57 | 'DMChannel',
58 | 'TextChannel',
59 | 'ThreadMember',
60 | 'Thread',
61 | 'VoiceChannel',
62 | 'Category',
63 | 'Emoji',
64 | 'MessageReaction',
65 | 'Guild',
66 | 'IntegrationApplication',
67 | 'BotIntegration',
68 | 'StreamIntegration',
69 | 'ResolvedInteractionData',
70 | 'Interaction',
71 | 'AutocompleteInteraction',
72 | 'CommandInteraction',
73 | 'ComponentInteraction',
74 | 'Invite',
75 | 'Member',
76 | 'InteractionMember',
77 | 'Attachment',
78 | 'MessageMentions',
79 | 'Message',
80 | 'Role',
81 | 'StickerItem',
82 | 'Sticker',
83 | 'User',
84 | 'BotUser',
85 | )
86 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_asset.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from urllib.parse import parse_qs, urlencode, urlsplit
3 |
4 | import attrs
5 | from typing_extensions import Literal, Self
6 |
7 | from .._utils import get_api
8 |
9 | __all__ = (
10 | 'Asset',
11 | )
12 |
13 |
14 | @attrs.define(frozen=True)
15 | class Asset:
16 | url: str
17 |
18 | BASE = 'https://cdn.discordapp.com'
19 |
20 | @classmethod
21 | def from_path(cls, path: str) -> Self:
22 | return cls(cls.BASE + path)
23 |
24 | def replace(
25 | self,
26 | *,
27 | fmt: Optional[Literal['jpeg', 'jpg', 'png', 'webp', 'gif', 'json']] = None,
28 | size: Optional[int] = None
29 | ) -> Self:
30 | url = urlsplit(self.url)
31 | path = url.path
32 | query = url.query
33 |
34 | if size is not None:
35 | if not (4096 >= size >= 16):
36 | raise ValueError('size argument must be between 16 and 4096.')
37 |
38 | elif size & (size - 1) != 0:
39 | # All powers of two only have one bit set: 1000
40 | # if we subtract 1, then (0111) AND it we should get 0 (0000).
41 | raise ValueError('size argument must be a power of two.')
42 |
43 | query = parse_qs(url.query)
44 | query['size'] = [str(size)]
45 | query = urlencode(query, doseq=True)
46 |
47 | if fmt is not None:
48 | if fmt not in {'jpeg', 'jpg', 'png', 'webp', 'gif', 'json'}:
49 | raise ValueError(
50 | "Image format must be one of: 'jpeg', 'jpg', 'png', 'webp', "
51 | "'gif, or 'json' (for Lottie)"
52 | )
53 |
54 | destination, _, _ = path.rpartition('.')
55 | path = f'{destination}.{fmt}'
56 |
57 | return self.__class__(f'{url.scheme}://{url.netloc}{path}?{query}')
58 |
59 | async def read(self) -> bytes:
60 | """Read the asset's content and return it as bytes."""
61 | return await get_api().read_asset(self.url)
62 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_emoji.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import ClassVar, Optional, Tuple
3 |
4 | import attrs
5 | from discord_typings import EmojiData, MessageReactionData
6 | from typing_extensions import Self
7 |
8 | from .._raw import RawEmoji, RawMessageReaction
9 | from .._utils import DISCORD_EPOCH, Model, Snowflake
10 | from . import _user
11 |
12 | __all__ = (
13 | 'Emoji',
14 | 'MessageReaction',
15 | )
16 |
17 |
18 | @attrs.define(eq=False, frozen=True)
19 | class Emoji(RawEmoji):
20 |
21 | user: Optional[_user.User]
22 |
23 | @classmethod
24 | def from_data(cls, data: EmojiData) -> Self:
25 | user = data.get('user')
26 | if user is not None:
27 | user = _user.User.from_data(user)
28 |
29 | return cls(
30 | id=int(data['id'] or DISCORD_EPOCH << 22),
31 | name=data.get('name') or '_',
32 | roles=tuple(Snowflake(int(s)) for s in data.get('roles', [])),
33 | user=user,
34 | require_colons=data.get('require_colons', True),
35 | managed=data.get('managed', False),
36 | animated=data.get('animated', False),
37 | available=data.get('available', True),
38 | )
39 |
40 |
41 | @attrs.define(frozen=True)
42 | class MessageReaction(RawMessageReaction):
43 | emoji: Emoji
44 |
45 | @classmethod
46 | def from_data(cls, data: MessageReactionData) -> Self:
47 | return cls(
48 | count=data['count'],
49 | emoji=Emoji.from_data(data['emoji']),
50 |
51 | me=data['me'],
52 | )
53 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_guild.py:
--------------------------------------------------------------------------------
1 | import attrs
2 |
3 | from .._raw import RawGuild
4 |
5 | __all__ = (
6 | 'Guild',
7 | )
8 |
9 | @attrs.define(eq=False, frozen=True)
10 | class Guild(RawGuild):
11 | ...
12 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_integrations.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import attrs
4 | from discord_typings import (
5 | DiscordIntegrationData, IntegrationApplicationData
6 | )
7 | from typing_extensions import Self
8 |
9 | from .._raw import (
10 | IntegrationAccount, RawBotIntegration,
11 | RawIntegrationApplication, RawStreamIntegration
12 | )
13 | from . import _user
14 |
15 | __all__ = (
16 | 'IntegrationApplication',
17 | 'BotIntegration',
18 | 'StreamIntegration',
19 | )
20 |
21 |
22 | @attrs.define(eq=False, frozen=True, kw_only=True)
23 | class IntegrationApplication(RawIntegrationApplication):
24 | user: Optional['_user.User']
25 |
26 | @classmethod
27 | def from_data(cls, data: IntegrationApplicationData) -> Self:
28 | user = data.get('bot')
29 | if user is not None:
30 | user = _user.User.from_data(user)
31 |
32 | return cls(
33 | id=int(data['id']),
34 | name=data['name'],
35 | icon=data.get('icon'),
36 | description=data['description'],
37 | summary=data['summary'],
38 | user=user
39 | )
40 |
41 |
42 | @attrs.define(eq=False, frozen=True, kw_only=True)
43 | class BotIntegration(RawBotIntegration):
44 | application: Optional[IntegrationApplication] = None
45 |
46 | @property
47 | def user(self) -> Optional['_user.User']:
48 | """The user associated with the integration."""
49 | return None if self.application is None else self.application.user
50 |
51 | @classmethod
52 | def from_data(cls, data: DiscordIntegrationData) -> Self:
53 | application = data.get('application')
54 | if application is not None:
55 | application = IntegrationApplication.from_data(application)
56 |
57 | return cls(
58 | id=int(data['id']),
59 | name=data['name'],
60 | type=data['type'],
61 | enabled=data['enabled'],
62 | account=IntegrationAccount.from_data(data['account']),
63 | application=application
64 | )
65 |
66 |
67 | @attrs.define(eq=False, frozen=True)
68 | class StreamIntegration(RawStreamIntegration):
69 | ...
70 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_invite.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 | from typing import Optional, Union
3 |
4 | import attrs
5 | from discord_typings import InviteCreateData, InviteData
6 | from typing_extensions import Self
7 |
8 | from .._raw import PartialChannel, RawInvite
9 | from .._utils import MISSING, get_api
10 | from . import _user
11 |
12 | __all__ = (
13 | 'Invite',
14 | )
15 |
16 |
17 | @attrs.define(frozen=True, kw_only=True)
18 | class Invite(RawInvite):
19 | """Representation of a Discord invite."""
20 |
21 | inviter: Optional[_user.User] # Override typehint
22 |
23 | @classmethod
24 | def from_data(cls, data: Union[InviteData, InviteCreateData]) -> Self:
25 | expires_at = data.get('expires_at')
26 | if expires_at is not None:
27 | expires_at = datetime.fromisoformat(expires_at)
28 |
29 | inviter = data.get('inviter')
30 | if inviter is not None:
31 | inviter = _user.User.from_data(inviter)
32 |
33 | channel = data.get('channel')
34 | if channel is not None:
35 | channel = PartialChannel.from_data(channel)
36 |
37 | return cls(
38 | code=data['code'],
39 | expires_at=expires_at,
40 | inviter=inviter,
41 | channel=channel
42 | )
43 |
44 | async def delete(self, *, reason: str = MISSING) -> Self:
45 | """Delete the invite.
46 |
47 | This method requires the `MANAGE_CHANNELS` permission on the channel
48 | that the invite belongs to, or `MANAGE_GUILD` on the guild.
49 |
50 | Parameters:
51 | reason: Audit log reason for deleting the invite.
52 |
53 | Returns:
54 | The most recent data of the now deleted invite.
55 | """
56 | data = await get_api().delete_invite(self.code, reason=reason)
57 | return self.__class__.from_data(data)
58 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_member.py:
--------------------------------------------------------------------------------
1 | import attrs
2 |
3 | from .._raw import RawInteractionMember, RawMember
4 | from ._user import User
5 |
6 | __all__ = (
7 | 'Member',
8 | 'InteractionMember',
9 | )
10 |
11 |
12 | @attrs.define(eq=False, frozen=True)
13 | class Member(RawMember, User):
14 | ...
15 |
16 |
17 | @attrs.define(eq=False, frozen=True)
18 | class InteractionMember(RawInteractionMember, Member):
19 | ...
20 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_message.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import Iterable, Optional, Sequence, SupportsInt, Tuple, Union
3 |
4 | import attrs
5 | from discord_typings import (
6 | AllowedMentionsData, AttachmentData, MessageCreateData, MessageData,
7 | MessageUpdateData
8 | )
9 | from typing_extensions import Self
10 |
11 | from .._raw import (
12 | ChannelMention, Embed, RawAttachment, RawMessage, RawMessageMentions
13 | )
14 | from .._utils import Snowflake, _get_as_snowflake
15 | from . import _emoji, _member, _user
16 |
17 | __all__ = (
18 | 'Attachment',
19 | 'MessageMentions',
20 | 'Message',
21 | )
22 |
23 |
24 | @attrs.define(eq=False)
25 | class Attachment(RawAttachment):
26 | ...
27 |
28 |
29 | @attrs.define(kw_only=True)
30 | class MessageMentions(RawMessageMentions):
31 | users: Union[Tuple['_user.User', ...], Tuple['_member.Member', ...]]
32 |
33 | roles: Tuple[Snowflake, ...]
34 |
35 | @classmethod
36 | def from_message(
37 | cls,
38 | data: Union[MessageData, MessageCreateData, MessageUpdateData]
39 | ) -> Self:
40 | if data['mentions'] and 'member' in data['mentions'][0]:
41 | # Pyright doesn't understand that the type has narrowed down to
42 | # List[UserMentionData] with the 'member' key.
43 | users = tuple(_member.Member.from_data(m['member'], m) for m in data['mentions']) # type: ignore
44 | else:
45 | users = tuple(_user.User.from_data(u) for u in data['mentions'])
46 |
47 | return cls(
48 | users=users,
49 | channels=tuple(
50 | ChannelMention.from_data(c)
51 | for c in data.get('mention_channels', [])
52 | ),
53 | roles=tuple(Snowflake(int(r)) for r in data['mention_roles']),
54 | )
55 |
56 |
57 | @attrs.define(eq=False, kw_only=True)
58 | class Message(RawMessage):
59 | author: Union[_user.User, _member.Member]
60 |
61 | channel_id: Snowflake
62 | guild_id: Optional[Snowflake] = None
63 |
64 | attachments: Tuple[Attachment, ...]
65 | reactions: Tuple['_emoji.MessageReaction', ...] = ()
66 | mentions: MessageMentions = MessageMentions()
67 |
68 | @classmethod
69 | def from_data(
70 | cls,
71 | data: Union[MessageData, MessageCreateData, MessageUpdateData]
72 | ) -> Self:
73 | if 'member' in data:
74 | author = _member.Member.from_data(data['member'], data['author'])
75 | else:
76 | author = _user.User.from_data(data['author'])
77 |
78 | return cls(
79 | id=int(data['id']),
80 | type=data['type'],
81 | author=author,
82 |
83 | channel_id=Snowflake(int(data['channel_id'])),
84 | guild_id=_get_as_snowflake(data, 'guild_id'),
85 |
86 | content=data['content'],
87 | tts=data['tts'],
88 | attachments=tuple(Attachment.from_data(a) for a in data['attachments']),
89 | embeds=tuple(Embed.from_data(e) for e in data['embeds']),
90 | reactions=tuple(_emoji.MessageReaction.from_data(r) for r in data.get('reactions', [])),
91 | mentions=MessageMentions.from_message(data),
92 |
93 | pinned=data['pinned'],
94 | )
95 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_role.py:
--------------------------------------------------------------------------------
1 | import attrs
2 |
3 | from .._raw import RawRole
4 | from .._utils import MISSING, get_api
5 |
6 | __all__ = (
7 | 'Role',
8 | )
9 |
10 |
11 | @attrs.define(eq=False)
12 | class Role(RawRole):
13 | ...
14 |
15 | async def delete(self, *, reason: str = MISSING) -> None:
16 | """Delete the role.
17 |
18 | This method requires the `MANAGE_ROLES` permission.
19 |
20 | Parameters:
21 | reason: Audit log reason for deleting the role.
22 | """
23 | if not self.guild_id:
24 | raise ValueError('Cannot delete role without known guild ID')
25 |
26 | await get_api().delete_role(self.guild_id, self.id, reason=reason)
27 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_sticker.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import attrs
4 | from discord_typings import StickerData
5 | from typing_extensions import Self
6 |
7 | from .._raw import RawSticker, RawStickerItem
8 | from .._utils import _get_as_snowflake
9 | from . import _user
10 |
11 | __all__ = (
12 | 'StickerItem',
13 | 'Sticker',
14 | )
15 |
16 |
17 | @attrs.define(eq=False, frozen=True, kw_only=True)
18 | class StickerItem(RawStickerItem):
19 | ...
20 |
21 |
22 | @attrs.define(eq=False, frozen=True, kw_only=True)
23 | class Sticker(RawSticker, StickerItem):
24 |
25 | user: Optional['_user.User']
26 |
27 | @classmethod
28 | def from_data(cls, data: StickerData) -> Self:
29 | user = data.get('user')
30 | if user is not None:
31 | user = _user.User.from_data(user)
32 |
33 | return cls(
34 | id=int(data['id']),
35 |
36 | name=data['name'],
37 | description=data.get('description'),
38 |
39 | pack_id=_get_as_snowflake(data, 'pack_id'),
40 | sort_value=data.get('sort_value'),
41 |
42 | tags=data['tags'],
43 | type=data['type'],
44 | format_type=data['format_type'],
45 |
46 | available=data.get('available', True),
47 | guild_id=_get_as_snowflake(data, 'guild_id'),
48 |
49 | user=user
50 | )
51 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_stateful/_user.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import attrs
4 | from typing_extensions import Self
5 |
6 | from .._raw import RawBotUser, RawUser
7 | from .._utils import MISSING, get_api
8 | from . import _channels # Potential circular imports
9 |
10 | __all__ = (
11 | 'User',
12 | 'BotUser',
13 | )
14 |
15 |
16 | @attrs.define(eq=False, frozen=True)
17 | class User(RawUser):
18 | ...
19 |
20 | async def create_dm(self) -> '_channels.DMChannel':
21 | """Create a DM with the user to be able to send messages.
22 |
23 | This method (endpoint) is idempotent and will return the same channel
24 | on subsequent calls.
25 |
26 | Returns:
27 | The created or reused DM channel.
28 | """
29 | data = await get_api().create_dm(self.id)
30 | return _channels.DMChannel.from_data(data)
31 |
32 |
33 | @attrs.define(eq=False, frozen=True)
34 | class BotUser(RawBotUser):
35 | ...
36 |
37 | async def edit(
38 | self,
39 | *,
40 | username: str = MISSING,
41 | avatar: Optional[str] = MISSING,
42 | ) -> Self:
43 | """Edit the current bot user.
44 |
45 | Parameters:
46 | username: New username to set.
47 | avatar: New base64-encoded image data for the avatar.
48 |
49 | Returns:
50 | New instance of the bot user with updated values.
51 | """
52 | data = await get_api().edit_my_user(username=username, avatar=avatar)
53 | return self.__class__.from_data(data)
54 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/_utils.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 | from typing import (
3 | TYPE_CHECKING, Any, Mapping, NoReturn, Optional, SupportsInt, Union
4 | )
5 |
6 | import attrs
7 | from typing_extensions import Self
8 |
9 | __all__ = (
10 | 'DISCORD_EPOCH',
11 | 'Model',
12 | 'Snowflake',
13 | )
14 |
15 |
16 | DISCORD_EPOCH = 1420070400000
17 |
18 |
19 | @attrs.define(frozen=True)
20 | class Model:
21 | """The root for all Wumpy objects, a Discord object with an ID.
22 |
23 | A Model is a simple wrapper over an integer - the Discord snowflake which
24 | is guaranteed by Discord to be unique. It tries to support as many
25 | operations as possible. This class is later used for all models in
26 | `wumpy-models` that are exposed.
27 |
28 | Attributes:
29 | id: The underlying integer value representing the Discord snowflake.
30 | """
31 |
32 | id: int
33 |
34 | def __repr__(self) -> str:
35 | return f'wumpy.models.Model(id={self.id})'
36 |
37 | def __hash__(self) -> int:
38 | return self.id >> 22
39 |
40 | def __int__(self) -> int:
41 | return self.id
42 |
43 | def __float__(self) -> float:
44 | return float(self.id)
45 |
46 | def __complex__(self) -> complex:
47 | return complex(self.id)
48 |
49 | def __index__(self) -> int:
50 | return self.id
51 |
52 | def __eq__(self, other: object) -> bool:
53 | if isinstance(other, int):
54 | value = other
55 | elif isinstance(other, self.__class__):
56 | value = other.id
57 | else:
58 | return NotImplemented
59 |
60 | return self.id == value
61 |
62 | def __ne__(self, other: object) -> bool:
63 | # There's a performance hit to not defining __ne__, even though
64 | # Python will automatically call __eq__ and invert it
65 |
66 | if isinstance(other, int):
67 | value = other
68 | elif isinstance(other, self.__class__):
69 | value = other.id
70 | else:
71 | return NotImplemented
72 |
73 | return self.id != value
74 |
75 | @property
76 | def created_at(self) -> datetime:
77 | timestamp = (self.id >> 22) + DISCORD_EPOCH
78 | return datetime.fromtimestamp(timestamp / 1000, tz=timezone.utc)
79 |
80 |
81 | @attrs.define(eq=False, frozen=True)
82 | class Snowflake(Model):
83 | """Standalone Discord snowflake.
84 |
85 | This is seperate from Model as not all methods on this class should be
86 | inherited to subclasses, such as the `from_datetime()` classmethod. Any
87 | standalone ID field will be an instance of this class.
88 |
89 | Attributes:
90 | id: The underlying integer value representing the Discord snowflake.
91 | """
92 |
93 | def __init__(self, id: Union[SupportsInt, str]) -> None:
94 | super().__init__(int(id))
95 |
96 | @property
97 | def worker_id(self) -> int:
98 | """Return the ID of the worker that created the snowflake."""
99 | return (self.id & 0x3E0000) >> 17
100 |
101 | @property
102 | def process_id(self) -> int:
103 | """Return the ID of the process that created the snowflake."""
104 | return (self.id & 0x1F000) >> 12
105 |
106 | @property
107 | def process_increment(self) -> int:
108 | """Return the increment of the process that created the snowflake."""
109 | return self.id & 0xFFF
110 |
111 | @classmethod
112 | def from_datetime(cls, dt: datetime) -> Self:
113 | """Craft a snowflake created at the specified time.
114 |
115 | This enables a neat trick for pagination through the Discord API as
116 | Discord only look at the timestamp it represents.
117 |
118 | Parameters:
119 | dt: The datetime of when the snowflake should be created.
120 |
121 | Returns:
122 | The snowflake created at the specified time.
123 | """
124 | return cls(int(dt.timestamp() * 1000 - DISCORD_EPOCH) << 22)
125 |
126 |
127 | def _get_as_snowflake(data: Optional[Mapping[str, Any]], key: str) -> Optional[Snowflake]:
128 | """Get a key as a snowflake.
129 |
130 | Returns None if `data` is None or does not have the key.
131 |
132 | Parameters:
133 | data: The optional mapping to get the key from.
134 | key: The key to attempt to look up.
135 |
136 | Returns:
137 | The value of the key wrapped in a Snowflake, if there was a mapping
138 | passed and the key could be found.
139 | """
140 | if data is None:
141 | return None
142 |
143 | value: Union[str, int, None] = data.get(key)
144 | return Snowflake(int(value)) if value is not None else None
145 |
146 | try:
147 | from wumpy.rest import MISSING, get_api
148 | except ImportError:
149 | if not TYPE_CHECKING:
150 | def get_api(subclass: Optional[type] = None, *, verify: bool = False) -> NoReturn:
151 | raise RuntimeError('There is no currently active API')
152 |
153 | MISSING = object()
154 |
--------------------------------------------------------------------------------
/library/wumpy-models/wumpy/models/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/library/wumpy-models/wumpy/models/py.typed
--------------------------------------------------------------------------------
/library/wumpy-rest/README.md:
--------------------------------------------------------------------------------
1 | # Wumpy-rest
2 |
3 | Richly and accurately typed wrapper around the Discord REST API.
4 |
5 | ## Usage
6 |
7 | The best way to use `wumpy-rest` is to import `APIClient`:
8 |
9 | ```python
10 | import anyio
11 | from wumpy.rest import APIClient
12 |
13 |
14 | TOKEN = 'ABC123.XYZ789'
15 |
16 |
17 | async def main():
18 | async with APIClient(TOKEN) as api:
19 | print(await api.fetch_my_user())
20 |
21 |
22 | anyio.run(main)
23 | ```
24 |
25 | `APIClient` is a class that implements all routes of the Discord API. This is
26 | made up of multiple route classes. You can create your own class with the
27 | routes you use:
28 |
29 | ```python
30 | from wumpy.rest import (
31 | ApplicationCommandRequester, InteractionRequester,
32 | HTTPXRequester
33 | )
34 |
35 |
36 | class MyAPIClient(ApplicationCommandRequester, InteractionRequester, HTTPXRequester):
37 |
38 | __slots__ = () # Save some memory for this class
39 | ```
40 |
41 | ### Files
42 |
43 | Some endpoints support uploading files, for these a file-like object is
44 | expected that's been opened in binary-mode (for example `'rb'`).
45 |
46 | For the message/interaction endpoints, remember to include a matching
47 | `attachment` object with `'id'` set to the index of the file.
48 |
49 | ## Ratelimiter
50 |
51 | You can pass a custom ratelimiter to the requester if you want to customize
52 | that behaviour. For more, [read the documentation](https://wumpy.rtfd.io).
53 | Here's an example of a ratelimiter that does no ratelimiting and does not
54 | handle any kind of `429`-responses.
55 |
56 | ```python
57 | from contextlib import asynccontextmanager
58 | from typing import (
59 | Any, AsyncContextManager, AsyncGenerator, Awaitable, Callable, Coroutine,
60 | Mapping
61 | )
62 |
63 | import anyio
64 | from wumpy.rest import APIClient
65 |
66 |
67 | class NoOpRatelimiter:
68 | """Ratelimiter implementation that does nothing; a no-op implementation."""
69 |
70 | async def __aenter__(self):
71 | return self
72 |
73 | async def __aexit__(
74 | self,
75 | exc_type: Optional[Type[BaseException]],
76 | exc_val: Optional[BaseException],
77 | exc_tb: Optional[TracebackType]
78 | ) -> object:
79 | pass
80 |
81 | @asynccontextmanager
82 | async def __call__(self, route: Route) -> AsyncGenerator[
83 | Callable[[Mapping[str, str]], Coroutine[Any, Any, object]],
84 | None
85 | ]:
86 | # The return type may look a little weird, but this is how
87 | # @asynccontextmanager works. You pass it a function that returns an
88 | # async generator (which yields what the asynchronous context manager
89 | # then returns).
90 | yield self.update
91 |
92 | async def update(self, headers: Mapping[str, str]) -> object:
93 | pass
94 |
95 |
96 | async def main():
97 | async with APIClient(TOKEN, ratelimiter=NoOpRatelimiter()) as api:
98 | print(await api.fetch_my_user())
99 |
100 |
101 | anyio.run(main)
102 | ```
103 |
--------------------------------------------------------------------------------
/library/wumpy-rest/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "wumpy-rest"
3 | version = "0.3.0"
4 | description = "Reusable and richly typed wrapper over the Discord REST API"
5 | readme = {file = "README.md", content-type="text/markdown"}
6 |
7 | # Both httpx and anyio has 3.6.2 as the minimum requirement
8 | # but @contextlib.asynccontextmanager was added in Python 3.7
9 | requires-python = ">=3.7"
10 |
11 | license = {text = "Dual-licensed under the Apache License 2.0 and the MIT License"}
12 | authors = [{name = "Bluenix", email = "bluenixdev@gmail.com"}]
13 |
14 | keywords = [
15 | "wumpy", "wumpus", "wrapper",
16 | "discord", "discord-api", "discord-bot", "discord-api-wrapper",
17 | "python-3"
18 | ]
19 | classifiers = [
20 | "Development Status :: 3 - Alpha",
21 | "Framework :: AnyIO",
22 | "Intended Audience :: Developers",
23 | "License :: OSI Approved :: MIT License",
24 | "License :: OSI Approved :: Apache Software License",
25 | "Natural Language :: English",
26 | "Operating System :: OS Independent",
27 | "Programming Language :: Python",
28 | "Programming Language :: Python :: 3",
29 | "Programming Language :: Python :: 3 :: Only",
30 | "Programming Language :: Python :: 3.7",
31 | "Programming Language :: Python :: 3.8",
32 | "Programming Language :: Python :: 3.9",
33 | "Programming Language :: Python :: 3.10",
34 | "Topic :: Internet",
35 | "Topic :: Internet :: WWW/HTTP",
36 | "Topic :: Software Development :: Libraries",
37 | "Topic :: Software Development :: Libraries :: Python Modules",
38 | "Typing :: Typed",
39 | ]
40 |
41 | dependencies = [
42 | "anyio >= 3.3.4, <4",
43 | "httpx[http2] >= 0.22, < 1",
44 | "discord-typings >= 0.5.0, < 1"
45 | ]
46 |
47 | [project.urls]
48 | Homepage = "https://github.com/wumpyproject/wumpy"
49 | Repository = "https://github.com/wumpyproject/wumpy/tree/main/library/wumpy-rest"
50 | Documentation = "https://wumpy.rtfd.io"
51 |
52 | [build-system]
53 | requires = ["flit_core >=3.5, <4"]
54 | build-backend = "flit_core.buildapi"
55 |
56 | [tool.flit.module]
57 | # This is a subpackage under the wumpy namespace package,
58 | # we need to tell flit this otherwise it tries to make the
59 | # import wumpy-rest rather than wumpy.rest
60 | name = "wumpy.rest"
61 |
--------------------------------------------------------------------------------
/library/wumpy-rest/wumpy/rest/__init__.py:
--------------------------------------------------------------------------------
1 | from ._config import (
2 | RatelimiterContext,
3 | abort_if_ratelimited,
4 | )
5 | from ._errors import (
6 | HTTPException,
7 | RateLimited,
8 | Forbidden,
9 | NotFound,
10 | ServerException,
11 | )
12 | from ._impl import (
13 | HTTPXRequester,
14 | APIClient,
15 | get_api,
16 | )
17 | from ._ratelimiter import (
18 | Ratelimiter,
19 | DictRatelimiter,
20 | )
21 | from ._requester import (
22 | Requester,
23 | )
24 | from ._route import (
25 | Route,
26 | )
27 | from ._utils import (
28 | MISSING,
29 | )
30 |
31 | __all__ = (
32 | 'RatelimiterContext',
33 | 'abort_if_ratelimited',
34 | 'HTTPException',
35 | 'RateLimited',
36 | 'Forbidden',
37 | 'NotFound',
38 | 'ServerException',
39 | 'HTTPXRequester',
40 | 'APIClient',
41 | 'get_api',
42 | 'Ratelimiter',
43 | 'DictRatelimiter',
44 | 'Requester',
45 | 'Route',
46 | 'MISSING',
47 | )
48 |
--------------------------------------------------------------------------------
/library/wumpy-rest/wumpy/rest/_config.py:
--------------------------------------------------------------------------------
1 | from contextvars import ContextVar, Token
2 | from types import TracebackType
3 | from typing import Optional, Type
4 |
5 | from typing_extensions import Self
6 |
7 | from ._errors import RequestException
8 |
9 | __all__ = (
10 | 'RatelimiterContext',
11 | 'abort_if_ratelimited',
12 | )
13 |
14 |
15 | _abort_if_ratelimited: ContextVar[bool] = ContextVar('_abort_if_ratelimited', default=False)
16 |
17 |
18 | class RatelimiterContext:
19 | """Ratelimiting context variables for the request made.
20 |
21 | This class provides attributes which are gotten from underlying context
22 | variables at instantiation time. These context variables allow
23 | transparently configuring ratelimiting without adding a bunch of parameters
24 | to each request - nor having to change the signature of ratelimiters for
25 | every update.
26 |
27 | It is adviced that all ratelimiters implement all of these behaviours. If
28 | some configuration cannot be implemented the ratelimiter should raise an
29 | error to signal to the developer that this configuration cannot be used.
30 |
31 | An instance of this class can be safely passed to a different context
32 | without having attributes change, thus an instance of this represents the
33 | current state at a certain point in time.
34 |
35 | Examples:
36 |
37 | ```python
38 | ctx = RatelimiterContext()
39 | print(ctx.abort_if_ratelimited)
40 | ```
41 |
42 | Attributes:
43 | abort_if_ratelimited:
44 | Abort the request if it will hit ratelimits.
45 |
46 | This ratelimiting configuration is what powers the context manager
47 | of the same name (`abort_if_ratelimited()`). It allows the user to
48 | specify that they wish to abort requests if they get ratelimited.
49 | """
50 |
51 | abort_if_ratelimited: bool
52 |
53 | def __new__(cls) -> Self:
54 | self = super().__new__(cls)
55 |
56 | self.abort_if_ratelimited = _abort_if_ratelimited.get()
57 |
58 | return self
59 |
60 |
61 | class _AbortRatelimitsManager:
62 |
63 | _aborted: Optional[bool]
64 | _previous: Token
65 |
66 | def __init__(self) -> None:
67 | self._aborted = None
68 |
69 | def __enter__(self) -> Self:
70 | self._previous = _abort_if_ratelimited.set(True)
71 |
72 | return self
73 |
74 | def __exit__(
75 | self,
76 | exc_type: Optional[Type[BaseException]] = None,
77 | exc_val: Optional[BaseException] = None,
78 | traceback: Optional[TracebackType] = None
79 | ) -> Optional[bool]:
80 | _abort_if_ratelimited.reset(self._previous)
81 |
82 | if isinstance(exc_val, RequestException) and exc_val.status_code in {408, 429}:
83 | self._aborted = True
84 | return True
85 |
86 | self._aborted = False
87 |
88 | @property
89 | def aborted(self) -> bool:
90 | if self._aborted is None:
91 | raise RuntimeError(
92 | "Cannot access 'aborted' before 'abort_if_ratelimited()' has been exited"
93 | )
94 |
95 | return self._aborted
96 |
97 |
98 | def abort_if_ratelimited() -> _AbortRatelimitsManager:
99 | """Abort requests if they are ratelimited.
100 |
101 | This function returns a context manager, that when entered, will abort any
102 | requests if they will be ratelimited. Useful for skipping somewhat
103 | pointless requests that you don't want to wait for.
104 |
105 | !!! info
106 | The global ratelimit of all requests is always abided. This function
107 | is for ignoring larger route-specific ratelimits.
108 |
109 | The way this is implemented is by catching the `Ratelimited` exception.
110 | That means that not all code inside of this context manager will run if the
111 | request gets ratelimited. Therefore, if you want to use the result of the
112 | request this code needs to be placed inside of the context manager.
113 |
114 | If you want to know whether the requests were aborted, you can check the
115 | `aborted` attribute on the context manager:
116 |
117 | ```python
118 | with abort_if_ratelimited() as abort_ratelimits:
119 | user = await api.fetch_user(344404945359077377)
120 | print(f"Found user {user['name']}#{user['discriminator]}")
121 |
122 | # Note that 'user' may or may not be defined here, depending on whether the
123 | # request was aborted or not. That's why we print inside of the context
124 | # manager.
125 |
126 | if abort_ratelimits.aborted:
127 | # Since the request was aborted, the above print did not get to run
128 | print('Skipped fetching because of ratelimits')
129 | ```
130 |
131 | For more complex situations, you can also nest this to only abort subset of
132 | requests at a time. This allows you to compose requests that get aborted on
133 | their own without aborting all requests inside of the parent.
134 |
135 | !!! note
136 | This function does not skip the ratelimiter, rather, it aborts the
137 | request *if* the ratelimiter wants to wait. You cannot use this to
138 | force requests to bypass the ratelimiter.
139 |
140 | Returns:
141 | A context manager which skips requests if they will be ratelimited.
142 | """
143 | return _AbortRatelimitsManager()
144 |
--------------------------------------------------------------------------------
/library/wumpy-rest/wumpy/rest/_errors.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Mapping, Optional, Union
2 |
3 | from httpx import codes
4 |
5 | __all__ = (
6 | 'HTTPException',
7 | 'RateLimited',
8 | 'Forbidden',
9 | 'NotFound',
10 | 'ServerException',
11 | )
12 |
13 |
14 | class HTTPException(Exception):
15 | """Base for all HTTP exceptions.
16 |
17 | The exceptions are smartly inherited in the following schema:
18 |
19 | HTTPException
20 | └── RequestException
21 | ├── Forbidden
22 | ├── NotFound
23 | └── ServerException
24 | """
25 |
26 | __slots__ = ()
27 |
28 |
29 | class RequestException(HTTPException):
30 | """Exception subclassed by exceptions relating to failed requests.
31 |
32 | Attributes:
33 | status_code: The HTTP status code of the response.
34 | status_phrase: The phrase that goes along with the status code.
35 | headers: The headers returned by the response.
36 | data: The body of the response (possibly None).
37 | errors: The errors returned by Discord in the body.
38 | message: The message returned by Discord in the body.
39 | code: The error code returned by Discord in the body.
40 | attempt:
41 | The number of times the request has been attempted, this should be
42 | used to implement exponential backoff in the Ratelimiter.
43 | """
44 |
45 | status_code: int
46 | status_phrase: str
47 | headers: Mapping[str, str]
48 |
49 | data: Union[str, Dict[str, Any], None]
50 |
51 | errors: Optional[Dict[str, Any]]
52 | message: str
53 | code: int
54 |
55 | attempt: int
56 |
57 | __slots__ = (
58 | 'status_code', 'status_phrase', 'headers', 'data', 'errors',
59 | 'message', 'code', 'attempt'
60 | )
61 |
62 | def __init__(
63 | self,
64 | status_code: int,
65 | headers: Mapping[str, str],
66 | data: Union[str, Dict[str, Any], None] = None,
67 | *,
68 | attempt: int = 0
69 | ) -> None:
70 | if isinstance(data, dict):
71 | message = data.get('message', '')
72 | code = data.get('code', 0)
73 |
74 | errors = data.get('errors')
75 | else:
76 | message = ''
77 | code = 0
78 | errors = None
79 |
80 | self.status_code = status_code
81 | self.status_phrase = codes.get_reason_phrase(self.status_code)
82 | self.headers = headers
83 |
84 | self.data = data
85 |
86 | self.errors = errors
87 | self.message = message
88 | self.code = code
89 |
90 | self.attempt = attempt
91 |
92 | super().__init__(
93 | '{0.status_code} {0.status_phrase} (Discord error code: {1}) {2}'.format(
94 | self, code, message
95 | )
96 | )
97 |
98 |
99 | class RateLimited(RequestException):
100 | """Exception raised when the client is being rate limited."""
101 |
102 | __slots__ = ()
103 |
104 |
105 | class Forbidden(RequestException):
106 | """Exception raised when the requester hits a 403 response."""
107 |
108 | __slots__ = ()
109 |
110 |
111 | class NotFound(RequestException):
112 | """Exception raised when the requester hits a 404 response."""
113 |
114 | __slots__ = ()
115 |
116 |
117 | class ServerException(RequestException):
118 | """Exception raised when the requester hits a 500 range response."""
119 |
120 | __slots__ = ()
121 |
--------------------------------------------------------------------------------
/library/wumpy-rest/wumpy/rest/_route.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Union
2 | from urllib.parse import quote as urlquote
3 |
4 | __all__ = (
5 | 'Route',
6 | )
7 |
8 |
9 | class Route:
10 | """A route that a request should be made to.
11 |
12 | Containing information such as the endpoint, and parameters to use. Mainly
13 | used to figure out ratelimit handling. If the request made should have a
14 | request body this should be passed to the requester.
15 |
16 | Attributes:
17 | method: The HTTP method to use.
18 | path: The path to the endpoint, concatenated to the `BASE` url.
19 | params:
20 | The parameters to format into the path, separated to allow easier
21 | ratelimit handling.
22 | """
23 |
24 | method: str
25 | path: str
26 | params: Dict[str, Union[str, int]]
27 |
28 | __slots__ = ('method', 'path', 'params')
29 |
30 | def __init__(self, method: str, path: str, **params: Union[str, int]) -> None:
31 | self.method = method
32 | self.path = path
33 |
34 | self.params = params
35 |
36 | def __eq__(self, other: object) -> bool:
37 | return isinstance(other, Route) and self.endpoint == other.endpoint
38 |
39 | def __repr__(self) -> str:
40 | return f''
41 |
42 | def __str__(self) -> str:
43 | return self.endpoint
44 |
45 | def __hash__(self) -> int:
46 | return hash(self.endpoint)
47 |
48 | @property
49 | def url(self) -> str:
50 | """Return a complete, formatted url that a request should be made to.
51 |
52 | This needs to be appended to the base URL after formatting.
53 | """
54 | return self.path.format_map(
55 | # Replace special characters with the %xx escapes
56 | {k: urlquote(v) if isinstance(v, str) else v for k, v in self.params.items()}
57 | )
58 |
59 | @property
60 | def endpoint(self) -> str:
61 | """Return the Discord endpoint this route will request."""
62 | return f'{self.method} {self.path}'
63 |
64 | @property
65 | def major_params(self) -> str:
66 | """Return a string of the formatted major parameters."""
67 | param = (
68 | self.params.get('webhook_id')
69 | or self.params.get('channel_id')
70 | or self.params.get('guild_id')
71 | )
72 |
73 | if param:
74 | # TODO: Discord handles messages over 14 days differently in terms
75 | # of ratelimiting because it is more costly.
76 | return f':{param}'
77 |
78 | return ''
79 |
--------------------------------------------------------------------------------
/library/wumpy-rest/wumpy/rest/_utils.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable
2 |
3 | from typing_extensions import Final, final
4 |
5 | __all__ = (
6 | 'MISSING',
7 | )
8 |
9 | dump_json: Callable[[Any], str]
10 | load_json: Callable[[str], Any]
11 |
12 | try:
13 | import orjson
14 |
15 | def orjson_compat(obj: Any) -> str:
16 | return orjson.dumps(obj).decode('utf-8')
17 | dump_json = orjson_compat
18 | load_json = orjson.loads
19 |
20 | except ImportError:
21 | import json
22 |
23 | dump_json = json.dumps
24 | load_json = json.loads
25 |
26 |
27 | @final
28 | class MissingType(object):
29 | """Representing an optional default when no value has been passed.
30 |
31 | This is mainly used as a sentinel value for defaults to work nicely
32 | with typehints, so that `Optional[X]` doesn't have to be used.
33 | """
34 |
35 | def __bool__(self) -> bool:
36 | return False
37 |
38 | def __repr__(self) -> str:
39 | return ''
40 |
41 |
42 | MISSING: Final[Any] = MissingType()
43 |
44 |
45 | # While it would make sense for get_api() to be implemented here, it is placed
46 | # in _impl.py for circular import purposes.
47 |
--------------------------------------------------------------------------------
/library/wumpy-rest/wumpy/rest/endpoints/__init__.py:
--------------------------------------------------------------------------------
1 | from ._channel import (
2 | ChannelEndpoints,
3 | )
4 | from ._commands import (
5 | ApplicationCommandEndpoints,
6 | )
7 | from ._gateway import (
8 | GatewayEndpoints,
9 | )
10 | from ._guild_template import (
11 | GuildTemplateEndpoints,
12 | )
13 | from ._guild import (
14 | GuildEndpoints,
15 | )
16 | from ._interactions import (
17 | InteractionEndpoints,
18 | )
19 | from ._sticker import (
20 | StickerEndpoints,
21 | )
22 | from ._user import (
23 | UserEndpoints,
24 | )
25 | from ._webhook import (
26 | WebhookEndpoints
27 | )
28 |
29 | __all__ = (
30 | 'ChannelEndpoints',
31 | 'ApplicationCommandEndpoints',
32 | 'GatewayEndpoints',
33 | 'GuildTemplateEndpoints',
34 | 'GuildEndpoints',
35 | 'InteractionEndpoints',
36 | 'StickerEndpoints',
37 | 'UserEndpoints',
38 | 'WebhookEndpoints',
39 | )
40 |
--------------------------------------------------------------------------------
/library/wumpy-rest/wumpy/rest/endpoints/_gateway.py:
--------------------------------------------------------------------------------
1 | from discord_typings import GetGatewayBotData
2 |
3 | from .._requester import Requester
4 | from .._route import Route
5 |
6 | __all__ = (
7 | 'GatewayEndpoints',
8 | )
9 |
10 |
11 | class GatewayEndpoints(Requester):
12 | """Endpoints for getting Discord gateway information."""
13 |
14 | __slots__ = ()
15 |
16 | async def fetch_gateway(self) -> str:
17 | """Fetch a single valid WSS URL.
18 |
19 | The data this method returns should be cached and will not change.
20 |
21 | Returns:
22 | The URL to connect with a WebSocket to.
23 | """
24 | d = await self.request(Route('GET', '/gateway'))
25 | return d['url']
26 |
27 | async def fetch_gateway_bot(self) -> GetGatewayBotData:
28 | """Fetch a valid WSS URL along with helpful metadata.
29 |
30 | Especially useful when running big bots with shards, the data returned
31 | by this call can change per-call as the bot joins/leaves guilds.
32 |
33 | Returns:
34 | Useful metadata about the gateway when connecting.
35 | """
36 | return await self.request(Route('GET', '/gateway/bot'))
37 |
--------------------------------------------------------------------------------
/library/wumpy-rest/wumpy/rest/endpoints/_user.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional, SupportsInt
2 |
3 | from discord_typings import DMChannelData, PartialGuildData, UserData
4 |
5 | from .._requester import Requester
6 | from .._route import Route
7 | from .._utils import MISSING
8 |
9 | __all__ = (
10 | 'UserEndpoints',
11 | )
12 |
13 |
14 | class UserEndpoints(Requester):
15 | """Endpoints for interacting with user data."""
16 |
17 | __slots__ = ()
18 |
19 | async def fetch_my_user(self) -> UserData:
20 | """Fetch the bot user account.
21 |
22 | This is not a shortcut to `fetch_user()`, it has a different ratelimit
23 | and returns a bit more information.
24 |
25 | Returns:
26 | The user object received from Discord.
27 | """
28 | return await self.request(Route('GET', '/users/@me'))
29 |
30 | async def fetch_user(self, user: SupportsInt) -> UserData:
31 | """Fetch a data about a user by its ID.
32 |
33 | You also do not need to share a guild with the user to fetch their
34 | (limited) information.
35 |
36 | Parameters:
37 | user: The ID of the user to fetch.
38 |
39 | Returns:
40 | The user object received from Discord.
41 | """
42 | return await self.request(Route('GET', '/users/{user_id}', user_id=int(user)))
43 |
44 | async def edit_my_user(
45 | self,
46 | *,
47 | username: str = MISSING,
48 | avatar: Optional[str] = MISSING,
49 | ) -> UserData:
50 | """Edit the bot user account.
51 |
52 | Parameters:
53 | username:
54 | The new bot user's name (can cause the discriminator to
55 | be randomized if there are already a user with that name).
56 | avatar:
57 | Image data in the Data URI scheme, or None to reset the avatar
58 | to the default Discord version according to the discriminator.
59 |
60 | Returns:
61 | The updated and new user object from Discord.
62 | """
63 | if username is MISSING and avatar is MISSING:
64 | raise TypeError("at least one of 'username' or 'avatar' is required")
65 |
66 | payload = {
67 | 'username': username,
68 | 'avatar': avatar,
69 | }
70 |
71 | return await self.request(Route('PATCH', '/users/@me'), json=payload)
72 |
73 | async def fetch_my_guilds(
74 | self,
75 | *,
76 | before: SupportsInt = MISSING,
77 | after: SupportsInt = MISSING,
78 | limit: int = 200
79 | ) -> List[PartialGuildData]:
80 | """Fetch all guilds that the bot user is in.
81 |
82 | This endpoint allows pagination for bots who are a member of more than
83 | 200 guilds using the `before` and `after` parameters.
84 |
85 | Parameters:
86 | before: Snowflake to fetch guilds before.
87 | after: Snowflake to fetch guilds after.
88 | limit: How many guilds to maximally return.
89 |
90 | Returns:
91 | A list of partial guilds the bot user is a part of.
92 | """
93 | params = {'limit': limit}
94 | if before is not MISSING:
95 | params['before'] = int(before)
96 | elif after is not MISSING:
97 | params['after'] = int(after)
98 |
99 | return await self.request(Route('GET', '/users/@me/guilds'))
100 |
101 | async def leave_guild(self, guild: SupportsInt) -> None:
102 | """Make the bot user leave the specified guild.
103 |
104 | Parameters:
105 | guild: The ID of the guild to leave.
106 | """
107 | await self.request(Route('DELETE', '/users/@me/guilds/{guild_id}', guild_id=int(guild)))
108 |
109 | async def create_dm(self, recipient: SupportsInt) -> DMChannelData:
110 | """Create a DM with the recipient.
111 |
112 | This method is safe to call several times to get the DM channel when
113 | needed. In fact, in other wrappers this is called everytime you send
114 | a message to a user.
115 |
116 | Parameters:
117 | recipient: The user to open a DM with.
118 |
119 | Returns:
120 | The DM channel object returned by Discord.
121 | """
122 | return await self.request(Route(
123 | 'POST', '/users/@me/channels'), json={'recipient_id': int(recipient)}
124 | )
125 |
--------------------------------------------------------------------------------
/library/wumpy-rest/wumpy/rest/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wumpyproject/wumpy/08b1628a8aa4bddf6bd25b4e9418247783fa6663/library/wumpy-rest/wumpy/rest/py.typed
--------------------------------------------------------------------------------
/readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | mkdocs:
4 | configuration: docs/mkdocs.yml
5 | fail_on_warning: false
6 |
7 | python:
8 | version: '3.8'
9 | install:
10 | - requirements: docs/requirements.txt
11 | - method: pip
12 | path: .
13 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | # IMPORTANT: This is only for in-dev installation of the whole Git repository.
4 | # It has no stability guarantees and if possible, it is recommended
5 | # to 'cd' into each subpackage and install them with '--pth-file'
6 | # during development. This setuptools installation is only to allow
7 | # installation from git+https://github.com/wumpyproject/wumpy
8 |
9 |
10 | with open('README.md', 'r', encoding='utf-8') as readme:
11 | long_description = readme.read()
12 |
13 |
14 | setup(
15 | name='wumpy',
16 |
17 | # This version is not representative of Wumpy's real version, rather it is
18 | # set to satisfy and be explicit about versions.
19 | version='0.1.0a0',
20 |
21 | description='Discord API Wrapper - Easy enough for Wumpus, and fast enough for Clyde!',
22 | long_description=long_description,
23 |
24 | author='Bluenix',
25 | author_email='bluenixdev@gmail.com',
26 |
27 | packages=[
28 | 'wumpy.bot', 'wumpy.cache', 'wumpy.gateway', 'wumpy.interactions',
29 | 'wumpy.interactions.commands', 'wumpy.interactions.components',
30 | 'wumpy.models', 'wumpy.rest.endpoints', 'wumpy.rest',
31 | ],
32 |
33 | package_dir={
34 | 'wumpy.bot': 'library/wumpy-bot/wumpy/bot',
35 | 'wumpy.cache': 'library/wumpy-cache/wumpy/cache',
36 | 'wumpy.gateway': 'library/wumpy-gateway/wumpy/gateway',
37 | 'wumpy.interactions': 'library/wumpy-interactions/wumpy/interactions',
38 | 'wumpy.models': 'library/wumpy-models/wumpy/models',
39 | 'wumpy.rest': 'library/wumpy-rest/wumpy/rest',
40 | },
41 | package_data={
42 | '': ['py.typed'],
43 | },
44 |
45 | # Sadly we have to duplicate these from the pyproject.toml files
46 | install_requires=[
47 | 'anyio >= 3.3.4, < 4',
48 | 'httpx[http2] >= 0.22, < 1',
49 | 'discord-typings >= 0.4.0, <1',
50 | 'discord-gateway >=0.3.0, <1',
51 | 'pynacl > 1, < 2',
52 | 'typing_extensions >= 4, <5',
53 | ],
54 | )
55 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # Testing an API Wrapper..
2 |
3 | It is quite hard to test an API wrapper. The actual interaction with Discord is
4 | extremely hard to replicate and will never be enough, hence it will never be
5 | correctly tested.
6 |
7 | That said, there are still things to test other than the interaction with
8 | Discord. Namely setting up commands, adding listeners and loading extensions.
9 |
10 | ## Running the test suite
11 |
12 | Firstly, install [pytest](https://docs.pytest.org/en/latest/) and
13 | [coverage.py](https://coverage.readthedocs.io/en/latest).
14 |
15 | Then run the test suit with:
16 |
17 | ```bash
18 | coverage run --branch -m pytest tests/
19 | ```
20 |
21 | You can then run the following to generate a coverage report:
22 |
23 | ```bash
24 | coverage.html
25 | ```
26 |
--------------------------------------------------------------------------------
/tests/wumpy-bot/extensions/data_passed.py:
--------------------------------------------------------------------------------
1 | def loader(_, data):
2 | assert data.get('passed') is True
3 |
4 | return lambda _: _
5 |
--------------------------------------------------------------------------------
/tests/wumpy-bot/extensions/empty.py:
--------------------------------------------------------------------------------
1 | from wumpy.bot import Extension
2 |
3 | ext = Extension()
4 |
--------------------------------------------------------------------------------
/tests/wumpy-bot/extensions/non_callable.py:
--------------------------------------------------------------------------------
1 | var = 5
2 |
--------------------------------------------------------------------------------
/tests/wumpy-bot/extensions/raises.py:
--------------------------------------------------------------------------------
1 | raise NotImplementedError()
2 |
--------------------------------------------------------------------------------
/tests/wumpy-bot/extensions/raises_load.py:
--------------------------------------------------------------------------------
1 | def loader(target, data):
2 | raise NotImplementedError
3 |
--------------------------------------------------------------------------------
/tests/wumpy-bot/extensions/raises_unload.py:
--------------------------------------------------------------------------------
1 | def unloader(target):
2 | raise NotImplementedError
3 |
4 |
5 | def loader(target, data):
6 | return unloader
7 |
--------------------------------------------------------------------------------
/tests/wumpy-bot/test_extension.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from wumpy.bot import (
3 | EventDispatcher, Extension, ExtensionFailure, ExtensionLoader
4 | )
5 | from wumpy.bot._extension import _is_submodule
6 |
7 |
8 | def test_is_submodule():
9 | # At the stage of when this is used all relative imports have been resolved
10 | # so this test only consists of absolute paths
11 |
12 | assert _is_submodule('wumpy', 'wumpy') is True
13 | assert _is_submodule('wumpy.extension', 'wumpy') is True
14 | assert _is_submodule('wumpy.interactions.commands.slash', 'wumpy.interactions') is True
15 |
16 | # Some of these are pretty stupid, but you need some tests that test the
17 | # obvious I guess..
18 | assert _is_submodule('wumpy', 'discord') is False
19 | assert _is_submodule('wumpy.models', 'discord.ext.commands') is False
20 | assert _is_submodule('wumpy.interactions', 'wumpy.commands') is False
21 |
22 |
23 | def test_data_runtimeerror():
24 | ext = Extension()
25 |
26 | with pytest.raises(RuntimeError):
27 | # 'data' is not yet accessible because the extensions hasn't been given
28 | # it by being loaded.
29 | _ = ext.data
30 |
31 |
32 | class TestExtensionTransfer:
33 | def test_callable(self):
34 | ext = Extension()
35 | dispatcher = EventDispatcher()
36 |
37 | unload = ext(dispatcher, {})
38 | unload(dispatcher)
39 |
40 | def test_data(self):
41 | ext = Extension()
42 | dispatcher = EventDispatcher()
43 |
44 | instance = object()
45 | ext.load(dispatcher, {'key': 'value', 'kwarg': 0, 'object': instance})
46 |
47 | assert ext.data == {'key': 'value', 'kwarg': 0, 'object': instance}
48 |
49 |
50 | class TestExtensionLoader:
51 | def test_relative_no_package(self):
52 | loader = ExtensionLoader()
53 |
54 | with pytest.raises(TypeError):
55 | loader.load_extension('.abc:xyz')
56 |
57 | def test_relative_too_far(self):
58 | loader = ExtensionLoader()
59 |
60 | with pytest.raises(ValueError):
61 | # Note: __package__ in this case is an empty string because of
62 | # where __main__ is in relation to this file.
63 | loader.load_extension('......abc:xyz', 'tests')
64 |
65 | def test_no_spec(self):
66 | loader = ExtensionLoader()
67 |
68 | with pytest.raises(ValueError):
69 | loader.load_extension('README:non_existant')
70 |
71 | def test_exec_exception(self):
72 | loader = ExtensionLoader()
73 |
74 | with pytest.raises(ExtensionFailure):
75 | loader.load_extension('extensions.raises:func')
76 |
77 | def test_bad_attribute(self):
78 | loader = ExtensionLoader()
79 |
80 | with pytest.raises(ValueError):
81 | loader.load_extension('extensions.empty:other')
82 |
83 | def test_non_callable(self):
84 | loader = ExtensionLoader()
85 |
86 | with pytest.raises(ExtensionFailure):
87 | loader.load_extension('extensions.non_callable:var')
88 |
89 | def test_loader_raises(self):
90 | loader = ExtensionLoader()
91 |
92 | with pytest.raises(ExtensionFailure):
93 | loader.load_extension('extensions.raises_load:loader')
94 |
95 | def test_data_passed(self):
96 | loader = ExtensionLoader()
97 |
98 | loader.load_extension('extensions.data_passed:loader', passed=True)
99 | # The test continues in that file
100 |
101 | # Because of how pytest runs the tests __package__ is an empty string,
102 | # causing the test to fail with a TypeError - there's no supported or real
103 | # reason to use relative imports here (unless you're writing tests :p).
104 |
105 | # def test_relative(self):
106 | # loader = ExtensionLoader()
107 | #
108 | # loader.load_extension('.extensions.empty:ext', __package__)
109 |
110 |
111 | class TestUnload:
112 | def test_relative_no_package(self):
113 | loader = ExtensionLoader()
114 |
115 | with pytest.raises(TypeError):
116 | loader.unload_extension('.abc:xyz')
117 |
118 | def test_relative_too_far(self):
119 | loader = ExtensionLoader()
120 |
121 | with pytest.raises(ValueError):
122 | loader.unload_extension('......abc:xyz', 'tests.wumpy')
123 |
124 | def test_raises_unload(self):
125 | loader = ExtensionLoader()
126 | loader.load_extension('extensions.raises_unload:loader')
127 |
128 | with pytest.raises(ExtensionFailure):
129 | loader.unload_extension('extensions.raises_unload')
130 |
131 | def test_unload_not_loaded(self):
132 | loader = ExtensionLoader()
133 |
134 | with pytest.raises(ValueError):
135 | loader.unload_extension('extensions.empty')
136 |
--------------------------------------------------------------------------------
/tests/wumpy-gateway/test_limiter.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from time import perf_counter
3 | from typing import NoReturn
4 | from unittest import mock
5 |
6 | import pytest
7 | from discord_gateway import Opcode
8 | from wumpy.gateway import DefaultGatewayLimiter
9 |
10 |
11 | class SimplerGatewayLimiter(DefaultGatewayLimiter):
12 | RATE = 3
13 | PER = 10
14 |
15 |
16 | class TestGatewayLimiter:
17 | @pytest.mark.anyio
18 | async def test_bypass(self) -> None:
19 | async def sleep(duration: float) -> NoReturn:
20 | raise RuntimeError("'sleep()' should not have been called")
21 |
22 | async with SimplerGatewayLimiter() as limiter:
23 | with mock.patch('anyio.sleep', sleep):
24 | for _ in range(SimplerGatewayLimiter.RATE + 1):
25 | async with limiter(Opcode.HEARTBEAT):
26 | pass
27 |
28 | @pytest.mark.anyio
29 | @pytest.mark.skipif(sys.version_info < (3, 8), reason='AsyncMock requires Python 3.8+')
30 | async def test_wait(self) -> None:
31 | slept = mock.AsyncMock()
32 |
33 | async with SimplerGatewayLimiter() as limiter:
34 | with mock.patch('anyio.sleep', slept):
35 | for _ in range(SimplerGatewayLimiter.RATE + 1):
36 | async with limiter(Opcode.PRESENCE_UPDATE):
37 | pass
38 |
39 | assert slept.call_count == 1
40 |
41 | @pytest.mark.anyio
42 | async def test_time_passed(self) -> None:
43 | async def sleep(duration: float) -> NoReturn:
44 | raise RuntimeError("'sleep()' should not have been called")
45 |
46 | time = perf_counter()
47 |
48 | def patched():
49 | return time + SimplerGatewayLimiter.PER * 1.1
50 |
51 | async with SimplerGatewayLimiter() as limiter:
52 | for _ in range(SimplerGatewayLimiter.RATE):
53 | async with limiter(Opcode.PRESENCE_UPDATE):
54 | pass
55 |
56 | with mock.patch('time.perf_counter', patched), mock.patch('anyio.sleep', sleep):
57 | async with limiter(Opcode.PRESENCE_UPDATE):
58 | pass
59 |
--------------------------------------------------------------------------------
/tests/wumpy-interactions/commands/test_context.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from wumpy.interactions import (
3 | CommandInteraction, CommandRegistrar, CommandType
4 | )
5 |
6 |
7 | @pytest.mark.parametrize('commandtype', [CommandType.message, CommandType.user])
8 | class TestContextCommand:
9 | def test_no_annotation(self, commandtype: CommandType):
10 | registrar = CommandRegistrar()
11 |
12 | # For context menu commands there is only one possible annotation so
13 | # we get enough information from the command type.
14 | @registrar.command(commandtype)
15 | async def test(interaction, argument):
16 | ...
17 |
18 | assert registrar.get_command('test') == test
19 |
20 | def test_non_async(self, commandtype: CommandType):
21 | registrar = CommandRegistrar()
22 |
23 | with pytest.raises(TypeError):
24 | @registrar.command(commandtype) # type: ignore
25 | def non_async(interaction, argument):
26 | ...
27 |
28 | def test_wrong_args(self, commandtype: CommandType):
29 | registrar = CommandRegistrar()
30 |
31 | with pytest.raises(TypeError):
32 | @registrar.command(commandtype) # type: ignore
33 | async def no_args():
34 | ...
35 |
36 | with pytest.raises(TypeError):
37 | @registrar.command(commandtype) # type: ignore
38 | async def too_many_args(interaction, argument, too_much):
39 | ...
40 |
41 | def test_bad_annotation(self, commandtype: CommandType):
42 | registrar = CommandRegistrar()
43 |
44 | with pytest.raises(TypeError):
45 | @registrar.command(commandtype) # type: ignore
46 | async def int_arg(interaction: CommandInteraction, argument: int):
47 | ...
48 |
49 | with pytest.raises(TypeError):
50 | @registrar.command(commandtype) # type: ignore
51 | async def str_arg(interaction: CommandInteraction, argument: str):
52 | ...
53 |
54 | def test_bad_interaction_annotation(self, commandtype: CommandType):
55 | registrar = CommandRegistrar()
56 |
57 | with pytest.raises(TypeError):
58 | @registrar.command(commandtype) # type: ignore
59 | async def bad_annotation(interaction: str, argument):
60 | ...
61 |
62 | def test_argument_name(self, commandtype: CommandType):
63 | # The command should work no matter the names of the arguments
64 | registrar = CommandRegistrar()
65 |
66 | @registrar.command(commandtype)
67 | async def weird_arg_name(ctx: CommandInteraction, target):
68 | ...
69 |
70 | def test_kwarg(self, commandtype: CommandType):
71 | registrar = CommandRegistrar()
72 |
73 | with pytest.raises(TypeError):
74 | @registrar.command(commandtype)
75 | async def kwargs(*, interaction: CommandInteraction, argument):
76 | ...
77 |
--------------------------------------------------------------------------------
/tests/wumpy-interactions/commands/test_decorators.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from wumpy.interactions import CommandRegistrar, Option, option
3 | from wumpy.interactions.commands._option import OptionClass
4 | from wumpy.models import ApplicationCommandOption
5 |
6 |
7 | def test_option_func():
8 | original = OptionClass(
9 | 'default', name='name', description='description',
10 | required=True, choices=['A', 'B', 'C'],
11 | type=ApplicationCommandOption.string
12 | )
13 |
14 | default = Option(
15 | 'default', name='name', description='description',
16 | required=True, choices=['A', 'B', 'C'],
17 | type=ApplicationCommandOption.string
18 | )
19 |
20 | # These two should be identical
21 |
22 | assert original.name == default.name
23 | assert original.description == default.description
24 | assert original.required == default.required
25 | assert original.default == default.default
26 | assert original.choices == default.choices
27 | assert original.type == default.type
28 |
29 |
30 | def test_option_func_min_max():
31 | original = OptionClass(
32 | 'default', name='name', description='description',
33 | min=500, max=1000,
34 | type=ApplicationCommandOption.integer
35 | )
36 |
37 | default = Option(
38 | 'default', name='name', description='description',
39 | min=500, max=1000,
40 | type=ApplicationCommandOption.integer
41 | )
42 |
43 | # These two should be identical
44 |
45 | assert original.name == default.name
46 | assert original.description == default.description
47 | assert original.default == default.default
48 | assert original.min == default.min
49 | assert original.max == default.max
50 | assert original.type == default.type
51 |
52 |
53 | def test_option_deco():
54 | registrar = CommandRegistrar()
55 |
56 | @option(
57 | 'other', name='else', type=str,
58 | choices={'Answer A': 'A', 'Answer B': 'B', 'Answer C': 'C'}
59 | )
60 | @option('arg', description='The only argument', type=int, min=0, max=10)
61 | @registrar.command()
62 | # The first argument will always be the interaction
63 | async def command(_, arg, other):
64 | """Option testing command."""
65 | ...
66 |
67 | assert command.options['arg'].description == 'The only argument'
68 | assert command.options['arg'].type == ApplicationCommandOption.integer
69 | assert command.options['arg'].min == 0
70 | assert command.options['arg'].max == 10
71 |
72 | # It should've been renamed now from 'other'
73 | assert command.options['else'].name == 'else'
74 | assert command.options['else'].type == ApplicationCommandOption.string
75 | assert command.options['else'].choices == {
76 | 'Answer A': 'A', 'Answer B': 'B', 'Answer C': 'C'
77 | }
78 |
79 |
80 | def test_option_deco_no_command():
81 | with pytest.raises(TypeError):
82 | @option('arg', choices=['A', 'B', 'C']) # type: ignore
83 | async def not_command(arg):
84 | ...
85 |
86 |
87 | def test_option_deco_no_param():
88 | registrar = CommandRegistrar()
89 |
90 | with pytest.raises(ValueError):
91 | @option('bad', description="Doesn't exist")
92 | @registrar.command()
93 | async def command(param):
94 | ...
95 |
--------------------------------------------------------------------------------
/tests/wumpy-models/test_base.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 |
3 | from wumpy.models import Model, Snowflake
4 |
5 |
6 | def test_instantiation() -> None:
7 | model = Model(344404945359077377)
8 | assert model.id == 344404945359077377
9 |
10 |
11 | def test_hashable() -> None:
12 | d = {Model(344404945359077377): True}
13 | assert d[Model(344404945359077377)]
14 |
15 |
16 | def test_conversion() -> None:
17 | id_ = 344404945359077377
18 | m = Model(id_)
19 |
20 | assert int(m) == id_
21 | assert float(m) == float(id_)
22 | assert complex(m) == complex(id_)
23 | assert bin(m) == bin(id_) # __index__
24 |
25 |
26 | def test_equal() -> None:
27 | assert Model(344404945359077377) == 344404945359077377
28 |
29 | assert Model(344404945359077377) == Model(344404945359077377)
30 | assert not Model(344404945359077377) == 'Bluenix#7543'
31 | assert not Model(344404945359077377) == Model(841509053422632990)
32 |
33 |
34 | def test_not_equal() -> None:
35 | assert Model(344404945359077377) != 841509053422632990
36 |
37 | assert Model(344404945359077377) != Model(841509053422632990)
38 | assert Model(344404945359077377) != 'Bluenix#7543'
39 | assert not Model(344404945359077377) != Model(344404945359077377)
40 |
41 |
42 | def test_created_at() -> None:
43 | dt = datetime.fromtimestamp(1502182937.708, timezone.utc)
44 | assert Model(344404945359077377).created_at == dt
45 |
46 |
47 | def test_snowflake_info() -> None:
48 | m = Snowflake(175928847299117063)
49 |
50 | assert m.worker_id == 1
51 | assert m.process_id == 0
52 | assert m.process_increment == 7
53 |
54 |
55 | def test_snowflake_from_datetime() -> None:
56 | dt = datetime.fromtimestamp(1462015105796 / 1000)
57 | assert Snowflake.from_datetime(dt) == Snowflake(175928847298985984)
58 |
--------------------------------------------------------------------------------
/tests/wumpy-models/test_flags.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from wumpy.models._flags import BitMask, DiscordFlags
3 | from wumpy.models._permissions import TriBitMask
4 |
5 |
6 | class TestMagicMethods:
7 | def test_equality(self):
8 | flag, other = DiscordFlags(0), DiscordFlags(0)
9 |
10 | assert flag == 0
11 | assert flag == other
12 | assert not flag == 'a'
13 |
14 | def test_not_equality(self):
15 | flag, other = DiscordFlags(0), DiscordFlags(1)
16 |
17 | assert flag != 1
18 | assert flag != other
19 | assert flag != 'a'
20 |
21 | def test_convertable(self):
22 | flag = DiscordFlags(123)
23 |
24 | assert int(flag) == 123
25 | assert float(flag) == 123.0
26 |
27 | def test_hashable(self):
28 | assert hash(DiscordFlags(0)) == hash(DiscordFlags(0))
29 | assert hash(DiscordFlags(123)) != hash(DiscordFlags(321))
30 |
31 | def test_bitwise_and(self):
32 | a, b = DiscordFlags(0b1010), DiscordFlags(0b0111)
33 | assert a & b == DiscordFlags(0b0010)
34 |
35 | x, y = DiscordFlags(0b0001), 0b1000
36 | assert x & y == DiscordFlags(0b0000)
37 |
38 | with pytest.raises(TypeError):
39 | DiscordFlags(0b1010) & 'a'
40 |
41 | def test_bitwise_xor(self):
42 | a, b = DiscordFlags(0b1100), DiscordFlags(0b0111)
43 | assert a ^ b == DiscordFlags(0b1011)
44 |
45 | x, y = DiscordFlags(0b1111), 0b1111
46 | assert x ^ y == DiscordFlags(0b0000)
47 |
48 | with pytest.raises(TypeError):
49 | DiscordFlags(0b1100) ^ 'a'
50 |
51 | def test_bitwise_or(self):
52 | a, b = DiscordFlags(0b0001), DiscordFlags(0b1000)
53 | assert a | b == DiscordFlags(0b1001)
54 |
55 | x, y = DiscordFlags(0b1000), 0b0010
56 | assert x | y == DiscordFlags(0b1010)
57 |
58 | with pytest.raises(TypeError):
59 | DiscordFlags(0b0000) | 'a'
60 |
61 |
62 | class Flag(DiscordFlags):
63 | red = BitMask(1 << 0)
64 | orange = BitMask(1 << 1)
65 | yellow = BitMask(1 << 2)
66 | green = BitMask(1 << 3)
67 | blue = BitMask(1 << 4)
68 | indigo = BitMask(1 << 5)
69 | violet = BitMask(1 << 6)
70 |
71 |
72 | class TestBitMask:
73 | def test_type_get(self):
74 | assert Flag.yellow == 1 << 2
75 | assert Flag.blue == 1 << 4
76 |
77 | def test_instance_get(self):
78 | instance = Flag(Flag.red | Flag.blue)
79 |
80 | assert instance.red
81 | assert instance.blue
82 |
83 |
84 | class GreenProfile:
85 | # Colour combinations that green can be with in a flag
86 | def __init__(self, allow: int, deny: int) -> None:
87 | self.allow = Flag(allow)
88 | self.deny = Flag(deny)
89 |
90 | red = TriBitMask(1 << 0)
91 | orange = TriBitMask(1 << 1)
92 | yellow = TriBitMask(1 << 2)
93 | green = TriBitMask(1 << 3)
94 | blue = TriBitMask(1 << 4)
95 | indigo = TriBitMask(1 << 5)
96 | violet = TriBitMask(1 << 6)
97 |
98 |
99 | class TestTriBitMask:
100 | def test_type_get(self):
101 | assert GreenProfile.green == 1 << 3
102 | assert GreenProfile.blue == 1 << 4
103 |
104 | def test_instance_get(self):
105 | instance = GreenProfile(
106 | GreenProfile.green | GreenProfile.blue,
107 | GreenProfile.red | GreenProfile.orange
108 | )
109 |
110 | assert instance.green
111 | assert not instance.orange
112 |
113 | assert instance.violet is None
114 |
--------------------------------------------------------------------------------
/tests/wumpy-models/test_message.py:
--------------------------------------------------------------------------------
1 | from wumpy.models import AllowedMentions
2 |
3 |
4 | def test_equal() -> None:
5 | assert AllowedMentions() == AllowedMentions()
6 | assert not AllowedMentions() == 'AllowedMentions'
7 |
8 |
9 | def test_instantion() -> None:
10 | am = AllowedMentions(everyone=False, users=(123,), roles=(456,), replied_user=True)
11 | assert am.everyone is False
12 | assert am.users == (123,)
13 | assert am.roles == (456,)
14 | assert am.replied_user is True
15 |
16 |
17 | def test_none() -> None:
18 | assert AllowedMentions.none() == {
19 | 'parse': [],
20 | 'replied_user': False,
21 | }
22 |
23 |
24 | def test_all() -> None:
25 | assert AllowedMentions.all() == {
26 | 'parse': ['everyone', 'users', 'roles'],
27 | 'replied_user': True,
28 | }
29 |
30 |
31 | def test_bitwise_or() -> None:
32 | a = AllowedMentions(everyone=False, replied_user=True)
33 | b = AllowedMentions(everyone=True, users=[123, 456, 789])
34 | assert a | b == AllowedMentions(everyone=True, users=[123, 456, 789], replied_user=True)
35 |
36 |
37 | def test_replace() -> None:
38 | replaced = AllowedMentions(everyone=False, roles=[123]).replace(everyone=True)
39 | assert replaced == AllowedMentions(everyone=True, roles=[123])
40 |
--------------------------------------------------------------------------------
/tests/wumpy-models/test_permissions.py:
--------------------------------------------------------------------------------
1 | from wumpy.models import PermissionOverwrite, Permissions, PermissionTarget
2 |
3 |
4 | def test_permission_build() -> None:
5 | perms = Permissions.build(
6 | send_messages=True, embed_links=True, add_reactions=True, connect=True
7 | )
8 |
9 | assert perms.value == 1067072
10 |
11 |
12 | def test_permission_replace() -> None:
13 | perms = Permissions.build(create_instant_invite=True, ban_members=True)
14 | assert perms.replace(kick_members=True, ban_members=False) == Permissions(3)
15 |
16 |
17 | def test_overwrite_build() -> None:
18 | overwrite = PermissionOverwrite.build(
19 | 344404945359077377, PermissionTarget.member,
20 | send_messages=True, send_messages_in_threads=True,
21 | mute_members=False, move_members=False, priority_speaker=False, stream=False
22 | )
23 |
24 | assert overwrite.allow == Permissions(274877908992)
25 | assert overwrite.deny == Permissions(20972288)
26 |
27 |
28 | def test_overwrite_equality() -> None:
29 | a = PermissionOverwrite.build(
30 | 344404945359077377, PermissionTarget.member,
31 | add_reactions=True, embed_links=False
32 | )
33 | b = PermissionOverwrite.build(
34 | 344404945359077377, PermissionTarget.member,
35 | add_reactions=True, embed_links=False
36 | )
37 | assert a == b
38 |
39 |
40 | def test_overwrite_different_id() -> None:
41 | a = PermissionOverwrite.build(
42 | 344404945359077377, PermissionTarget.member,
43 | connect=True, speak=False
44 | )
45 | b = PermissionOverwrite.build(
46 | 841509053422632990, PermissionTarget.member,
47 | connect=True, speak=False
48 | )
49 | assert not a == b
50 | assert a != b
51 |
52 |
53 | def test_overwrite_inequality() -> None:
54 | a = PermissionOverwrite.build(
55 | 344404945359077377, PermissionTarget.member,
56 | manage_roles=True, manage_nicknames=True, manage_channels=False
57 | )
58 | b = PermissionOverwrite.build(
59 | 344404945359077377, PermissionTarget.member,
60 | manage_nicknames=True, manage_channels=True
61 | )
62 | assert a != b
63 |
64 |
65 | def test_overwrite_replace() -> None:
66 | overwrite = PermissionOverwrite.build(
67 | 344404945359077377, PermissionTarget.member,
68 | send_messages=True, send_messages_in_threads=True,
69 | add_reactions=False, embed_links=False
70 | )
71 | replaced = overwrite.replace(speak=True, send_messages_in_threads=False, embed_links=None)
72 | assert replaced.allow == Permissions(2099200)
73 | assert replaced.deny == Permissions(274877907008)
74 |
--------------------------------------------------------------------------------
/tests/wumpy-rest/test_ratelimiter.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 |
3 | import pytest
4 | from wumpy.rest import DictRatelimiter
5 |
6 | if TYPE_CHECKING:
7 | from wumpy.testing.suites import RatelimiterSuite
8 | else:
9 | __mod = pytest.importorskip(
10 | 'wumpy.testing.suites', reason='Wumpy-testing is required for the ratelimiter tests'
11 | )
12 | RatelimiterSuite = __mod.RatelimiterSuite
13 | del __mod
14 |
15 |
16 | class TestDictRatelimiter(RatelimiterSuite):
17 | def get_impl(self) -> DictRatelimiter:
18 | return DictRatelimiter()
19 |
--------------------------------------------------------------------------------