disagreement 0.0.1__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,92 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, Callable, Coroutine, List, Optional, TYPE_CHECKING
5
+
6
+ from .item import Item
7
+ from ..enums import ComponentType
8
+ from ..models import SelectOption
9
+
10
+ if TYPE_CHECKING:
11
+ from ..interactions import Interaction
12
+
13
+
14
+ class Select(Item):
15
+ """Represents a select menu component in a View.
16
+
17
+ Args:
18
+ custom_id (str): The developer-defined identifier for the select menu.
19
+ options (List[SelectOption]): The choices in the select menu.
20
+ placeholder (Optional[str]): The placeholder text that is shown if nothing is selected.
21
+ min_values (int): The minimum number of items that must be chosen.
22
+ max_values (int): The maximum number of items that can be chosen.
23
+ disabled (bool): Whether the select menu is disabled.
24
+ row (Optional[int]): The row the select menu should be placed in, from 0 to 4.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ *,
30
+ custom_id: str,
31
+ options: List[SelectOption],
32
+ placeholder: Optional[str] = None,
33
+ min_values: int = 1,
34
+ max_values: int = 1,
35
+ disabled: bool = False,
36
+ row: Optional[int] = None,
37
+ ):
38
+ super().__init__(type=ComponentType.STRING_SELECT)
39
+ self.custom_id = custom_id
40
+ self.options = options
41
+ self.placeholder = placeholder
42
+ self.min_values = min_values
43
+ self.max_values = max_values
44
+ self.disabled = disabled
45
+ self._row = row
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ """Converts the select menu to a dictionary that can be sent to Discord."""
49
+ payload = {
50
+ "type": ComponentType.STRING_SELECT.value,
51
+ "custom_id": self.custom_id,
52
+ "options": [option.to_dict() for option in self.options],
53
+ "disabled": self.disabled,
54
+ }
55
+ if self.placeholder:
56
+ payload["placeholder"] = self.placeholder
57
+ if self.min_values is not None:
58
+ payload["min_values"] = self.min_values
59
+ if self.max_values is not None:
60
+ payload["max_values"] = self.max_values
61
+ return payload
62
+
63
+
64
+ def select(
65
+ *,
66
+ custom_id: str,
67
+ options: List[SelectOption],
68
+ placeholder: Optional[str] = None,
69
+ min_values: int = 1,
70
+ max_values: int = 1,
71
+ disabled: bool = False,
72
+ row: Optional[int] = None,
73
+ ) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Select]:
74
+ """A decorator to create a select menu in a View."""
75
+
76
+ def decorator(func: Callable[..., Coroutine[Any, Any, Any]]) -> Select:
77
+ if not asyncio.iscoroutinefunction(func):
78
+ raise TypeError("Select callback must be a coroutine function.")
79
+
80
+ item = Select(
81
+ custom_id=custom_id,
82
+ options=options,
83
+ placeholder=placeholder,
84
+ min_values=min_values,
85
+ max_values=max_values,
86
+ disabled=disabled,
87
+ row=row,
88
+ )
89
+ item.callback = func
90
+ return item
91
+
92
+ return decorator
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import uuid
5
+ from typing import Any, Callable, Coroutine, Dict, List, Optional, TYPE_CHECKING
6
+
7
+ from ..models import ActionRow
8
+ from .item import Item
9
+
10
+ if TYPE_CHECKING:
11
+ from ..client import Client
12
+ from ..interactions import Interaction
13
+
14
+
15
+ class View:
16
+ """Represents a container for UI components that can be sent with a message.
17
+
18
+ Args:
19
+ timeout (Optional[float]): The number of seconds to wait for an interaction before the view times out.
20
+ Defaults to 180.
21
+ """
22
+
23
+ def __init__(self, *, timeout: Optional[float] = 180.0):
24
+ self.timeout = timeout
25
+ self.id = str(uuid.uuid4())
26
+ self.__children: List[Item] = []
27
+ self.__stopped = asyncio.Event()
28
+ self._client: Optional[Client] = None
29
+ self._message_id: Optional[str] = None
30
+
31
+ for item in self.__class__.__dict__.values():
32
+ if isinstance(item, Item):
33
+ self.add_item(item)
34
+
35
+ @property
36
+ def children(self) -> List[Item]:
37
+ return self.__children
38
+
39
+ def add_item(self, item: Item):
40
+ """Adds an item to the view."""
41
+ if not isinstance(item, Item):
42
+ raise TypeError("Only instances of 'Item' can be added to a View.")
43
+
44
+ if len(self.__children) >= 25:
45
+ raise ValueError("A view can only have a maximum of 25 components.")
46
+
47
+ item._view = self
48
+ self.__children.append(item)
49
+
50
+ @property
51
+ def message_id(self) -> Optional[str]:
52
+ return self._message_id
53
+
54
+ @message_id.setter
55
+ def message_id(self, value: str):
56
+ self._message_id = value
57
+
58
+ def to_components(self) -> List[ActionRow]:
59
+ """Converts the view's children into a list of ActionRow components.
60
+
61
+ This retains the original, simple layout behaviour where each item is
62
+ placed in its own :class:`ActionRow` to ensure backward compatibility.
63
+ """
64
+
65
+ rows: List[ActionRow] = []
66
+
67
+ for item in self.children:
68
+ if item.custom_id is None:
69
+ item.custom_id = (
70
+ f"{self.id}:{item.__class__.__name__}:{len(self.__children)}"
71
+ )
72
+
73
+ rows.append(ActionRow(components=[item]))
74
+
75
+ return rows
76
+
77
+ def layout_components_advanced(self) -> List[ActionRow]:
78
+ """Group compatible components into rows following Discord rules."""
79
+
80
+ rows: List[ActionRow] = []
81
+
82
+ for item in self.children:
83
+ if item.custom_id is None:
84
+ item.custom_id = (
85
+ f"{self.id}:{item.__class__.__name__}:{len(self.__children)}"
86
+ )
87
+
88
+ target_row = item.row
89
+ if target_row is not None:
90
+ if not 0 <= target_row <= 4:
91
+ raise ValueError("Row index must be between 0 and 4.")
92
+
93
+ while len(rows) <= target_row:
94
+ if len(rows) >= 5:
95
+ raise ValueError("A view can have at most 5 action rows.")
96
+ rows.append(ActionRow())
97
+
98
+ rows[target_row].add_component(item)
99
+ continue
100
+
101
+ placed = False
102
+ for row in rows:
103
+ try:
104
+ row.add_component(item)
105
+ placed = True
106
+ break
107
+ except ValueError:
108
+ continue
109
+
110
+ if not placed:
111
+ if len(rows) >= 5:
112
+ raise ValueError("A view can have at most 5 action rows.")
113
+ new_row = ActionRow([item])
114
+ rows.append(new_row)
115
+
116
+ return rows
117
+
118
+ def to_components_payload(self) -> List[Dict[str, Any]]:
119
+ """Converts the view's children into a list of component dictionaries
120
+ that can be sent to the Discord API."""
121
+ return [row.to_dict() for row in self.to_components()]
122
+
123
+ async def _dispatch(self, interaction: Interaction):
124
+ """Called by the client to dispatch an interaction to the correct item."""
125
+ if self.timeout is not None:
126
+ self.__stopped.set() # Reset the timeout on each interaction
127
+ self.__stopped.clear()
128
+
129
+ if interaction.data:
130
+ custom_id = interaction.data.custom_id
131
+ for child in self.children:
132
+ if child.custom_id == custom_id:
133
+ if child.callback:
134
+ await child.callback(self, interaction)
135
+ break
136
+
137
+ async def wait(self) -> bool:
138
+ """Waits until the view has stopped interacting."""
139
+ return await self.__stopped.wait()
140
+
141
+ def stop(self):
142
+ """Stops the view from listening to interactions."""
143
+ if not self.__stopped.is_set():
144
+ self.__stopped.set()
145
+
146
+ async def on_timeout(self):
147
+ """Called when the view times out."""
148
+ pass # User can override this
149
+
150
+ async def _start(self, client: Client):
151
+ """Starts the view's internal listener."""
152
+ self._client = client
153
+ if self.timeout is not None:
154
+ asyncio.create_task(self._timeout_task())
155
+
156
+ async def _timeout_task(self):
157
+ """The task that waits for the timeout and then stops the view."""
158
+ try:
159
+ await asyncio.wait_for(self.wait(), timeout=self.timeout)
160
+ except asyncio.TimeoutError:
161
+ self.stop()
162
+ await self.on_timeout()
163
+ if self._client and self._message_id:
164
+ # Remove the view from the client's listeners
165
+ self._client._views.pop(self._message_id, None)
@@ -0,0 +1,120 @@
1
+ # disagreement/voice_client.py
2
+ """Voice gateway and UDP audio client."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import contextlib
8
+ import socket
9
+ from typing import Optional, Sequence
10
+
11
+ import aiohttp
12
+
13
+
14
+ class VoiceClient:
15
+ """Handles the Discord voice WebSocket connection and UDP streaming."""
16
+
17
+ def __init__(
18
+ self,
19
+ endpoint: str,
20
+ session_id: str,
21
+ token: str,
22
+ guild_id: int,
23
+ user_id: int,
24
+ *,
25
+ ws=None,
26
+ udp: Optional[socket.socket] = None,
27
+ loop: Optional[asyncio.AbstractEventLoop] = None,
28
+ verbose: bool = False,
29
+ ) -> None:
30
+ self.endpoint = endpoint
31
+ self.session_id = session_id
32
+ self.token = token
33
+ self.guild_id = str(guild_id)
34
+ self.user_id = str(user_id)
35
+ self._ws: Optional[aiohttp.ClientWebSocketResponse] = ws
36
+ self._udp = udp
37
+ self._session: Optional[aiohttp.ClientSession] = None
38
+ self._heartbeat_task: Optional[asyncio.Task] = None
39
+ self._heartbeat_interval: Optional[float] = None
40
+ self._loop = loop or asyncio.get_event_loop()
41
+ self.verbose = verbose
42
+ self.ssrc: Optional[int] = None
43
+ self.secret_key: Optional[Sequence[int]] = None
44
+ self._server_ip: Optional[str] = None
45
+ self._server_port: Optional[int] = None
46
+
47
+ async def connect(self) -> None:
48
+ if self._ws is None:
49
+ self._session = aiohttp.ClientSession()
50
+ self._ws = await self._session.ws_connect(self.endpoint)
51
+
52
+ hello = await self._ws.receive_json()
53
+ self._heartbeat_interval = hello["d"]["heartbeat_interval"] / 1000
54
+ self._heartbeat_task = self._loop.create_task(self._heartbeat())
55
+
56
+ await self._ws.send_json(
57
+ {
58
+ "op": 0,
59
+ "d": {
60
+ "server_id": self.guild_id,
61
+ "user_id": self.user_id,
62
+ "session_id": self.session_id,
63
+ "token": self.token,
64
+ },
65
+ }
66
+ )
67
+
68
+ ready = await self._ws.receive_json()
69
+ data = ready["d"]
70
+ self.ssrc = data["ssrc"]
71
+ self._server_ip = data["ip"]
72
+ self._server_port = data["port"]
73
+
74
+ if self._udp is None:
75
+ self._udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
76
+ self._udp.connect((self._server_ip, self._server_port))
77
+
78
+ await self._ws.send_json(
79
+ {
80
+ "op": 1,
81
+ "d": {
82
+ "protocol": "udp",
83
+ "data": {
84
+ "address": self._udp.getsockname()[0],
85
+ "port": self._udp.getsockname()[1],
86
+ "mode": "xsalsa20_poly1305",
87
+ },
88
+ },
89
+ }
90
+ )
91
+
92
+ session_desc = await self._ws.receive_json()
93
+ self.secret_key = session_desc["d"].get("secret_key")
94
+
95
+ async def _heartbeat(self) -> None:
96
+ assert self._ws is not None
97
+ assert self._heartbeat_interval is not None
98
+ try:
99
+ while True:
100
+ await self._ws.send_json({"op": 3, "d": int(self._loop.time() * 1000)})
101
+ await asyncio.sleep(self._heartbeat_interval)
102
+ except asyncio.CancelledError:
103
+ pass
104
+
105
+ async def send_audio_frame(self, frame: bytes) -> None:
106
+ if not self._udp:
107
+ raise RuntimeError("UDP socket not initialised")
108
+ self._udp.send(frame)
109
+
110
+ async def close(self) -> None:
111
+ if self._heartbeat_task:
112
+ self._heartbeat_task.cancel()
113
+ with contextlib.suppress(asyncio.CancelledError):
114
+ await self._heartbeat_task
115
+ if self._ws:
116
+ await self._ws.close()
117
+ if self._session:
118
+ await self._session.close()
119
+ if self._udp:
120
+ self._udp.close()
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: disagreement
3
+ Version: 0.0.1
4
+ Summary: A Python library for the Discord API.
5
+ Author-email: Slipstream <me@slipstreamm.dev>
6
+ License: BSD 3-Clause
7
+ Project-URL: Homepage, https://github.com/Slipstreamm/disagreement
8
+ Project-URL: Issues, https://github.com/Slipstreamm/disagreement/issues
9
+ Keywords: discord,api,bot,async,aiohttp
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Internet
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: aiohttp<4.0.0,>=3.9.0
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=8.0.0; extra == "test"
27
+ Requires-Dist: pytest-asyncio>=1.0.0; extra == "test"
28
+ Requires-Dist: hypothesis>=6.89.0; extra == "test"
29
+ Provides-Extra: dev
30
+ Requires-Dist: dotenv>=0.0.5; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # Disagreement
34
+
35
+ A Python library for interacting with the Discord API, with a focus on bot development.
36
+
37
+ ## Features
38
+
39
+ - Asynchronous design using `aiohttp`
40
+ - Gateway and HTTP API clients
41
+ - Slash command framework
42
+ - Message component helpers
43
+ - Built-in caching layer
44
+ - Experimental voice support
45
+ - Helpful error handling utilities
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ python -m pip install -U pip
51
+ pip install disagreement
52
+ # or install from source for development
53
+ pip install -e .
54
+ ```
55
+
56
+ Requires Python 3.11 or newer.
57
+
58
+ ## Basic Usage
59
+
60
+ ```python
61
+ import asyncio
62
+ import os
63
+ import disagreement
64
+
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__':
86
+ asyncio.run(main())
87
+ ```
88
+
89
+ ### Global Error Handling
90
+
91
+ To ensure unexpected errors don't crash your bot, you can enable the library's
92
+ global error handler:
93
+
94
+ ```python
95
+ import disagreement
96
+
97
+ disagreement.setup_global_error_handler()
98
+ ```
99
+
100
+ Call this early in your program to log unhandled exceptions instead of letting
101
+ them terminate the process.
102
+
103
+ ### Configuring Logging
104
+
105
+ Use :func:`disagreement.logging_config.setup_logging` to configure logging for
106
+ your bot. The helper accepts a logging level and an optional file path.
107
+
108
+ ```python
109
+ import logging
110
+ from disagreement.logging_config import setup_logging
111
+
112
+ setup_logging(logging.INFO)
113
+ # Or log to a file
114
+ setup_logging(logging.DEBUG, file="bot.log")
115
+ ```
116
+
117
+ ### Defining Subcommands with `AppCommandGroup`
118
+
119
+ ```python
120
+ from disagreement.ext.app_commands import AppCommandGroup
121
+
122
+ settings = AppCommandGroup("settings", "Manage settings")
123
+
124
+ @settings.command(name="show")
125
+ async def show(ctx):
126
+ """Displays a setting."""
127
+ ...
128
+
129
+ @settings.group("admin", description="Admin settings")
130
+ def admin_group():
131
+ pass
132
+
133
+ @admin_group.command(name="set")
134
+ async def set_setting(ctx, key: str, value: str):
135
+ ...
136
+ ## Fetching Guilds
137
+
138
+ Use `Client.fetch_guild` to retrieve a guild from the Discord API if it
139
+ isn't already cached. This is useful when working with guild IDs from
140
+ outside the gateway events.
141
+
142
+ ```python
143
+ guild = await client.fetch_guild("123456789012345678")
144
+ roles = await client.fetch_roles(guild.id)
145
+ ```
146
+
147
+ ## Sharding
148
+
149
+ To run your bot across multiple gateway shards, pass `shard_count` when creating
150
+ the client:
151
+
152
+ ```python
153
+ client = disagreement.Client(token=BOT_TOKEN, shard_count=2)
154
+ ```
155
+
156
+ See `examples/sharded_bot.py` for a full example.
157
+
158
+ ## Contributing
159
+
160
+ Contributions are welcome! Please open an issue or submit a pull request.
161
+
162
+ See the [docs](docs/) directory for detailed guides on components, slash commands, caching, and voice features.
163
+
@@ -0,0 +1,32 @@
1
+ disagreement/__init__.py,sha256=ylGJLwgNmt7B71khVq2O0fG__4kjnkA35rZFX5tEDrk,907
2
+ disagreement/cache.py,sha256=juabGFl4naQih5OUIVN2aN-vAfw2ZC2cI38s4nGEn8U,1525
3
+ disagreement/client.py,sha256=pFXj7z7R1kJWz3_GWOzDYrF1S-nQ1agjcXnRTSh-PWE,45820
4
+ disagreement/components.py,sha256=W_R9iMETkQj6sr-Lzk2n7hLwLNaLWT4vBPArIPHQUNc,5232
5
+ disagreement/enums.py,sha256=LLeXdYKcx4TUhlojNV5X4NDuvscMbnteWRNW79d0C2c,9668
6
+ disagreement/error_handler.py,sha256=c2lb6aTMnhTtITQuR6axZUtEaasYKUgmdSxAHEkeq50,1028
7
+ disagreement/errors.py,sha256=rCr9jVAzK8wsS6mxieeWpffKhTDX--sHuOBz45kwsAA,3215
8
+ disagreement/event_dispatcher.py,sha256=BevGAi72qXAHN_FqCOSdvVhOhINLeI2ojyVLmvrSKJ0,9851
9
+ disagreement/gateway.py,sha256=V46WyZE1duVEvVcBYsaThWd60Xg9T2Qsqu9Y02rQwXo,21315
10
+ disagreement/http.py,sha256=1lHIEq2RRVOzzSpfj9TNGprJsMW_nhbj_8-fPr0IupI,23986
11
+ disagreement/hybrid_context.py,sha256=VYCmcreTZdPBU9v-Cy48W38vgWO2t8nM2ulC6_z4HjU,1095
12
+ disagreement/i18n.py,sha256=1L4rcFuKP0XjHk9dVwbNh4BkLk2ZlxxZ_-tecGWa9S0,718
13
+ disagreement/interactions.py,sha256=corwLVsbWM2JXHk5u8VR_Qp3GINLcKFo2y7dyI53QFA,21645
14
+ disagreement/logging_config.py,sha256=4q6baQPE6X_0lfaBTFMU1uqc03x5SbJqo2hsApdDFac,686
15
+ disagreement/models.py,sha256=TMvGP17h1LK72SlA4vNUId76LdonKAUCTzqeNJRFzVQ,61475
16
+ disagreement/oauth.py,sha256=TfDdCwg1J7osM9wDi61dtNBA5BrQk5DeQrrHsYycH34,2810
17
+ disagreement/permissions.py,sha256=7g5cIlg-evHXOL0-pmtT5EwqcB-stXot1HZSLz724sE,3008
18
+ disagreement/rate_limiter.py,sha256=ubwR_UTPs2MotipBdtqpgwQKx0IHt2I3cdfFcXTFv7g,2521
19
+ disagreement/shard_manager.py,sha256=R0HXruJ0Wda_3ftTztQx7kpI182jyuAMsjU6fDtz8Us,2039
20
+ disagreement/typing.py,sha256=_1oFWfZ4HyH5Q3bnF7CO24s79z-3_B5Qb69kWiwLhhU,1242
21
+ disagreement/voice_client.py,sha256=KdFEH8PI6v45olTYoW_-DVOuTCk8SGg7BgSzxOQsILs,3869
22
+ disagreement/ui/__init__.py,sha256=PLA6eHiq9cu7JDOKS-7MKtaFhlqswjbI4AEUlpnbgO0,307
23
+ disagreement/ui/button.py,sha256=GHbY3-yMrvv6u674-qYONocuC1e2a0flEWjPKwJXKDo,3163
24
+ disagreement/ui/item.py,sha256=bm-EmQEZpe8Kt8JrRw-o0uQdccgjErORcFsBqaXOcV8,1112
25
+ disagreement/ui/modal.py,sha256=FLWFy_VkZ9UAPumX3Q_bT0q7M06O1Q7XzBLhCZyhYhI,4120
26
+ disagreement/ui/select.py,sha256=XjWRlWkA09QZaDDLn-wDDOWIuj0Mb4VCWJEOAaExZXw,3018
27
+ 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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2025, Slipstream
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this
7
+ list of conditions and the following disclaimer.
8
+
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ 3. Neither the name of the copyright holder nor the names of its
14
+ contributors may be used to endorse or promote products derived from
15
+ this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ disagreement