Skip to content

Commit 8f85b64

Browse files
committedOct 31, 2021
chore: add linting & release actions
1 parent b72cefb commit 8f85b64

15 files changed

+661
-507
lines changed
 

‎.github/workflows/lint.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Test
2+
on:
3+
push:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
jobs:
8+
lint:
9+
name: Run Flake8
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v2
14+
15+
- name: Initialize Python 3.7
16+
uses: actions/setup-python@v1
17+
with:
18+
python-version: 3.7
19+
20+
- name: Install dependencies
21+
run: |
22+
python -m pip install --upgrade pip
23+
pip install flake8
24+
25+
- name: Lint with flake8
26+
run: flake8 .
27+

‎.github/workflows/release.yml

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Release
2+
on:
3+
release:
4+
types: [published]
5+
6+
jobs:
7+
tag:
8+
name: Add/update 'latest' tag
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v2
14+
15+
- name: Run latest-tag
16+
uses: EndBug/latest-tag@v1
17+
env:
18+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19+
20+
publish:
21+
name: Publish on PyPI
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- uses: actions/checkout@v2
26+
27+
- name: Initialize Python 3.7
28+
uses: actions/setup-python@v1
29+
with:
30+
python-version: 3.7
31+
32+
- name: Install dependencies
33+
run: |
34+
python -m pip install --upgrade pip
35+
pip install twine
36+
37+
- name: Build
38+
run: python setup.py sdist bdist_wheel
39+
40+
- name: Publish
41+
uses: pypa/gh-action-pypi-publish@release/v1
42+
with:
43+
user: __token__
44+
password: ${{ secrets.PYPI_API_TOKEN }}

‎.gitignore

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
test.py
2-
upload.sh
1+
etc
32
dbots/__pycache__/**
43
dist
54
build

‎dbots/__init__.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
from .service import *
1414
from .poster import Poster, ClientPoster, AsyncLoop
1515

16-
VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial')
16+
VersionInfo = namedtuple(
17+
'VersionInfo', 'major minor micro releaselevel serial')
1718
version_info = VersionInfo(
18-
major = 3, minor = 0, micro = 0,
19-
releaselevel = 'final', serial = 0
19+
major=3, minor=0, micro=0,
20+
releaselevel='final', serial=0
2021
)
2122

2223
try:
@@ -26,4 +27,4 @@ class NullHandler(logging.Handler):
2627
def emit(self, record):
2728
pass
2829

29-
logging.getLogger(__name__).addHandler(NullHandler())
30+
logging.getLogger(__name__).addHandler(NullHandler())

‎dbots/client_filler.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .errors import ClientException
22

3+
34
class ClientFiller:
45
"""A class that gets certain values from a client."""
56

@@ -54,6 +55,7 @@ def shard_count(self) -> int or None:
5455
"""
5556
return None
5657

58+
5759
class DiscordPy(ClientFiller):
5860
"""Represents the client filler for discord.py clients."""
5961

@@ -85,9 +87,10 @@ def shard_count(self):
8587
else:
8688
return self.client.shard_count
8789

90+
8891
ClientFiller.CLIENT_KEYMAP = {
8992
'discord.py': DiscordPy,
9093
'discordpy': DiscordPy,
9194
'd.py': DiscordPy,
9295
'dpy': DiscordPy,
93-
}
96+
}

‎dbots/errors.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@ class DBotsException(Exception):
55
"""
66
pass
77

8+
89
class PosterException(DBotsException):
910
"""Exception that is thrown when the poster encounters an error."""
1011
pass
1112

13+
1214
class ClientException(PosterException):
1315
"""Exception that is thrown when the options for a client is invalid."""
1416
pass
1517

18+
1619
class ServiceException(PosterException):
1720
"""Exception that is thrown when the options for a service is invalid."""
1821
pass
1922

23+
2024
class APIKeyException(PosterException):
2125
"""Exception that is thrown when an API key is invalid."""
2226
pass
2327

28+
2429
class HTTPException(DBotsException):
2530
"""Exception that is thrown when an HTTP request has an error."""
2631
def __init__(self, response):
@@ -34,30 +39,35 @@ def __init__(self, response):
3439

3540
super().__init__(fmt.format(self.response, self.body))
3641

42+
3743
class HTTPForbidden(HTTPException):
3844
"""Exception that's thrown for when status code 403 occurs.
3945
Subclass of :exc:`HTTPException`
4046
"""
4147
pass
4248

49+
4350
class HTTPUnauthorized(HTTPException):
4451
"""Exception that's thrown for when status code 403 occurs.
4552
Subclass of :exc:`HTTPException`
4653
"""
4754
pass
4855

56+
4957
class HTTPNotFound(HTTPException):
5058
"""Exception that's thrown for when status code 404 occurs.
5159
Subclass of :exc:`HTTPException`
5260
"""
5361
pass
5462

63+
5564
class EndpointRequiresToken(DBotsException):
56-
"""Exception that's thrown for when an endpoint is being used without a token."""
65+
"""Exception that's thrown for when an endpoint is used without a token."""
5766
def __init__(self):
5867
super().__init__('This endpoint requires a token.')
5968

69+
6070
class PostingUnsupported(ServiceException):
6171
"""Exception that's thrown for services that cannot be posted to."""
6272
def __init__(self):
63-
super().__init__('This service does not support posting.')
73+
super().__init__('This service does not support posting.')

‎dbots/eventhandler.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
log = logging.getLogger(__name__)
88

9+
910
class _ClientEventTask(asyncio.Task):
1011
def __init__(self, original_coro, event_name, coro, *, loop):
1112
super().__init__(coro, loop=loop)
@@ -22,8 +23,9 @@ def __repr__(self):
2223
info.append(('exception', repr(self._exception)))
2324
return '<ClientEventTask {}>'.format(' '.join('%s=%s' % t for t in info))
2425

26+
2527
class EventHandler:
26-
def __init__(self, loop = None):
28+
def __init__(self, loop=None):
2729
self.loop = asyncio.get_event_loop() if loop is None else loop
2830
self._listeners = {}
2931

@@ -41,7 +43,7 @@ def event(self, name_or_coro):
4143
log.debug('%s has successfully been registered as an event', event_name)
4244
return name_or_coro
4345
else:
44-
def inner(coro):
46+
def inner(coro):
4547
if not asyncio.iscoroutinefunction(coro):
4648
raise TypeError('event registered must be a coroutine function')
4749
if name_or_coro in self._listeners:
@@ -50,7 +52,7 @@ def inner(coro):
5052
self._listeners[name_or_coro] = [coro]
5153
log.debug('%s has successfully been registered as an event using named decorator', name_or_coro)
5254
return coro
53-
return inner
55+
return inner
5456
return
5557

5658
async def _run_event(self, coro, event_name, *args, **kwargs):
@@ -83,4 +85,4 @@ def dispatch(self, event, *args, **kwargs):
8385
except AttributeError:
8486
pass
8587
else:
86-
self._schedule_event(coro, event, *args, **kwargs)
88+
self._schedule_event(coro, event, *args, **kwargs)

‎dbots/http.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
log = logging.getLogger(__name__)
1111

12+
1213
class HTTPClient:
1314
"""Represents an HTTP client that can send requests."""
1415

@@ -35,7 +36,7 @@ def recreate_session(self):
3536
async def request(self, method, path, **kwargs):
3637
# Evaluate kwargs
3738

38-
if not 'headers' in kwargs:
39+
if 'headers' not in kwargs:
3940
kwargs['headers'] = {
4041
'User-Agent': self.user_agent,
4142
}
@@ -45,7 +46,7 @@ async def request(self, method, path, **kwargs):
4546
if 'json' in kwargs:
4647
kwargs['headers']['Content-Type'] = 'application/json'
4748
kwargs['data'] = HTTPClient.to_json(kwargs.pop('json'))
48-
49+
4950
if self.proxy is not None:
5051
kwargs['proxy'] = self.proxy
5152
if self.proxy_auth is not None:
@@ -54,7 +55,7 @@ async def request(self, method, path, **kwargs):
5455
url = path
5556
if self.base_url:
5657
url = self.base_url + path
57-
58+
5859
if 'query' in kwargs:
5960
url = url + '?' + _encode_query(kwargs['query'])
6061
del kwargs['query']
@@ -63,7 +64,7 @@ async def request(self, method, path, **kwargs):
6364
log.debug('%s %s with %s has returned %s', method, url, kwargs.get('data'), r.status)
6465

6566
response = HTTPResponse(r, await r.text(encoding='utf-8'))
66-
67+
6768
if 300 > r.status >= 200:
6869
log.debug('%s %s has received %s', method, url, response.body)
6970
return response
@@ -124,4 +125,4 @@ def __repr__(self):
124125
('method', self.method),
125126
('url', self.url)
126127
]
127-
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
128+
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))

‎dbots/poster.py

+30-26
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
log = logging.getLogger(__name__)
1111

12+
1213
class AsyncLoop:
1314
def __init__(self, timeout, callback):
1415
self._timeout = timeout
@@ -23,11 +24,13 @@ async def _job(self):
2324
def cancel(self):
2425
self._task.cancel()
2526

27+
2628
def _ensure_coro(func):
2729
if not asyncio.iscoroutinefunction(func):
2830
func = asyncio.coroutine(func)
2931
return func
3032

33+
3134
class Poster(EventHandler):
3235
"""
3336
A class that posts server count to listing sites.
@@ -66,9 +69,9 @@ class Poster(EventHandler):
6669

6770
def __init__(
6871
self, client_id, server_count, user_count,
69-
voice_connections, on_custom_post = None, **options
72+
voice_connections, on_custom_post=None, **options
7073
):
71-
super().__init__(loop = options.pop('loop', None))
74+
super().__init__(loop=options.pop('loop', None))
7275

7376
self._loop = None
7477
self._client_id = client_id
@@ -81,7 +84,7 @@ def __init__(
8184

8285
proxy = options.pop('proxy', None)
8386
proxy_auth = options.pop('proxy_auth', None)
84-
self.http = HTTPClient(proxy = proxy, proxy_auth = proxy_auth)
87+
self.http = HTTPClient(proxy=proxy, proxy_auth=proxy_auth)
8588
self.api_keys = options.pop('api_keys', {})
8689

8790
setattr(self, 'server_count', _ensure_coro(server_count))
@@ -98,26 +101,26 @@ def __repr__(self):
98101
('shard_count', self.shard_count),
99102
]
100103
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
101-
104+
102105
# property fill-ins
103-
106+
104107
@property
105108
def client_id(self) -> str or None:
106109
"""The client ID of the poster."""
107110
return self._client_id
108-
111+
109112
@property
110113
def shard_id(self) -> int or None:
111114
"""The shard ID of the poster."""
112115
return self._shard_id
113-
116+
114117
@property
115118
def shard_count(self) -> int or None:
116119
"""The shard count of the poster."""
117120
return self._shard_count
118121

119122
# api key management
120-
123+
121124
def set_key(self, service: str, key: str) -> str:
122125
"""
123126
Sets an API key.
@@ -155,10 +158,10 @@ def remove_key(self, service: str) -> str:
155158
key = self.api_keys[service]
156159
del self.api_keys[service]
157160
return key
158-
161+
159162
# loop management
160-
161-
def start_loop(self, interval = 1800) -> AsyncLoop:
163+
164+
def start_loop(self, interval=1800) -> AsyncLoop:
162165
"""
163166
Creates a loop that posts to all services every `n` seconds.
164167
@@ -171,14 +174,14 @@ def start_loop(self, interval = 1800) -> AsyncLoop:
171174
self._loop = AsyncLoop(interval, self.__on_loop)
172175
log.debug('Started loop %s', interval)
173176
return self._loop
174-
177+
175178
def kill_loop(self):
176179
"""Cancels the current posting loop."""
177180
if self._loop:
178181
log.debug('Ending loop')
179182
self._loop.cancel()
180183
self._loop = None
181-
184+
182185
async def __on_loop(self):
183186
log.debug('Loop ran')
184187
try:
@@ -193,7 +196,7 @@ async def __on_loop(self):
193196

194197
# post management
195198

196-
async def post(self, service = None) -> HTTPResponse:
199+
async def post(self, service=None) -> HTTPResponse:
197200
"""
198201
Posts the current clients server count to a service.
199202
@@ -206,10 +209,10 @@ async def post(self, service = None) -> HTTPResponse:
206209
users = await self.user_count()
207210
connections = await self.voice_connections()
208211
return await self.manual_post(servers, service, users, connections)
209-
212+
210213
async def manual_post(
211-
self, server_count, service = None,
212-
user_count = None, voice_connections = None
214+
self, server_count, service=None,
215+
user_count=None, voice_connections=None
213216
) -> HTTPResponse:
214217
"""
215218
Manually posts a server count to a service.
@@ -227,9 +230,9 @@ async def manual_post(
227230
"""
228231
if service == 'custom' and hasattr(self, 'on_custom_post'):
229232
return await self.on_custom_post(
230-
self, server_count = server_count,
231-
user_count = user_count,
232-
voice_connections = voice_connections
233+
self, server_count=server_count,
234+
user_count=user_count,
235+
voice_connections=voice_connections
233236
)
234237
if len(self.api_keys) == 0:
235238
raise APIKeyException('No API Keys available')
@@ -241,9 +244,9 @@ async def manual_post(
241244
for key in keys:
242245
try:
243246
responses.append(await self.manual_post(
244-
server_count = server_count,
245-
service = key, user_count = user_count,
246-
voice_connections = voice_connections
247+
server_count=server_count,
248+
service=key, user_count=user_count,
249+
voice_connections=voice_connections
247250
))
248251
except Exception as error:
249252
responses.append(error)
@@ -270,6 +273,7 @@ async def manual_post(
270273
self.dispatch('post_fail', error)
271274
raise error
272275

276+
273277
class ClientPoster(Poster):
274278
"""
275279
A class that posts certain client values to listing sites.
@@ -317,15 +321,15 @@ def __repr__(self):
317321
('sharding', self._sharding),
318322
]
319323
return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
320-
324+
321325
@property
322326
def client_id(self) -> str or None:
323327
return self.filler.client_id
324-
328+
325329
@property
326330
def shard_id(self) -> int or None:
327331
return self._shard_id or self.filler.shard_id if self._sharding else None
328-
332+
329333
@property
330334
def shard_count(self) -> int or None:
331335
return self._shard_count or self.filler.shard_count if self._sharding else None

‎dbots/service.py

+459-433
Large diffs are not rendered by default.

‎examples/client_poster.py

+19-9
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
quit()
66
import dbots
77

8+
89
# Go to https://requestbin.net and replace the path with the given URL
910
class TestService(dbots.Service):
1011
@staticmethod
1112
def _post(
12-
http_client, bot_id, token, server_count = 0, user_count = 0,
13-
voice_connections = 0, shard_count = None, shard_id = None
13+
http_client, bot_id, token, server_count=0, user_count=0,
14+
voice_connections=0, shard_count=None, shard_id=None
1415
):
1516
payload = {
1617
'server_count': server_count,
@@ -21,42 +22,50 @@ def _post(
2122
payload['shard_id'] = shard_id
2223
payload['shard_count'] = shard_count
2324
return http_client.request(
24-
method = 'POST',
25-
path = 'http://requestbin.net/r/XXXXXX',
26-
query = { 'id': bot_id },
27-
headers = { 'Authorization': token },
28-
json = payload
25+
method='POST',
26+
path='http://requestbin.net/r/XXXXXX',
27+
query={'id': bot_id},
28+
headers={'Authorization': token},
29+
json=payload
2930
)
3031

32+
3133
dbots.Service.SERVICE_KEYMAP['test'] = TestService
3234

35+
3336
client = discord.Client()
34-
poster = dbots.ClientPoster(client, 'discord.py', api_keys = {
37+
poster = dbots.ClientPoster(client, 'discord.py', api_keys={
3538
'test': 'token'
3639
})
3740

41+
3842
@poster.event
3943
async def on_post(response):
4044
print('Post:', response)
4145

46+
4247
@poster.event
4348
async def on_post_fail(error):
4449
print('Post Fail:', error)
4550

51+
4652
@poster.event
4753
async def on_auto_post(response):
4854
print('Auto-Post:', response)
4955

56+
5057
@poster.event
5158
async def on_auto_post_fail(error):
5259
print('Auto-Post Fail:', error)
5360

61+
5462
@client.event
5563
async def on_ready():
5664
print('Logged on as', client.user)
5765
await poster.post()
5866
poster.start_loop(10)
5967

68+
6069
@client.event
6170
async def on_message(message):
6271
# don't respond to ourselves
@@ -75,4 +84,5 @@ async def on_message(message):
7584
if message.content.startswith('p>'):
7685
await poster.post()
7786

78-
client.run('XXX')
87+
88+
client.run('XXX')

‎examples/custom_post.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,30 @@
22

33
client_id = '1234567890'
44

5+
56
def server_count():
6-
return 100
7+
return 100
8+
79

810
def user_count():
9-
return 100
11+
return 100
12+
1013

1114
def voice_connections():
12-
return 0
15+
return 0
16+
1317

1418
def custom_post(poster, server_count, user_count, voice_connections):
15-
print(f'[CUSTOM_POST] Poster: {poster}, ServerCount: {server_count}, UserCount: {user_count}, VConnections: {voice_connections}')
19+
print(
20+
f'[CUSTOM_POST] Poster: {poster}, ServerCount: {server_count},',
21+
f'UserCount: {user_count}, VConnections: {voice_connections}'
22+
)
23+
1624

1725
poster = dbots.Poster(
1826
client_id, server_count, user_count, voice_connections,
19-
on_custom_post = custom_post,
20-
api_keys = {
27+
on_custom_post=custom_post,
28+
api_keys={
2129
'customservice': '…'
2230
}
2331
)

‎examples/custom_service.py

+20-13
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import dbots
22

3+
34
class CustomService(dbots.Service):
45
@staticmethod
56
def aliases():
67
return ['customservice']
78

89
@staticmethod
910
def _post(
10-
http_client, bot_id, token, server_count = 0, user_count = 0,
11-
voice_connections = 0, shard_count = None, shard_id = None
11+
http_client, bot_id, token, server_count=0, user_count=0,
12+
voice_connections=0, shard_count=None, shard_id=None
1213
):
1314
payload = {
1415
'server_count': server_count,
@@ -19,26 +20,32 @@ def _post(
1920
payload['shard_id'] = shard_id
2021
payload['shard_count'] = shard_count
2122
return http_client.request(
22-
method = 'POST',
23-
path = 'http://httpbin.org/post',
24-
query = { 'id': bot_id },
25-
headers = { 'Authorization': token },
26-
json = payload
23+
method='POST',
24+
path='http://httpbin.org/post',
25+
query={'id': bot_id},
26+
headers={'Authorization': token},
27+
json=payload
2728
)
2829

30+
2931
dbots.Service.SERVICES.append(CustomService)
3032

3133
client_id = '1234567890'
3234

35+
3336
def server_count():
34-
return 100
37+
return 100
38+
3539

3640
def user_count():
37-
return 100
41+
return 100
42+
3843

3944
def voice_connections():
40-
return 0
45+
return 0
46+
4147

42-
poster = dbots.Poster(client_id, server_count, user_count, voice_connections, api_keys = {
43-
'customservice': '…'
44-
})
48+
poster = dbots.Poster(
49+
client_id, server_count, user_count, voice_connections, api_keys={
50+
'customservice': '…'
51+
})

‎setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
requirements = []
55
with open('requirements.txt') as f:
6-
requirements = f.read().splitlines()
6+
requirements = f.read().splitlines()
77

88
version = ''
99
with open('dbots/__init__.py') as f:
@@ -40,4 +40,4 @@
4040
'Topic :: Utilities',
4141
],
4242
python_requires='>=3.6',
43-
)
43+
)

‎tox.ini

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[flake8]
2+
ignore = F401, F403, E251
3+
exclude =
4+
.git,
5+
__pycache__,
6+
docs/conf.py,
7+
build,
8+
dist,
9+
etc
10+
show-source = true
11+
statistics = true
12+
max-line-length = 127

0 commit comments

Comments
 (0)
Please sign in to comment.