diff --git a/disco/api/http.py b/disco/api/http.py index 79e6184..8bffb0d 100644 --- a/disco/api/http.py +++ b/disco/api/http.py @@ -264,11 +264,11 @@ class HTTPClient(LoggingClass): # Possibly wait if we're rate limited response.rate_limited_duration = self.limiter.check(bucket) - self.log.debug('KW: {}'.format(kwargs)) + self.log.debug('KW: %s', kwargs) # Make the actual request url = self.BASE_URL + route[1].format(**args) - self.log.info('{} {} ({})'.format(route[0].value, url, kwargs.get('params'))) + self.log.info('%s %s (%s)', route[0].value, url, kwargs.get('params')) r = self.session.request(route[0].value, url, **kwargs) if self.after_request: @@ -282,7 +282,7 @@ class HTTPClient(LoggingClass): if r.status_code < 400: return r elif r.status_code != 429 and 400 <= r.status_code < 500: - self.log.warning('Request failed with code {}: {}'.format(r.status_code, r.content)) + self.log.warning('Request failed with code %s: %s', r.status_code, r.content) response.exception = APIException(r) raise response.exception else: diff --git a/disco/api/ratelimit.py b/disco/api/ratelimit.py index 03a244b..ab50488 100644 --- a/disco/api/ratelimit.py +++ b/disco/api/ratelimit.py @@ -99,7 +99,7 @@ class RouteState(LoggingClass): self.event = gevent.event.Event() delay = (self.reset_time - time.time()) + .5 - self.log.debug('Cooling down bucket {} for {} seconds'.format(self, delay)) + self.log.debug('Cooling down bucket %s for %s seconds', self, delay) gevent.sleep(delay) self.event.set() self.event = None diff --git a/disco/bot/bot.py b/disco/bot/bot.py index da9445c..0679381 100644 --- a/disco/bot/bot.py +++ b/disco/bot/bot.py @@ -154,8 +154,7 @@ class Bot(LoggingClass): except ImportError: self.log.warning('Failed to enable HTTP server, Flask is not installed') else: - self.log.info('Starting HTTP server bound to {}:{}'.format(self.config.http_host, - self.config.http_port)) + self.log.info('Starting HTTP server bound to %s:%s', self.config.http_host, self.config.http_port) self.http = Flask('disco') self.http_server = WSGIServer((self.config.http_host, self.config.http_port), self.http) self.http_server_greenlet = gevent.spawn(self.http_server.serve_forever) @@ -235,7 +234,7 @@ class Bot(LoggingClass): Computes all possible abbreviations for a command grouping. """ # For the first pass, we just want to compute each groups possible - # abbreviations that don't conflict with each other. + # abbreviations that don't conflict with eachother. possible = {} for group in groups: for index in range(1, len(group)): @@ -245,7 +244,7 @@ class Bot(LoggingClass): else: possible[current] = group - # Now, we want to compute the actual shortest abbreviation out of the + # Now, we want to compute the actual shortest abbreivation out of the # possible ones result = {} for abbrev, group in six.iteritems(possible): @@ -450,7 +449,7 @@ class Bot(LoggingClass): inst = inst(self, config) if inst.__class__.__name__ in self.plugins: - self.log.warning('Attempted to add already added plugin {}'.format(inst.__class__.__name__)) + self.log.warning('Attempted to add already added plugin %s', inst.__class__.__name__) raise Exception('Cannot add already added plugin: {}'.format(inst.__class__.__name__)) self.ctx['plugin'] = self.plugins[inst.__class__.__name__] = inst @@ -468,7 +467,7 @@ class Bot(LoggingClass): Plugin class to unload and remove. """ if cls.__name__ not in self.plugins: - raise Exception('Cannot remove non-existent plugin: {}'.format(cls.__name__)) + raise Exception('Cannot remove non-existant plugin: {}'.format(cls.__name__)) ctx = {} self.plugins[cls.__name__].unload(ctx) @@ -496,7 +495,7 @@ class Bot(LoggingClass): """ Adds and loads a plugin, based on its module path. """ - self.log.info('Adding plugin module at path "{}"'.format(path)) + self.log.info('Adding plugin module at path "%s"', path) mod = importlib.import_module(path) loaded = False diff --git a/disco/cli.py b/disco/cli.py index 9fa0733..c1e96fb 100644 --- a/disco/cli.py +++ b/disco/cli.py @@ -103,7 +103,7 @@ def disco_main(run=False): if run: (bot or client).run_forever() - return bot or client + return (bot or client) if __name__ == '__main__': diff --git a/disco/gateway/client.py b/disco/gateway/client.py index 8786825..58ccc5b 100644 --- a/disco/gateway/client.py +++ b/disco/gateway/client.py @@ -75,7 +75,7 @@ class GatewayClient(LoggingClass): return self._send(op, data) def _send(self, op, data): - self.log.debug('GatewayClient.send {}'.format(op)) + self.log.debug('GatewayClient.send %s', op) self.packets.emit((SEND, op), data) self.ws.send(self.encoder.encode({ 'op': op.value, @@ -96,7 +96,7 @@ class GatewayClient(LoggingClass): def handle_dispatch(self, packet): obj = GatewayEvent.from_dispatch(self.client, packet) - self.log.debug('GatewayClient.handle_dispatch {}'.format(obj.__class__.__name__)) + self.log.debug('GatewayClient.handle_dispatch %s', obj.__class__.__name__) self.client.events.emit(obj.__class__.__name__, obj) if self.replaying: self.replayed_events += 1 @@ -119,7 +119,7 @@ class GatewayClient(LoggingClass): self.ws.close() def handle_hello(self, packet): - self.log.info('Received HELLO, starting heartbeat...') + self.log.info('Received HELLO, starting heartbeater...') self._heartbeat_task = gevent.spawn(self.heartbeat_task, packet['d']['heartbeat_interval']) def on_ready(self, ready): @@ -128,7 +128,7 @@ class GatewayClient(LoggingClass): self.reconnects = 0 def on_resumed(self, _): - self.log.info('RESUME completed, replayed {} events'.format(self.replayed_events)) + self.log.info('RESUME completed, replayed %s events', self.replayed_events) self.reconnects = 0 self.replaying = False @@ -144,7 +144,7 @@ class GatewayClient(LoggingClass): if self.zlib_stream_enabled: gateway_url += '&compress=zlib-stream' - self.log.info('Opening websocket connection to URL `{}`'.format(gateway_url)) + self.log.info('Opening websocket connection to URL `%s`', gateway_url) self.ws = Websocket(gateway_url) self.ws.emitter.on('on_open', self.on_open) self.ws.emitter.on('on_error', self.on_error) @@ -194,14 +194,14 @@ class GatewayClient(LoggingClass): if isinstance(error, KeyboardInterrupt): self.shutting_down = True self.ws_event.set() - raise Exception('WS received error: {}'.format(error)) + raise Exception('WS recieved error: %s', error) def on_open(self): if self.zlib_stream_enabled: self._zlib = zlib.decompressobj() if self.seq and self.session_id: - self.log.info('WS Opened: attempting resume w/ SID: {} SEQ: {}'.format(self.session_id, self.seq)) + self.log.info('WS Opened: attempting resume w/ SID: %s SEQ: %s', self.session_id, self.seq) self.replaying = True self.send(OPCode.RESUME, { 'token': self.client.config.token, @@ -230,7 +230,7 @@ class GatewayClient(LoggingClass): # Make sure we cleanup any old data self._buffer = None - # Kill heartbeat, a reconnect/resume will trigger a HELLO which will + # Kill heartbeater, a reconnect/resume will trigger a HELLO which will # respawn it if self._heartbeat_task: self._heartbeat_task.kill() @@ -244,7 +244,7 @@ class GatewayClient(LoggingClass): # Track reconnect attempts self.reconnects += 1 - self.log.info('WS Closed: [{}] {} ({})'.format(code, reason, self.reconnects)) + self.log.info('WS Closed: [%s] %s (%s)', code, reason, self.reconnects) if self.max_reconnects and self.reconnects > self.max_reconnects: raise Exception('Failed to reconnect after {} attempts, giving up'.format(self.max_reconnects)) @@ -254,8 +254,7 @@ class GatewayClient(LoggingClass): self.session_id = None wait_time = self.reconnects * 5 - self.log.info('Will attempt to {} after {} seconds'.format('resume' if self.session_id else 'reconnect', - wait_time)) + self.log.info('Will attempt to %s after %s seconds', 'resume' if self.session_id else 'reconnect', wait_time) gevent.sleep(wait_time) # Reconnect diff --git a/disco/state.py b/disco/state.py index 6b76787..71b813d 100644 --- a/disco/state.py +++ b/disco/state.py @@ -214,7 +214,7 @@ class State(object): def on_guild_delete(self, event): if event.id in self.guilds: - # Just delete the guild, channel references will fail + # Just delete the guild, channel references will fall del self.guilds[event.id] if event.id in self.voice_clients: diff --git a/disco/types/base.py b/disco/types/base.py index 9cd4905..7df638a 100644 --- a/disco/types/base.py +++ b/disco/types/base.py @@ -215,7 +215,7 @@ def datetime(data): except (ValueError, TypeError): continue - raise ValueError('Failed to convert `{}` to datetime'.format(data)) + raise ValueError('Failed to conver `{}` to datetime'.format(data)) def text(obj): diff --git a/disco/types/channel.py b/disco/types/channel.py index e7ae84b..48e7694 100644 --- a/disco/types/channel.py +++ b/disco/types/channel.py @@ -457,21 +457,21 @@ class Channel(SlottedModel, Permissible): """ Sets the channels bitrate. """ - assert self.is_voice + assert (self.is_voice) return self.client.api.channels_modify(self.id, bitrate=bitrate, reason=reason) def set_user_limit(self, user_limit, reason=None): """ Sets the channels user limit. """ - assert self.is_voice + assert (self.is_voice) return self.client.api.channels_modify(self.id, user_limit=user_limit, reason=reason) def set_parent(self, parent, reason=None): """ Sets the channels parent. """ - assert self.is_guild + assert (self.is_guild) return self.client.api.channels_modify( self.id, parent_id=to_snowflake(parent) if parent else parent, diff --git a/disco/types/guild.py b/disco/types/guild.py index 69ef478..b02091d 100644 --- a/disco/types/guild.py +++ b/disco/types/guild.py @@ -75,7 +75,7 @@ class GuildEmoji(Emoji): @property def url(self): - return 'https://media.discordapp.net/emojis/{}.{}'.format(self.id, 'gif' if self.animated else 'png') + return 'https://discordapp.com/api/emojis/{}.{}'.format(self.id, 'gif' if self.animated else 'png') @cached_property def guild(self): @@ -505,19 +505,19 @@ class Guild(SlottedModel, Permissible): if not self.icon: return '' - return 'https://media.discordapp.net/icons/{}/{}.{}?size={}'.format(self.id, self.icon, fmt, size) + return 'https://cdn.discordapp.com/icons/{}/{}.{}?size={}'.format(self.id, self.icon, fmt, size) def get_splash_url(self, fmt='webp', size=1024): if not self.splash: return '' - return 'https://media.discordapp.net/splashes/{}/{}.{}?size={}'.format(self.id, self.splash, fmt, size) + return 'https://cdn.discordapp.com/splashes/{}/{}.{}?size={}'.format(self.id, self.splash, fmt, size) def get_banner_url(self, fmt='webp', size=1024): if not self.banner: return '' - return 'https://media.discordapp.net/banners/{}/{}.{}?size={}'.format(self.id, self.banner, fmt, size) + return 'https://cdn.discordapp.com/banners/{}/{}.{}?size={}'.format(self.id, self.banner, fmt, size) @property def icon_url(self): diff --git a/disco/types/user.py b/disco/types/user.py index a5d5fa3..63e9542 100644 --- a/disco/types/user.py +++ b/disco/types/user.py @@ -26,11 +26,11 @@ class User(SlottedModel, with_equality('id'), with_hash('id')): if not self.avatar: return 'https://cdn.discordapp.com/embed/avatars/{}.png'.format(self.default_avatar.value) if fmt is not None: - return 'https://media.discordapp.net/avatars/{}/{}.{}?size={}'.format(self.id, self.avatar, fmt, size) + return 'https://cdn.discordapp.com/avatars/{}/{}.{}?size={}'.format(self.id, self.avatar, fmt, size) if self.avatar.startswith('a_'): - return 'https://media.discordapp.net/avatars/{}/{}.gif?size={}'.format(self.id, self.avatar, size) + return 'https://cdn.discordapp.com/avatars/{}/{}.gif?size={}'.format(self.id, self.avatar, size) else: - return 'https://media.discordapp.net/avatars/{}/{}.webp?size={}'.format(self.id, self.avatar, size) + return 'https://cdn.discordapp.com/avatars/{}/{}.webp?size={}'.format(self.id, self.avatar, size) @property def default_avatar(self): diff --git a/disco/voice/client.py b/disco/voice/client.py index 9e5ce0e..2a48346 100644 --- a/disco/voice/client.py +++ b/disco/voice/client.py @@ -143,7 +143,7 @@ class VoiceClient(LoggingClass): return self.ssrc + 3 def set_state(self, state): - self.log.debug('[{}] state {} -> {}'.format(self, self.state, state)) + self.log.debug('[%s] state %s -> %s', self, self.state, state) prev_state = self.state self.state = state self.state_emitter.emit(state, prev_state) @@ -154,8 +154,7 @@ class VoiceClient(LoggingClass): return self.log.info( - '[{}] Set endpoint from VOICE_SERVER_UPDATE (state = {} / endpoint = {})'.format(self, self.state, - endpoint)) + '[%s] Set endpoint from VOICE_SERVER_UPDATE (state = %s / endpoint = %s)', self, self.state, endpoint) self.endpoint = endpoint @@ -211,13 +210,13 @@ class VoiceClient(LoggingClass): def send(self, op, data): if self.ws and self.ws.sock and self.ws.sock.connected: - self.log.debug('[{}] sending OP {} (data = {})'.format(self, op, data)) + self.log.debug('[%s] sending OP %s (data = %s)', self, op, data) self.ws.send(self.encoder.encode({ 'op': op.value, 'd': data, }), self.encoder.OPCODE) else: - self.log.debug('[{}] dropping because ws is closed OP {} (data = {})'.format(self, op, data)) + self.log.debug('[%s] dropping because ws is closed OP %s (data = %s)', self, op, data) def on_voice_client_connect(self, data): user_id = int(data['user_id']) @@ -242,13 +241,12 @@ class VoiceClient(LoggingClass): self.udp.set_audio_codec(data['audio_codec']) def on_voice_hello(self, data): - self.log.info('[{}] Received Voice HELLO payload, starting heartbeat'.format(self)) + self.log.info('[%s] Received Voice HELLO payload, starting heartbeater', self) self._heartbeat_task = gevent.spawn(self._heartbeat, data['heartbeat_interval']) self.set_state(VoiceState.AUTHENTICATED) def on_voice_ready(self, data): - self.log.info('[{}] Received Voice READY payload, attempting to negotiate voice connection w/ remote'.format( - self)) + self.log.info('[%s] Received Voice READY payload, attempting to negotiate voice connection w/ remote', self) self.set_state(VoiceState.CONNECTING) self.ssrc = data['ssrc'] self.ip = data['ip'] @@ -258,12 +256,12 @@ class VoiceClient(LoggingClass): for mode in self.SUPPORTED_MODES: if mode in data['modes']: self.mode = mode - self.log.debug('[{}] Selected mode {}'.format(self, mode)) + self.log.debug('[%s] Selected mode %s', self, mode) break else: raise Exception('Failed to find a supported voice mode') - self.log.debug('[{}] Attempting IP discovery over UDP to {}:{}'.format(self, self.ip, self.port)) + self.log.debug('[%s] Attempting IP discovery over UDP to %s:%s', self, self.ip, self.port) self.udp = UDPVoiceClient(self) ip, port = self.udp.connect(self.ip, self.port) @@ -283,8 +281,7 @@ class VoiceClient(LoggingClass): 'payload_type': RTPPayloadTypes.get(codec).value, }) - self.log.debug('[{}] IP discovery completed (ip = {}, port = {}), sending SELECT_PROTOCOL'.format(self, ip, - port)) + self.log.debug('[%s] IP discovery completed (ip = %s, port = %s), sending SELECT_PROTOCOL', self, ip, port) self.send(VoiceOPCode.SELECT_PROTOCOL, { 'protocol': 'udp', 'data': { @@ -301,11 +298,11 @@ class VoiceClient(LoggingClass): }) def on_voice_resumed(self, data): - self.log.info('[{}] Received resumed'.format(self)) + self.log.info('[%s] Received resumed', self) self.set_state(VoiceState.CONNECTED) def on_voice_sdp(self, sdp): - self.log.info('[{}] Received session description, connection completed'.format(self)) + self.log.info('[%s] Received session description, connection completed', self) self.mode = sdp['mode'] self.audio_codec = sdp['audio_codec'] @@ -344,7 +341,7 @@ class VoiceClient(LoggingClass): self.log.exception('Failed to parse voice gateway message: ') def on_error(self, err): - self.log.error('[{}] Voice websocket error: {}'.format(self, err)) + self.log.error('[%s] Voice websocket error: %s', self, err) def on_open(self): if self._identified: @@ -363,7 +360,7 @@ class VoiceClient(LoggingClass): }) def on_close(self, code, reason): - self.log.warning('[{}] Voice websocket closed: [{}] {} ({})'.format(self, code, reason, self._reconnects)) + self.log.warning('[%s] Voice websocket closed: [%s] %s (%s)', self, code, reason, self._reconnects) if self._heartbeat_task: self._heartbeat_task.kill() @@ -375,7 +372,7 @@ class VoiceClient(LoggingClass): if self.state == VoiceState.DISCONNECTED: return - self.log.info('[{}] Attempting Websocket Resumption'.format(self)) + self.log.info('[%s] Attempting Websocket Resumption', self) self.set_state(VoiceState.RECONNECTING) @@ -399,8 +396,7 @@ class VoiceClient(LoggingClass): wait_time = 1 self.log.info( - '[{}] Will attempt to {} after {} seconds'.format(self, 'resume' if self._identified else 'reconnect', - wait_time)) + '[%s] Will attempt to %s after %s seconds', self, 'resume' if self._identified else 'reconnect', wait_time) gevent.sleep(wait_time) self._connect_and_run() @@ -410,17 +406,17 @@ class VoiceClient(LoggingClass): channel_id = self.server_id if not channel_id: - raise VoiceException('[{}] cannot connect to an empty channel id'.format(self)) + raise VoiceException('[%s] cannot connect to an empty channel id', self) if self.channel_id == channel_id: if self.state == VoiceState.CONNECTED: - self.log.debug('[{}] Already connected to {}, returning'.format(self, self.channel)) + self.log.debug('[%s] Already connected to %s, returning', self, self.channel) return self else: if self.state == VoiceState.CONNECTED: - self.log.debug('[{}] Moving to channel {}'.format(self, channel_id)) + self.log.debug('[%s] Moving to channel %s', self, channel_id) else: - self.log.debug('[{}] Attempting connection to channel id {}'.format(self, channel_id)) + self.log.debug('[%s] Attempting connection to channel id %s', self, channel_id) self.set_state(VoiceState.AWAITING_ENDPOINT) self.set_voice_state(channel_id, **kwargs) @@ -435,7 +431,7 @@ class VoiceClient(LoggingClass): if self.state == VoiceState.DISCONNECTED: return - self.log.debug('[{}] disconnect called'.format(self)) + self.log.debug('[%s] disconnect called', self) self.set_state(VoiceState.DISCONNECTED) del self.client.state.voice_clients[self.server_id] diff --git a/disco/voice/udp.py b/disco/voice/udp.py index 35f86c9..c3fc05c 100644 --- a/disco/voice/udp.py +++ b/disco/voice/udp.py @@ -7,7 +7,7 @@ from collections import namedtuple try: import nacl.secret except ImportError: - print('WARNING: PyNaCl is not installed, voice support is disabled') + print('WARNING: nacl is not installed, voice support is disabled') from holster.enum import Enum @@ -103,8 +103,7 @@ class UDPVoiceClient(LoggingClass): ptype = RTPPayloadTypes.get(codec) self._rtp_audio_header[1] = ptype.value - self.log.debug('[{}] Set UDP\'s Audio Codec to {}, RTP payload type {}'.format(self.vc, ptype.name, - ptype.value)) + self.log.debug('[%s] Set UDP\'s Audio Codec to %s, RTP payload type %s', self.vc, ptype.name, ptype.value) def increment_timestamp(self, by): self.timestamp += by @@ -169,7 +168,7 @@ class UDPVoiceClient(LoggingClass): # Data cannot be less than the bare minimum, just ignore if len(data) <= 12: - self.log.debug('[{}] [VoiceData] Received voice data under 13 bytes'.format(self.vc)) + self.log.debug('[%s] [VoiceData] Received voice data under 13 bytes', self.vc) continue first, second = struct.unpack_from('>BB', data) @@ -221,16 +220,14 @@ class UDPVoiceClient(LoggingClass): # Check if rtp version is 2 if rtp.version != 2: - self.log.debug('[{}] [VoiceData] Received an invalid RTP packet version, {}'.format(self.vc, - rtp.version)) + self.log.debug('[%s] [VoiceData] Received an invalid RTP packet version, %s', self.vc, rtp.version) continue payload_type = RTPPayloadTypes.get(rtp.payload_type) # Unsupported payload type received if not payload_type: - self.log.debug('[{}] [VoiceData] Received unsupported payload type, {}'.format(self.vc, - rtp.payload_type)) + self.log.debug('[%s] [VoiceData] Received unsupported payload type, %s', self.vc, rtp.payload_type) continue nonce = bytearray(24) @@ -243,13 +240,13 @@ class UDPVoiceClient(LoggingClass): elif self.vc.mode == 'xsalsa20_poly1305': nonce[:12] = data[:12] else: - self.log.debug('[{}] [VoiceData] Unsupported Encryption Mode, {}'.format(self.vc, self.vc.mode)) + self.log.debug('[%s] [VoiceData] Unsupported Encryption Mode, %s', self.vc, self.vc.mode) continue try: data = self._secret_box.decrypt(bytes(data[12:]), bytes(nonce)) except Exception: - self.log.debug('[{}] [VoiceData] Failed to decode data from ssrc {}'.format(self.vc, rtp.ssrc)) + self.log.debug('[%s] [VoiceData] Failed to decode data from ssrc %s', self.vc, rtp.ssrc) continue # RFC3550 Section 5.1 (Padding) @@ -271,11 +268,11 @@ class UDPVoiceClient(LoggingClass): first_byte, = struct.unpack_from('>B', data[:offset]) offset += 1 - rtp_extension_identifier = first_byte & 0xF + rtp_extension_identifer = first_byte & 0xF rtp_extension_len = ((first_byte >> 4) & 0xF) + 1 - # Ignore data if identifier == 15, so skip if this is set as 0 - if rtp_extension_identifier: + # Ignore data if identifer == 15, so skip if this is set as 0 + if rtp_extension_identifer: fields.append(data[offset:offset + rtp_extension_len]) offset += rtp_extension_len @@ -293,7 +290,7 @@ class UDPVoiceClient(LoggingClass): # RFC3550 Section 5.3: Profile-Specific Modifications to the RTP Header # clients send it sometimes, definitely on fresh connects to a server, dunno what to do here if rtp.marker: - self.log.debug('[{}] [VoiceData] Received RTP data with the marker set, skipping'.format(self.vc)) + self.log.debug('[%s] [VoiceData] Received RTP data with the marker set, skipping', self.vc) continue payload = VoiceData( @@ -331,7 +328,7 @@ class UDPVoiceClient(LoggingClass): try: data, addr = gevent.spawn(lambda: self.conn.recvfrom(70)).get(timeout=timeout) except gevent.Timeout: - return None, None + return (None, None) # Read IP and port ip = str(data[4:]).split('\x00', 1)[0] @@ -341,4 +338,4 @@ class UDPVoiceClient(LoggingClass): self.connected = True self._run_task = gevent.spawn(self.run) - return ip, port + return (ip, port) diff --git a/setup.py b/setup.py index c7a497d..eae8156 100644 --- a/setup.py +++ b/setup.py @@ -10,16 +10,16 @@ with open('README.md') as f: readme = f.read() extras_require = { - 'voice': ['pynacl>=1.2.1'], - 'http': ['flask>=0.12.2'], - 'yaml': ['pyyaml>=3.12'], + 'voice': ['pynacl==1.2.1'], + 'http': ['flask==0.12.2'], + 'yaml': ['pyyaml==3.12'], 'music': ['youtube_dl>=2018.1.21'], 'performance': [ 'erlpack==0.3.2' if sys.version_info.major == 2 else 'earl-etf==2.1.2', 'ujson==1.35', 'wsaccel==0.6.2', ], - 'sharding': ['gipc>=0.6.0'], + 'sharding': ['gipc==0.6.0'], 'docs': ['biblio==0.0.4'], }