disagreement 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ # disagreement/ext/commands/errors.py
2
+
3
+ """
4
+ Custom exceptions for the command extension.
5
+ """
6
+
7
+ from disagreement.errors import DisagreementException
8
+
9
+
10
+ class CommandError(DisagreementException):
11
+ """Base exception for errors raised by the commands extension."""
12
+
13
+ pass
14
+
15
+
16
+ class CommandNotFound(CommandError):
17
+ """Exception raised when a command is not found."""
18
+
19
+ def __init__(self, command_name: str):
20
+ self.command_name = command_name
21
+ super().__init__(f"Command '{command_name}' not found.")
22
+
23
+
24
+ class BadArgument(CommandError):
25
+ """Exception raised when a command argument fails to parse or validate."""
26
+
27
+ pass
28
+
29
+
30
+ class MissingRequiredArgument(BadArgument):
31
+ """Exception raised when a required command argument is missing."""
32
+
33
+ def __init__(self, param_name: str):
34
+ self.param_name = param_name
35
+ super().__init__(f"Missing required argument: {param_name}")
36
+
37
+
38
+ class ArgumentParsingError(BadArgument):
39
+ """Exception raised during the argument parsing process."""
40
+
41
+ pass
42
+
43
+
44
+ class CheckFailure(CommandError):
45
+ """Exception raised when a command check fails."""
46
+
47
+ pass
48
+
49
+
50
+ class CheckAnyFailure(CheckFailure):
51
+ """Raised when :func:`check_any` fails all checks."""
52
+
53
+ def __init__(self, errors: list[CheckFailure]):
54
+ self.errors = errors
55
+ msg = "; ".join(str(e) for e in errors)
56
+ super().__init__(f"All checks failed: {msg}")
57
+
58
+
59
+ class CommandOnCooldown(CheckFailure):
60
+ """Raised when a command is invoked while on cooldown."""
61
+
62
+ def __init__(self, retry_after: float):
63
+ self.retry_after = retry_after
64
+ super().__init__(f"Command is on cooldown. Retry in {retry_after:.2f}s")
65
+
66
+
67
+ class CommandInvokeError(CommandError):
68
+ """Exception raised when an error occurs during command invocation."""
69
+
70
+ def __init__(self, original: Exception):
71
+ self.original = original
72
+ super().__init__(f"Error during command invocation: {original}")
73
+
74
+
75
+ # Add more specific errors as needed, e.g., UserNotFound, ChannelNotFound, etc.
76
+ # These might inherit from BadArgument.
@@ -0,0 +1,37 @@
1
+ # disagreement/ext/commands/help.py
2
+
3
+ from typing import List, Optional
4
+
5
+ from .core import Command, CommandContext, CommandHandler
6
+
7
+
8
+ class HelpCommand(Command):
9
+ """Built-in command that displays help information for other commands."""
10
+
11
+ def __init__(self, handler: CommandHandler) -> None:
12
+ self.handler = handler
13
+
14
+ async def callback(ctx: CommandContext, command: Optional[str] = None) -> None:
15
+ if command:
16
+ cmd = handler.get_command(command)
17
+ if not cmd or cmd.name.lower() != command.lower():
18
+ await ctx.send(f"Command '{command}' not found.")
19
+ return
20
+ description = cmd.description or cmd.brief or "No description provided."
21
+ await ctx.send(f"**{ctx.prefix}{cmd.name}**\n{description}")
22
+ else:
23
+ lines: List[str] = []
24
+ for registered in dict.fromkeys(handler.commands.values()):
25
+ brief = registered.brief or registered.description or ""
26
+ lines.append(f"{ctx.prefix}{registered.name} - {brief}".strip())
27
+ if lines:
28
+ await ctx.send("\n".join(lines))
29
+ else:
30
+ await ctx.send("No commands available.")
31
+
32
+ super().__init__(
33
+ callback,
34
+ name="help",
35
+ brief="Show command help.",
36
+ description="Displays help for commands.",
37
+ )
@@ -0,0 +1,103 @@
1
+ # disagreement/ext/commands/view.py
2
+
3
+ import re
4
+
5
+
6
+ class StringView:
7
+ """
8
+ A utility class to help with parsing strings, particularly for command arguments.
9
+ It keeps track of the current position in the string and provides methods
10
+ to read parts of it.
11
+ """
12
+
13
+ def __init__(self, buffer: str):
14
+ self.buffer: str = buffer
15
+ self.original: str = buffer # Keep original for error reporting if needed
16
+ self.index: int = 0
17
+ self.end: int = len(buffer)
18
+ self.previous: int = 0 # Index before the last successful read
19
+
20
+ @property
21
+ def remaining(self) -> str:
22
+ """Returns the rest of the string that hasn't been consumed."""
23
+ return self.buffer[self.index :]
24
+
25
+ @property
26
+ def eof(self) -> bool:
27
+ """Checks if the end of the string has been reached."""
28
+ return self.index >= self.end
29
+
30
+ def skip_whitespace(self) -> None:
31
+ """Skips any leading whitespace from the current position."""
32
+ while not self.eof and self.buffer[self.index].isspace():
33
+ self.index += 1
34
+
35
+ def get_word(self) -> str:
36
+ """
37
+ Reads a "word" from the current position.
38
+ A word is a sequence of non-whitespace characters.
39
+ """
40
+ self.skip_whitespace()
41
+ if self.eof:
42
+ return ""
43
+
44
+ self.previous = self.index
45
+ match = re.match(r"\S+", self.buffer[self.index :])
46
+ if match:
47
+ word = match.group(0)
48
+ self.index += len(word)
49
+ return word
50
+ return "" # Should not happen if not eof and skip_whitespace was called
51
+
52
+ def get_quoted_string(self) -> str:
53
+ """
54
+ Reads a string enclosed in double quotes.
55
+ Handles escaped quotes inside the string.
56
+ """
57
+ self.skip_whitespace()
58
+ if self.eof or self.buffer[self.index] != '"':
59
+ return "" # Or raise an error, or return None
60
+
61
+ self.previous = self.index
62
+ self.index += 1 # Skip the opening quote
63
+ result = []
64
+ escaped = False
65
+
66
+ while not self.eof:
67
+ char = self.buffer[self.index]
68
+ self.index += 1
69
+
70
+ if escaped:
71
+ result.append(char)
72
+ escaped = False
73
+ elif char == "\\":
74
+ escaped = True
75
+ elif char == '"':
76
+ return "".join(result) # Closing quote found
77
+ else:
78
+ result.append(char)
79
+
80
+ # If loop finishes, means EOF was reached before closing quote
81
+ # This is an error condition. Restore index and indicate failure.
82
+ self.index = self.previous
83
+ # Consider raising an error like UnterminatedQuotedStringError
84
+ return "" # Or raise
85
+
86
+ def read_rest(self) -> str:
87
+ """Reads all remaining characters from the current position."""
88
+ self.skip_whitespace()
89
+ if self.eof:
90
+ return ""
91
+
92
+ self.previous = self.index
93
+ result = self.buffer[self.index :]
94
+ self.index = self.end
95
+ return result
96
+
97
+ def undo(self) -> None:
98
+ """Resets the current position to before the last successful read."""
99
+ self.index = self.previous
100
+
101
+ # Could add more methods like:
102
+ # peek() - look at next char without consuming
103
+ # match_regex(pattern) - consume if regex matches
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import import_module
4
+ import sys
5
+ from types import ModuleType
6
+ from typing import Dict
7
+
8
+ __all__ = ["load_extension", "unload_extension"]
9
+
10
+ _loaded_extensions: Dict[str, ModuleType] = {}
11
+
12
+
13
+ def load_extension(name: str) -> ModuleType:
14
+ """Load an extension by name.
15
+
16
+ The extension module must define a ``setup`` coroutine or function that
17
+ will be called after loading. Any value returned by ``setup`` is ignored.
18
+ """
19
+
20
+ if name in _loaded_extensions:
21
+ raise ValueError(f"Extension '{name}' already loaded")
22
+
23
+ module = import_module(name)
24
+
25
+ if not hasattr(module, "setup"):
26
+ raise ImportError(f"Extension '{name}' does not define a setup function")
27
+
28
+ module.setup()
29
+ _loaded_extensions[name] = module
30
+ return module
31
+
32
+
33
+ def unload_extension(name: str) -> None:
34
+ """Unload a previously loaded extension."""
35
+
36
+ module = _loaded_extensions.pop(name, None)
37
+ if module is None:
38
+ raise ValueError(f"Extension '{name}' is not loaded")
39
+
40
+ if hasattr(module, "teardown"):
41
+ module.teardown()
42
+
43
+ sys.modules.pop(name, None)
@@ -0,0 +1,89 @@
1
+ import asyncio
2
+ from typing import Any, Awaitable, Callable, Optional
3
+
4
+ __all__ = ["loop", "Task"]
5
+
6
+
7
+ class Task:
8
+ """Simple repeating task."""
9
+
10
+ def __init__(self, coro: Callable[..., Awaitable[Any]], *, seconds: float) -> None:
11
+ self._coro = coro
12
+ self._seconds = float(seconds)
13
+ self._task: Optional[asyncio.Task[None]] = None
14
+
15
+ async def _run(self, *args: Any, **kwargs: Any) -> None:
16
+ try:
17
+ while True:
18
+ await self._coro(*args, **kwargs)
19
+ await asyncio.sleep(self._seconds)
20
+ except asyncio.CancelledError:
21
+ pass
22
+
23
+ def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]:
24
+ if self._task is None or self._task.done():
25
+ self._task = asyncio.create_task(self._run(*args, **kwargs))
26
+ return self._task
27
+
28
+ def stop(self) -> None:
29
+ if self._task is not None:
30
+ self._task.cancel()
31
+ self._task = None
32
+
33
+ @property
34
+ def running(self) -> bool:
35
+ return self._task is not None and not self._task.done()
36
+
37
+
38
+ class _Loop:
39
+ def __init__(self, func: Callable[..., Awaitable[Any]], seconds: float) -> None:
40
+ self.func = func
41
+ self.seconds = seconds
42
+ self._task: Optional[Task] = None
43
+ self._owner: Any = None
44
+
45
+ def __get__(self, obj: Any, objtype: Any) -> "_BoundLoop":
46
+ return _BoundLoop(self, obj)
47
+
48
+ def _coro(self, *args: Any, **kwargs: Any) -> Awaitable[Any]:
49
+ if self._owner is None:
50
+ return self.func(*args, **kwargs)
51
+ return self.func(self._owner, *args, **kwargs)
52
+
53
+ def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]:
54
+ self._task = Task(self._coro, seconds=self.seconds)
55
+ return self._task.start(*args, **kwargs)
56
+
57
+ def stop(self) -> None:
58
+ if self._task is not None:
59
+ self._task.stop()
60
+
61
+ @property
62
+ def running(self) -> bool:
63
+ return self._task.running if self._task else False
64
+
65
+
66
+ class _BoundLoop:
67
+ def __init__(self, parent: _Loop, owner: Any) -> None:
68
+ self._parent = parent
69
+ self._owner = owner
70
+
71
+ def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]:
72
+ self._parent._owner = self._owner
73
+ return self._parent.start(*args, **kwargs)
74
+
75
+ def stop(self) -> None:
76
+ self._parent.stop()
77
+
78
+ @property
79
+ def running(self) -> bool:
80
+ return self._parent.running
81
+
82
+
83
+ def loop(*, seconds: float) -> Callable[[Callable[..., Awaitable[Any]]], _Loop]:
84
+ """Decorator to create a looping task."""
85
+
86
+ def decorator(func: Callable[..., Awaitable[Any]]) -> _Loop:
87
+ return _Loop(func, seconds)
88
+
89
+ return decorator
disagreement/gateway.py CHANGED
@@ -166,6 +166,7 @@ class GatewayClient:
166
166
  print(
167
167
  f"Sent RESUME for session {self._session_id} at sequence {self._last_sequence}."
168
168
  )
169
+
169
170
  async def update_presence(
170
171
  self,
171
172
  status: str,
@@ -179,14 +180,16 @@ class GatewayClient:
179
180
  "op": GatewayOpcode.PRESENCE_UPDATE,
180
181
  "d": {
181
182
  "since": since,
182
- "activities": [
183
- {
184
- "name": activity_name,
185
- "type": activity_type,
186
- }
187
- ]
188
- if activity_name
189
- else [],
183
+ "activities": (
184
+ [
185
+ {
186
+ "name": activity_name,
187
+ "type": activity_type,
188
+ }
189
+ ]
190
+ if activity_name
191
+ else []
192
+ ),
190
193
  "status": status,
191
194
  "afk": afk,
192
195
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: A Python library for the Discord API.
5
5
  Author-email: Slipstream <me@slipstreamm.dev>
6
6
  License: BSD 3-Clause
@@ -60,29 +60,33 @@ Requires Python 3.11 or newer.
60
60
  ```python
61
61
  import asyncio
62
62
  import os
63
+
63
64
  import disagreement
65
+ from disagreement.ext import commands
66
+
67
+
68
+ class Basics(commands.Cog):
69
+ def __init__(self, client: disagreement.Client) -> None:
70
+ super().__init__(client)
71
+
72
+ @commands.command()
73
+ async def ping(self, ctx: commands.CommandContext) -> None:
74
+ await ctx.reply("Pong!")
75
+
76
+
77
+ token = os.getenv("DISCORD_BOT_TOKEN")
78
+ if not token:
79
+ raise RuntimeError("DISCORD_BOT_TOKEN environment variable not set")
80
+
81
+ client = disagreement.Client(token=token, command_prefix="!")
82
+ client.add_cog(Basics(client))
64
83
 
65
- # Ensure DISCORD_BOT_TOKEN is set in your environment
66
- client = disagreement.Client(token=os.environ.get("DISCORD_BOT_TOKEN"))
67
-
68
- @client.on_event('MESSAGE_CREATE')
69
- async def on_message(message: disagreement.Message):
70
- print(f"Received: {message.content} from {message.author.username}")
71
- if message.content.lower() == '!ping':
72
- await message.reply('Pong!')
73
-
74
- async def main():
75
- if not client.token:
76
- print("Error: DISCORD_BOT_TOKEN environment variable not set.")
77
- return
78
- try:
79
- async with client:
80
- await asyncio.Future() # run until cancelled
81
- except KeyboardInterrupt:
82
- print("Bot shutting down...")
83
- # Add any other specific exception handling from your library, e.g., disagreement.AuthenticationError
84
-
85
- if __name__ == '__main__':
84
+
85
+ async def main() -> None:
86
+ await client.run()
87
+
88
+
89
+ if __name__ == "__main__":
86
90
  asyncio.run(main())
87
91
  ```
88
92
 
@@ -117,21 +121,20 @@ setup_logging(logging.DEBUG, file="bot.log")
117
121
  ### Defining Subcommands with `AppCommandGroup`
118
122
 
119
123
  ```python
120
- from disagreement.ext.app_commands import AppCommandGroup
124
+ from disagreement.ext.app_commands import AppCommandGroup, slash_command
125
+ from disagreement.ext.app_commands.context import AppCommandContext
121
126
 
122
- settings = AppCommandGroup("settings", "Manage settings")
127
+ settings_group = AppCommandGroup("settings", "Manage settings")
128
+ admin_group = AppCommandGroup("admin", "Admin settings", parent=settings_group)
123
129
 
124
- @settings.command(name="show")
125
- async def show(ctx):
126
- """Displays a setting."""
130
+
131
+ @slash_command(name="show", description="Display a setting.", parent=settings_group)
132
+ async def show(ctx: AppCommandContext, key: str):
127
133
  ...
128
134
 
129
- @settings.group("admin", description="Admin settings")
130
- def admin_group():
131
- pass
132
135
 
133
- @admin_group.command(name="set")
134
- async def set_setting(ctx, key: str, value: str):
136
+ @slash_command(name="set", description="Update a setting.", parent=admin_group)
137
+ async def set_setting(ctx: AppCommandContext, key: str, value: str):
135
138
  ...
136
139
  ## Fetching Guilds
137
140
 
@@ -161,3 +164,7 @@ Contributions are welcome! Please open an issue or submit a pull request.
161
164
 
162
165
  See the [docs](docs/) directory for detailed guides on components, slash commands, caching, and voice features.
163
166
 
167
+ ## License
168
+
169
+ This project is licensed under the BSD 3-Clause license. See the [LICENSE](LICENSE) file for details.
170
+
@@ -1,4 +1,4 @@
1
- disagreement/__init__.py,sha256=ylGJLwgNmt7B71khVq2O0fG__4kjnkA35rZFX5tEDrk,907
1
+ disagreement/__init__.py,sha256=EHUd2pLLOoobJJ9HD_Vw0us5BbOshMKNvjl7toEVBp8,907
2
2
  disagreement/cache.py,sha256=juabGFl4naQih5OUIVN2aN-vAfw2ZC2cI38s4nGEn8U,1525
3
3
  disagreement/client.py,sha256=pFXj7z7R1kJWz3_GWOzDYrF1S-nQ1agjcXnRTSh-PWE,45820
4
4
  disagreement/components.py,sha256=W_R9iMETkQj6sr-Lzk2n7hLwLNaLWT4vBPArIPHQUNc,5232
@@ -6,7 +6,7 @@ disagreement/enums.py,sha256=LLeXdYKcx4TUhlojNV5X4NDuvscMbnteWRNW79d0C2c,9668
6
6
  disagreement/error_handler.py,sha256=c2lb6aTMnhTtITQuR6axZUtEaasYKUgmdSxAHEkeq50,1028
7
7
  disagreement/errors.py,sha256=rCr9jVAzK8wsS6mxieeWpffKhTDX--sHuOBz45kwsAA,3215
8
8
  disagreement/event_dispatcher.py,sha256=BevGAi72qXAHN_FqCOSdvVhOhINLeI2ojyVLmvrSKJ0,9851
9
- disagreement/gateway.py,sha256=V46WyZE1duVEvVcBYsaThWd60Xg9T2Qsqu9Y02rQwXo,21315
9
+ disagreement/gateway.py,sha256=85WSEZregWUvuc0M-DjErOCJM697mqHwdRw9Iz-w7aU,21384
10
10
  disagreement/http.py,sha256=1lHIEq2RRVOzzSpfj9TNGprJsMW_nhbj_8-fPr0IupI,23986
11
11
  disagreement/hybrid_context.py,sha256=VYCmcreTZdPBU9v-Cy48W38vgWO2t8nM2ulC6_z4HjU,1095
12
12
  disagreement/i18n.py,sha256=1L4rcFuKP0XjHk9dVwbNh4BkLk2ZlxxZ_-tecGWa9S0,718
@@ -19,14 +19,31 @@ disagreement/rate_limiter.py,sha256=ubwR_UTPs2MotipBdtqpgwQKx0IHt2I3cdfFcXTFv7g,
19
19
  disagreement/shard_manager.py,sha256=R0HXruJ0Wda_3ftTztQx7kpI182jyuAMsjU6fDtz8Us,2039
20
20
  disagreement/typing.py,sha256=_1oFWfZ4HyH5Q3bnF7CO24s79z-3_B5Qb69kWiwLhhU,1242
21
21
  disagreement/voice_client.py,sha256=KdFEH8PI6v45olTYoW_-DVOuTCk8SGg7BgSzxOQsILs,3869
22
+ disagreement/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ disagreement/ext/loader.py,sha256=Gl2Btaimw_Tm159zLhni9f_Q7pXhOAA4uEpolB4mGAI,1128
24
+ disagreement/ext/tasks.py,sha256=nlsmpHAhbITKe2Rp-jM8KWdruykNf1MTRrSrXdFMWUE,2671
25
+ disagreement/ext/app_commands/__init__.py,sha256=DQ3vHz2EaJ_hTlvFRpRSofPlCuFueRoLgLriECI5xFg,991
26
+ disagreement/ext/app_commands/commands.py,sha256=cY9gyXovnyBIEWmhUPe8YZfw_WXCA1qXWDWTMmX_Vm8,24095
27
+ disagreement/ext/app_commands/context.py,sha256=Xcm4Ka5K5uTQGviixF5LeCDdOdF9YQS5F7lZi2m--8s,20831
28
+ disagreement/ext/app_commands/converters.py,sha256=J1VEmo-7H9K7kGPJodu5FX4RmFFI1BuzhlQAEs2MsD4,21036
29
+ disagreement/ext/app_commands/decorators.py,sha256=smgx5RHedmgn7wxhBza78rQo5Guz8PEDDOvK5niJlMc,23236
30
+ disagreement/ext/app_commands/handler.py,sha256=XO9yLgcV7aIxzhTMgFcQ1Tbr4GRZRfDBzkIAkiu6mw8,26045
31
+ disagreement/ext/commands/__init__.py,sha256=Yw--e6mhE_qjgNg1qYM4yvRAOZfZD6avQ_IOGH0IQxA,1051
32
+ disagreement/ext/commands/cog.py,sha256=U57yMrUpqj3_-W1-koyfGgH43MZG_JzJOl46kTur7iA,6636
33
+ disagreement/ext/commands/converters.py,sha256=mh8xJr1FIiah6bdYy0KsdccfYcPii2Yc_IdhzCTw5uE,5864
34
+ disagreement/ext/commands/core.py,sha256=CoX38qL-5WIola_KnShb3Za-uZSLhpfBW1pPkHw2f84,18905
35
+ disagreement/ext/commands/decorators.py,sha256=ca06AuRznoe4ZTma9EmO-lw9kQRliOHWlQcpURb840o,5525
36
+ disagreement/ext/commands/errors.py,sha256=cG5sPA-osUq2gts5scrl5yT-BHEYVHLTb4TULjAmbaY,2065
37
+ disagreement/ext/commands/help.py,sha256=yw0ydupOsPwmnhsIIoxa93xjj9MAcBcGfD8BXa7V8G8,1456
38
+ disagreement/ext/commands/view.py,sha256=3Wo4gGJX9fb65qw8yHFwMjnAeJvMJAx19rZNHz-ZDUs,3315
22
39
  disagreement/ui/__init__.py,sha256=PLA6eHiq9cu7JDOKS-7MKtaFhlqswjbI4AEUlpnbgO0,307
23
40
  disagreement/ui/button.py,sha256=GHbY3-yMrvv6u674-qYONocuC1e2a0flEWjPKwJXKDo,3163
24
41
  disagreement/ui/item.py,sha256=bm-EmQEZpe8Kt8JrRw-o0uQdccgjErORcFsBqaXOcV8,1112
25
42
  disagreement/ui/modal.py,sha256=FLWFy_VkZ9UAPumX3Q_bT0q7M06O1Q7XzBLhCZyhYhI,4120
26
43
  disagreement/ui/select.py,sha256=XjWRlWkA09QZaDDLn-wDDOWIuj0Mb4VCWJEOAaExZXw,3018
27
44
  disagreement/ui/view.py,sha256=QhWoYt39QKXwl1X6Mkm5gNNEqd8bt7O505lSpiG0L04,5567
28
- disagreement-0.0.1.dist-info/licenses/LICENSE,sha256=zfmtgJhVFHnqT7a8LAQFthXu5bP7EEHmEL99trV66Ew,1474
29
- disagreement-0.0.1.dist-info/METADATA,sha256=r9liBFa4OLtdW91XF2A7yJ7VcaOiBkmRbKVG74NEsHc,4533
30
- disagreement-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- disagreement-0.0.1.dist-info/top_level.txt,sha256=t7FY_3iaYhdB6X6y9VybJ2j7UZbVeRUe9wRgH8d5Gtk,13
32
- disagreement-0.0.1.dist-info/RECORD,,
45
+ disagreement-0.0.2.dist-info/licenses/LICENSE,sha256=zfmtgJhVFHnqT7a8LAQFthXu5bP7EEHmEL99trV66Ew,1474
46
+ disagreement-0.0.2.dist-info/METADATA,sha256=8Sc8qa3lQKVkXWV-9TbXV-fDi0gbWjrOm0a0pPlFTTg,4645
47
+ disagreement-0.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
+ disagreement-0.0.2.dist-info/top_level.txt,sha256=t7FY_3iaYhdB6X6y9VybJ2j7UZbVeRUe9wRgH8d5Gtk,13
49
+ disagreement-0.0.2.dist-info/RECORD,,