Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
dblpy.egg-info/
topggpy.egg-info/
topgg/__pycache__/
**/__pycache__/
.ruff_cache/
.vscode/
build/
docs/_build/
dist/
/docs/_build
/docs/_templates
.vscode
/.idea/
__pycache__
.coverage
.pytest_cache/
topggpy.egg-info/
23 changes: 19 additions & 4 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
Copyright 2021 Assanali Mukhanov & Top.gg
The MIT License (MIT)

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:
Copyright (c) 2021 Assanali Mukhanov & Top.gg
Copyright (c) 2024-2025 null8626 & Top.gg

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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 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 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.
21 changes: 0 additions & 21 deletions mypy.ini

This file was deleted.

15 changes: 6 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,33 @@ requires = ["setuptools"]

[project]
name = "topggpy"
version = "3.0.0"
description = "A community-maintained Python API Client for the Top.gg API."
version = "1.5.0"
description = "A simple API wrapper for Top.gg written in Python."
readme = "README.md"
license = { text = "MIT" }
authors = [{ name = "null8626" }, { name = "Top.gg" }]
keywords = ["discord", "discord-bot", "topgg"]
dependencies = ["aiohttp>=3.12.15"]
dependencies = ["aiohttp>=3.13.1"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"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",
"Programming Language :: Python :: 3.14",
"Topic :: Internet",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Utilities"
]
requires-python = ">=3.9"

[project.optional-dependencies]
dev = ["mock>=5.2.0", "pytest>=8.4.2", "pytest-asyncio>=1.2.0", "pytest-mock>=3.15.0", "pytest-cov>=7.0.0", "ruff>=0.13.0"]
requires-python = ">=3.10"

[project.urls]
Documentation = "https://topggpy.readthedocs.io/en/latest/"
"Raw API Documentation" = "https://docs.top.gg/docs/"
Repository = "https://github.com/top-gg-community/python-sdk"
"Support server" = "https://discord.gg/dbl"
"Support server" = "https://discord.gg/EYHTgJX"
8 changes: 0 additions & 8 deletions pytest.ini

This file was deleted.

2 changes: 0 additions & 2 deletions scripts/format.sh

This file was deleted.

190 changes: 95 additions & 95 deletions tests/test_autopost.py
Original file line number Diff line number Diff line change
@@ -1,95 +1,95 @@
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 HTTPException, TopGGException
MOCK_TOKEN = ".eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=."
@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=HTTPException(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(HTTPException):
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 = HTTPException(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 HTTPException, TopGGException


MOCK_TOKEN = ".eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=."


@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=HTTPException(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(HTTPException):
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 = HTTPException(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
56 changes: 28 additions & 28 deletions tests/test_ratelimiter.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import pytest
from topgg.ratelimiter import Ratelimiter
n = period = 10
@pytest.fixture
def limiter() -> Ratelimiter:
return Ratelimiter(max_calls=n, period=period)
@pytest.mark.asyncio
async def test_AsyncRateLimiter_calls(limiter: Ratelimiter) -> None:
for _ in range(n):
async with limiter:
pass
assert len(limiter._Ratelimiter__calls) == limiter._Ratelimiter__max_calls == n
@pytest.mark.asyncio
async def test_AsyncRateLimiter_timespan_property(limiter: Ratelimiter) -> None:
for _ in range(n):
async with limiter:
pass
assert limiter._timespan < period
import pytest

from topgg.ratelimiter import Ratelimiter

n = period = 10


@pytest.fixture
def limiter() -> Ratelimiter:
return Ratelimiter(max_calls=n, period=period)


@pytest.mark.asyncio
async def test_AsyncRateLimiter_calls(limiter: Ratelimiter) -> None:
for _ in range(n):
async with limiter:
pass

assert len(limiter._Ratelimiter__calls) == limiter._Ratelimiter__max_calls == n


@pytest.mark.asyncio
async def test_AsyncRateLimiter_timespan_property(limiter: Ratelimiter) -> None:
for _ in range(n):
async with limiter:
pass

assert limiter._timespan < period
Loading
Loading