diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..44b4224b --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* eol=lf \ No newline at end of file diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml deleted file mode 100644 index 60b37a8f..00000000 --- a/.github/workflows/python-package.yml +++ /dev/null @@ -1,41 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Test Python package - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [ 3.6, 3.7, 3.8, 3.9 ] - - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install testing dependencies - uses: py-actions/py-dependency-install@v2 - with: - path: "requirements-dev.txt" - - name: Install itself - run: | - python setup.py install - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics - - name: Test with pytest - run: | - pytest diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 1eba4d89..00000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -name: Upload Python Package - -on: - release: - types: [ created ] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine -r requirements.txt - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..bc2c4346 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: Publish +on: + release: + types: [created] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.13 + - name: Install dependencies + run: python3 -m pip install build twine + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + python3 -m build + python3 -m twine upload dist/* \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..fa960764 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: Run tests +on: + push: + branches: [master] + paths: + - '.github/workflows/test.yml' + - 'topgg/**/*.py' + pull_request: + paths: + - '.github/workflows/test.yml' + - 'topgg/**/*.py' +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.9, '3.10', 3.11, 3.12, 3.13] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: python3 -m pip install . -r requirements-dev.txt + - name: Run tests + run: python3 -m pytest + - name: Install ruff + if: ${{ matrix.python-version == 3.13 }} + run: python3 -m pip install ruff + - name: Lint with ruff + if: ${{ matrix.python-version == 3.13 }} + run: python3 -m ruff check \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3d7edd2f..52fd1028 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ -dblpy.egg-info/ topggpy.egg-info/ -topgg/__pycache__/ build/ dist/ -/docs/_build -/docs/_templates -.vscode -/.idea/ -__pycache__ +docs/_build +docs/_templates +.vscode/ +.idea/ +**/__pycache__/ .coverage +.ruff_cache/ \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 317f1d34..00000000 --- a/.isort.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[settings] -profile=black -multi_line_output=3 \ No newline at end of file diff --git a/.readthedocs.yml b/.readthedocs.yml index 388f9a1f..1bbdc13e 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,13 +1,11 @@ version: 2 - -sphinx: - configuration: docs/conf.py - build: - image: latest - + os: ubuntu-24.04 + tools: + python: '3.13' +sphinx: + configuration: docs/conf.py python: - version: 3.8 install: - - requirements: requirements.txt - - requirements: requirements-docs.txt + - requirements: requirements.txt + - requirements: requirements-docs.txt \ No newline at end of file diff --git a/LICENSE b/LICENSE index 96aaaf80..4624af32 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ Copyright 2021 Assanali Mukhanov & Top.gg +Copyright 2024-2025 null8626 & Top.gg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/MANIFEST.in b/MANIFEST.in index dc068afa..bf4d57b9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,16 @@ -include LICENSE -include requirements.txt -include README.rst +prune .github +prune .ruff_cache +prune docs +prune examples +prune tests +exclude .gitattributes +exclude .gitignore +exclude .readthedocs.yml +exclude mypy.ini +exclude pytest.ini +exclude requirements.txt +exclude requirements-dev.txt +exclude requirements-docs.txt +exclude ruff.toml +exclude ISSUE_TEMPLATE.md +exclude PULL_REQUEST_TEMPLATE.md \ No newline at end of file diff --git a/README.rst b/README.rst index 5bdddce6..928930b5 100644 --- a/README.rst +++ b/README.rst @@ -4,42 +4,30 @@ Top.gg Python Library .. image:: https://img.shields.io/pypi/v/topggpy.svg :target: https://pypi.python.org/pypi/topggpy - :alt: View on PyPi -.. image:: https://img.shields.io/pypi/pyversions/topggpy.svg - :target: https://pypi.python.org/pypi/topggpy - :alt: v1.0.0 -.. image:: https://readthedocs.org/projects/topggpy/badge/?version=latest + :alt: View on PyPI +.. image:: https://img.shields.io/pypi/dm/topggpy?style=flat-square :target: https://topggpy.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status + :alt: Monthly PyPI downloads -A simple API wrapper for `Top.gg `_ written in Python, supporting discord.py. +A simple API wrapper for `Top.gg `_ written in Python. Installation ------------ -Install via pip (recommended) - -.. code:: bash - - pip3 install topggpy - -Install from source - .. code:: bash - pip3 install git+https://github.com/top-gg/python-sdk/ + pip install topggpy Documentation ------------- -Documentation can be found `here `_ +Documentation can be found `here `_ Features -------- * POST server count * GET bot info, server count, upvote info -* GET all bots * GET user info * GET widgets (large and small) including custom ones. See `docs.top.gg `_ for more info. * GET weekend status diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css deleted file mode 100644 index c7e6795e..00000000 --- a/docs/_static/css/custom.css +++ /dev/null @@ -1,7 +0,0 @@ -header #logo-container img { - height: 100px; -} - -#search input[type="text"] { - font-size: 1em; -} \ No newline at end of file diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 00000000..ad108bb2 Binary files /dev/null and b/docs/_static/favicon.ico differ diff --git a/docs/_static/img/favicon-16x16.png b/docs/_static/img/favicon-16x16.png deleted file mode 100644 index 58c60e92..00000000 Binary files a/docs/_static/img/favicon-16x16.png and /dev/null differ diff --git a/docs/_static/script.js b/docs/_static/script.js new file mode 100644 index 00000000..c8cfd5b7 --- /dev/null +++ b/docs/_static/script.js @@ -0,0 +1,20 @@ +document.addEventListener('load', () => { + try { + document.querySelector('.edit-this-page').remove() + + // remove these useless crap that appears on official readthedocs builds + document.querySelector('#furo-readthedocs-versions').remove() + document.querySelector('.injected').remove() + } catch { + // we're building this locally, forget it + } +}) + +for (const label of document.querySelectorAll('.sidebar-container label')) { + const link = [...label.parentElement.children].find(child => child.nodeName === 'A') + + link.addEventListener('click', event => { + event.preventDefault() + label.click() + }) +} \ No newline at end of file diff --git a/docs/_static/style.css b/docs/_static/style.css new file mode 100644 index 00000000..ee1e4dd5 --- /dev/null +++ b/docs/_static/style.css @@ -0,0 +1,48 @@ +body { + --color-link-underline: rgba(0, 0, 0, 0); + --color-link-underline--hover: var(--color-link); + --color-inline-code-background: rgba(0, 0, 0, 0); + --color-api-background-hover: var(--color-background-primary); + --color-highlight-on-target: var(--color-background-primary) !important; + + --font-stack: "Inter", sans-serif !important; + --font-stack--monospace: "Roboto Mono", monospace !important; +} + +aside.toc-drawer { + visibility: hidden; +} + +#furo-readthedocs-versions, .injected, .edit-this-page, .related-pages, .headerlink { + visibility: hidden; + user-select: none; +} + +dd dt { + color: var(--color-foreground-secondary); +} + +aside.toc-drawer .docutils:hover, .sidebar-brand-text:hover { + transition: 0.15s; + filter: opacity(75%); +} + +.highlight .c1, em { + font-style: normal !important; +} + +.highlight .nn { + text-decoration: none !important; +} + +h1 { + font-weight: 900; +} + +.sidebar-brand-text { + font-weight: bolder; +} + +.sidebar-scroll .reference.internal { + color: var(--color-brand-primary); +} \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index 69691659..00000000 --- a/docs/api.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. currentmodule:: topgg - -############# -API Reference -############# - -The following section outlines the API of topggpy. - -Index: - - .. toctree:: - :maxdepth: 2 - - api/autopost - api/client - api/data - api/errors - api/types - api/webhook \ No newline at end of file diff --git a/docs/api/autopost.rst b/docs/api/autopost.rst index 668af79a..0151646d 100644 --- a/docs/api/autopost.rst +++ b/docs/api/autopost.rst @@ -1,6 +1,6 @@ -####################### -Auto-post API Reference -####################### +################## +Autopost reference +################## .. automodule:: topgg.autopost :members: diff --git a/docs/api/client.rst b/docs/api/client.rst index 1bac1971..1d8966e8 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -1,6 +1,6 @@ -#################### -Client API Reference -#################### +################ +Client reference +################ .. automodule:: topgg.client :members: diff --git a/docs/api/data.rst b/docs/api/data.rst index 3f10ff2e..090494ef 100644 --- a/docs/api/data.rst +++ b/docs/api/data.rst @@ -1,6 +1,6 @@ -################## -Data API Reference -################## +############## +Data reference +############## .. automodule:: topgg.data :members: diff --git a/docs/api/errors.rst b/docs/api/errors.rst index 804fdfa3..d54af678 100644 --- a/docs/api/errors.rst +++ b/docs/api/errors.rst @@ -1,7 +1,6 @@ -#################### -Errors API Reference -#################### +################ +Errors reference +################ .. automodule:: topgg.errors - :members: - :inherited-members: \ No newline at end of file + :members: \ No newline at end of file diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 00000000..4c4e05bd --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,13 @@ +############# +API reference +############# + + .. toctree:: + :maxdepth: 2 + + autopost + client + data + errors + types + webhook \ No newline at end of file diff --git a/docs/api/types.rst b/docs/api/types.rst index a6a70f84..14b983db 100644 --- a/docs/api/types.rst +++ b/docs/api/types.rst @@ -1,6 +1,6 @@ -#################### -Models API Reference -#################### +################ +Models reference +################ .. automodule:: topgg.types :members: diff --git a/docs/api/webhook.rst b/docs/api/webhook.rst index 53a41c92..c1b067a5 100644 --- a/docs/api/webhook.rst +++ b/docs/api/webhook.rst @@ -1,6 +1,6 @@ -##################### -Webhook API Reference -##################### +################# +Webhook reference +################# .. automodule:: topgg.webhook :members: diff --git a/docs/conf.py b/docs/conf.py index 2d368576..b2308b58 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,23 +21,10 @@ import os import sys -import alabaster - sys.path.insert(0, os.path.abspath("../")) -from topgg import __version__ as version - -# import re +from topgg import __version__ as version -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.viewcode", @@ -45,181 +32,61 @@ "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", + "sphinx_reredirects", ] autodoc_member_order = "groupwise" extlinks = { - "issue": ("https://github.com/top-gg/python-sdk/issues/%s", "GH-"), + "issue": ("https://github.com/top-gg-community/python-sdk/issues/%s", "#%s"), } intersphinx_mapping = { "py": ("https://docs.python.org/3", None), - "discord": ("https://discordpy.readthedocs.io/en/latest/", None), "aiohttp": ("https://docs.aiohttp.org/en/stable/", None), } -releases_github_path = "top-gg/python-sdk" - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] +redirects = { + "repository": "https://github.com/top-gg-community/python-sdk", + "support": "https://discord.gg/dbl", + "raw-api-reference": "https://docs.top.gg/docs/", + "api/index": "autopost.html", + "examples/index": "discord_py.html", + "examples/discord_py": "https://github.com/Top-gg-Community/python-sdk/tree/master/examples/discordpy_example", + "examples/hikari": "https://github.com/Top-gg-Community/python-sdk/tree/master/examples/hikari_example", +} -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] +releases_github_path = "top-gg-community/python-sdk" source_suffix = ".rst" - -# The master toctree document. master_doc = "index" -# General information about the project. project = "topggpy" -copyright = "2021, Assanali Mukhanov" -author = "Assanali Mukhanov" - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. - -# with open('../dbl/__init__.py') as f: -# version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) -# The full version, including alpha/beta/rc tags. +copyright = "2021 Assanali Mukhanov; 2024-2025 null8626" +author = "null8626" release = version -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path +language = "en" exclude_patterns = ["_build"] -# -- Options for HTML output ---------------------------------------------- - -html_theme_options = {"navigation_depth": 2} -html_theme_path = [alabaster.get_path()] -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "insegel" - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. +html_js_files = ["script.js"] +html_css_files = [ + "style.css", + "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Roboto+Mono&display=swap", +] +html_favicon = "_static/favicon.ico" +html_theme = "furo" html_logo = "topgg.svg" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -# html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# html_search_scorer = 'scorer.js' - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. htmlhelp_basename = "topggpydoc" -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "topggpy.tex", "topggpy Documentation", "Assanali Mukhanov", "manual"), ] -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). man_pages = [(master_doc, "topggpy", "topggpy Documentation", [author], 1)] -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) texinfo_documents = [ ( master_doc, diff --git a/docs/examples/discord_py.rst b/docs/examples/discord_py.rst new file mode 100644 index 00000000..e77808d8 --- /dev/null +++ b/docs/examples/discord_py.rst @@ -0,0 +1,5 @@ +================== +Discord.py example +================== + +You should be redirected in a few moments. Otherwise, click here: https://github.com/Top-gg-Community/python-sdk/tree/master/examples/discordpy_example \ No newline at end of file diff --git a/docs/examples/hikari.rst b/docs/examples/hikari.rst new file mode 100644 index 00000000..89fc6513 --- /dev/null +++ b/docs/examples/hikari.rst @@ -0,0 +1,5 @@ +============== +Hikari example +============== + +You should be redirected in a few moments. Otherwise, click here: https://github.com/Top-gg-Community/python-sdk/tree/master/examples/hikari_example \ No newline at end of file diff --git a/docs/examples/index.rst b/docs/examples/index.rst new file mode 100644 index 00000000..20573261 --- /dev/null +++ b/docs/examples/index.rst @@ -0,0 +1,9 @@ +######## +Examples +######## + + .. toctree:: + :maxdepth: 2 + + discord_py + hikari \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index a634d380..1ce0a3dd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,19 +3,52 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -################################### -Welcome to topggpy's documentation! -################################### +##################### +Top.gg Python Library +##################### -.. toctree:: - :maxdepth: 1 +.. image:: https://img.shields.io/pypi/v/topggpy.svg + :target: https://pypi.python.org/pypi/topggpy + :alt: View on PyPI +.. image:: https://img.shields.io/pypi/dm/topggpy?style=flat-square + :target: https://topggpy.readthedocs.io/en/latest/?badge=latest + :alt: Monthly PyPI downloads + +A simple API wrapper for `Top.gg `_ written in Python. + +Installation +------------ + +.. code:: bash + + pip install topggpy - api - whats_new +Features +-------- -Indices and tables -================== +* POST server count +* GET bot info, server count, upvote info +* GET user info +* GET widgets (large and small) including custom ones. See `docs.top.gg `_ for more info. +* GET weekend status +* Built-in webhook to handle Top.gg votes +* Automated server count posting +* Searching for bots via the API + +Additional information +---------------------- + +* Before using the webhook provided by this library, make sure that you have specified port open. +* Optimal values for port are between 1024 and 49151. +* If you happen to need help implementing topggpy in your bot, feel free to ask in the ``#development`` or ``#api`` channels in our `Discord server `_. + +.. toctree:: + :maxdepth: 2 + :hidden: -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + api/index.rst + examples/index.rst + whats-new + support-server + repository + raw-api-reference \ No newline at end of file diff --git a/docs/raw-api-reference.rst b/docs/raw-api-reference.rst new file mode 100644 index 00000000..7486b294 --- /dev/null +++ b/docs/raw-api-reference.rst @@ -0,0 +1,5 @@ +================= +Raw API reference +================= + +You should be redirected in a few moments. Otherwise, click here: https://docs.top.gg/docs/ \ No newline at end of file diff --git a/docs/repository.rst b/docs/repository.rst new file mode 100644 index 00000000..a542ad6a --- /dev/null +++ b/docs/repository.rst @@ -0,0 +1,5 @@ +================= +GitHub repository +================= + +You should be redirected in a few moments. Otherwise, click here: https://github.com/top-gg-community/python-sdk \ No newline at end of file diff --git a/docs/support-server.rst b/docs/support-server.rst new file mode 100644 index 00000000..531270f0 --- /dev/null +++ b/docs/support-server.rst @@ -0,0 +1,5 @@ +============== +Support server +============== + +You should be redirected in a few moments. Otherwise, click here: https://discord.gg/dbl \ No newline at end of file diff --git a/docs/topgg.svg b/docs/topgg.svg index 9afe2351..63f58125 100644 --- a/docs/topgg.svg +++ b/docs/topgg.svg @@ -1,10 +1,4 @@ - - - - - + + + diff --git a/docs/whats_new.rst b/docs/whats-new.rst similarity index 50% rename from docs/whats_new.rst rename to docs/whats-new.rst index 8fc1d0e9..93082ee5 100644 --- a/docs/whats_new.rst +++ b/docs/whats-new.rst @@ -1,33 +1,50 @@ -.. currentmodule:: topgg - -.. _whats_new: - ########## -What's New +What's new ########## This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +v2.0.1 +====== +* Added Python 3.12 support (:issue:`78`) +* Dropped Python 3.6, 3.7, and 3.8 support (:issue:`75`) +* Removed the need to manually set a ``default_bot_id`` property +* Posting shard-related data is now deprecated +* :attr:`.BotData.certified_bot` is now deprecated +* :attr:`.BotData.def_avatar` is now deprecated +* :attr:`.BotData.discriminator` is now deprecated +* :attr:`.BotData.lib` is now deprecated +* :attr:`.BotData.guilds` is now deprecated +* :attr:`.BotStatsData.shards` is now deprecated +* :attr:`.BotStatsData.shard_count` is now deprecated +* :attr:`.UserData.certified_dev` is now deprecated +* :attr:`.UserData.discriminator` is now deprecated +* :meth:`.DBLClient.get_guild_count` no longer accepts a ``bot_id`` argument +* :meth:`.DBLClient.get_bot_votes` no longer raises a :class:`.ClientException` without a ``default_bot_id`` property +* :meth:`.DBLClient.get_bot_info` no longer raises a :class:`.ClientException` without a ``default_bot_id`` property +* :meth:`.DBLClient.generate_widget` no longer raises a :class:`.ClientException` without a ``default_bot_id`` property +* Documentation overhaul + v2.0.0a ======= -* :obj:`~.DBLClient` now doesn't take in ``discord.Client`` instance +* :obj:`.DBLClient` now doesn't take in ``discord.Client`` instance * Introduced new `autopost `__ and `data injection `__ API * `Webhook `__ API breaking changes * No longer depends on any Discord API wrapper -* :obj:`~.GuildVoteData` alias +* :obj:`.GuildVoteData` alias v1.4.0 ====== -* The type of data passed to ``on_dbl_vote`` has been changed from :class:`dict` to :obj:`BotVoteData` -* The type of data passed to ``on_dsl_vote`` has been changed from :class:`dict` to :obj:`ServerVoteData` +* The type of data passed to ``on_dbl_vote`` has been changed from :class:`.dict` to :obj:`BotVoteData` +* The type of data passed to ``on_dsl_vote`` has been changed from :class:`.dict` to :obj:`ServerVoteData` v1.3.0 ====== * Introduced `global ratelimiter `__ to follow Top.gg global ratelimits - * Fixed an :exc:`AttributeError` raised by :meth:`HTTPClient.request` + * Fixed an :exc:`AttributeError` raised by :meth:`.HTTPClient.request` * `Resource-specific ratelimit `__ is now actually resource-specific @@ -35,41 +52,41 @@ v1.2.0 ====== * Introduced global ratelimiter along with bot endpoints ratelimiter -* Follow consistency with typing in :class:`HTTPClient` and :class:`DBLClient` along with updated docstrings (:issue:`55`) +* Follow consistency with typing in :class:`.HTTPClient` and :class:`.DBLClient` along with updated docstrings (:issue:`55`) v1.1.0 ====== * Introduced `data models `__ - * :meth:`DBLClient.get_bot_votes` now returns a list of :class:`BriefUserData` objects + * :meth:`.DBLClient.get_bot_votes` now returns a list of :class:`.BriefUserData` objects - * :meth:`DBLClient.get_bot_info` now returns a :class:`BotData` object + * :meth:`.DBLClient.get_bot_info` now returns a :class:`.BotData` object - * :meth:`DBLClient.get_guild_count` now returns a :class:`BotStatsData` object + * :meth:`.DBLClient.get_guild_count` now returns a :class:`.BotStatsData` object - * :meth:`DBLClient.get_user_info` now returns a :class:`UserData` object + * :meth:`.DBLClient.get_user_info` now returns a :class:`.UserData` object -* :meth:`WebhookManager.run` now returns an :class:`asyncio.Task`, meaning it can now be optionally awaited +* :meth:`.WebhookManager.run` now returns an :class:`.asyncio.Task`, meaning it can now be optionally awaited v1.0.1 ====== -* :attr:`WebhookManager.webserver` now instead returns :class:`aiohttp.web.Application` for ease of use +* :attr:`.WebhookManager.webserver` now instead returns :class:`.aiohttp.web.Application` for ease of use v1.0.0 ====== * Renamed the module folder from ``dbl`` to ``topgg`` -* Added ``post_shard_count`` argument to :meth:`DBLClient.post_guild_count` +* Added ``post_shard_count`` argument to :meth:`.DBLClient.post_guild_count` * Autopost now supports automatic shard posting (:issue:`42`) * Large webhook system rework, read the :obj:`api/webhook` section for more * Added support for server webhooks -* Renamed ``DBLException`` to :class:`TopGGException` -* Renamed ``DBLClient.get_bot_upvotes()`` to :meth:`DBLClient.get_bot_votes` -* Added :meth:`DBLClient.generate_widget` along with the ``widgets`` section in the documentation +* Renamed ``DBLException`` to :class:`.TopGGException` +* Renamed ``DBLClient.get_bot_upvotes()`` to :meth:`.DBLClient.get_bot_votes` +* Added :meth:`.DBLClient.generate_widget` along with the ``widgets`` section in the documentation * Implemented a properly working ratelimiter * Added :func:`on_autopost_error` * All autopost events now follow ``on_autopost_x`` naming format, e.g. :func:`on_autopost_error`, :func:`on_autopost_success` @@ -78,7 +95,7 @@ v1.0.0 v0.4.0 ====== -* :meth:`DBLClient.post_guild_count` now supports a custom ``guild_count`` argument, which accepts either an integer or list of integers +* :meth:`.DBLClient.post_guild_count` now supports a custom ``guild_count`` argument, which accepts either an integer or list of integers * Reworked how shard info is posted * Removed ``InvalidArgument`` and ``ConnectionClosed`` exceptions * Added ``ServerError`` exception @@ -87,12 +104,12 @@ v0.3.3 ====== * Internal changes regarding support of Top.gg migration -* Fixed errors raised when using :meth:`DBLClient.close` without built-in webhook +* Fixed errors raised when using :meth:`.DBLClient.close` without built-in webhook v0.3.2 ====== -* ``Client`` class has been renamed to ``DBLClient`` +* ``Client`` class has been renamed to :class:`.DBLClient` v0.3.1 ====== @@ -104,7 +121,7 @@ v0.3.1 v0.3.0 ====== -* :class:`DBLClient` now has ``autopost`` kwarg that will post server count automatically every 30 minutes +* :class:`.DBLClient` now has ``autopost`` kwarg that will post server count automatically every 30 minutes * Fixed code 403 errors * Added ``on_dbl_vote``, an event that is called when you test your webhook * Added ``on_dbl_test``, an event that is called when someone tests your webhook @@ -114,7 +131,7 @@ v0.2.1 * Added webhook * Removed support for discord.py versions lower than 1.0.0 -* Made :meth:`DBLClient.get_weekend_status` return a boolean value +* Made :meth:`.DBLClient.get_weekend_status` return a boolean value * Added webhook example in README * Removed ``post_server_count`` and ``get_server_count`` @@ -129,9 +146,9 @@ v0.2.0 * Made ``get_server_count`` an alias for ``get_guild_count`` -* Added :meth:`DBLClient.get_weekend_status` -* Removed all parameters from :meth:`DBLClient.get_upvote_info` -* Added limit to :meth:`DBLClient.get_bots` +* Added :meth:`.DBLClient.get_weekend_status` +* Removed all parameters from :meth:`.DBLClient.get_upvote_info` +* Added limit to :meth:`.DBLClient.get_bots` * Fixed example in README v0.1.6 diff --git a/examples/discordpy_example/__main__.py b/examples/discordpy_example/__main__.py index f1c1f6dd..9ae6595b 100644 --- a/examples/discordpy_example/__main__.py +++ b/examples/discordpy_example/__main__.py @@ -1,26 +1,29 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Norizon - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -import discord +""" +The MIT License (MIT) + +Copyright (c) 2021 Norizon +Copyright (c) 2024-2025 null8626 + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +import discord import topgg from .callbacks import autopost, webhook @@ -39,7 +42,6 @@ @client.event async def on_ready(): assert client.user is not None - dblclient.default_bot_id = client.user.id # if it's ready, then the event loop's run, # hence it's safe starting the autopost here diff --git a/examples/discordpy_example/callbacks/autopost.py b/examples/discordpy_example/callbacks/autopost.py index e6592a6d..2ca58b78 100644 --- a/examples/discordpy_example/callbacks/autopost.py +++ b/examples/discordpy_example/callbacks/autopost.py @@ -1,28 +1,29 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Norizon - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -import sys +""" +The MIT License (MIT) + +Copyright (c) 2021 Norizon + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +import sys import discord - import topgg diff --git a/examples/discordpy_example/callbacks/webhook.py b/examples/discordpy_example/callbacks/webhook.py index 358753c1..2207547c 100644 --- a/examples/discordpy_example/callbacks/webhook.py +++ b/examples/discordpy_example/callbacks/webhook.py @@ -1,27 +1,28 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Norizon - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. +""" +The MIT License (MIT) + +Copyright (c) 2021 Norizon + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" # import discord - import topgg diff --git a/examples/hikari_example/__main__.py b/examples/hikari_example/__main__.py index 0bef502f..f23b6b4c 100644 --- a/examples/hikari_example/__main__.py +++ b/examples/hikari_example/__main__.py @@ -1,26 +1,29 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Norizon - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -import hikari +""" +The MIT License (MIT) + +Copyright (c) 2021 Norizon +Copyright (c) 2024-2025 null8626 + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +import hikari import topgg from .callbacks import autopost, webhook @@ -40,7 +43,6 @@ async def on_started(event: hikari.StartedEvent): me: hikari.OwnUser = event.app.get_me() assert me is not None - dblclient.default_bot_id = me.id # since StartedEvent is a lifetime event # this event will only get dispatched once diff --git a/examples/hikari_example/callbacks/autopost.py b/examples/hikari_example/callbacks/autopost.py index 3ac467b3..e09a0165 100644 --- a/examples/hikari_example/callbacks/autopost.py +++ b/examples/hikari_example/callbacks/autopost.py @@ -1,34 +1,36 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Norizon - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -import logging +""" +The MIT License (MIT) + +Copyright (c) 2021 Norizon + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +import logging import hikari - import topgg # from ..events.autopost import AutoPostErrorEvent, AutoPostSuccessEvent _LOGGER = logging.getLogger("callbacks.autopost") + # these functions can be async too! def on_autopost_success( # uncomment this if you want to get access to app @@ -56,6 +58,4 @@ def on_autopost_error( def stats(app: hikari.GatewayBot = topgg.data(hikari.GatewayBot)): - return topgg.StatsWrapper( - guild_count=len(app.cache.get_guilds_view()), shard_count=app.shard_count - ) + return topgg.StatsWrapper(guild_count=len(app.cache.get_guilds_view())) diff --git a/examples/hikari_example/callbacks/webhook.py b/examples/hikari_example/callbacks/webhook.py index 50c53a73..222f9445 100644 --- a/examples/hikari_example/callbacks/webhook.py +++ b/examples/hikari_example/callbacks/webhook.py @@ -1,29 +1,29 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Norizon - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. +""" +The MIT License (MIT) + +Copyright (c) 2021 Norizon + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" import logging - import topgg - # import hikari @@ -31,6 +31,7 @@ _LOGGER = logging.getLogger("callbacks.webhook") + # this can be async too! @topgg.endpoint("/dblwebhook", topgg.WebhookType.BOT, "youshallnotpass") async def endpoint( diff --git a/examples/hikari_example/events/autopost.py b/examples/hikari_example/events/autopost.py index ddc7aa22..02e9967d 100644 --- a/examples/hikari_example/events/autopost.py +++ b/examples/hikari_example/events/autopost.py @@ -1,24 +1,27 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Norizon - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. +""" +The MIT License (MIT) + +Copyright (c) 2021 Norizon + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + import attr import hikari diff --git a/examples/hikari_example/events/webhook.py b/examples/hikari_example/events/webhook.py index b9b6d21f..8183b742 100644 --- a/examples/hikari_example/events/webhook.py +++ b/examples/hikari_example/events/webhook.py @@ -1,27 +1,29 @@ -# The MIT License (MIT) +""" +The MIT License (MIT) -# Copyright (c) 2021 Norizon +Copyright (c) 2021 Norizon -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. import attr import hikari - import topgg diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..5534328b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools"] + +[project] +name = "topggpy" +version = "2.0.1" +description = "A simple API wrapper for Top.gg written in Python." +readme = "README.rst" +license = { text = "MIT" } +authors = [{ name = "null8626" }, { name = "Top.gg" }] +keywords = ["discord", "discord-bot", "topgg"] +dependencies = ["aiohttp>=3.11.12"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Internet", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities" +] +requires-python = ">=3.9" + +[project.urls] +Documentation = "https://topggpy.readthedocs.io/en/latest/" +"Raw API Documentation" = "https://docs.top.gg/docs/" +"Release notes" = "https://topggpy.readthedocs.io/en/latest/whats_new.html" +Repository = "https://github.com/top-gg-community/python-sdk" +"Support server" = "https://discord.gg/dbl" \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index 919cbc45..83633087 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,8 +1,4 @@ -[pytest] -xfail_strict = true -norecursedirs = docs *.egg-info .git - -filterwarnings = - ignore::DeprecationWarning - +[pytest] +xfail_strict = true +norecursedirs = docs *.egg-info .git addopts = --cov=topgg \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index e5d5d951..36ad3056 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,13 +1,9 @@ -# Formatting -git+https://github.com/timothycrosley/isort -git+https://github.com/psf/black - -# Unit Testing -mock -pytest -pytest-asyncio -pytest-mock -pytest-cov - -# Linting -flake8 +# Formatting and Linting +ruff + +# Unit Testing +mock +pytest +pytest-asyncio +pytest-mock +pytest-cov \ No newline at end of file diff --git a/requirements-docs.txt b/requirements-docs.txt index e75c15e9..8315d05b 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,2 +1,3 @@ +furo sphinx -insegel +sphinx-reredirects \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9ad05803..4143b58b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -aiohttp>=3.6.0,<3.9.0 \ No newline at end of file +aiohttp>=3.11.12 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..650555f8 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,9 @@ +line-length = 120 + +[format] +docstring-code-format = true +docstring-code-line-length = 120 +line-ending = "lf" + +[lint] +ignore = ["E722", "F401", "F403"] \ No newline at end of file diff --git a/scripts/format.sh b/scripts/format.sh deleted file mode 100644 index 4fa2e31d..00000000 --- a/scripts/format.sh +++ /dev/null @@ -1,2 +0,0 @@ -black . -isort . \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 36733afb..00000000 --- a/setup.py +++ /dev/null @@ -1,64 +0,0 @@ -import os -import pathlib -import re -import types - -from setuptools import find_packages, setup - -HERE = pathlib.Path(__file__).parent - -txt = (HERE / "topgg" / "__init__.py").read_text("utf-8") - -groups = {} - -for match in re.finditer(r'__(?P.*)__\s*=\s*"(?P[^"]+)"\r?', txt): - group = match.groupdict() - groups[group["identifier"]] = group["value"] - -metadata = types.SimpleNamespace(**groups) - -on_rtd = os.getenv("READTHEDOCS") == "True" - -with open("requirements.txt") as f: - requirements = f.read().splitlines() - -if on_rtd: - requirements.append("sphinxcontrib-napoleon") - requirements.append("sphinx-rtd-dark-mode") - -with open("README.rst") as f: - readme = f.read() - -setup( - name="topggpy", - author=f"{metadata.author}, Top.gg", - author_email="shivaco.osu@gmail.com", - maintainer=f"{metadata.maintainer}, Top.gg", - url="https://github.com/top-gg/python-sdk", - version=metadata.version, - packages=find_packages(), - license=metadata.license, - description="A simple API wrapper for Top.gg written in Python.", - long_description=readme, - package_data={"topgg": ["py.typed"]}, - include_package_data=True, - python_requires=">= 3.6", - install_requires=requirements, - keywords="discord bot server list discordservers serverlist discordbots botlist topgg top.gg", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: MIT License", - "Intended Audience :: Developers", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Topic :: Internet", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Utilities", - ], -) diff --git a/tests/test_autopost.py b/tests/test_autopost.py index a4f8ee7a..115297cc 100644 --- a/tests/test_autopost.py +++ b/tests/test_autopost.py @@ -1,92 +1,87 @@ -import datetime - -import mock -import pytest -from aiohttp import ClientSession -from pytest_mock import MockerFixture - -from topgg import DBLClient, StatsWrapper -from topgg.autopost import AutoPoster -from topgg.errors import ServerError, TopGGException, Unauthorized - - -@pytest.fixture -def session() -> ClientSession: - return mock.Mock(ClientSession) - - -@pytest.fixture -def autopost(session: ClientSession) -> AutoPoster: - return AutoPoster(DBLClient("", session=session)) - - -@pytest.mark.asyncio -async def test_AutoPoster_breaks_autopost_loop_on_401( - mocker: MockerFixture, session: ClientSession -) -> None: - response = mock.Mock("reason, status") - response.reason = "Unauthorized" - response.status = 401 - - mocker.patch( - "topgg.DBLClient.post_guild_count", side_effect=Unauthorized(response, {}) - ) - - callback = mock.Mock() - autopost = DBLClient("", session=session).autopost().stats(callback) - assert isinstance(autopost, AutoPoster) - assert not isinstance(autopost.stats()(callback), AutoPoster) - - with pytest.raises(Unauthorized): - await autopost.start() - - callback.assert_called_once() - assert not autopost.is_running - - -@pytest.mark.asyncio -async def test_AutoPoster_raises_missing_stats(autopost: AutoPoster) -> None: - with pytest.raises( - TopGGException, match="you must provide a callback that returns the stats." - ): - await autopost.start() - - -@pytest.mark.asyncio -async def test_AutoPoster_raises_already_running(autopost: AutoPoster) -> None: - autopost.stats(mock.Mock()).start() - with pytest.raises(TopGGException, match="the autopost is already running."): - await autopost.start() - - -@pytest.mark.asyncio -async def test_AutoPoster_interval_too_short(autopost: AutoPoster) -> None: - with pytest.raises(ValueError, match="interval must be greated than 900 seconds."): - autopost.set_interval(50) - - -@pytest.mark.asyncio -async def test_AutoPoster_error_callback( - mocker: MockerFixture, autopost: AutoPoster -) -> None: - error_callback = mock.Mock() - response = mock.Mock("reason, status") - response.reason = "Internal Server Error" - response.status = 500 - side_effect = ServerError(response, {}) - - mocker.patch("topgg.DBLClient.post_guild_count", side_effect=side_effect) - task = autopost.on_error(error_callback).stats(mock.Mock()).start() - autopost.stop() - await task - error_callback.assert_called_once_with(side_effect) - - -def test_AutoPoster_interval(autopost: AutoPoster): - assert autopost.interval == 900 - autopost.set_interval(datetime.timedelta(hours=1)) - assert autopost.interval == 3600 - autopost.interval = datetime.timedelta(hours=2) - assert autopost.interval == 7200 - autopost.interval = 3600 - assert autopost.interval == 3600 +import datetime + +import mock +import pytest +from aiohttp import ClientSession +from pytest_mock import MockerFixture + +from topgg import DBLClient +from topgg.autopost import AutoPoster +from topgg.errors import ServerError, TopGGException, Unauthorized + + +MOCK_TOKEN = "amogus.eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWUsImlhdCI6MTY5OTk4NDYyM30.amogus" + + +@pytest.fixture +def session() -> ClientSession: + return mock.Mock(ClientSession) + + +@pytest.fixture +def autopost(session: ClientSession) -> AutoPoster: + return AutoPoster(DBLClient(MOCK_TOKEN, session=session)) + + +@pytest.mark.asyncio +async def test_AutoPoster_breaks_autopost_loop_on_401(mocker: MockerFixture, session: ClientSession) -> None: + response = mock.Mock("reason, status") + response.reason = "Unauthorized" + response.status = 401 + + mocker.patch("topgg.DBLClient.post_guild_count", side_effect=Unauthorized(response, {})) + + callback = mock.Mock() + autopost = DBLClient(MOCK_TOKEN, session=session).autopost().stats(callback) + assert isinstance(autopost, AutoPoster) + assert not isinstance(autopost.stats()(callback), AutoPoster) + + with pytest.raises(Unauthorized): + await autopost.start() + + callback.assert_called_once() + assert not autopost.is_running + + +@pytest.mark.asyncio +async def test_AutoPoster_raises_missing_stats(autopost: AutoPoster) -> None: + with pytest.raises(TopGGException, match="you must provide a callback that returns the stats."): + await autopost.start() + + +@pytest.mark.asyncio +async def test_AutoPoster_raises_already_running(autopost: AutoPoster) -> None: + autopost.stats(mock.Mock()).start() + with pytest.raises(TopGGException, match="the autopost is already running."): + await autopost.start() + + +@pytest.mark.asyncio +async def test_AutoPoster_interval_too_short(autopost: AutoPoster) -> None: + with pytest.raises(ValueError, match="interval must be greated than 900 seconds."): + autopost.set_interval(50) + + +@pytest.mark.asyncio +async def test_AutoPoster_error_callback(mocker: MockerFixture, autopost: AutoPoster) -> None: + error_callback = mock.Mock() + response = mock.Mock("reason, status") + response.reason = "Internal Server Error" + response.status = 500 + side_effect = ServerError(response, {}) + + mocker.patch("topgg.DBLClient.post_guild_count", side_effect=side_effect) + task = autopost.on_error(error_callback).stats(mock.Mock()).start() + autopost.stop() + await task + error_callback.assert_called_once_with(side_effect) + + +def test_AutoPoster_interval(autopost: AutoPoster): + assert autopost.interval == 900 + autopost.set_interval(datetime.timedelta(hours=1)) + assert autopost.interval == 3600 + autopost.interval = datetime.timedelta(hours=2) + assert autopost.interval == 7200 + autopost.interval = 3600 + assert autopost.interval == 3600 diff --git a/tests/test_client.py b/tests/test_client.py index fb634ead..b74acea6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,5 +1,3 @@ -import typing as t - import mock import pytest from aiohttp import ClientSession @@ -8,6 +6,9 @@ from topgg import errors +MOCK_TOKEN = "amogus.eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWUsImlhdCI6MTY5OTk4NDYyM30.amogus" + + @pytest.fixture def session() -> ClientSession: return mock.Mock(ClientSession) @@ -15,14 +16,14 @@ def session() -> ClientSession: @pytest.fixture def client() -> topgg.DBLClient: - client = topgg.DBLClient(token="TOKEN", default_bot_id=1234) + client = topgg.DBLClient(token=MOCK_TOKEN) client.http = mock.Mock(topgg.http.HTTPClient) return client @pytest.mark.asyncio async def test_HTTPClient_with_external_session(session: ClientSession): - http = topgg.http.HTTPClient("TOKEN", session=session) + http = topgg.http.HTTPClient(MOCK_TOKEN, session=session) assert not http._own_session await http.close() session.close.assert_not_called() @@ -30,57 +31,23 @@ async def test_HTTPClient_with_external_session(session: ClientSession): @pytest.mark.asyncio async def test_HTTPClient_with_no_external_session(session: ClientSession): - http = topgg.http.HTTPClient("TOKEN") + http = topgg.http.HTTPClient(MOCK_TOKEN) http.session = session assert http._own_session await http.close() session.close.assert_called_once() -@pytest.mark.asyncio -async def test_DBLClient_get_bot_votes_with_no_default_bot_id(): - client = topgg.DBLClient("TOKEN") - with pytest.raises( - errors.ClientException, - match="you must set default_bot_id when constructing the client.", - ): - await client.get_bot_votes() - - @pytest.mark.asyncio async def test_DBLClient_post_guild_count_with_no_args(): - client = topgg.DBLClient("TOKEN", default_bot_id=1234) - with pytest.raises(TypeError, match="stats or guild_count must be provided."): + client = topgg.DBLClient(MOCK_TOKEN) + with pytest.raises(TypeError, match="guild_count must be provided."): await client.post_guild_count() -@pytest.mark.parametrize( - "method, kwargs", - [ - (topgg.DBLClient.get_guild_count, {}), - (topgg.DBLClient.get_bot_info, {}), - ( - topgg.DBLClient.generate_widget, - { - "options": topgg.types.WidgetOptions(), - }, - ), - ], -) -@pytest.mark.asyncio -async def test_DBLClient_get_guild_count_with_no_id( - method: t.Callable, kwargs: t.Dict[str, t.Any] -): - client = topgg.DBLClient("TOKEN") - with pytest.raises( - errors.ClientException, match="bot_id or default_bot_id is unset." - ): - await method(client, **kwargs) - - @pytest.mark.asyncio async def test_closed_DBLClient_raises_exception(): - client = topgg.DBLClient("TOKEN") + client = topgg.DBLClient(MOCK_TOKEN) assert not client.is_closed await client.close() assert client.is_closed @@ -88,6 +55,15 @@ async def test_closed_DBLClient_raises_exception(): await client.get_weekend_status() +@pytest.mark.asyncio +async def test_DBLClient_bot_id(): + client = topgg.DBLClient(MOCK_TOKEN) + assert not client.is_closed + assert client.bot_id == 1026525568344264724 + await client.close() + assert client.is_closed + + @pytest.mark.asyncio async def test_DBLClient_get_weekend_status(client: topgg.DBLClient): client.http.get_weekend_status = mock.AsyncMock() @@ -116,13 +92,6 @@ async def test_DBLClient_get_bot_votes(client: topgg.DBLClient): client.http.get_bot_votes.assert_called_once() -@pytest.mark.asyncio -async def test_DBLClient_get_bots(client: topgg.DBLClient): - client.http.get_bots = mock.AsyncMock(return_value={"results": []}) - await client.get_bots() - client.http.get_bots.assert_called_once() - - @pytest.mark.asyncio async def test_DBLClient_get_user_info(client: topgg.DBLClient): client.http.get_user_info = mock.AsyncMock(return_value={}) diff --git a/tests/test_data_container.py b/tests/test_data_container.py index 978574fb..f89466ea 100644 --- a/tests/test_data_container.py +++ b/tests/test_data_container.py @@ -13,20 +13,13 @@ def data_container() -> DataContainerMixin: return dc -async def _async_callback( - text: str = data(str), number: int = data(int), mapping: dict = data(dict) -): - ... +async def _async_callback(text: str = data(str), number: int = data(int), mapping: dict = data(dict)): ... -def _sync_callback( - text: str = data(str), number: int = data(int), mapping: dict = data(dict) -): - ... +def _sync_callback(text: str = data(str), number: int = data(int), mapping: dict = data(dict)): ... -def _invalid_callback(number: float = data(float)): - ... +def _invalid_callback(number: float = data(float)): ... @pytest.mark.asyncio @@ -42,8 +35,7 @@ async def test_data_container_invoke_sync_callback(data_container: DataContainer def test_data_container_raises_data_already_exists(data_container: DataContainerMixin): with pytest.raises( TopGGException, - match=" already exists. If you wish to override it, " - "pass True into the override parameter.", + match=" already exists. If you wish to override it, " "pass True into the override parameter.", ): data_container.set_data("TEST") diff --git a/tests/test_ratelimiter.py b/tests/test_ratelimiter.py index f1fbed6b..9153b3a1 100644 --- a/tests/test_ratelimiter.py +++ b/tests/test_ratelimiter.py @@ -1,28 +1,28 @@ -import pytest - -from topgg.ratelimiter import AsyncRateLimiter - -n = period = 10 - - -@pytest.fixture -def limiter() -> AsyncRateLimiter: - return AsyncRateLimiter(max_calls=n, period=period) - - -@pytest.mark.asyncio -async def test_AsyncRateLimiter_calls(limiter: AsyncRateLimiter) -> None: - for _ in range(n): - async with limiter: - pass - - assert len(limiter.calls) == limiter.max_calls == n - - -@pytest.mark.asyncio -async def test_AsyncRateLimiter_timespan_property(limiter: AsyncRateLimiter) -> None: - for _ in range(n): - async with limiter: - pass - - assert limiter._timespan < period +import pytest + +from topgg.ratelimiter import AsyncRateLimiter + +n = period = 10 + + +@pytest.fixture +def limiter() -> AsyncRateLimiter: + return AsyncRateLimiter(max_calls=n, period=period) + + +@pytest.mark.asyncio +async def test_AsyncRateLimiter_calls(limiter: AsyncRateLimiter) -> None: + for _ in range(n): + async with limiter: + pass + + assert len(limiter.calls) == limiter.max_calls == n + + +@pytest.mark.asyncio +async def test_AsyncRateLimiter_timespan_property(limiter: AsyncRateLimiter) -> None: + for _ in range(n): + async with limiter: + pass + + assert limiter._timespan < period diff --git a/tests/test_type.py b/tests/test_type.py index caec363c..f53abf6c 100644 --- a/tests/test_type.py +++ b/tests/test_type.py @@ -3,7 +3,6 @@ from topgg import types d: dict = { - "defAvatar": "6debd47ed13483642cf09e832ed0bc1b", "invite": "", "website": "https://top.gg", "support": "KYZsaFb", @@ -127,7 +126,7 @@ def test_bot_data_fields(bot_data: types.BotData) -> None: for attr in bot_data: if "id" in attr.lower(): assert isinstance(bot_data[attr], int) or bot_data[attr] is None - elif attr in ("owners", "guilds"): + elif attr == "owners": for item in bot_data[attr]: assert isinstance(item, int) assert bot_data.get(attr) == bot_data[attr] == getattr(bot_data, attr) @@ -143,12 +142,7 @@ def test_widget_options_fields(widget_options: types.WidgetOptions) -> None: for attr in widget_options: if "id" in attr.lower(): assert isinstance(widget_options[attr], int) or widget_options[attr] is None - assert ( - widget_options.get(attr) - == widget_options[attr] - == widget_options[attr] - == getattr(widget_options, attr) - ) + assert widget_options.get(attr) == widget_options[attr] == widget_options[attr] == getattr(widget_options, attr) def test_vote_data_fields(vote_data: types.VoteDataDict) -> None: @@ -165,11 +159,7 @@ def test_bot_vote_data_fields(bot_vote_data: types.BotVoteData) -> None: assert isinstance(bot_vote_data["bot"], int) for attr in bot_vote_data: - assert ( - getattr(bot_vote_data, attr) - == bot_vote_data.get(attr) - == bot_vote_data[attr] - ) + assert getattr(bot_vote_data, attr) == bot_vote_data.get(attr) == bot_vote_data[attr] def test_server_vote_data_fields(server_vote_data: types.BotVoteData) -> None: @@ -178,20 +168,11 @@ def test_server_vote_data_fields(server_vote_data: types.BotVoteData) -> None: assert isinstance(server_vote_data["guild"], int) for attr in server_vote_data: - assert ( - getattr(server_vote_data, attr) - == server_vote_data.get(attr) - == server_vote_data[attr] - ) + assert getattr(server_vote_data, attr) == server_vote_data.get(attr) == server_vote_data[attr] def test_bot_stats_data_attrs(bot_stats_data: types.BotStatsData) -> None: - for count in ("server_count", "shard_count"): - assert isinstance(bot_stats_data[count], int) or bot_stats_data[count] is None - assert isinstance(bot_stats_data.shards, list) - if bot_stats_data.shards: - for shard in bot_stats_data.shards: - assert isinstance(shard, int) + assert isinstance(bot_stats_data["server_count"], int) or bot_stats_data["server_count"] is None def test_user_data_attrs(user_data: types.UserData) -> None: diff --git a/tests/test_webhook.py b/tests/test_webhook.py index 8ef3c71d..db1da09d 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -1,80 +1,76 @@ -import typing as t - -import aiohttp -import mock -import pytest - -from topgg import WebhookManager, WebhookType -from topgg.errors import TopGGException - -auth = "youshallnotpass" - - -@pytest.fixture -def webhook_manager() -> WebhookManager: - return ( - WebhookManager() - .endpoint() - .type(WebhookType.BOT) - .auth(auth) - .route("/dbl") - .callback(print) - .add_to_manager() - .endpoint() - .type(WebhookType.GUILD) - .auth(auth) - .route("/dsl") - .callback(print) - .add_to_manager() - ) - - -def test_WebhookManager_routes(webhook_manager: WebhookManager) -> None: - assert len(webhook_manager.app.router.routes()) == 2 - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "headers, result, state", - [({"authorization": auth}, 200, True), ({}, 401, False)], -) -async def test_WebhookManager_validates_auth( - webhook_manager: WebhookManager, headers: t.Dict[str, str], result: int, state: bool -) -> None: - await webhook_manager.start(5000) - - try: - for path in ("dbl", "dsl"): - async with aiohttp.request( - "POST", f"http://localhost:5000/{path}", headers=headers, json={} - ) as r: - assert r.status == result - finally: - await webhook_manager.close() - assert not webhook_manager.is_running - - -def test_WebhookEndpoint_callback_unset(webhook_manager: WebhookManager): - with pytest.raises( - TopGGException, - match="endpoint missing callback.", - ): - webhook_manager.endpoint().add_to_manager() - - -def test_WebhookEndpoint_route_unset(webhook_manager: WebhookManager): - with pytest.raises( - TopGGException, - match="endpoint missing type.", - ): - webhook_manager.endpoint().callback(mock.Mock()).add_to_manager() - - -def test_WebhookEndpoint_type_unset(webhook_manager: WebhookManager): - with pytest.raises( - TopGGException, - match="endpoint missing route.", - ): - webhook_manager.endpoint().callback(mock.Mock()).type( - WebhookType.BOT - ).add_to_manager() +import typing as t + +import aiohttp +import mock +import pytest + +from topgg import WebhookManager, WebhookType +from topgg.errors import TopGGException + +auth = "youshallnotpass" + + +@pytest.fixture +def webhook_manager() -> WebhookManager: + return ( + WebhookManager() + .endpoint() + .type(WebhookType.BOT) + .auth(auth) + .route("/dbl") + .callback(print) + .add_to_manager() + .endpoint() + .type(WebhookType.GUILD) + .auth(auth) + .route("/dsl") + .callback(print) + .add_to_manager() + ) + + +def test_WebhookManager_routes(webhook_manager: WebhookManager) -> None: + assert len(webhook_manager.app.router.routes()) == 2 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "headers, result, state", + [({"authorization": auth}, 200, True), ({}, 401, False)], +) +async def test_WebhookManager_validates_auth( + webhook_manager: WebhookManager, headers: t.Dict[str, str], result: int, state: bool +) -> None: + await webhook_manager.start(5000) + + try: + for path in ("dbl", "dsl"): + async with aiohttp.request("POST", f"http://localhost:5000/{path}", headers=headers, json={}) as r: + assert r.status == result + finally: + await webhook_manager.close() + assert not webhook_manager.is_running + + +def test_WebhookEndpoint_callback_unset(webhook_manager: WebhookManager): + with pytest.raises( + TopGGException, + match="endpoint missing callback.", + ): + webhook_manager.endpoint().add_to_manager() + + +def test_WebhookEndpoint_route_unset(webhook_manager: WebhookManager): + with pytest.raises( + TopGGException, + match="endpoint missing type.", + ): + webhook_manager.endpoint().callback(mock.Mock()).add_to_manager() + + +def test_WebhookEndpoint_type_unset(webhook_manager: WebhookManager): + with pytest.raises( + TopGGException, + match="endpoint missing route.", + ): + webhook_manager.endpoint().callback(mock.Mock()).type(WebhookType.BOT).add_to_manager() diff --git a/topgg/__init__.py b/topgg/__init__.py index 1a9025eb..d3e6b2fa 100644 --- a/topgg/__init__.py +++ b/topgg/__init__.py @@ -1,18 +1,16 @@ -# -*- coding: utf-8 -*- - """ Top.gg Python API Wrapper ~~~~~~~~~~~~~~~~~~~~~~~~~ A basic wrapper for the Top.gg API. :copyright: (c) 2021 Assanali Mukhanov & Top.gg +:copyright: (c) 2024-2025 null8626 & Top.gg :license: MIT, see LICENSE for more details. """ __title__ = "topggpy" -__author__ = "Assanali Mukhanov" -__maintainer__ = "Norizon" +__author__ = "null8626" __license__ = "MIT" -__version__ = "2.0.0a1" +__version__ = "2.0.1" from .autopost import * from .client import * diff --git a/topgg/autopost.py b/topgg/autopost.py index 3bfe4afa..28a2abf8 100644 --- a/topgg/autopost.py +++ b/topgg/autopost.py @@ -1,26 +1,28 @@ -# The MIT License (MIT) +""" +The MIT License (MIT) -# Copyright (c) 2021 Norizon +Copyright (c) 2021 Norizon -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" -__all__ = ["AutoPoster"] +__all__ = ("AutoPoster",) import asyncio import datetime @@ -54,7 +56,7 @@ class AutoPoster: An instance of DBLClient. """ - __slots__ = ( + __slots__: t.Tuple[str, ...] = ( "_error", "_success", "_interval", @@ -78,17 +80,13 @@ def __init__(self, client: "DBLClient") -> None: def _default_error_handler(self, exception: Exception) -> None: print("Ignoring exception in auto post loop:", file=sys.stderr) - traceback.print_exception( - type(exception), exception, exception.__traceback__, file=sys.stderr - ) + traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr) @t.overload - def on_success(self, callback: None) -> t.Callable[[CallbackT], CallbackT]: - ... + def on_success(self, callback: None) -> t.Callable[[CallbackT], CallbackT]: ... @t.overload - def on_success(self, callback: CallbackT) -> "AutoPoster": - ... + def on_success(self, callback: CallbackT) -> "AutoPoster": ... def on_success(self, callback: t.Any = None) -> t.Any: """ @@ -103,15 +101,15 @@ def on_success(self, callback: t.Any = None) -> t.Any: # The following are valid. autopost = dblclient.autopost().on_success(lambda: print("Success!")) + # Used as decorator, the decorated function will become the AutoPoster object. @autopost.on_success - def autopost(): - ... + def autopost(): ... + # Used as decorator factory, the decorated function will still be the function itself. @autopost.on_success() - def on_success(): - ... + def on_success(): ... """ if callback is not None: self._success = callback @@ -124,12 +122,10 @@ def decorator(callback: CallbackT) -> CallbackT: return decorator @t.overload - def on_error(self, callback: None) -> t.Callable[[CallbackT], CallbackT]: - ... + def on_error(self, callback: None) -> t.Callable[[CallbackT], CallbackT]: ... @t.overload - def on_error(self, callback: CallbackT) -> "AutoPoster": - ... + def on_error(self, callback: CallbackT) -> "AutoPoster": ... def on_error(self, callback: t.Any = None) -> t.Any: """ @@ -148,15 +144,15 @@ def on_error(self, callback: t.Any = None) -> t.Any: # The following are valid. autopost = dblclient.autopost().on_error(lambda exc: print("Failed posting stats!", exc)) + # Used as decorator, the decorated function will become the AutoPoster object. @autopost.on_error - def autopost(exc: Exception): - ... + def autopost(exc: Exception): ... + # Used as decorator factory, the decorated function will still be the function itself. @autopost.on_error() - def on_error(exc: Exception): - ... + def on_error(exc: Exception): ... """ if callback is not None: self._error = callback @@ -169,12 +165,10 @@ def decorator(callback: CallbackT) -> CallbackT: return decorator @t.overload - def stats(self, callback: None) -> t.Callable[[StatsCallbackT], StatsCallbackT]: - ... + def stats(self, callback: None) -> t.Callable[[StatsCallbackT], StatsCallbackT]: ... @t.overload - def stats(self, callback: StatsCallbackT) -> "AutoPoster": - ... + def stats(self, callback: StatsCallbackT) -> "AutoPoster": ... def stats(self, callback: t.Any = None) -> t.Any: """ @@ -192,15 +186,13 @@ def stats(self, callback: t.Any = None) -> t.Any: # In this example, we fetch the stats from a Discord client instance. client = Client(...) dblclient = topgg.DBLClient(TOKEN).set_data(client) - autopost = ( - dblclient - .autopost() - .on_success(lambda: print("Successfully posted the stats!") - ) + autopost = dblclient.autopost().on_success(lambda: print("Successfully posted the stats!")) + @autopost.stats() def get_stats(client: Client = topgg.data(Client)): - return topgg.StatsWrapper(guild_count=len(client.guilds), shard_count=len(client.shards)) + return topgg.StatsWrapper(guild_count=len(client.guilds)) + # somewhere after the event loop has started autopost.start() @@ -230,11 +222,11 @@ def set_interval(self, seconds: t.Union[float, datetime.timedelta]) -> "AutoPost Sets the interval between posting stats. Args: - seconds (:obj:`typing.Union` [ :obj:`float`, :obj:`datetime.timedelta` ]) + seconds (Union[:obj:`float`, :obj:`datetime.timedelta`]) The interval. Raises: - :obj:`ValueError` + ValueError If the provided interval is less than 900 seconds. """ if isinstance(seconds, datetime.timedelta): @@ -263,7 +255,7 @@ def _fut_done_callback(self, future: "asyncio.Future") -> None: async def _internal_loop(self) -> None: try: - while 1: + while True: stats = await self.client._invoke_callback(self._stats) try: await self.client.post_guild_count(stats) @@ -272,12 +264,11 @@ async def _internal_loop(self) -> None: if isinstance(err, errors.Unauthorized): raise err from None else: - on_success = getattr(self, "_success", None) - if on_success: + if on_success := getattr(self, "_success", None): await self.client._invoke_callback(on_success) if self._stopping: - return None + return await asyncio.sleep(self.interval) finally: @@ -291,13 +282,11 @@ def start(self) -> "asyncio.Task[None]": This method must be called when the event loop has already running! Raises: - :obj:`~.errors.TopGGException` + :exc:`~.errors.TopGGException` If there's no callback provided or the autopost is already running. """ if not hasattr(self, "_stats"): - raise errors.TopGGException( - "you must provide a callback that returns the stats." - ) + raise errors.TopGGException("you must provide a callback that returns the stats.") if self.is_running: raise errors.TopGGException("the autopost is already running.") @@ -315,7 +304,7 @@ def stop(self) -> None: because this will post once before stopping as opposed to cancel immediately. """ if not self.is_running: - return None + return self._stopping = True @@ -332,4 +321,3 @@ def cancel(self) -> None: self._task.cancel() self._refresh_state() - return None diff --git a/topgg/client.py b/topgg/client.py index 0f1a72db..6213c221 100644 --- a/topgg/client.py +++ b/topgg/client.py @@ -1,30 +1,34 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - -# Copyright (c) 2021 Assanali Mukhanov - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -__all__ = ["DBLClient"] - +""" +The MIT License (MIT) + +Copyright (c) 2021 Assanali Mukhanov +Copyright (c) 2024-2025 null8626 + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +__all__ = ("DBLClient",) + +import base64 +import json import typing as t +import warnings import aiohttp @@ -45,28 +49,34 @@ class DBLClient(DataContainerMixin): token (:obj:`str`): Your bot's Top.gg API Token. Keyword Args: - default_bot_id (:obj:`typing.Optional` [ :obj:`int` ]) - The default bot_id. You can override this by passing it when calling a method. - session (:class:`aiohttp.ClientSession`) + session (:class:`~aiohttp.ClientSession`) An `aiohttp session`_ to use for requests to the API. **kwargs: - Arbitrary kwargs to be passed to :class:`aiohttp.ClientSession` if session was not provided. + Arbitrary kwargs to be passed to :class:`~aiohttp.ClientSession` if session was not provided. """ - __slots__ = ("http", "default_bot_id", "_token", "_is_closed", "_autopost") + __slots__: t.Tuple[str, ...] = ("http", "bot_id", "_token", "_is_closed", "_autopost") + http: HTTPClient def __init__( self, token: str, *, - default_bot_id: t.Optional[int] = None, session: t.Optional[aiohttp.ClientSession] = None, **kwargs: t.Any, ) -> None: super().__init__() self._token = token - self.default_bot_id = default_bot_id + + try: + encoded_json = token.split(".")[1] + encoded_json += "=" * (4 - (len(encoded_json) % 4)) + + self.bot_id = int(json.loads(base64.b64decode(encoded_json))["id"]) + except: + raise errors.ClientException("invalid token.") + self._is_closed = False if session is not None: self.http = HTTPClient(token, session=session) @@ -83,13 +93,6 @@ async def _ensure_session(self) -> None: if not hasattr(self, "http"): self.http = HTTPClient(self._token, session=None) - def _validate_and_get_bot_id(self, bot_id: t.Optional[int]) -> int: - bot_id = bot_id or self.default_bot_id - if bot_id is None: - raise errors.ClientException("bot_id or default_bot_id is unset.") - - return bot_id - async def get_weekend_status(self) -> bool: """Gets weekend status from Top.gg. @@ -97,7 +100,7 @@ async def get_weekend_status(self) -> bool: :obj:`bool`: The boolean value of weekend status. Raises: - :obj:`~.errors.ClientStateException` + :exc:`~.errors.ClientStateException` If the client has been closed. """ await self._ensure_session() @@ -105,8 +108,7 @@ async def get_weekend_status(self) -> bool: return data["is_weekend"] @t.overload - async def post_guild_count(self, stats: types.StatsWrapper) -> None: - ... + async def post_guild_count(self, stats: types.StatsWrapper) -> None: ... @t.overload async def post_guild_count( @@ -115,8 +117,7 @@ async def post_guild_count( guild_count: t.Union[int, t.List[int]], shard_count: t.Optional[int] = None, shard_id: t.Optional[int] = None, - ) -> None: - ... + ) -> None: ... async def post_guild_count( self, @@ -126,63 +127,57 @@ async def post_guild_count( shard_count: t.Any = None, shard_id: t.Any = None, ) -> None: - """Posts your bot's guild count and shards info to Top.gg. - - .. _0 based indexing : https://en.wikipedia.org/wiki/Zero-based_numbering + """Posts your bot's guild count to Top.gg. Warning: You can't provide both args and kwargs at once. Args: stats (:obj:`~.types.StatsWrapper`) - An instance of StatsWrapper containing guild_count, shard_count, and shard_id. + An instance of StatsWrapper containing guild_count. Keyword Arguments: - guild_count (:obj:`typing.Optional` [:obj:`typing.Union` [ :obj:`int`, :obj:`list` [ :obj:`int` ]]]) - Number of guilds the bot is in. Applies the number to a shard instead if shards are specified. + guild_count (Optional[:obj:`int`]) + Number of guilds the bot is in. If not specified, length of provided client's property `.guilds` will be posted. - shard_count (:obj:`.typing.Optional` [ :obj:`int` ]) - The total number of shards. - shard_id (:obj:`.typing.Optional` [ :obj:`int` ]) - The index of the current shard. Top.gg uses `0 based indexing`_ for shards. Raises: TypeError If no argument is provided. - :obj:`~.errors.ClientStateException` + :exc:`~.errors.ClientStateException` If the client has been closed. """ if stats: + warnings.warn( + "Using stats no longer has a use by Top.gg API v0. Soon, all you need is just your bot's server count.", + DeprecationWarning, + ) + guild_count = stats.guild_count - shard_count = stats.shard_count - shard_id = stats.shard_id + + if stats.shard_count or stats.shard_id: + warnings.warn("Posting shard-related data no longer has a use by Top.gg API v0.", DeprecationWarning) elif guild_count is None: - raise TypeError("stats or guild_count must be provided.") + raise TypeError("guild_count must be provided.") + elif shard_count or shard_id: + warnings.warn("Posting shard-related data no longer has a use by Top.gg API v0.", DeprecationWarning) + await self._ensure_session() - await self.http.post_guild_count(guild_count, shard_count, shard_id) + await self.http.post_guild_count(guild_count) - async def get_guild_count( - self, bot_id: t.Optional[int] = None - ) -> types.BotStatsData: - """Gets a bot's guild count and shard info from Top.gg. - - Args: - bot_id (int) - ID of the bot you want to look up. Defaults to the provided Client object. + async def get_guild_count(self) -> types.BotStatsData: + """Gets this bot's guild count from Top.gg. Returns: :obj:`~.types.BotStatsData`: - The guild count and shards of a bot on Top.gg. + The guild count on Top.gg. Raises: - :obj:`~.errors.ClientException` - If neither bot_id or default_bot_id was set. - :obj:`~.errors.ClientStateException` + :exc:`~.errors.ClientStateException` If the client has been closed. """ - bot_id = self._validate_and_get_bot_id(bot_id) await self._ensure_session() - response = await self.http.get_guild_count(bot_id) + response = await self.http.get_guild_count(self.bot_id) return types.BotStatsData(**response) async def get_bot_votes(self) -> t.List[types.BriefUserData]: @@ -192,21 +187,15 @@ async def get_bot_votes(self) -> t.List[types.BriefUserData]: This API endpoint is only available to the bot's owner. Returns: - :obj:`list` [ :obj:`~.types.BriefUserData` ]: + List[:obj:`~.types.BriefUserData`]: Users who voted for your bot. Raises: - :obj:`~.errors.ClientException` - If default_bot_id isn't provided when constructing the client. - :obj:`~.errors.ClientStateException` + :exc:`~.errors.ClientStateException` If the client has been closed. """ - if not self.default_bot_id: - raise errors.ClientException( - "you must set default_bot_id when constructing the client." - ) await self._ensure_session() - response = await self.http.get_bot_votes(self.default_bot_id) + response = await self.http.get_bot_votes(self.bot_id) return [types.BriefUserData(**user) for user in response] async def get_bot_info(self, bot_id: t.Optional[int] = None) -> types.BotData: @@ -216,7 +205,7 @@ async def get_bot_info(self, bot_id: t.Optional[int] = None) -> types.BotData: Args: bot_id (int) - ID of the bot to look up. Defaults to the provided Client object. + ID of the bot to look up. Defaults to this bot's ID. Returns: :obj:`~.types.BotData`: @@ -224,14 +213,11 @@ async def get_bot_info(self, bot_id: t.Optional[int] = None) -> types.BotData: `here `_. Raises: - :obj:`~.errors.ClientException` - If neither bot_id or default_bot_id was set. - :obj:`~.errors.ClientStateException` + :exc:`~.errors.ClientStateException` If the client has been closed. """ - bot_id = self._validate_and_get_bot_id(bot_id) await self._ensure_session() - response = await self.http.get_bot_info(bot_id) + response = await self.http.get_bot_info(bot_id or self.bot_id) return types.BotData(**response) async def get_bots( @@ -242,38 +228,12 @@ async def get_bots( search: t.Optional[t.Dict[str, t.Any]] = None, fields: t.Optional[t.List[str]] = None, ) -> types.DataDict[str, t.Any]: - """This function is a coroutine. - - Gets information about listed bots on Top.gg. - - Args: - limit (int) - The number of results to look up. Defaults to 50. Max 500 allowed. - offset (int) - The amount of bots to skip. Defaults to 0. - sort (str) - The field to sort by. Prefix with ``-`` to reverse the order. - search (:obj:`dict` [ :obj:`str`, :obj:`typing.Any` ]) - The search data. - fields (:obj:`list` [ :obj:`str` ]) - Fields to output. - - Returns: - :obj:`~.types.DataDict`: - Info on bots that match the search query on Top.gg. - - Raises: - :obj:`~.errors.ClientStateException` - If the client has been closed. - """ sort = sort or "" search = search or {} fields = fields or [] await self._ensure_session() response = await self.http.get_bots(limit, offset, sort, search, fields) - response["results"] = [ - types.BotData(**bot_data) for bot_data in response["results"] - ] + response["results"] = [types.BotData(**bot_data) for bot_data in response["results"]] return types.DataDict(**response) async def get_user_info(self, user_id: int) -> types.UserData: @@ -290,7 +250,7 @@ async def get_user_info(self, user_id: int) -> types.UserData: Information about a Top.gg user. Raises: - :obj:`~.errors.ClientStateException` + :exc:`~.errors.ClientStateException` If the client has been closed. """ await self._ensure_session() @@ -308,18 +268,11 @@ async def get_user_vote(self, user_id: int) -> bool: :obj:`bool`: Info about the user's vote. Raises: - :obj:`~.errors.ClientException` - If default_bot_id isn't provided when constructing the client. - :obj:`~.errors.ClientStateException` + :exc:`~.errors.ClientStateException` If the client has been closed. """ - if not self.default_bot_id: - raise errors.ClientException( - "you must set default_bot_id when constructing the client." - ) - await self._ensure_session() - data = await self.http.get_user_vote(self.default_bot_id, user_id) + data = await self.http.get_user_vote(self.bot_id, user_id) return bool(data["voted"]) def generate_widget(self, *, options: types.WidgetOptions) -> str: @@ -334,28 +287,22 @@ def generate_widget(self, *, options: types.WidgetOptions) -> str: str: Generated widget URL. Raises: - :obj:`~.errors.ClientException` - If bot_id or default_bot_id is unset. TypeError: If options passed is not of type WidgetOptions. """ if not isinstance(options, types.WidgetOptions): - raise TypeError( - "options argument passed to generate_widget must be of type WidgetOptions" - ) - - bot_id = options.id or self.default_bot_id - if bot_id is None: - raise errors.ClientException("bot_id or default_bot_id is unset.") + raise TypeError("options argument passed to generate_widget must be of type WidgetOptions") + bot_id = options.id or self.bot_id widget_query = f"noavatar={str(options.noavatar).lower()}" + for key, value in options.colors.items(): widget_query += f"&{key.lower()}{'' if key.lower().endswith('color') else 'color'}={value:x}" + widget_format = options.format widget_type = f"/{options.type}" if options.type else "" - url = f"""https://top.gg/api/widget{widget_type}/{bot_id}.{widget_format}?{widget_query}""" - return url + return f"https://top.gg/api/widget{widget_type}/{bot_id}.{widget_format}?{widget_query}" async def close(self) -> None: """Closes all connections.""" diff --git a/topgg/data.py b/topgg/data.py index 7126d3bf..b21d0a4c 100644 --- a/topgg/data.py +++ b/topgg/data.py @@ -1,26 +1,31 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Norizon - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -__all__ = ["data", "DataContainerMixin"] +""" +The MIT License (MIT) + +Copyright (c) 2021 Norizon + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +__all__ = ( + "data", + "DataContainerMixin", +) import inspect import typing as t @@ -36,7 +41,7 @@ def data(type_: t.Type[T]) -> T: Represents the injected data. This should be set as the parameter's default value. Args: - `type_` (:obj:`type` [ :obj:`T` ]) + `type_` (:obj:`type` [ :obj:`T`]) The type of the injected data. Returns: @@ -52,15 +57,16 @@ def data(type_: t.Type[T]) -> T: dblclient = topgg.DBLClient(TOKEN).set_data(client) autopost: topgg.AutoPoster = dblclient.autopost() + @autopost.stats() def get_stats(client: Client = topgg.data(Client)): - return topgg.StatsWrapper(guild_count=len(client.guilds), shard_count=len(client.shards)) + return topgg.StatsWrapper(guild_count=len(client.guilds)) """ return t.cast(T, Data(type_)) class Data(t.Generic[T]): - __slots__ = ("type",) + __slots__: t.Tuple[str, ...] = ("type",) def __init__(self, type_: t.Type[T]) -> None: self.type: t.Type[T] = type_ @@ -74,25 +80,23 @@ class DataContainerMixin: as arguments in your functions. """ - __slots__ = ("_data",) + __slots__: t.Tuple[str, ...] = ("_data",) def __init__(self) -> None: self._data: t.Dict[t.Type, t.Any] = {type(self): self} - def set_data( - self: DataContainerT, data_: t.Any, *, override: bool = False - ) -> DataContainerT: + def set_data(self: DataContainerT, data_: t.Any, *, override: bool = False) -> DataContainerT: """ Sets data to be available in your functions. Args: - `data_` (:obj:`typing.Any`) + `data_` (Any) The data to be injected. override (:obj:`bool`) Whether or not to override another instance that already exists. Raises: - :obj:`~.errors.TopGGException` + :exc:`~.errors.TopGGException` If override is False and another instance of the same type exists. """ type_ = type(data_) @@ -105,20 +109,16 @@ def set_data( return self @t.overload - def get_data(self, type_: t.Type[T]) -> t.Optional[T]: - ... + def get_data(self, type_: t.Type[T]) -> t.Optional[T]: ... @t.overload - def get_data(self, type_: t.Type[T], default: t.Any = None) -> t.Any: - ... + def get_data(self, type_: t.Type[T], default: t.Any = None) -> t.Any: ... def get_data(self, type_: t.Any, default: t.Any = None) -> t.Any: """Gets the injected data.""" return self._data.get(type_, default) - async def _invoke_callback( - self, callback: t.Callable[..., T], *args: t.Any, **kwargs: t.Any - ) -> T: + async def _invoke_callback(self, callback: t.Callable[..., T], *args: t.Any, **kwargs: t.Any) -> T: parameters: t.Mapping[str, inspect.Parameter] try: parameters = inspect.signature(callback).parameters @@ -128,8 +128,7 @@ async def _invoke_callback( signatures: t.Dict[str, Data] = { k: v.default for k, v in parameters.items() - if v.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD - and isinstance(v.default, Data) + if v.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD and isinstance(v.default, Data) } for k, v in signatures.items(): diff --git a/topgg/errors.py b/topgg/errors.py index d8c157a5..e15d39be 100644 --- a/topgg/errors.py +++ b/topgg/errors.py @@ -1,28 +1,28 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - -# Copyright (c) 2021 Assanali Mukhanov - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -__all__ = [ +""" +The MIT License (MIT) + +Copyright (c) 2021 Assanali Mukhanov + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +__all__ = ( "TopGGException", "ClientException", "ClientStateException", @@ -32,7 +32,7 @@ "Forbidden", "NotFound", "ServerError", -] +) from typing import TYPE_CHECKING, Union @@ -48,14 +48,14 @@ class TopGGException(Exception): class ClientException(TopGGException): - """Exception that's thrown when an operation in the :class:`~.DBLClient` fails. + """Exception that's thrown when an operation in the :class:`.DBLClient` fails. These are usually for exceptions that happened due to user input. """ class ClientStateException(ClientException): - """Exception that's thrown when an operation happens in a closed :obj:`~.DBLClient` instance.""" + """Exception that's thrown when an operation happens in a closed :obj:`.DBLClient` instance.""" class HTTPException(TopGGException): diff --git a/topgg/http.py b/topgg/http.py index 08160d67..c3411e36 100644 --- a/topgg/http.py +++ b/topgg/http.py @@ -1,35 +1,37 @@ -# -*- coding: utf-8 -*- +""" +The MIT License (MIT) -# The MIT License (MIT) +Copyright (c) 2021 Assanali Mukhanov +Copyright (c) 2024-2025 null8626 -# Copyright (c) 2021 Assanali Mukhanov +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -__all__ = ["HTTPClient"] +__all__ = ("HTTPClient",) import asyncio import json import logging import sys -from datetime import datetime -from typing import Any, Coroutine, Dict, Iterable, List, Optional, Sequence, Union, cast +import warnings +from time import time +from typing import Any, Coroutine, Dict, Iterable, List, Optional, Sequence, Union, cast, Tuple import aiohttp from aiohttp import ClientResponse @@ -49,6 +51,9 @@ async def _json_or_text( return text +BASE = "https://top.gg/api" + + class HTTPClient: """Represents an HTTP client sending HTTP requests to the Top.gg API. @@ -65,6 +70,16 @@ class HTTPClient: Arbitrary kwargs to be passed to :class:`aiohttp.ClientSession`. """ + __slots__: Tuple[str, ...] = ( + "token", + "_own_session", + "session", + "global_rate_limiter", + "bot_rate_limiter", + "rate_limiters", + "user_agent", + ) + def __init__( self, token: str, @@ -72,32 +87,21 @@ def __init__( session: Optional[aiohttp.ClientSession] = None, **kwargs: Any, ) -> None: - self.BASE = "https://top.gg/api" self.token = token self._own_session = session is None self.session: aiohttp.ClientSession = session or aiohttp.ClientSession(**kwargs) - self.global_rate_limiter = AsyncRateLimiter( - max_calls=99, period=1, callback=_rate_limit_handler - ) - self.bot_rate_limiter = AsyncRateLimiter( - max_calls=59, period=60, callback=_rate_limit_handler - ) - self.rate_limiters = AsyncRateLimiterManager( - [self.global_rate_limiter, self.bot_rate_limiter] - ) + self.global_rate_limiter = AsyncRateLimiter(max_calls=99, period=1, callback=_rate_limit_handler) + self.bot_rate_limiter = AsyncRateLimiter(max_calls=59, period=60, callback=_rate_limit_handler) + self.rate_limiters = AsyncRateLimiterManager([self.global_rate_limiter, self.bot_rate_limiter]) self.user_agent = ( - f"topggpy (https://github.com/top-gg/python-sdk {__version__}) Python/" + f"topggpy (https://github.com/top-gg-community/python-sdk {__version__}) Python/" f"{sys.version_info[0]}.{sys.version_info[1]} aiohttp/{aiohttp.__version__}" ) async def request(self, method: str, endpoint: str, **kwargs: Any) -> dict: """Handles requests to the API.""" - rate_limiters = ( - self.rate_limiters - if endpoint.startswith("/bots") - else self.global_rate_limiter - ) - url = f"{self.BASE}{endpoint}" + rate_limiters = self.rate_limiters if endpoint.startswith("/bots") else self.global_rate_limiter + url = BASE + endpoint if not self.token: raise errors.UnauthorizedDetected("Top.gg API token not provided") @@ -171,20 +175,9 @@ async def close(self) -> None: if self._own_session: await self.session.close() - async def post_guild_count( - self, - guild_count: Optional[Union[int, List[int]]], - shard_count: Optional[int], - shard_id: Optional[int], - ) -> None: - """Posts bot's guild count and shards info on Top.gg.""" - payload = {"server_count": guild_count} - if shard_count: - payload["shard_count"] = shard_count - if shard_id: - payload["shard_id"] = shard_id - - await self.request("POST", "/bots/stats", json=payload) + async def post_guild_count(self, guild_count: Optional[int]) -> None: + """Posts bot's guild count on Top.gg.""" + await self.request("POST", "/bots/stats", json={"server_count": guild_count}) def get_weekend_status(self) -> Coroutine[Any, Any, dict]: """Gets the weekend status from Top.gg.""" @@ -210,10 +203,9 @@ def get_bots( search: Dict[str, str], fields: Sequence[str], ) -> Coroutine[Any, Any, dict]: - """Gets an object of bots on Top.gg.""" limit = min(limit, 500) fields = ", ".join(fields) - search = " ".join([f"{field}: {value}" for field, value in search.items()]) + search = " ".join(f"{field}: {value}" for field, value in search.items()) return self.request( "GET", @@ -238,15 +230,16 @@ def get_user_vote(self, bot_id: int, user_id: int) -> Coroutine[Any, Any, dict]: async def _rate_limit_handler(until: float) -> None: """Handles the displayed message when we are ratelimited.""" - duration = round(until - datetime.utcnow().timestamp()) + duration = round(until - time()) mins = duration / 60 - fmt = ( - "We have exhausted a ratelimit quota. Retrying in %.2f seconds (%.3f minutes)." - ) + fmt = "We have exhausted a ratelimit quota. Retrying in %.2f seconds (%.3f minutes)." _LOGGER.warning(fmt, duration, mins) def to_json(obj: Any) -> str: - if json.__name__ == "ujson": - return json.dumps(obj, ensure_ascii=True) - return json.dumps(obj, separators=(",", ":"), ensure_ascii=True) + kwargs = {"ensure_ascii": True} + + if json.__name__ != "ujson": + kwargs["separators"] = (",", ":") + + return json.dumps(obj, **kwargs) diff --git a/topgg/py.typed b/topgg/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/topgg/ratelimiter.py b/topgg/ratelimiter.py index 028a98ee..84ea5dad 100644 --- a/topgg/ratelimiter.py +++ b/topgg/ratelimiter.py @@ -1,32 +1,32 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - -# Copyright (c) 2021 Assanali Mukhanov - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. +""" +The MIT License (MIT) + +Copyright (c) 2021 Assanali Mukhanov + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" import asyncio import collections -from datetime import datetime +from time import time from types import TracebackType -from typing import Any, Awaitable, Callable, List, Optional, Type +from typing import Any, Awaitable, Callable, List, Optional, Type, Tuple class AsyncRateLimiter: @@ -34,6 +34,8 @@ class AsyncRateLimiter: Provides rate limiting for an operation with a configurable number of requests for a time period. """ + __slots__: Tuple[str, ...] = ("__lock", "callback", "max_calls", "period", "calls") + __lock: asyncio.Lock callback: Optional[Callable[[float], Awaitable[Any]]] max_calls: int @@ -60,11 +62,10 @@ def __init__( async def __aenter__(self) -> "AsyncRateLimiter": async with self.__lock: if len(self.calls) >= self.max_calls: - until = datetime.utcnow().timestamp() + self.period - self._timespan + until = time() + self.period - self._timespan if self.callback: asyncio.ensure_future(self.callback(until)) - sleep_time = until - datetime.utcnow().timestamp() - if sleep_time > 0: + if (sleep_time := until - time()) > 0: await asyncio.sleep(sleep_time) return self @@ -76,7 +77,7 @@ async def __aexit__( ) -> None: async with self.__lock: # Store the last operation timestamp. - self.calls.append(datetime.utcnow().timestamp()) + self.calls.append(time()) while self._timespan >= self.period: self.calls.popleft() @@ -87,13 +88,17 @@ def _timespan(self) -> float: class AsyncRateLimiterManager: + __slots__: Tuple[str, ...] = ("rate_limiters",) + rate_limiters: List[AsyncRateLimiter] def __init__(self, rate_limiters: List[AsyncRateLimiter]): self.rate_limiters = rate_limiters async def __aenter__(self) -> "AsyncRateLimiterManager": - [await manager.__aenter__() for manager in self.rate_limiters] + for manager in self.rate_limiters: + await manager.__aenter__() + return self async def __aexit__( @@ -102,9 +107,4 @@ async def __aexit__( exc_val: BaseException, exc_tb: TracebackType, ) -> None: - await asyncio.gather( - *[ - manager.__aexit__(exc_type, exc_val, exc_tb) - for manager in self.rate_limiters - ] - ) + await asyncio.gather(*(manager.__aexit__(exc_type, exc_val, exc_tb) for manager in self.rate_limiters)) diff --git a/topgg/types.py b/topgg/types.py index 2da13f95..42553d11 100644 --- a/topgg/types.py +++ b/topgg/types.py @@ -1,31 +1,32 @@ -# -*- coding: utf-8 -*- +""" +The MIT License (MIT) -# The MIT License (MIT) +Copyright (c) 2021 Assanali Mukhanov -# Copyright (c) 2021 Assanali Mukhanov +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -__all__ = ["WidgetOptions", "StatsWrapper"] +__all__ = ("WidgetOptions", "StatsWrapper") import dataclasses import typing as t +import warnings from datetime import datetime KT = t.TypeVar("KT") @@ -35,14 +36,13 @@ def camel_to_snake(string: str) -> str: - return "".join(["_" + c.lower() if c.isupper() else c for c in string]).lstrip("_") + return "".join("_" + c.lower() if c.isupper() else c for c in string).lstrip("_") def parse_vote_dict(d: dict) -> dict: data = d.copy() - query = data.get("query", "").lstrip("?") - if query: + if query := data.get("query", "").lstrip("?"): query_dict = {k: v for k, v in [pair.split("=") for pair in query.split("&")]} data["query"] = DataDict(**query_dict) else: @@ -55,8 +55,7 @@ def parse_vote_dict(d: dict) -> dict: data["guild"] = int(data["guild"]) for key, value in data.copy().items(): - converted_key = camel_to_snake(key) - if key != converted_key: + if key != (converted_key := camel_to_snake(key)): del data[key] data[converted_key] = value @@ -67,20 +66,17 @@ def parse_dict(d: dict) -> dict: data = d.copy() for key, value in data.copy().items(): - if "id" in key.lower(): - if value == "": - value = None - else: - if isinstance(value, str) and value.isdigit(): - value = int(value) - else: - continue - elif value == "": + if value == "": value = None + elif "id" in key.lower(): + if isinstance(value, str) and value.isdigit(): + value = int(value) + else: + continue - converted_key = camel_to_snake(key) - if key != converted_key: + if key != (converted_key := camel_to_snake(key)): del data[key] + data[converted_key] = value return data @@ -89,13 +85,17 @@ def parse_dict(d: dict) -> dict: def parse_bot_dict(d: dict) -> dict: data = parse_dict(d.copy()) - if data.get("date") and not isinstance(data["date"], datetime): - data["date"] = datetime.strptime(data["date"], "%Y-%m-%dT%H:%M:%S.%fZ") + if (date := data.get("date")) and not isinstance(date, datetime): + data["date"] = datetime.fromisoformat(date.replace("Z", "+00:00")) + + if owners := data.get("owners"): + data["owners"] = [int(e) for e in owners] - if data.get("owners"): - data["owners"] = [int(e) for e in data["owners"]] - if data.get("guilds"): - data["guilds"] = [int(e) for e in data["guilds"]] + # TODO: remove this soon + data.pop("defAvatar", None) + data.pop("discriminator", None) + data.pop("guilds", None) + data.pop("certifiedBot", None) for key, value in data.copy().items(): converted_key = camel_to_snake(key) @@ -109,6 +109,10 @@ def parse_bot_dict(d: dict) -> dict: def parse_user_dict(d: dict) -> dict: data = d.copy() + # TODO: remove this soon + data.pop("discriminator", None) + data.pop("certifiedDev", None) + data["social"] = SocialData(**data.get("social", {})) return data @@ -119,10 +123,6 @@ def parse_bot_stats_dict(d: dict) -> dict: if "server_count" not in data: data["server_count"] = None - if "shards" not in data: - data["shards"] = [] - if "shard_count" not in data: - data["shard_count"] = None return data @@ -142,6 +142,8 @@ class WidgetOptions(DataDict[str, t.Any]): """Model that represents widget options that are passed to Top.gg widget URL generated via :meth:`DBLClient.generate_widget`.""" + __slots__: t.Tuple[str, ...] = () + id: t.Optional[int] """ID of a bot to generate the widget for. Must resolve to an ID of a listed bot when converted to a string.""" colors: Colors @@ -202,21 +204,17 @@ class BotData(DataDict[str, t.Any]): """Model that contains information about a listed bot on top.gg. The data this model contains can be found `here `__.""" + __slots__: t.Tuple[str, ...] = () + id: int """The ID of the bot.""" username: str """The username of the bot.""" - discriminator: str - """The discriminator of the bot.""" - avatar: t.Optional[str] """The avatar hash of the bot.""" - def_avatar: str - """The avatar hash of the bot's default avatar.""" - prefix: str """The prefix of the bot.""" @@ -241,18 +239,12 @@ class BotData(DataDict[str, t.Any]): owners: t.List[int] """The IDs of the owners of the bot.""" - guilds: t.List[int] - """The guilds the bot is in.""" - invite: t.Optional[str] """The invite URL of the bot.""" date: datetime """The time the bot was added.""" - certified_bot: bool - """Whether or not the bot is certified.""" - vanity: t.Optional[str] """The vanity URL of the bot.""" @@ -268,24 +260,77 @@ class BotData(DataDict[str, t.Any]): def __init__(self, **kwargs: t.Any): super().__init__(**parse_bot_dict(kwargs)) + @property + def def_avatar(self) -> t.Optional[str]: + warnings.warn( + "def_avatar is no longer supported by Top.gg API v0. At the moment, this will always be None.", + DeprecationWarning, + ) + + @property + def discriminator(self) -> str: + warnings.warn( + "Discriminators are no longer supported by Top.gg API v0. At the moment, this will always be '0'.", + DeprecationWarning, + ) + return "0" + + @property + def lib(self) -> t.Optional[str]: + warnings.warn( + "lib is no longer supported by Top.gg API v0. At the moment, this will always be None.", + DeprecationWarning, + ) + + @property + def guilds(self) -> t.List[int]: + warnings.warn( + "Guilds list is no longer supported by Top.gg API v0. At the moment, this will always be an empty list.", + DeprecationWarning, + ) + return [] + + @property + def certified_bot(self) -> bool: + warnings.warn( + "Certified bot is no longer supported by Top.gg API v0. At the moment, this will always be False.", + DeprecationWarning, + ) + return False + class BotStatsData(DataDict[str, t.Any]): - """Model that contains information about a listed bot's guild and shard count.""" + """Model that contains information about a listed bot's guild count.""" + + __slots__: t.Tuple[str, ...] = () server_count: t.Optional[int] """The amount of servers the bot is in.""" - shards: t.List[int] - """The amount of servers the bot is in per shard.""" - shard_count: t.Optional[int] - """The amount of shards a bot has.""" def __init__(self, **kwargs: t.Any): super().__init__(**parse_bot_stats_dict(kwargs)) + @property + def shards(self) -> t.List[int]: + warnings.warn( + "Shard-related data is no longer supported by Top.gg API v0. At the moment, this will always return an empty list.", + DeprecationWarning, + ) + return [] + + @property + def shard_count(self) -> t.Optional[int]: + warnings.warn( + "Shard-related data is no longer supported by Top.gg API v0. At the moment, this will always return None.", + DeprecationWarning, + ) + class BriefUserData(DataDict[str, t.Any]): """Model that contains brief information about a Top.gg user.""" + __slots__: t.Tuple[str, ...] = () + id: int """The Discord ID of the user.""" username: str @@ -302,6 +347,8 @@ def __init__(self, **kwargs: t.Any): class SocialData(DataDict[str, str]): """Model that contains social information about a top.gg user.""" + __slots__: t.Tuple[str, ...] = () + youtube: str """The YouTube channel ID of the user.""" reddit: str @@ -318,15 +365,14 @@ class UserData(DataDict[str, t.Any]): """Model that contains information about a top.gg user. The data this model contains can be found `here `__.""" + __slots__: t.Tuple[str, ...] = () + id: int """The ID of the user.""" username: str """The username of the user.""" - discriminator: str - """The discriminator of the user.""" - social: SocialData """The social data of the user.""" @@ -336,9 +382,6 @@ class UserData(DataDict[str, t.Any]): supporter: bool """Whether or not the user is a supporter.""" - certified_dev: bool - """Whether or not the user is a certified dev.""" - mod: bool """Whether or not the user is a Top.gg mod.""" @@ -351,16 +394,34 @@ class UserData(DataDict[str, t.Any]): def __init__(self, **kwargs: t.Any): super().__init__(**parse_user_dict(kwargs)) + @property + def certified_dev(self) -> bool: + warnings.warn( + "Certified dev is no longer supported by Top.gg API v0. At the moment, this will always be False.", + DeprecationWarning, + ) + return False + + @property + def discriminator(self) -> str: + warnings.warn( + "Discriminators are no longer supported by Top.gg API v0. At the moment, this will always be '0'.", + DeprecationWarning, + ) + return "0" + class VoteDataDict(DataDict[str, t.Any]): """Base model that represents received information from Top.gg via webhooks.""" + __slots__: t.Tuple[str, ...] = () + type: str """Type of the action (``upvote`` or ``test``).""" user: int """ID of the voter.""" query: DataDict - """Query parameters in :obj:`~.DataDict`.""" + """Query parameters in :obj:`.DataDict`.""" def __init__(self, **kwargs: t.Any): super().__init__(**parse_vote_dict(kwargs)) @@ -369,6 +430,8 @@ def __init__(self, **kwargs: t.Any): class BotVoteData(VoteDataDict): """Model that contains information about a bot vote.""" + __slots__: t.Tuple[str, ...] = () + bot: int """ID of the bot the user voted for.""" is_weekend: bool @@ -378,6 +441,8 @@ class BotVoteData(VoteDataDict): class GuildVoteData(VoteDataDict): """Model that contains information about a guild vote.""" + __slots__: t.Tuple[str, ...] = () + guild: int """ID of the guild the user voted for.""" @@ -391,7 +456,10 @@ class StatsWrapper: """The guild count.""" shard_count: t.Optional[int] = None - """The shard count.""" - shard_id: t.Optional[int] = None - """The shard ID the guild count belongs to.""" + + def __init__(self, guild_count: int, **kwargs): + if kwargs.get("shard_count") or kwargs.get("shard_id"): + warnings.warn("Posting shard-related data no longer has a use by Top.gg API v0.", DeprecationWarning) + + self.guild_count = guild_count diff --git a/topgg/webhook.py b/topgg/webhook.py index 4b94ec2b..9b947d8f 100644 --- a/topgg/webhook.py +++ b/topgg/webhook.py @@ -1,34 +1,35 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - -# Copyright (c) 2021 Assanali Mukhanov - -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -__all__ = [ +""" +The MIT License (MIT) + +Copyright (c) 2021 Assanali Mukhanov +Copyright (c) 2024-2025 null8626 + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +__all__ = ( "endpoint", "BoundWebhookEndpoint", "WebhookEndpoint", "WebhookManager", "WebhookType", -] +) import enum import typing as t @@ -51,6 +52,8 @@ class WebhookType(enum.Enum): """An enum that represents the type of an endpoint.""" + __slots__: t.Tuple[str, ...] = () + BOT = enum.auto() """Marks the endpoint as a bot webhook.""" @@ -63,38 +66,38 @@ class WebhookManager(DataContainerMixin): A class for managing Top.gg webhooks. """ + __slots__: t.Tuple[str, ...] = ("__app", "_webserver", "_is_running") + __app: web.Application _webserver: web.TCPSite - _is_closed: bool - __slots__ = ("__app", "_webserver", "_is_running") + _is_running: bool def __init__(self) -> None: super().__init__() + self.__app = web.Application() self._is_running = False @t.overload - def endpoint(self, endpoint_: None = None) -> "BoundWebhookEndpoint": - ... + def endpoint(self, endpoint_: None = None) -> "BoundWebhookEndpoint": ... @t.overload - def endpoint(self, endpoint_: "WebhookEndpoint") -> "WebhookManager": - ... + def endpoint(self, endpoint_: "WebhookEndpoint") -> "WebhookManager": ... def endpoint(self, endpoint_: t.Optional["WebhookEndpoint"] = None) -> t.Any: """Helper method that returns a WebhookEndpoint object. Args: - `endpoint_` (:obj:`typing.Optional` [ :obj:`WebhookEndpoint` ]) + `endpoint_` (Optional[:obj:`WebhookEndpoint`]) The endpoint to add. Returns: - :obj:`typing.Union` [ :obj:`WebhookManager`, :obj:`BoundWebhookEndpoint` ]: + Union[:obj:`WebhookManager`, :obj:`BoundWebhookEndpoint`]: An instance of :obj:`WebhookManager` if endpoint was provided, otherwise :obj:`BoundWebhookEndpoint`. Raises: - :obj:`~.errors.TopGGException` + :exc:`~.errors.TopGGException` If the endpoint is lacking attributes. """ if endpoint_: @@ -109,9 +112,7 @@ def endpoint(self, endpoint_: t.Optional["WebhookEndpoint"] = None) -> t.Any: self.app.router.add_post( endpoint_._route, - self._get_handler( - endpoint_._type, endpoint_._auth, endpoint_._callback - ), + self._get_handler(endpoint_._type, endpoint_._auth, endpoint_._callback), ) return self @@ -124,6 +125,7 @@ async def start(self, port: int) -> None: port (int) The port to run the webhook on. """ + runner = web.AppRunner(self.__app) await runner.setup() self._webserver = web.TCPSite(runner, "0.0.0.0", port) @@ -140,19 +142,19 @@ def app(self) -> web.Application: """Returns the internal web application that handles webhook requests. Returns: - :class:`aiohttp.web.Application`: + :class:`~aiohttp.web.Application`: The internal web application. """ return self.__app async def close(self) -> None: """Stops the webhook.""" + await self._webserver.stop() + await self.__app.shutdown() self._is_running = False - def _get_handler( - self, type_: WebhookType, auth: str, callback: t.Callable[..., t.Any] - ) -> _HandlerT: + def _get_handler(self, type_: WebhookType, auth: str, callback: t.Callable[..., t.Any]) -> _HandlerT: async def _handler(request: aiohttp.web.Request) -> web.Response: if request.headers.get("Authorization", "") != auth: return web.Response(status=401, text="Unauthorized") @@ -175,7 +177,7 @@ class WebhookEndpoint: A helper class to setup webhook endpoint. """ - __slots__ = ("_callback", "_auth", "_route", "_type") + __slots__: t.Tuple[str, ...] = ("_callback", "_auth", "_route", "_type") def __init__(self) -> None: self._auth = "" @@ -225,12 +227,10 @@ def auth(self: T, auth_: str) -> T: return self @t.overload - def callback(self, callback_: None) -> t.Callable[[CallbackT], CallbackT]: - ... + def callback(self, callback_: None) -> t.Callable[[CallbackT], CallbackT]: ... @t.overload - def callback(self: T, callback_: CallbackT) -> T: - ... + def callback(self: T, callback_: CallbackT) -> T: ... def callback(self, callback_: t.Any = None) -> t.Any: """ @@ -245,25 +245,21 @@ def callback(self, callback_: t.Any = None) -> t.Any: import topgg webhook_manager = topgg.WebhookManager() - endpoint = ( - topgg.WebhookEndpoint() - .type(topgg.WebhookType.BOT) - .route("/dblwebhook") - .auth("youshallnotpass") - ) + endpoint = topgg.WebhookEndpoint().type(topgg.WebhookType.BOT).route("/dblwebhook").auth("youshallnotpass") # The following are valid. endpoint.callback(lambda vote_data: print("Receives a vote!", vote_data)) + # Used as decorator, the decorated function will become the WebhookEndpoint object. @endpoint.callback - def endpoint(vote_data: topgg.BotVoteData): - ... + def endpoint(vote_data: topgg.BotVoteData): ... + # Used as decorator factory, the decorated function will still be the function itself. @endpoint.callback() - def on_vote(vote_data: topgg.BotVoteData): - ... + def on_vote(vote_data: topgg.BotVoteData): ... + webhook_manager.endpoint(endpoint) """ @@ -286,30 +282,27 @@ class BoundWebhookEndpoint(WebhookEndpoint): import topgg webhook_manager = ( - topgg.WebhookManager() - .endpoint() - .type(topgg.WebhookType.BOT) - .route("/dblwebhook") - .auth("youshallnotpass") + topgg.WebhookManager().endpoint().type(topgg.WebhookType.BOT).route("/dblwebhook").auth("youshallnotpass") ) # The following are valid. endpoint.callback(lambda vote_data: print("Receives a vote!", vote_data)) + # Used as decorator, the decorated function will become the BoundWebhookEndpoint object. @endpoint.callback - def endpoint(vote_data: topgg.BotVoteData): - ... + def endpoint(vote_data: topgg.BotVoteData): ... + # Used as decorator factory, the decorated function will still be the function itself. @endpoint.callback() - def on_vote(vote_data: topgg.BotVoteData): - ... + def on_vote(vote_data: topgg.BotVoteData): ... + endpoint.add_to_manager() """ - __slots__ = ("manager",) + __slots__: t.Tuple[str, ...] = ("manager",) def __init__(self, manager: WebhookManager): super().__init__() @@ -330,9 +323,7 @@ def add_to_manager(self) -> WebhookManager: return self.manager -def endpoint( - route: str, type: WebhookType, auth: str = "" -) -> t.Callable[[t.Callable[..., t.Any]], WebhookEndpoint]: +def endpoint(route: str, type: WebhookType, auth: str = "") -> t.Callable[[t.Callable[..., t.Any]], WebhookEndpoint]: """ A decorator factory for instantiating WebhookEndpoint. @@ -345,7 +336,7 @@ def endpoint( The auth for the endpoint. Returns: - :obj:`typing.Callable` [[ :obj:`typing.Callable` [..., :obj:`typing.Any` ]], :obj:`WebhookEndpoint` ]: + Callable[[Callable[..., Any]], :obj:`WebhookEndpoint`]: The actual decorator. :Example: @@ -353,13 +344,13 @@ def endpoint( import topgg + @topgg.endpoint("/dblwebhook", WebhookType.BOT, "youshallnotpass") async def on_vote( vote_data: topgg.BotVoteData, # database here is an injected data database: Database = topgg.data(Database), - ): - ... + ): ... """ def decorator(func: t.Callable[..., t.Any]) -> WebhookEndpoint: