├── .gitignore ├── README.md ├── dashed ├── __init__.py ├── __main__.py ├── discord.py ├── embeds.py ├── interaction.py ├── loader.py └── server.py ├── example ├── echo.py └── math.py ├── poetry.lock └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | /dist/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dashed 2 | 3 | simple sdk for creating and executing Discord slash commands and rich interactions. 4 | 5 | ## example 6 | 7 | ```python 8 | import asyncio 9 | import dashed 10 | 11 | 12 | @dashed.command(description="It's like an echo chamber in here!") 13 | async def echo(ctx: dashed.InteractionContext, message: str): 14 | return ctx.reply(content=message) 15 | 16 | 17 | @dashed.command(description="Add two numbers") 18 | async def sum(ctx: dashed.InteractionContext, x: int, y: int): 19 | return ctx.reply(content=f"Result: `{x + y}`") 20 | 21 | 22 | @dashed.command(description="Suprise") 23 | async def boo(ctx: dashed.InteractionContext): 24 | async def later(ctx: dashed.DeferredInteractionContext): 25 | await asyncio.sleep(5) 26 | await ctx.update(content="Boo!") 27 | 28 | # We can decide here whether to respond or defer 29 | return ctx.defer(later) 30 | 31 | 32 | @dashed.command(description="Deferred by default") 33 | async def deferred(ctx: dashed.DeferredInteractionContext, message: str): 34 | # Because we receive a "DeferredInteractionContext", this will always be deferred 35 | await asyncio.sleep(5) 36 | await ctx.update(content="It works") 37 | ``` 38 | 39 | ## registering commands 40 | 41 | ```sh 42 | $ python -m dashed register-commands --load-from-file example/math.py --application-id ... --delete-unknown 43 | ``` 44 | 45 | ## running 46 | 47 | ```sh 48 | $ python -m dashed serve --load-from-file example/math.py --bind 0.0.0.0:8689 49 | ``` -------------------------------------------------------------------------------- /dashed/__init__.py: -------------------------------------------------------------------------------- 1 | from .interaction import InteractionContext, DeferredInteractionContext 2 | from .discord import Channel, User, Role, Mentionable 3 | from .loader import command, Group, TypeWithChoices 4 | from .embeds import Embed, EmbedField 5 | 6 | __all__ = [ 7 | "command", 8 | "Group", 9 | "InteractionContext", 10 | "DeferredInteractionContext", 11 | "Channel", 12 | "User", 13 | "Role", 14 | "Mentionable", 15 | "TypeWithChoices", 16 | "Embed", 17 | "EmbedField", 18 | ] 19 | -------------------------------------------------------------------------------- /dashed/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import os 4 | import pathlib 5 | import sys 6 | 7 | from nacl.signing import VerifyKey 8 | 9 | from dashed import server 10 | from dashed.discord import DiscordAPIClient 11 | from dashed.loader import DashedContext, load_from_file 12 | 13 | ENV_ARGS = { 14 | "token": "DASHED_DISCORD_TOKEN", 15 | "application_key": "DASHED_DISCORD_APPLICATION_KEY", 16 | } 17 | 18 | 19 | async def _get_context(args, not_required=None) -> DashedContext: 20 | for k, v in ENV_ARGS.items(): 21 | if getattr(args, k, None) is None: 22 | value = os.environ.get(v) 23 | if value is None: 24 | if not_required is not None and k in not_required: 25 | continue 26 | 27 | print( 28 | f"Error: {k.replace('_', '-')} was not passed and {v} env variable was not set" 29 | ) 30 | return sys.exit(1) 31 | 32 | setattr(args, k, value) 33 | 34 | api = DiscordAPIClient(token=args.token) 35 | 36 | modules = [] 37 | for file_path in args.load_from_file or []: 38 | modules.append(await load_from_file(pathlib.Path(file_path))) 39 | 40 | commands = {} 41 | for module in modules: 42 | for command in module.commands: 43 | commands[command.name] = command 44 | 45 | groups = {} 46 | for module in modules: 47 | for group in module.groups: 48 | groups[group.name] = group 49 | 50 | return DashedContext( 51 | client=api, 52 | application_key=VerifyKey(bytes.fromhex(args.application_key)) 53 | if hasattr(args, "application_key") 54 | else None, 55 | commands=commands, 56 | groups=groups, 57 | ) 58 | 59 | 60 | async def _register_commands(args): 61 | ctx = await _get_context(args, not_required={"application_key"}) 62 | 63 | if args.delete_unknown: 64 | existing_commands = await ctx.client.get_global_application_commands( 65 | args.application_id 66 | ) 67 | for command in existing_commands: 68 | await ctx.client.delete_global_application_command( 69 | args.application_id, command["id"] 70 | ) 71 | 72 | await ctx.register_commands(args.application_id) 73 | await ctx.register_groups(args.application_id) 74 | await ctx.client.close() 75 | 76 | 77 | async def _serve(args): 78 | ctx = await _get_context(args) 79 | await server.run(args.host, args.port, ctx) 80 | await ctx.client.close() 81 | 82 | 83 | parser = argparse.ArgumentParser("dashed") 84 | subparsers = parser.add_subparsers() 85 | 86 | register_commands_parser = subparsers.add_parser("register-commands") 87 | register_commands_parser.set_defaults(fn=_register_commands) 88 | register_commands_parser.add_argument( 89 | "--application-id", help="Discord Application ID", required=True 90 | ) 91 | register_commands_parser.add_argument( 92 | "-d", 93 | "--delete-unknown", 94 | help="Delete unknown (global) slash commands", 95 | action="store_true", 96 | ) 97 | register_commands_parser.add_argument( 98 | "--load-from-file", 99 | action="append", 100 | help="Load interactions and slash commands from a Python file", 101 | ) 102 | 103 | serve_parser = subparsers.add_parser("serve") 104 | serve_parser.set_defaults(fn=_serve) 105 | serve_parser.add_argument( 106 | "--load-from-file", 107 | action="append", 108 | help="Load interactions and slash commands from a Python file", 109 | ) 110 | serve_parser.add_argument( 111 | "--host", 112 | default="localhost", 113 | help="The host string to bind on", 114 | ) 115 | serve_parser.add_argument("--port", default=8689, type=int, help="The port to bind on") 116 | serve_parser.add_argument("--token", help="Discord Bot Token") 117 | serve_parser.add_argument("--application-key", help="Discord Application Key") 118 | 119 | 120 | def _run_in_loop(fn): 121 | event_loop = asyncio.get_event_loop() 122 | try: 123 | event_loop.run_until_complete(fn) 124 | finally: 125 | event_loop.close() 126 | 127 | 128 | async def main(): 129 | args = parser.parse_args() 130 | if not hasattr(args, "fn"): 131 | parser.print_help() 132 | return 133 | 134 | await args.fn(args) 135 | 136 | 137 | _run_in_loop(main()) 138 | -------------------------------------------------------------------------------- /dashed/discord.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import dataclasses 3 | from enum import IntEnum 4 | from typing import List, Literal, Optional, Union 5 | 6 | import httpx 7 | 8 | from dashed.embeds import Embed 9 | 10 | 11 | class ChannelType(IntEnum): 12 | GUILD_TEXT = 0 13 | DM = 1 14 | GUILD_VOICE = 2 15 | GROUP_DM = 3 16 | GUILD_CATEGORY = 4 17 | GUILD_NEWS = 5 18 | GUILD_STORE = 6 19 | GUILD_NEWS_THREAD = 10 20 | GUILD_PUBLIC_THREAD = 11 21 | GUILD_PRIVATE_THREAD = 12 22 | GUILD_STAGE_VOICE = 13 23 | 24 | 25 | @dataclasses.dataclass 26 | class Channel: 27 | id: str 28 | name: str 29 | permissions: str 30 | type: ChannelType 31 | 32 | 33 | @dataclasses.dataclass 34 | class User: 35 | id: str 36 | username: str 37 | discriminator: str 38 | public_flags: int 39 | avatar: str 40 | bot: bool = False 41 | 42 | def mention(self): 43 | return f"<@{self.id}>" 44 | 45 | 46 | @dataclasses.dataclass 47 | class Role: 48 | pass 49 | 50 | 51 | @dataclasses.dataclass 52 | class Mentionable: 53 | pass 54 | 55 | 56 | class InteractionRequestType(IntEnum): 57 | PING = 1 58 | APPLICATION_COMMAND = 2 59 | MESSAGE_COMPONENT = 3 60 | 61 | 62 | class InteractionResponseType(IntEnum): 63 | PONG = 1 64 | CHANNEL_MESSAGE_WITH_SOURCE = 4 65 | DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5 66 | DEFERRED_UPDATE_MESSAGE = 6 67 | UPDATE_MESSAGE = 7 68 | 69 | 70 | class ApplicationCommandOptionType(IntEnum): 71 | SUB_COMMAND = 1 72 | SUB_COMMAND_GROUP = 2 73 | STRING = 3 74 | INTEGER = 4 75 | BOOLEAN = 5 76 | USER = 6 77 | CHANNEL = 7 78 | ROLE = 8 79 | MENTIONABLE = 9 80 | 81 | 82 | @dataclasses.dataclass 83 | class AllowedMentions: 84 | parse: List[Literal["roles", "users", "everyone"]] 85 | roles: List[int] = dataclasses.field(default_factory=list) 86 | users: List[int] = dataclasses.field(default_factory=list) 87 | replied_user: bool = False 88 | 89 | 90 | @dataclasses.dataclass 91 | class ApplicationCommandCallbackData: 92 | tts: Optional[bool] = None 93 | content: Optional[str] = None 94 | embeds: Optional[List[Embed]] = None 95 | allowed_mentions: Optional[AllowedMentions] = None 96 | flags: Optional[int] = None 97 | components: Optional[List] = None 98 | 99 | 100 | @dataclasses.dataclass 101 | class ApplicationCommandOptionChoice: 102 | name: str 103 | value: Union[int, str] 104 | 105 | 106 | @dataclasses.dataclass 107 | class ApplicationCommandOption: 108 | type: ApplicationCommandOptionType 109 | name: str 110 | description: str 111 | required: bool = False 112 | choices: List[ApplicationCommandOptionChoice] = dataclasses.field( 113 | default_factory=list 114 | ) 115 | options: List["ApplicationCommandOption"] = dataclasses.field(default_factory=list) 116 | 117 | 118 | @dataclasses.dataclass 119 | class ApplicationCommandDescription: 120 | name: str 121 | description: str 122 | options: List[ApplicationCommandOption] 123 | default_permission: bool = True 124 | 125 | 126 | @dataclasses.dataclass 127 | class WebhookEditBody: 128 | content: Optional[str] = None 129 | allowed_mentions: Optional[AllowedMentions] = None 130 | embeds: Optional[List[Embed]] = None 131 | 132 | 133 | class DiscordAPIClient: 134 | def __init__(self, token): 135 | self._http = httpx.AsyncClient( 136 | headers={ 137 | "Authorization": f"Bot {token}", 138 | "User-Agent": "DiscordBot (https://github.com/b1naryth1ef/dashed dev)", 139 | } 140 | ) 141 | 142 | async def close(self): 143 | await self._http.aclose() 144 | 145 | def _url(self, path): 146 | return f"https://discord.com/api/v8{path}" 147 | 148 | async def _check(self, response): 149 | if "X-RateLimit-Reset-After" in response.headers: 150 | if int(response.headers["X-RateLimit-Remaining"]) == 0: 151 | await asyncio.sleep(float(response.headers["X-RateLimit-Reset-After"])) 152 | 153 | try: 154 | response.raise_for_status() 155 | except Exception: 156 | print("Response: ", response.json()) 157 | raise 158 | 159 | async def _get(self, path): 160 | r = await self._http.get(self._url(path)) 161 | await self._check(r) 162 | return r.json() 163 | 164 | async def _patch(self, path, **kwargs): 165 | r = await self._http.patch(self._url(path), **kwargs) 166 | await self._check(r) 167 | return r.json() 168 | 169 | async def _post(self, path, **kwargs): 170 | r = await self._http.post(self._url(path), **kwargs) 171 | await self._check(r) 172 | return r.json() 173 | 174 | async def _delete(self, path): 175 | r = await self._http.delete(self._url(path)) 176 | await self._check(r) 177 | 178 | async def get_global_application_commands(self, application_id): 179 | return await self._get(f"/applications/{application_id}/commands") 180 | 181 | async def create_global_application_command(self, application_id, body): 182 | return await self._post(f"/applications/{application_id}/commands", json=body) 183 | 184 | async def delete_global_application_command(self, application_id, command_id): 185 | await self._delete(f"/applications/{application_id}/commands/{command_id}") 186 | 187 | async def edit_original_interaction_response( 188 | self, application_id, interaction_token, body 189 | ): 190 | return await self._patch( 191 | f"/webhooks/{application_id}/{interaction_token}/messages/@original", 192 | json=body, 193 | ) 194 | -------------------------------------------------------------------------------- /dashed/embeds.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | from typing import List, Literal, Optional 3 | 4 | 5 | class EmbedFooter: 6 | pass 7 | 8 | 9 | class EmbedImage: 10 | pass 11 | 12 | 13 | @dataclasses.dataclass 14 | class EmbedThumbnail: 15 | url: Optional[str] = None 16 | proxy_url: Optional[str] = None 17 | height: Optional[int] = None 18 | width: Optional[int] = None 19 | 20 | 21 | class EmbedVideo: 22 | pass 23 | 24 | 25 | class EmbedProvider: 26 | pass 27 | 28 | 29 | class EmbedAuthor: 30 | pass 31 | 32 | 33 | @dataclasses.dataclass 34 | class EmbedField: 35 | name: str 36 | value: str 37 | inline: Optional[bool] = False 38 | 39 | 40 | @dataclasses.dataclass 41 | class Embed: 42 | title: Optional[str] = None 43 | type: Optional[ 44 | Literal["rich", "image", "video", "gifv", "article", "link"] 45 | ] = "rich" 46 | description: Optional[str] = None 47 | url: Optional[str] = None 48 | timestamp: Optional[str] = None 49 | color: Optional[int] = None 50 | footer: Optional[EmbedFooter] = None 51 | image: Optional[EmbedImage] = None 52 | thumbnail: Optional[EmbedThumbnail] = None 53 | video: Optional[EmbedVideo] = None 54 | provider: Optional[EmbedProvider] = None 55 | author: Optional[EmbedAuthor] = None 56 | fields: Optional[List[EmbedField]] = None 57 | -------------------------------------------------------------------------------- /dashed/interaction.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from dashed.loader import DashedContext 3 | import dataclasses 4 | from typing import Any, Dict 5 | from .discord import ( 6 | ApplicationCommandCallbackData, 7 | InteractionResponseType, 8 | WebhookEditBody, 9 | ) 10 | 11 | 12 | class InteractionContext: 13 | def __init__(self, ctx: DashedContext, data: Dict[str, Any]): 14 | self.ctx = ctx 15 | self.data = data 16 | 17 | def reply(self, **kwargs): 18 | return { 19 | "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, 20 | "data": dataclasses.asdict(ApplicationCommandCallbackData(**kwargs)), 21 | } 22 | 23 | def defer(self, fn): 24 | asyncio.ensure_future(fn(DeferredInteractionContext(self))) 25 | return { 26 | "type": InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, 27 | } 28 | 29 | 30 | class DeferredInteractionContext: 31 | def __init__(self, original_interaction_context): 32 | self.original_interaction_context = original_interaction_context 33 | 34 | @property 35 | def ctx(self): 36 | return self.original_interaction_context.ctx 37 | 38 | @property 39 | def data(self): 40 | return self.original_interaction_context.data 41 | 42 | async def update(self, **kwargs): 43 | await self.ctx.client.edit_original_interaction_response( 44 | self.data["application_id"], 45 | self.data["token"], 46 | dataclasses.asdict(WebhookEditBody(**kwargs)), 47 | ) 48 | -------------------------------------------------------------------------------- /dashed/loader.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import inspect 3 | import pathlib 4 | import typing 5 | from typing import Any, Dict, List, Optional, OrderedDict, Tuple, Union 6 | 7 | from dashed.discord import ( 8 | ApplicationCommandDescription, 9 | ApplicationCommandOption, 10 | ApplicationCommandOptionType, 11 | Channel, 12 | DiscordAPIClient, 13 | Mentionable, 14 | Role, 15 | User, 16 | ) 17 | 18 | _registered_groups_buffer = [] 19 | _registered_commands_buffer = [] 20 | 21 | 22 | def _flush_registered_commands_buffer() -> List: 23 | global _registered_commands_buffer 24 | result = _registered_commands_buffer 25 | _registered_commands_buffer = [] 26 | return result 27 | 28 | 29 | def command(*, description: str, name: Optional[str] = None): 30 | global _registered_commands_buffer 31 | 32 | def command_decorator(fn): 33 | _registered_commands_buffer.append( 34 | DashedCommand(name=name or fn.__name__, description=description, fn=fn) 35 | ) 36 | return fn 37 | 38 | return command_decorator 39 | 40 | 41 | def _flush_registered_groups_buffer() -> List: 42 | global _registered_groups_buffer 43 | result = _registered_groups_buffer 44 | _registered_groups_buffer = [] 45 | return result 46 | 47 | 48 | class TypeWithChoices: 49 | def __init__(self, inner_type: type, choices: Dict[str, Union[str, int]]): 50 | self.inner_type = inner_type 51 | self.choices = choices 52 | 53 | 54 | @dataclasses.dataclass 55 | class Group: 56 | name: str 57 | description: str 58 | commands: Dict[str, "DashedCommand"] = dataclasses.field(default_factory=dict) 59 | children: Dict[str, "Group"] = dataclasses.field(default_factory=dict) 60 | 61 | def lookup(self, options): 62 | assert len(options) == 1 63 | target_name = options[0]["name"] 64 | 65 | if target_name in self.commands: 66 | return self.commands[target_name], options[0]["options"] 67 | 68 | return self.children[target_name].lookup(options[0]["options"]) 69 | 70 | def __post_init__(self): 71 | global _registered_groups_buffer 72 | _registered_groups_buffer.append(self) 73 | 74 | def subgroup(self, name: str, description: str): 75 | group = Group(name=name, description=description) 76 | self.children[name] = group 77 | return group 78 | 79 | def command(self, *, description: str, name: Optional[str] = None): 80 | def command_decorator(fn): 81 | self.commands[name or fn.__name__] = DashedCommand( 82 | name=name or fn.__name__, description=description, fn=fn 83 | ) 84 | return fn 85 | 86 | return command_decorator 87 | 88 | 89 | @dataclasses.dataclass 90 | class DashedCommand: 91 | name: str 92 | description: str 93 | fn: Any 94 | deferred: bool = False 95 | 96 | def __post_init__(self): 97 | from dashed.interaction import DeferredInteractionContext 98 | 99 | hints = typing.get_type_hints(self.fn) 100 | self.deferred = hints["ctx"] == DeferredInteractionContext 101 | 102 | 103 | @dataclasses.dataclass 104 | class DashedModule: 105 | name: str 106 | commands: List[DashedCommand] 107 | groups: List[Group] 108 | 109 | 110 | @dataclasses.dataclass 111 | class DashedContext: 112 | client: DiscordAPIClient 113 | application_key: str 114 | commands: Dict[str, DashedCommand] 115 | groups: Dict[str, Group] 116 | 117 | async def register_commands(self, application_id): 118 | for command in self.commands.values(): 119 | await self.client.create_global_application_command( 120 | application_id, 121 | dataclasses.asdict(_get_application_command_description(command)), 122 | ) 123 | 124 | async def register_groups(self, application_id): 125 | for group in self.groups.values(): 126 | await self.client.create_global_application_command( 127 | application_id, 128 | dataclasses.asdict(_get_application_group_description(group)), 129 | ) 130 | 131 | 132 | async def load_from_file(path: pathlib.Path) -> DashedModule: 133 | import importlib.machinery 134 | 135 | module = importlib.machinery.SourceFileLoader( 136 | f"dashed.runtime.module.{path.stem}", str(path) 137 | ).load_module() 138 | 139 | if hasattr(module, "initialize"): 140 | await module.initialize() 141 | 142 | return DashedModule( 143 | name=path.stem, 144 | commands=_flush_registered_commands_buffer(), 145 | groups=_flush_registered_groups_buffer(), 146 | ) 147 | 148 | 149 | def _get_options_for_group(group: Group) -> List[ApplicationCommandOption]: 150 | options = [] 151 | 152 | for command in group.commands.values(): 153 | options.append( 154 | ApplicationCommandOption( 155 | type=ApplicationCommandOptionType.SUB_COMMAND, 156 | name=command.name, 157 | description=command.description, 158 | options=list(_get_command_args(command).values()), 159 | ) 160 | ) 161 | 162 | for subgroup in group.children.values(): 163 | options.append( 164 | ApplicationCommandOption( 165 | type=ApplicationCommandOptionType.SUB_COMMAND_GROUP, 166 | name=subgroup.name, 167 | description=subgroup.description, 168 | options=_get_options_for_group(subgroup), 169 | ) 170 | ) 171 | 172 | return options 173 | 174 | 175 | def _get_application_group_description(group: Group) -> ApplicationCommandDescription: 176 | return ApplicationCommandDescription( 177 | name=group.name, 178 | description=group.description, 179 | options=_get_options_for_group(group), 180 | ) 181 | 182 | 183 | def _get_application_command_description( 184 | command: "DashedCommand", 185 | ) -> ApplicationCommandDescription: 186 | return ApplicationCommandDescription( 187 | name=command.name, 188 | description=command.description, 189 | options=list(_get_command_args(command).values()), 190 | ) 191 | 192 | 193 | def _get_option_type_and_choices( 194 | type_: type, 195 | ) -> Tuple[ApplicationCommandOptionType, Optional[List[Dict[str, Any]]]]: 196 | if type_ is str: 197 | return ApplicationCommandOptionType.STRING, None 198 | elif type_ is int: 199 | return ApplicationCommandOptionType.INTEGER, None 200 | elif type_ is bool: 201 | return ApplicationCommandOptionType.BOOLEAN, None 202 | elif type_ is User: 203 | return ApplicationCommandOptionType.USER, None 204 | elif type_ is Channel: 205 | return ApplicationCommandOptionType.CHANNEL, None 206 | elif type_ is Role: 207 | return ApplicationCommandOptionType.ROLE, None 208 | elif type_ is Mentionable: 209 | return ApplicationCommandOptionType.MENTIONABLE, None 210 | elif isinstance(type_, TypeWithChoices): 211 | inner, choices = _get_option_type_and_choices(type_.inner_type) 212 | assert choices is None, "cannot have nested TypeWithOptions" 213 | return inner, [{"name": k, "value": v} for k, v in type_.choices.items()] 214 | else: 215 | raise Exception( 216 | f"Could not determine application command option type for {type_}" 217 | ) 218 | 219 | 220 | def _get_command_args(command: DashedCommand) -> Dict[str, ApplicationCommandOption]: 221 | signature = inspect.signature(command.fn) 222 | type_hint = typing.get_type_hints(command.fn) 223 | 224 | assert list(type_hint.keys())[0] == "ctx" 225 | args = {k for k in list(type_hint.keys())[1:]} 226 | arg_types = {k: type_hint[k] for k in args} 227 | 228 | opts = OrderedDict() 229 | for arg in args: 230 | required = signature.parameters[arg].default is inspect.Parameter.empty 231 | 232 | option_type, choices = _get_option_type_and_choices(arg_types[arg]) 233 | opts[arg] = ApplicationCommandOption( 234 | type=option_type, 235 | name=arg, 236 | description=arg, 237 | required=required, 238 | choices=choices, 239 | ) 240 | return opts 241 | -------------------------------------------------------------------------------- /dashed/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from typing import Any, Dict, List 4 | 5 | from aiohttp import web 6 | from nacl.exceptions import BadSignatureError 7 | 8 | from dashed.discord import ( 9 | ApplicationCommandOptionType, 10 | Channel, 11 | InteractionRequestType, 12 | InteractionResponseType, 13 | User, 14 | ) 15 | from dashed.interaction import DeferredInteractionContext, InteractionContext 16 | from dashed.loader import DashedContext, _get_command_args 17 | 18 | 19 | def respond_json(data) -> web.Response: 20 | return web.Response( 21 | body=json.dumps(data).encode("utf-8"), content_type="application/json" 22 | ) 23 | 24 | 25 | def _get_options_data( 26 | command_data: Dict[str, Any], options: List[Any] 27 | ) -> Dict[str, Any]: 28 | result = {} 29 | 30 | for option in options: 31 | if option["type"] == ApplicationCommandOptionType.CHANNEL: 32 | result[option["name"]] = Channel( 33 | **command_data["resolved"]["channels"][option["value"]] 34 | ) 35 | elif option["type"] == ApplicationCommandOptionType.USER: 36 | result[option["name"]] = User( 37 | **command_data["resolved"]["users"][option["value"]] 38 | ) 39 | else: 40 | result[option["name"]] = option["value"] 41 | return result 42 | 43 | 44 | async def handle_interactions_request(request): 45 | signature = request.headers.get("X-Signature-Ed25519") 46 | timestamp = request.headers.get("X-Signature-Timestamp") 47 | 48 | body = await request.read() 49 | 50 | try: 51 | request.app["ctx"].application_key.verify( 52 | timestamp.encode("utf-8") + body, bytes.fromhex(signature) 53 | ) 54 | except BadSignatureError: 55 | return web.Response(text="bad signature", status=400) 56 | 57 | data = json.loads(body) 58 | 59 | if data["type"] == InteractionRequestType.PING: 60 | return respond_json({"type": InteractionResponseType.PONG}) 61 | elif data["type"] == InteractionRequestType.APPLICATION_COMMAND: 62 | command_data = data["data"] 63 | 64 | target_command = None 65 | options_data = None 66 | if command_data["name"] in request.app["ctx"].commands: 67 | target_command = request.app["ctx"].commands[command_data["name"]] 68 | options_data = command_data.get("options", []) 69 | elif command_data["name"] in request.app["ctx"].groups: 70 | target_command, options_data = ( 71 | request.app["ctx"] 72 | .groups[command_data["name"]] 73 | .lookup(command_data["options"]) 74 | ) 75 | else: 76 | print("No target command", data) 77 | return web.Response(text="unknown command", status=400) 78 | 79 | options = _get_options_data(command_data, options_data) 80 | args = {k: options[k] for k in _get_command_args(target_command).keys()} 81 | 82 | interaction_context = InteractionContext(request.app["ctx"], data) 83 | 84 | if target_command.deferred: 85 | asyncio.ensure_future( 86 | target_command.fn( 87 | DeferredInteractionContext(interaction_context), **args 88 | ) 89 | ) 90 | return respond_json( 91 | {"type": InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE} 92 | ) 93 | 94 | result = await target_command.fn(interaction_context, **args) 95 | return respond_json(result) 96 | 97 | return respond_json({}) 98 | 99 | 100 | async def run(host: str, port: int, ctx: DashedContext): 101 | app = web.Application() 102 | app["ctx"] = ctx 103 | app.add_routes([web.post("/interactions", handle_interactions_request)]) 104 | await web._run_app(app, host=host, port=port) 105 | -------------------------------------------------------------------------------- /example/echo.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import dashed 3 | 4 | 5 | @dashed.command(description="It's like an echo chamber in here!") 6 | async def echo(ctx: dashed.InteractionContext, message: str): 7 | return ctx.reply(content=message) 8 | 9 | 10 | @dashed.command(description="Deferred by default") 11 | async def deferred(ctx: dashed.DeferredInteractionContext, message: str): 12 | await asyncio.sleep(5) 13 | await ctx.update(content="It works") 14 | 15 | 16 | @dashed.command(description="Suprise") 17 | async def boo(ctx: dashed.InteractionContext): 18 | async def later(ctx: dashed.DeferredInteractionContext): 19 | await asyncio.sleep(5) 20 | await ctx.update(content="Boo!") 21 | 22 | return ctx.defer(later) 23 | 24 | 25 | @dashed.command(name="channel-info", description="Get information about a channel") 26 | async def channel_info(ctx: dashed.InteractionContext, channel: dashed.Channel): 27 | print(channel, ctx.data) 28 | return ctx.reply(content="Cool") 29 | -------------------------------------------------------------------------------- /example/math.py: -------------------------------------------------------------------------------- 1 | import dashed 2 | 3 | math = dashed.Group("math", description="do some math") 4 | 5 | 6 | async def initialize(): 7 | print("Initialize") 8 | 9 | 10 | @math.command(description="add some numbers") 11 | async def add(ctx: dashed.InteractionContext, x: int, y: int): 12 | return ctx.reply(content=f"{x+y}") 13 | 14 | 15 | config = math.subgroup("config", description="math config") 16 | 17 | 18 | @config.command(description="set value") 19 | async def set(ctx: dashed.InteractionContext, key: str, value: str): 20 | return ctx.reply(content="Set!") 21 | 22 | 23 | @config.command(description="get value") 24 | async def get(ctx: dashed.InteractionContext, key: str): 25 | return ctx.reply(content="Get!") 26 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aiohttp" 3 | version = "3.7.4.post0" 4 | description = "Async http client/server framework (asyncio)" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | async-timeout = ">=3.0,<4.0" 11 | attrs = ">=17.3.0" 12 | chardet = ">=2.0,<5.0" 13 | multidict = ">=4.5,<7.0" 14 | typing-extensions = ">=3.6.5" 15 | yarl = ">=1.0,<2.0" 16 | 17 | [package.extras] 18 | speedups = ["aiodns", "brotlipy", "cchardet"] 19 | 20 | [[package]] 21 | name = "anyio" 22 | version = "3.2.1" 23 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 24 | category = "main" 25 | optional = false 26 | python-versions = ">=3.6.2" 27 | 28 | [package.dependencies] 29 | idna = ">=2.8" 30 | sniffio = ">=1.1" 31 | 32 | [package.extras] 33 | doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] 34 | test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] 35 | trio = ["trio (>=0.16)"] 36 | 37 | [[package]] 38 | name = "async-timeout" 39 | version = "3.0.1" 40 | description = "Timeout context manager for asyncio programs" 41 | category = "main" 42 | optional = false 43 | python-versions = ">=3.5.3" 44 | 45 | [[package]] 46 | name = "attrs" 47 | version = "21.2.0" 48 | description = "Classes Without Boilerplate" 49 | category = "main" 50 | optional = false 51 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 52 | 53 | [package.extras] 54 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 55 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 56 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 57 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 58 | 59 | [[package]] 60 | name = "certifi" 61 | version = "2021.5.30" 62 | description = "Python package for providing Mozilla's CA Bundle." 63 | category = "main" 64 | optional = false 65 | python-versions = "*" 66 | 67 | [[package]] 68 | name = "cffi" 69 | version = "1.14.5" 70 | description = "Foreign Function Interface for Python calling C code." 71 | category = "main" 72 | optional = false 73 | python-versions = "*" 74 | 75 | [package.dependencies] 76 | pycparser = "*" 77 | 78 | [[package]] 79 | name = "chardet" 80 | version = "4.0.0" 81 | description = "Universal encoding detector for Python 2 and 3" 82 | category = "main" 83 | optional = false 84 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 85 | 86 | [[package]] 87 | name = "h11" 88 | version = "0.12.0" 89 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 90 | category = "main" 91 | optional = false 92 | python-versions = ">=3.6" 93 | 94 | [[package]] 95 | name = "httpcore" 96 | version = "0.13.6" 97 | description = "A minimal low-level HTTP client." 98 | category = "main" 99 | optional = false 100 | python-versions = ">=3.6" 101 | 102 | [package.dependencies] 103 | anyio = ">=3.0.0,<4.0.0" 104 | h11 = ">=0.11,<0.13" 105 | sniffio = ">=1.0.0,<2.0.0" 106 | 107 | [package.extras] 108 | http2 = ["h2 (>=3,<5)"] 109 | 110 | [[package]] 111 | name = "httpx" 112 | version = "0.18.2" 113 | description = "The next generation HTTP client." 114 | category = "main" 115 | optional = false 116 | python-versions = ">=3.6" 117 | 118 | [package.dependencies] 119 | certifi = "*" 120 | httpcore = ">=0.13.3,<0.14.0" 121 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} 122 | sniffio = "*" 123 | 124 | [package.extras] 125 | brotli = ["brotlicffi (>=1.0.0,<2.0.0)"] 126 | http2 = ["h2 (>=3.0.0,<4.0.0)"] 127 | 128 | [[package]] 129 | name = "idna" 130 | version = "3.2" 131 | description = "Internationalized Domain Names in Applications (IDNA)" 132 | category = "main" 133 | optional = false 134 | python-versions = ">=3.5" 135 | 136 | [[package]] 137 | name = "multidict" 138 | version = "5.1.0" 139 | description = "multidict implementation" 140 | category = "main" 141 | optional = false 142 | python-versions = ">=3.6" 143 | 144 | [[package]] 145 | name = "pycparser" 146 | version = "2.20" 147 | description = "C parser in Python" 148 | category = "main" 149 | optional = false 150 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 151 | 152 | [[package]] 153 | name = "pynacl" 154 | version = "1.4.0" 155 | description = "Python binding to the Networking and Cryptography (NaCl) library" 156 | category = "main" 157 | optional = false 158 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 159 | 160 | [package.dependencies] 161 | cffi = ">=1.4.1" 162 | six = "*" 163 | 164 | [package.extras] 165 | docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] 166 | tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] 167 | 168 | [[package]] 169 | name = "rfc3986" 170 | version = "1.5.0" 171 | description = "Validating URI References per RFC 3986" 172 | category = "main" 173 | optional = false 174 | python-versions = "*" 175 | 176 | [package.dependencies] 177 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} 178 | 179 | [package.extras] 180 | idna2008 = ["idna"] 181 | 182 | [[package]] 183 | name = "six" 184 | version = "1.16.0" 185 | description = "Python 2 and 3 compatibility utilities" 186 | category = "main" 187 | optional = false 188 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 189 | 190 | [[package]] 191 | name = "sniffio" 192 | version = "1.2.0" 193 | description = "Sniff out which async library your code is running under" 194 | category = "main" 195 | optional = false 196 | python-versions = ">=3.5" 197 | 198 | [[package]] 199 | name = "typing-extensions" 200 | version = "3.10.0.0" 201 | description = "Backported and Experimental Type Hints for Python 3.5+" 202 | category = "main" 203 | optional = false 204 | python-versions = "*" 205 | 206 | [[package]] 207 | name = "yarl" 208 | version = "1.6.3" 209 | description = "Yet another URL library" 210 | category = "main" 211 | optional = false 212 | python-versions = ">=3.6" 213 | 214 | [package.dependencies] 215 | idna = ">=2.0" 216 | multidict = ">=4.0" 217 | 218 | [metadata] 219 | lock-version = "1.1" 220 | python-versions = "^3.8" 221 | content-hash = "d920576c9926c647d5c5b5cb719a61f20011e6fbea3d3cb94b52319dc8d32189" 222 | 223 | [metadata.files] 224 | aiohttp = [ 225 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, 226 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, 227 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, 228 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, 229 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, 230 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, 231 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, 232 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, 233 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, 234 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, 235 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, 236 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, 237 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, 238 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, 239 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, 240 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, 241 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, 242 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, 243 | {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, 244 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, 245 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, 246 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, 247 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, 248 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, 249 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, 250 | {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, 251 | {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, 252 | {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, 253 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, 254 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, 255 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, 256 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, 257 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, 258 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, 259 | {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, 260 | {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, 261 | {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, 262 | ] 263 | anyio = [ 264 | {file = "anyio-3.2.1-py3-none-any.whl", hash = "sha256:442678a3c7e1cdcdbc37dcfe4527aa851b1b0c9162653b516e9f509821691d50"}, 265 | {file = "anyio-3.2.1.tar.gz", hash = "sha256:07968db9fa7c1ca5435a133dc62f988d84ef78e1d9b22814a59d1c62618afbc5"}, 266 | ] 267 | async-timeout = [ 268 | {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, 269 | {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, 270 | ] 271 | attrs = [ 272 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 273 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 274 | ] 275 | certifi = [ 276 | {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, 277 | {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, 278 | ] 279 | cffi = [ 280 | {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, 281 | {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, 282 | {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, 283 | {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, 284 | {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, 285 | {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, 286 | {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, 287 | {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, 288 | {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, 289 | {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, 290 | {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, 291 | {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, 292 | {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, 293 | {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, 294 | {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, 295 | {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, 296 | {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, 297 | {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, 298 | {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, 299 | {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, 300 | {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, 301 | {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, 302 | {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, 303 | {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, 304 | {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, 305 | {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, 306 | {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, 307 | {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, 308 | {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, 309 | {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, 310 | {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, 311 | {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, 312 | {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, 313 | {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, 314 | {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, 315 | {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, 316 | {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, 317 | ] 318 | chardet = [ 319 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 320 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 321 | ] 322 | h11 = [ 323 | {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, 324 | {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, 325 | ] 326 | httpcore = [ 327 | {file = "httpcore-0.13.6-py3-none-any.whl", hash = "sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"}, 328 | {file = "httpcore-0.13.6.tar.gz", hash = "sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e"}, 329 | ] 330 | httpx = [ 331 | {file = "httpx-0.18.2-py3-none-any.whl", hash = "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c"}, 332 | {file = "httpx-0.18.2.tar.gz", hash = "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"}, 333 | ] 334 | idna = [ 335 | {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, 336 | {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, 337 | ] 338 | multidict = [ 339 | {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, 340 | {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, 341 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"}, 342 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"}, 343 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"}, 344 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"}, 345 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"}, 346 | {file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"}, 347 | {file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"}, 348 | {file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"}, 349 | {file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"}, 350 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"}, 351 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"}, 352 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"}, 353 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"}, 354 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"}, 355 | {file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"}, 356 | {file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"}, 357 | {file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"}, 358 | {file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"}, 359 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"}, 360 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"}, 361 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"}, 362 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"}, 363 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"}, 364 | {file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"}, 365 | {file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"}, 366 | {file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"}, 367 | {file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"}, 368 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"}, 369 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"}, 370 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"}, 371 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"}, 372 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"}, 373 | {file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"}, 374 | {file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"}, 375 | {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"}, 376 | ] 377 | pycparser = [ 378 | {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, 379 | {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, 380 | ] 381 | pynacl = [ 382 | {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, 383 | {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, 384 | {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, 385 | {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, 386 | {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, 387 | {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, 388 | {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, 389 | {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, 390 | {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, 391 | {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, 392 | {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, 393 | {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, 394 | {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, 395 | {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, 396 | {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, 397 | {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, 398 | {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, 399 | {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, 400 | ] 401 | rfc3986 = [ 402 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 403 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 404 | ] 405 | six = [ 406 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 407 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 408 | ] 409 | sniffio = [ 410 | {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, 411 | {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, 412 | ] 413 | typing-extensions = [ 414 | {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, 415 | {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, 416 | {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, 417 | ] 418 | yarl = [ 419 | {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, 420 | {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"}, 421 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"}, 422 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"}, 423 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"}, 424 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"}, 425 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"}, 426 | {file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"}, 427 | {file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"}, 428 | {file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"}, 429 | {file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"}, 430 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"}, 431 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"}, 432 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"}, 433 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"}, 434 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"}, 435 | {file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"}, 436 | {file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"}, 437 | {file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"}, 438 | {file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"}, 439 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"}, 440 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"}, 441 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"}, 442 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"}, 443 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"}, 444 | {file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"}, 445 | {file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"}, 446 | {file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"}, 447 | {file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"}, 448 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"}, 449 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"}, 450 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"}, 451 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"}, 452 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"}, 453 | {file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"}, 454 | {file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"}, 455 | {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, 456 | ] 457 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "dashed" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["andrei "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | aiohttp = "^3.7.4" 10 | PyNaCl = "^1.4.0" 11 | httpx = "^0.18.2" 12 | 13 | [tool.poetry.dev-dependencies] 14 | 15 | [build-system] 16 | requires = ["poetry-core>=1.0.0"] 17 | build-backend = "poetry.core.masonry.api" 18 | --------------------------------------------------------------------------------