Initial commit
This commit is contained in:
commit
8ca93c1bab
7 changed files with 514 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
data
|
||||||
30
bot.py
Normal file
30
bot.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from breadtube_bot.manager import DiscordManager
|
||||||
|
from breadtube_bot.objects import TextChannel
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
bot_token = Path('data/discord_bot_token.txt').read_text(encoding='utf-8').strip()
|
||||||
|
guild_id = 1306964577812086824
|
||||||
|
manager = DiscordManager(bot_token=bot_token, guild_id=guild_id)
|
||||||
|
_categories, text_channel = manager.list_channels()
|
||||||
|
breadtube_channel: TextChannel | None = None
|
||||||
|
for channel in text_channel:
|
||||||
|
if channel.name == 'breadtube-bot':
|
||||||
|
breadtube_channel = channel
|
||||||
|
break
|
||||||
|
|
||||||
|
if breadtube_channel is None:
|
||||||
|
print('Cannot find beadtube-bot channel')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
messages = manager.list_text_channel_messages(breadtube_channel)
|
||||||
|
for message in messages:
|
||||||
|
print(message)
|
||||||
|
manager.delete_message(message)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
33
breadtube_bot/api.py
Normal file
33
breadtube_bot/api.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ApiVersion(Enum):
|
||||||
|
V10 = 10
|
||||||
|
V9 = 9
|
||||||
|
V8 = 8
|
||||||
|
V7 = 7
|
||||||
|
V6 = 6
|
||||||
|
|
||||||
|
|
||||||
|
class ApiAction(Enum):
|
||||||
|
DELETE = 'DELETE'
|
||||||
|
GET = 'GET'
|
||||||
|
POST = 'POST'
|
||||||
|
|
||||||
|
|
||||||
|
class Api:
|
||||||
|
class Guild:
|
||||||
|
@staticmethod
|
||||||
|
def list_guilds(guild_id: int) -> tuple[ApiAction, str]:
|
||||||
|
return ApiAction.GET, f'/guilds/{guild_id}/channels'
|
||||||
|
|
||||||
|
class Message:
|
||||||
|
@staticmethod
|
||||||
|
def delete(channel_id: int, message_id: int) -> tuple[ApiAction, str]:
|
||||||
|
return ApiAction.DELETE, f'/channels/{channel_id}/messages/{message_id}'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_by_channel(channel_id: int, limit: int | None = None) -> tuple[ApiAction, str]:
|
||||||
|
if limit is not None and not (0 < limit <= 100): # noqa: PLR2004
|
||||||
|
raise RuntimeError('Cannot list messages by channel with limit outside [0, 100] range')
|
||||||
|
return ApiAction.GET, f'/channels/{channel_id}/messages'
|
||||||
160
breadtube_bot/manager.py
Normal file
160
breadtube_bot/manager.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
import tomllib
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from .api import Api, ApiAction, ApiVersion
|
||||||
|
from .objects import (
|
||||||
|
ChannelCategory, ChannelFlags, ChannelType, Message, Overwrite, OverwriteType, Permissions, TextChannel, User)
|
||||||
|
|
||||||
|
|
||||||
|
HTTPHeaders = dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _RateLimit:
|
||||||
|
remaining: int
|
||||||
|
next_reset: float
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordManager:
|
||||||
|
@staticmethod
|
||||||
|
def _get_code_version() -> str:
|
||||||
|
pyproject_path = Path(__file__).parents[1] / 'pyproject.toml'
|
||||||
|
if not pyproject_path.exists():
|
||||||
|
raise RuntimeError('Cannot current bot version')
|
||||||
|
return tomllib.loads(pyproject_path.read_text(encoding='utf-8'))['project']['version']
|
||||||
|
|
||||||
|
def __init__(self, bot_token: str, guild_id: int) -> None:
|
||||||
|
self.guild_id = guild_id
|
||||||
|
self._bot_token = bot_token
|
||||||
|
|
||||||
|
self.rate_limit: _RateLimit = _RateLimit(remaining=1, next_reset=0)
|
||||||
|
self.version = self._get_code_version()
|
||||||
|
|
||||||
|
def _update_rate_limit(self, headers: HTTPHeaders):
|
||||||
|
for header_key in ['x-ratelimit-remaining', 'x-ratelimit-reset']:
|
||||||
|
if header_key not in headers:
|
||||||
|
print(f'Warning: no "{header_key}" found in headers')
|
||||||
|
return
|
||||||
|
self.rate_limit.remaining = int(headers['x-ratelimit-remaining'])
|
||||||
|
self.rate_limit.next_reset = float(headers['x-ratelimit-reset'])
|
||||||
|
|
||||||
|
def _send_request(self, api_action: ApiAction, endpoint: str, api_version: ApiVersion = ApiVersion.V10,
|
||||||
|
expected_code: int = 200) -> tuple[
|
||||||
|
HTTPHeaders, dict]:
|
||||||
|
timeout = 3
|
||||||
|
min_api_version = 9
|
||||||
|
|
||||||
|
if api_action == ApiAction.POST:
|
||||||
|
raise NotImplementedError
|
||||||
|
if api_version.value < min_api_version:
|
||||||
|
print(f'Warning: using deprecated API version {api_version} (minimum non deprecated is {min_api_version})')
|
||||||
|
url = f'https://discord.com/api/v{api_version.value}{endpoint}'
|
||||||
|
request = urllib.request.Request(url)
|
||||||
|
request.add_header('User-Agent', f'BreadTube (v{self.version})')
|
||||||
|
request.add_header('Accept', 'application/json')
|
||||||
|
request.add_header('Authorization', f'Bot {self._bot_token}')
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||||
|
if response.status != expected_code:
|
||||||
|
raise RuntimeError(
|
||||||
|
f'Unexpected code {response.status} (expected: {expected_code}) -> {response.read().decode()}')
|
||||||
|
return dict(response.getheaders()), json.loads(response.read().decode())
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
raise RuntimeError(
|
||||||
|
f'HTTP error calling API ({url}): {error}:\nHeaders:\n{error.headers}Body:\n{error.read()}') from error
|
||||||
|
except urllib.error.URLError as error:
|
||||||
|
raise RuntimeError(f'URL error calling API ({url}): {error}') from error
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_overwrite(info: dict) -> Overwrite:
|
||||||
|
return Overwrite(
|
||||||
|
id=int(info['id']),
|
||||||
|
type=OverwriteType(info['type']),
|
||||||
|
allow=Permissions(int(info['allow'])),
|
||||||
|
deny=Permissions(int(info['deny']))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_channel_category(self, info: dict) -> ChannelCategory:
|
||||||
|
parent_id: str | None = info.get('parent_id')
|
||||||
|
return ChannelCategory(
|
||||||
|
id=int(info['id']),
|
||||||
|
guild_id=int(info['guild_id']),
|
||||||
|
position=int(info['position']),
|
||||||
|
permission_overwrites=[self._parse_overwrite(o) for o in info['permission_overwrites']],
|
||||||
|
name=info.get('name'),
|
||||||
|
parent_id=int(parent_id) if parent_id is not None else None,
|
||||||
|
flags=ChannelFlags(info['flags']),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_text_channel(self, info: dict) -> TextChannel:
|
||||||
|
parent_id: str | None = info.get('parent_id')
|
||||||
|
last_message_id: str | None = info.get('last_message_id')
|
||||||
|
last_pin_timestamp: str | None = info.get('last_pin_timestamp')
|
||||||
|
return TextChannel(
|
||||||
|
id=int(info['id']),
|
||||||
|
guild_id=int(info['guild_id']),
|
||||||
|
position=int(info['position']),
|
||||||
|
permission_overwrites=[self._parse_overwrite(o) for o in info['permission_overwrites']],
|
||||||
|
name=info.get('name'),
|
||||||
|
topic=info.get('topic'),
|
||||||
|
nsfw=info['nsfw'],
|
||||||
|
last_message_id=int(last_message_id) if last_message_id is not None else None,
|
||||||
|
rate_limit_per_user=int(info['rate_limit_per_user']),
|
||||||
|
parent_id=int(parent_id) if parent_id is not None else None,
|
||||||
|
last_pin_timestamp=(datetime.fromisoformat(last_pin_timestamp) if last_pin_timestamp is not None else None),
|
||||||
|
flags=ChannelFlags(info['flags']),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_user(info: dict) -> User:
|
||||||
|
return User(
|
||||||
|
id=int(info['id']),
|
||||||
|
username=info['username'],
|
||||||
|
discriminator=info['discriminator'],
|
||||||
|
global_name=info.get('global_name')
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_message(self, info: dict) -> Message:
|
||||||
|
edited_timestamp: str | None = info.get('edited_timestamp')
|
||||||
|
return Message(
|
||||||
|
id=int(info['id']),
|
||||||
|
channel_id=int(info['channel_id']),
|
||||||
|
author=(self._parse_user(info['author']) if info.get('webhook_id') is None else User(
|
||||||
|
id=info['webhook_id'], username='webhook', discriminator='webhook', global_name=None)),
|
||||||
|
content=info['content'],
|
||||||
|
timestamp=datetime.fromisoformat(info['timestamp']),
|
||||||
|
edited_timestamp=datetime.fromisoformat(edited_timestamp) if edited_timestamp is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_message(self, message: Message):
|
||||||
|
try:
|
||||||
|
headers, _ = self._send_request(
|
||||||
|
*Api.Message.delete(channel_id=message.channel_id, message_id=message.id), expected_code=204)
|
||||||
|
self._update_rate_limit(headers)
|
||||||
|
print(f'Message {message.id} deleted')
|
||||||
|
except RuntimeError as error:
|
||||||
|
print(error)
|
||||||
|
|
||||||
|
def list_channels(self) -> tuple[list[ChannelCategory], list[TextChannel]]:
|
||||||
|
headers, channels = self._send_request(*Api.Guild.list_guilds(self.guild_id))
|
||||||
|
self._update_rate_limit(headers)
|
||||||
|
categories: list[ChannelCategory] = []
|
||||||
|
text_channels: list[TextChannel] = []
|
||||||
|
for channel in channels:
|
||||||
|
channel_type = ChannelType(channel['type'])
|
||||||
|
match channel_type:
|
||||||
|
case ChannelType.GUILD_CATEGORY:
|
||||||
|
categories.append(self._parse_channel_category(channel))
|
||||||
|
case ChannelType.GUILD_TEXT:
|
||||||
|
text_channels.append(self._parse_text_channel(channel))
|
||||||
|
return categories, text_channels
|
||||||
|
|
||||||
|
def list_text_channel_messages(self, channel: TextChannel) -> list:
|
||||||
|
headers, messages = self._send_request(*Api.Message.list_by_channel(channel.id))
|
||||||
|
self._update_rate_limit(headers)
|
||||||
|
return [self._parse_message(m) for m in messages]
|
||||||
199
breadtube_bot/objects.py
Normal file
199
breadtube_bot/objects.py
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum, IntFlag
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelType(Enum):
|
||||||
|
GUILD_TEXT = 0
|
||||||
|
DM = 1
|
||||||
|
GUILD_VOICE = 2
|
||||||
|
GROUP_DM = 3
|
||||||
|
GUILD_CATEGORY = 4
|
||||||
|
GUILD_ANNOUNCEMENT = 5
|
||||||
|
ANNOUNCEMENT_THREAD = 10
|
||||||
|
PUBLIC_THREAD = 11
|
||||||
|
PRIVATE_THREAD = 12
|
||||||
|
GUILD_STAGE_VOICE = 13
|
||||||
|
GUILD_DIRECTORY = 14
|
||||||
|
GUILD_FORUM = 15
|
||||||
|
GUILD_MEDIA = 16
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelFlags(IntFlag):
|
||||||
|
# this thread is pinned to the top of its parent GUILD_FORUM or GUILD_MEDIA channel
|
||||||
|
PINNED = 1 << 1
|
||||||
|
# whether a tag is required to be specified when creating a thread in a GUILD_FORUM or a GUILD_MEDIA channel.
|
||||||
|
# Tags are specified in the applied_tags field.
|
||||||
|
REQUIRE_TAG = 1 << 4
|
||||||
|
# when set hides the embedded media download options. Available only for media channels
|
||||||
|
HIDE_MEDIA_DOWNLOAD_OPTIONS = 1 << 15
|
||||||
|
|
||||||
|
|
||||||
|
class OverwriteType(Enum):
|
||||||
|
ROLE = 0
|
||||||
|
MEMBER = 1
|
||||||
|
|
||||||
|
|
||||||
|
class Permissions(IntFlag):
|
||||||
|
# Allows creation of instant invites
|
||||||
|
CREATE_INSTANT_INVITE = 1 << 0
|
||||||
|
# Allows kicking members
|
||||||
|
KICK_MEMBERS = 1 << 1
|
||||||
|
# Allows banning members
|
||||||
|
BAN_MEMBERS = 1 << 2
|
||||||
|
# Allows all permissions and bypasses channel permission overwrites
|
||||||
|
ADMINISTRATOR = 1 << 3
|
||||||
|
# Allows management and editing of channels
|
||||||
|
MANAGE_CHANNELS = 1 << 4
|
||||||
|
# Allows management and editing of the guild
|
||||||
|
MANAGE_GUILD = 1 << 5
|
||||||
|
# Allows for adding new reactions to messages.
|
||||||
|
# This permission does not apply to reacting with an existing reaction on a message.
|
||||||
|
ADD_REACTIONS = 1 << 6
|
||||||
|
# Allows for viewing of audit logs
|
||||||
|
VIEW_AUDIT_LOG = 1 << 7
|
||||||
|
# Allows for using priority speaker in a voice channel
|
||||||
|
PRIORITY_SPEAKER = 1 << 8
|
||||||
|
# Allows the user to go live
|
||||||
|
STREAM = 1 << 9
|
||||||
|
# Allows guild members to view a channel,
|
||||||
|
# which includes reading messages in text channels and joining voice channels
|
||||||
|
VIEW_CHANNEL = 1 << 10
|
||||||
|
# Allows for sending messages in a channel and creating threads in a forum
|
||||||
|
# (does not allow sending messages in threads)
|
||||||
|
SEND_MESSAGES = 1 << 11
|
||||||
|
# Allows for sending of /tts messages
|
||||||
|
SEND_TTS_MESSAGES = 1 << 12
|
||||||
|
# Allows for deletion of other users messages
|
||||||
|
MANAGE_MESSAGES = 1 << 13
|
||||||
|
# Links sent by users with this permission will be auto-embedded
|
||||||
|
EMBED_LINKS = 1 << 14
|
||||||
|
# Allows for uploading images and files
|
||||||
|
ATTACH_FILES = 1 << 15
|
||||||
|
# Allows for reading of message history
|
||||||
|
READ_MESSAGE_HISTORY = 1 << 16
|
||||||
|
# Allows for using the @everyone tag to notify all users in a channel,
|
||||||
|
# and the @here tag to notify all online users in a channel
|
||||||
|
MENTION_EVERYONE = 1 << 17
|
||||||
|
# Allows the usage of custom emojis from other servers
|
||||||
|
USE_EXTERNAL_EMOJIS = 1 << 18
|
||||||
|
# Allows for viewing guild insights
|
||||||
|
VIEW_GUILD_INSIGHTS = 1 << 19
|
||||||
|
# Allows for joining of a voice channel
|
||||||
|
CONNECT = 1 << 20
|
||||||
|
# Allows for speaking in a voice channel
|
||||||
|
SPEAK = 1 << 21
|
||||||
|
# Allows for muting members in a voice channel
|
||||||
|
MUTE_MEMBERS = 1 << 22
|
||||||
|
# Allows for deafening of members in a voice channel
|
||||||
|
DEAFEN_MEMBERS = 1 << 23
|
||||||
|
# Allows for moving of members between voice channels
|
||||||
|
MOVE_MEMBERS = 1 << 24
|
||||||
|
# Allows for using voice-activity-detection in a voice channel
|
||||||
|
USE_VAD = 1 << 25
|
||||||
|
# Allows for modification of own nickname
|
||||||
|
CHANGE_NICKNAME = 1 << 26
|
||||||
|
# Allows for modification of other users nicknames
|
||||||
|
MANAGE_NICKNAMES = 1 << 27
|
||||||
|
# Allows management and editing of roles
|
||||||
|
MANAGE_ROLES = 1 << 28
|
||||||
|
# Allows management and editing of webhooks
|
||||||
|
MANAGE_WEBHOOKS = 1 << 29
|
||||||
|
# Allows for editing and deleting emojis, stickers, and soundboard sounds created by all users
|
||||||
|
MANAGE_GUILD_EXPRESSIONS = 1 << 30
|
||||||
|
# Allows members to use application commands, including slash commands and context menu commands.
|
||||||
|
USE_APPLICATION_COMMANDS = 1 << 31
|
||||||
|
# Allows for requesting to speak in stage channels. (This permission is under active development
|
||||||
|
# and may be changed or removed.)
|
||||||
|
REQUEST_TO_SPEAK = 1 << 32
|
||||||
|
# Allows for editing and deleting scheduled events created by all users
|
||||||
|
MANAGE_EVENTS = 1 << 33
|
||||||
|
# Allows for deleting and archiving threads, and viewing all private threads
|
||||||
|
MANAGE_THREADS = 1 << 34
|
||||||
|
# Allows for creating public and announcement threads
|
||||||
|
CREATE_PUBLIC_THREADS = 1 << 35
|
||||||
|
# Allows for creating private threads
|
||||||
|
CREATE_PRIVATE_THREADS = 1 << 36
|
||||||
|
# Allows the usage of custom stickers from other servers
|
||||||
|
USE_EXTERNAL_STICKERS = 1 << 37
|
||||||
|
# Allows for sending messages in threads
|
||||||
|
SEND_MESSAGES_IN_THREADS = 1 << 38
|
||||||
|
# Allows for using Activities (applications with the EMBEDDED flag)
|
||||||
|
USE_EMBEDDED_ACTIVITIES = 1 << 39
|
||||||
|
# Allows for timing out users to prevent them from sending or reacting to messages in chat and threads,
|
||||||
|
# and from speaking in voice and stage channels
|
||||||
|
MODERATE_MEMBERS = 1 << 40
|
||||||
|
# Allows for viewing role subscription insights
|
||||||
|
VIEW_CREATOR_MONETIZATION_ANALYTICS = 1 << 41
|
||||||
|
# Allows for using soundboard in a voice channel
|
||||||
|
USE_SOUNDBOARD = 1 << 42
|
||||||
|
# Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created
|
||||||
|
# by the current user. Not yet available to developers, see changelog.
|
||||||
|
CREATE_GUILD_EXPRESSIONS = 1 << 43
|
||||||
|
# Allows for creating scheduled events, and editing and deleting those created by the current user.
|
||||||
|
# Not yet available to developers, see changelog.
|
||||||
|
CREATE_EVENTS = 1 << 44
|
||||||
|
# Allows the usage of custom soundboard sounds from other servers
|
||||||
|
USE_EXTERNAL_SOUNDS = 1 << 45
|
||||||
|
# Allows sending voice messages
|
||||||
|
SEND_VOICE_MESSAGES = 1 << 46
|
||||||
|
# Allows sending polls
|
||||||
|
SEND_POLLS = 1 << 49
|
||||||
|
# Allows user-installed apps to send public responses. When disabled, users will still be allowed to use their apps
|
||||||
|
# but the responses will be ephemeral. This only applies to apps not also installed to the server.
|
||||||
|
USE_EXTERNAL_APPS = 1 << 50
|
||||||
|
# Allows pinning and unpinning messages
|
||||||
|
PIN_MESSAGES = 1 << 51
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Overwrite:
|
||||||
|
id: int
|
||||||
|
type: OverwriteType
|
||||||
|
allow: Permissions
|
||||||
|
deny: Permissions
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChannelCategory:
|
||||||
|
id: int
|
||||||
|
guild_id: int
|
||||||
|
position: int
|
||||||
|
permission_overwrites: list[Overwrite]
|
||||||
|
name: str | None
|
||||||
|
parent_id: int | None
|
||||||
|
flags: ChannelFlags
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TextChannel:
|
||||||
|
id: int
|
||||||
|
guild_id: int
|
||||||
|
position: int
|
||||||
|
permission_overwrites: list[Overwrite]
|
||||||
|
name: str | None
|
||||||
|
topic: str | None
|
||||||
|
nsfw: bool
|
||||||
|
last_message_id: int | None
|
||||||
|
rate_limit_per_user: int
|
||||||
|
parent_id: int | None
|
||||||
|
last_pin_timestamp: datetime | None
|
||||||
|
flags: ChannelFlags
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class User: # TODO : complete attributes
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
discriminator: str
|
||||||
|
global_name: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Message: # TODO : complete attributes
|
||||||
|
id: int
|
||||||
|
channel_id: int
|
||||||
|
author: User
|
||||||
|
content: str
|
||||||
|
timestamp: datetime
|
||||||
|
edited_timestamp: datetime | None
|
||||||
27
google_api_test.py
Normal file
27
google_api_test.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
api_key = Path('data/google_api_key.txt').read_text(encoding='utf-8').strip()
|
||||||
|
timeout = 3
|
||||||
|
http_code_ok = 200
|
||||||
|
request = urllib.request.Request(
|
||||||
|
'https://www.googleapis.com/youtube/v3/search?part=snippet'
|
||||||
|
f'&channelId=UC-kM5kL9CgjN9s9pim089gg&maxResults=10&order=date&key={api_key}')
|
||||||
|
request.add_header('Accept', 'application/json')
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||||
|
if response.status != http_code_ok:
|
||||||
|
raise RuntimeError(
|
||||||
|
f'Non-OK response (status: {response.status}) : {response.read()}')
|
||||||
|
print(response.read().decode())
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
raise RuntimeError(f'Cannot call API: {error}: {error.read()}') from error
|
||||||
|
except urllib.error.URLError as error:
|
||||||
|
raise RuntimeError(f'Cannot call API: {error}') from error
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
62
pyproject.toml
Normal file
62
pyproject.toml
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
[project]
|
||||||
|
name = "bot-discord"
|
||||||
|
version = "0.0.1"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = ["google-api-python-client==2.182.0"]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = ["pytest"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
cache-dir = "/tmp/ruff"
|
||||||
|
exclude = [
|
||||||
|
".git",
|
||||||
|
".ruff_cache",
|
||||||
|
".venv"
|
||||||
|
]
|
||||||
|
line-length = 120
|
||||||
|
indent-width = 4
|
||||||
|
target-version = "py310"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
combine_as_imports = true
|
||||||
|
force_sort_within_sections = true
|
||||||
|
lexicographical = true
|
||||||
|
lines_after_imports = 2
|
||||||
|
multi_line_output = 4
|
||||||
|
no_sections = false
|
||||||
|
order_by_type = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-p no:cacheprovider -s -vv"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["."]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
preview = true
|
||||||
|
select = ["A", "ARG", "B", "C", "E", "F", "FURB", "G", "I","ICN", "ISC", "PERF", "PIE", "PL", "PLE", "PTH",
|
||||||
|
"Q", "RET", "RSE", "RUF", "SLF", "SIM", "T20", "TCH", "UP", "W"]
|
||||||
|
ignore = ["E275", "FURB140", "I001", "PERF203", "RET502", "RET503", "SIM105", "T201"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests/*" = ["SLF001"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-quotes]
|
||||||
|
inline-quotes = "single"
|
||||||
|
|
||||||
|
[tool.ruff.lint.pylint]
|
||||||
|
max-args=16
|
||||||
|
max-branches=24
|
||||||
|
max-locals=16
|
||||||
|
max-nested-blocks=8
|
||||||
|
max-public-methods=16
|
||||||
|
max-returns=8
|
||||||
|
max-statements=96
|
||||||
|
|
||||||
|
[tool.ruff.lint.mccabe]
|
||||||
|
max-complexity = 20
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
command_line = "-m pytest"
|
||||||
|
omit = ["tests/*"]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue