objectspell 0.1.0__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,13 @@
1
+ """
2
+ ObjectSpell: a small asyncio framework for wiring components together with signals.
3
+
4
+ The public API lives in `objectspell.struct`.
5
+ """
6
+ from importlib.metadata import PackageNotFoundError, version
7
+
8
+ try:
9
+ __version__ = version("objectspell")
10
+ except PackageNotFoundError: # running from a source tree, not an install
11
+ __version__ = "0.0.0.dev0"
12
+
13
+ __all__ = ("__version__",)
@@ -0,0 +1,123 @@
1
+ """
2
+ Starts and stops a topology, after checking that its wiring makes sense.
3
+ """
4
+ import inspect
5
+ from contextlib import AsyncExitStack
6
+ from typing import cast
7
+
8
+ # Imported directly rather than via `objectspell.struct`: struct imports this module, so
9
+ # going back through it would make struct's own import order load-bearing.
10
+ from objectspell.emitter import Emitter as BaseEmitter
11
+ from objectspell.state import State
12
+
13
+
14
+ class Emitter(BaseEmitter):
15
+ """
16
+ The two signals the Connector sends: one once everything is wired, one to shut down.
17
+ """
18
+ async def connected(self) -> None:
19
+ pass
20
+
21
+ async def disconnected(self) -> None:
22
+ pass
23
+
24
+
25
+ class Connector(State, Emitter):
26
+ """
27
+ Starts and stops a topology.
28
+
29
+ `connect` opens every State's listener loop, wires emitters to receivers, then emits
30
+ `connected`. `disconnect` emits `disconnected`, which every State listens for, so that
31
+ single signal shuts the whole topology down.
32
+ """
33
+
34
+ async def connect(self, states: list[State]) -> None:
35
+ """
36
+ Check the wiring, start every State, link senders to listeners, then emit `connected`.
37
+
38
+ Does not return until the topology has shut down, so it is normally the whole program.
39
+ """
40
+ states.append(self)
41
+
42
+ emitters: dict[str, BaseEmitter] = {}
43
+ routes: dict[str, set[str]] = {}
44
+
45
+ for state in states:
46
+ state_class = type(state)
47
+
48
+ for base_class in state_class.__bases__:
49
+ if BaseEmitter in base_class.__bases__:
50
+ if state_class.__name__ in emitters:
51
+ raise RuntimeError(
52
+ f"Two connected States are both named {state_class.__name__!r}. "
53
+ f"Routing is by class name, so State names must be unique."
54
+ )
55
+
56
+ # The base-class check above establishes that this State is an Emitter.
57
+ emitters[state_class.__name__] = cast(BaseEmitter, state)
58
+ routes[state_class.__name__] = self._o9l_signal_routes(state_class=state_class)
59
+
60
+ # Checked before any listener starts. A broken topology never delivers the signal that
61
+ # stops the listeners, so raising once they are running would hang in __aexit__.
62
+ self._o9l_check_wiring(states=states, routes=routes)
63
+
64
+ async with AsyncExitStack() as stack:
65
+ for state in states:
66
+ await stack.enter_async_context(state)
67
+
68
+ for state in states:
69
+ state.o9l_connecting(emitters)
70
+
71
+ await self.connected()
72
+
73
+ async def disconnect(self) -> None:
74
+ """
75
+ Emit `disconnected`. Every State listens for it, so this stops all of them.
76
+ """
77
+ await self.disconnected()
78
+
79
+ def _o9l_signal_routes(self, state_class: type[State]) -> set[str]:
80
+ """
81
+ The signals a State sends: the public async methods declared by its Emitter base.
82
+
83
+ Read from the base class, not from the State itself, because Emitter.__new__
84
+ overwrites those methods on the State with generated senders.
85
+ """
86
+ routes: set[str] = set()
87
+
88
+ for base_class in state_class.__bases__:
89
+ if BaseEmitter in base_class.__bases__:
90
+ routes.update(
91
+ attr_name
92
+ for (attr_name, attr_ref) in base_class.__dict__.items()
93
+ if not attr_name.startswith("_") and inspect.iscoroutinefunction(attr_ref)
94
+ )
95
+
96
+ return routes
97
+
98
+ def _o9l_check_wiring(self, states: list[State], routes: dict[str, set[str]]) -> None:
99
+ """
100
+ Verify the topology before any signal moves.
101
+
102
+ Both problems caught here would otherwise surface far from their cause: a receiver
103
+ named after nothing leaves the system waiting for a signal that never arrives, and a
104
+ receiver missing a route fails only once that route is used, often during shutdown.
105
+ """
106
+ for state in states:
107
+ state_name = type(state).__name__
108
+
109
+ for (channel, receiver_class) in state.o9l_receiver_classes().items():
110
+ if channel not in routes:
111
+ channels = ", ".join(sorted(routes)) or "none"
112
+ raise RuntimeError(
113
+ f"{state_name} declares a receiver named {channel!r}, but no connected State "
114
+ f"emits on that channel. A receiver class must be named exactly after the "
115
+ f"State whose signals it handles. Connected channels: {channels}."
116
+ )
117
+
118
+ if missing := sorted(routes[channel] - receiver_class.__dict__.keys()):
119
+ raise RuntimeError(
120
+ f"{state_name}.{channel} does not handle every signal on channel "
121
+ f"{channel!r}: missing {', '.join(missing)}. A receiver must define one "
122
+ f"method per signal its channel declares."
123
+ )
objectspell/emitter.py ADDED
@@ -0,0 +1,69 @@
1
+ """
2
+ The sending half of the wiring: turns declared async methods into signal senders.
3
+ """
4
+ import inspect
5
+ from typing import Any
6
+
7
+ from objectspell.receiver import Receivable
8
+ from objectspell.types import Signal, SignalSender
9
+
10
+
11
+ class Emitter:
12
+ """
13
+ A base class for signal emitters. Subclasses can define async methods that act as signal triggers.
14
+ When triggered, the Emitter sends a Signal to all connected Receivables.
15
+ """
16
+ _o9l_receivables: list[Receivable]
17
+
18
+ def __new__(cls, *args: Any, **kwargs: Any) -> "Emitter":
19
+ """
20
+ Replace every public async method declared by the Emitter base with a sender.
21
+
22
+ The declared bodies are never run: they only name the signal and its arguments.
23
+ """
24
+ for base_class in cls.__bases__:
25
+ if Emitter in base_class.__bases__:
26
+ for (attr_name, attr_ref) in base_class.__dict__.items():
27
+ if not attr_name.startswith("_") and inspect.iscoroutinefunction(attr_ref):
28
+ setattr(cls, attr_name, cls._o9l_signal_sender(signal_route=attr_name))
29
+
30
+ return super().__new__(cls)
31
+
32
+ @classmethod
33
+ def _o9l_signal_sender(cls, signal_route: str) -> SignalSender:
34
+ """
35
+ Build the sender that replaces one declared method.
36
+
37
+ `cls` is the concrete State being created, so its name becomes the signal's channel.
38
+ """
39
+ async def _send(self: Any, **kwargs: Any) -> None:
40
+ for receivable in self._o9l_receivables:
41
+ await receivable.o9l_receive(
42
+ signal=Signal(
43
+ channel=cls.__name__,
44
+ route=signal_route,
45
+ message=kwargs
46
+ )
47
+ )
48
+
49
+ return _send
50
+
51
+ def __init__(self) -> None:
52
+ """
53
+ Start with nothing connected. The Connector fills this in during `connect`.
54
+ """
55
+ super().__init__()
56
+
57
+ self._o9l_receivables = []
58
+
59
+ def o9l_connect(self, receivable: Receivable) -> None:
60
+ """
61
+ Connect a Receivable to this Emitter. The Receivable will receive Signals when this Emitter triggers.
62
+ """
63
+ if receivable not in self._o9l_receivables:
64
+ self._o9l_receivables.append(receivable)
65
+ else:
66
+ raise RuntimeError(
67
+ f"Receivable {receivable.__class__.__name__} already connected "
68
+ f"to {self.__class__.__name__}"
69
+ )
objectspell/py.typed ADDED
File without changes
@@ -0,0 +1,21 @@
1
+ """
2
+ The receiving half of the wiring: where a signal is delivered, and how handlers are grouped.
3
+ """
4
+ from objectspell.types import Signal
5
+
6
+
7
+ class Receivable:
8
+ """
9
+ Anything a Signal can be delivered to. State is the only implementation.
10
+ """
11
+ async def o9l_receive(self, signal: Signal) -> None:
12
+ raise NotImplementedError
13
+
14
+
15
+ class Receiver:
16
+ """
17
+ Marks a class as a group of signal handlers rather than a component.
18
+
19
+ Subclass it alongside the State the handlers belong to, and name the subclass after the
20
+ State whose signals it handles. That name is the wiring.
21
+ """
objectspell/state.py ADDED
@@ -0,0 +1,178 @@
1
+ """
2
+ A component: its signal queue, its listener loop, and the receivers routed to it.
3
+ """
4
+ import asyncio
5
+ import inspect
6
+ from types import TracebackType
7
+ from typing import Any, cast
8
+
9
+ from objectspell.emitter import Emitter
10
+ from objectspell.receiver import Receivable, Receiver
11
+ from objectspell.types import Signal, SignalHandler
12
+
13
+
14
+ class State(Receivable):
15
+ """
16
+ A base class for maintaining state and managing signal routing between Emitters and Receivers.
17
+ It runs an asynchronous listener loop to process incoming signals.
18
+ """
19
+ _o9l_receiver_classes: dict[str, type[Receiver]]
20
+
21
+ _o9l_queue: asyncio.Queue[Signal]
22
+ _o9l_is_stopped: bool
23
+ _o9l_listener: asyncio.Task[None]
24
+
25
+ def __new__(cls, *args: Any, **kwargs: Any) -> "State":
26
+ """
27
+ Collect this class's Receiver subclasses, keyed by class name -- the channel they listen to.
28
+
29
+ Any State that listens to something also listens to the Connector, so it can be told
30
+ to stop. If no Connector receiver was written, the default one is used.
31
+ """
32
+ cls._o9l_receiver_classes = {}
33
+
34
+ for sub_class in cls.__subclasses__():
35
+ if Receiver in sub_class.__bases__:
36
+ channel = sub_class.__name__
37
+ cls._o9l_receiver_classes[channel] = cast(type[Receiver], sub_class)
38
+
39
+ if cls._o9l_receiver_classes:
40
+ if "Connector" in cls._o9l_receiver_classes:
41
+ cls._o9l_enrich_connector(connector_cls=cls._o9l_receiver_classes["Connector"])
42
+ else:
43
+ cls._o9l_receiver_classes["Connector"] = Connector
44
+
45
+ return super().__new__(cls)
46
+
47
+ @classmethod
48
+ def _o9l_enrich_connector(cls, connector_cls: type[Receiver]) -> None:
49
+ """
50
+ Keep the built-in Connector behaviour when a custom Connector receiver overrides it.
51
+
52
+ Without this, writing your own `disconnected` would replace the one that stops the
53
+ listener, and the State would never shut down.
54
+ """
55
+ for (attr_name, attr_ref) in Connector.__dict__.items():
56
+ if (
57
+ not attr_name.startswith("_")
58
+ and inspect.iscoroutinefunction(attr_ref)
59
+ and (signal_handler := connector_cls.__dict__.get(attr_name))
60
+ ):
61
+ setattr(
62
+ connector_cls,
63
+ attr_name,
64
+ cls._o9l_extended_signal_handler(signal_handler=signal_handler, extra_handler=attr_ref)
65
+ )
66
+
67
+ @classmethod
68
+ def _o9l_extended_signal_handler(cls, signal_handler: SignalHandler, extra_handler: SignalHandler) -> SignalHandler:
69
+ """
70
+ Chain two handlers into one, running the custom handler before the built-in one.
71
+ """
72
+ async def _handle(self: Any, **kwargs: Any) -> None:
73
+ await signal_handler(self, **kwargs)
74
+ await extra_handler(self, **kwargs)
75
+
76
+ return _handle
77
+
78
+ def __init__(self) -> None:
79
+ """
80
+ Create the signal queue. The listener that drains it starts on `__aenter__`.
81
+ """
82
+ super().__init__()
83
+
84
+ self._o9l_queue = asyncio.Queue()
85
+ self._o9l_is_stopped = False
86
+
87
+ async def __aenter__(self) -> "State":
88
+ """
89
+ Enter the asynchronous context, starting the signal listener loop if receivers are present.
90
+ """
91
+ if self._o9l_receiver_classes:
92
+ # Start listening for signals
93
+ self._o9l_listener = asyncio.create_task(self._o9l_listening())
94
+
95
+ return self
96
+
97
+ async def __aexit__(
98
+ self,
99
+ exc_type: type[BaseException] | None,
100
+ exc_value: BaseException | None,
101
+ traceback: TracebackType | None,
102
+ ) -> None:
103
+ """
104
+ Exit the asynchronous context, waiting for the listener to complete if it was started.
105
+ """
106
+ if self._o9l_receiver_classes:
107
+ # Wait for the listener to complete listening for signals
108
+ await self._o9l_listener
109
+
110
+ def o9l_receiver_classes(self) -> dict[str, type[Receiver]]:
111
+ """
112
+ The receiver classes of this State, keyed by the channel each one listens to.
113
+ """
114
+ return self._o9l_receiver_classes
115
+
116
+ def o9l_connecting(self, emitters: dict[str, Emitter]) -> None:
117
+ """
118
+ Connect the provided emitters to this State based on registered receiver classes.
119
+ """
120
+ for channel in self._o9l_receiver_classes.keys():
121
+ if emitter := emitters.get(channel):
122
+ emitter.o9l_connect(receivable=self)
123
+
124
+ async def o9l_receive(self, signal: Signal) -> None:
125
+ """
126
+ Receive a signal and enqueue it for processing by the listener loop.
127
+ """
128
+ await self._o9l_queue.put(signal)
129
+
130
+ async def _o9l_listening(self) -> None:
131
+ """
132
+ Start processing signals in the queue
133
+ """
134
+ while not self._o9l_is_stopped:
135
+ try:
136
+ signal = await self._o9l_queue.get()
137
+
138
+ if receiver_class := self._o9l_receiver_classes.get(signal.channel):
139
+ if signal_handler := receiver_class.__dict__.get(signal.route):
140
+ await signal_handler(self, **signal.message)
141
+ else:
142
+ raise RuntimeError(
143
+ f"No handler found for {signal.channel}.{signal.route} "
144
+ f"in {receiver_class.__name__}"
145
+ )
146
+ else:
147
+ raise RuntimeError(f"[{self.__class__.__name__}] No receiver found for {signal.channel}")
148
+
149
+ self._o9l_queue.task_done()
150
+ except asyncio.CancelledError:
151
+ self._o9l_is_stopped = True
152
+
153
+ def _o9l_stop_listening(self) -> None:
154
+ """
155
+ Stop processing signals
156
+ """
157
+ self._o9l_is_stopped = True
158
+
159
+
160
+ class Connector(Receiver, State):
161
+ """
162
+ The default handling of the Connector channel, given to every State that listens.
163
+
164
+ This is what makes one `disconnected` signal stop the whole topology. Not to be confused
165
+ with `objectspell.connector.Connector`, the component you instantiate.
166
+ """
167
+
168
+ async def connected(self) -> None:
169
+ """
170
+ Nothing to do by default. Override it in your own receiver named `Connector`.
171
+ """
172
+ pass
173
+
174
+ async def disconnected(self) -> None:
175
+ """
176
+ Stop this State's listener loop.
177
+ """
178
+ self._o9l_stop_listening()
objectspell/struct.py ADDED
@@ -0,0 +1,15 @@
1
+ """
2
+ Core components of the objectspell library.
3
+ Provides Emitter, Receiver, State, and Connector for building asynchronous messaging topologies.
4
+ """
5
+ from objectspell.connector import Connector
6
+ from objectspell.emitter import Emitter
7
+ from objectspell.receiver import Receiver
8
+ from objectspell.state import State
9
+
10
+ __all__ = (
11
+ "Emitter",
12
+ "Receiver",
13
+ "State",
14
+ "Connector",
15
+ )
objectspell/types.py ADDED
@@ -0,0 +1,22 @@
1
+ """
2
+ What a signal is, and the shape of the callables that send and handle one.
3
+ """
4
+ from collections.abc import Awaitable, Callable
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class Signal:
11
+ """
12
+ One message in flight: which State sent it, which method it maps to, and its payload.
13
+ """
14
+ channel: str
15
+ route: str
16
+ message: dict[str, Any]
17
+
18
+
19
+ # Both are unbound methods called as `fn(self, **message)`, so the parameters
20
+ # cannot be described more precisely than `...`.
21
+ type SignalSender = Callable[..., Awaitable[None]]
22
+ type SignalHandler = Callable[..., Awaitable[None]]
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: objectspell
3
+ Version: 0.1.0
4
+ Summary: A library for building asynchronous messaging topologies using declarative implicit wiring.
5
+ Keywords: asyncio,event-driven,messaging,signals,pubsub
6
+ Author: Max Sukhorukov
7
+ Author-email: Max Sukhorukov <signaldetect@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Project-URL: Homepage, https://github.com/invertedpoint/objectspell-py
20
+ Project-URL: Issues, https://github.com/invertedpoint/objectspell-py/issues
21
+ Description-Content-Type: text/markdown
22
+
23
+ # ObjectSpell
24
+
25
+ A small asyncio framework for wiring components together with signals, without registries,
26
+ decorators, or callback lists. You declare what a component sends and what it listens to;
27
+ ObjectSpell connects them by name.
28
+
29
+ Zero dependencies. Python 3.12+.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install objectspell # or: uv add objectspell
35
+ ```
36
+
37
+ ## Quick start
38
+
39
+ Each component lives in its own module. Here `X` sends a message, `Y` prints it, and the
40
+ program stops once `Y` is done.
41
+
42
+ **`x.py`** — sends `something_happened`
43
+
44
+ ```python
45
+ from objectspell import struct
46
+
47
+
48
+ class Emitter(struct.Emitter):
49
+ async def something_happened(self, message: str) -> None:
50
+ pass # a declaration, not an implementation
51
+
52
+
53
+ class X(struct.State, Emitter):
54
+ async def action(self) -> None:
55
+ await self.something_happened(message="OK Computer")
56
+
57
+
58
+ class Connector(struct.Receiver, X):
59
+ async def connected(self) -> None:
60
+ await self.action()
61
+
62
+ async def disconnected(self) -> None:
63
+ pass
64
+ ```
65
+
66
+ **`y.py`** — listens to `X`, then reports it is done
67
+
68
+ ```python
69
+ from objectspell import struct
70
+
71
+
72
+ class Emitter(struct.Emitter):
73
+ async def completed(self) -> None:
74
+ pass
75
+
76
+
77
+ class Y(struct.State, Emitter):
78
+ async def show(self, message: str) -> None:
79
+ print("Y received:", message)
80
+ await self.completed()
81
+
82
+
83
+ class X(struct.Receiver, Y):
84
+ async def something_happened(self, message: str) -> None:
85
+ await self.show(message)
86
+ ```
87
+
88
+ **`connector.py`** — stops everything when `Y` is done
89
+
90
+ ```python
91
+ from objectspell import struct
92
+ from objectspell.struct import Connector
93
+
94
+
95
+ class Y(struct.Receiver, Connector):
96
+ async def completed(self) -> None:
97
+ await self.disconnect()
98
+ ```
99
+
100
+ **`main.py`**
101
+
102
+ ```python
103
+ import asyncio
104
+
105
+ from connector import Connector
106
+ from x import X
107
+ from y import Y
108
+
109
+
110
+ async def main() -> None:
111
+ await Connector().connect([X(), Y()])
112
+
113
+
114
+ if __name__ == "__main__":
115
+ asyncio.run(main())
116
+ ```
117
+
118
+ ```console
119
+ $ python main.py
120
+ Y received: OK Computer
121
+ ```
122
+
123
+ Notice that `X` and `Y` never import each other. The only thing linking them is the class
124
+ name: `y.py` declares `class X(struct.Receiver, Y)`, which means "on `Y`, handle signals
125
+ from `X`".
126
+
127
+ ## The four pieces
128
+
129
+ - **Emitter** — declares the signals a component can send. The method bodies are never run;
130
+ ObjectSpell replaces them with senders.
131
+ - **State** — a component. It owns a queue and handles one signal at a time.
132
+ - **Receiver** — a handler group attached to a State, **named after the State it listens to**.
133
+ - **Connector** — starts and stops the whole topology.
134
+
135
+ ## Documentation
136
+
137
+ - [Concepts](https://github.com/invertedpoint/objectspell-py/blob/main/docs/concepts.md) — the four pieces and how a signal travels
138
+ - [Wiring](https://github.com/invertedpoint/objectspell-py/blob/main/docs/wiring.md) — the naming rule, and why each component gets its own module
139
+ - [Lifecycle](https://github.com/invertedpoint/objectspell-py/blob/main/docs/lifecycle.md) — starting up, shutting down, and message order
140
+ - [Limitations](https://github.com/invertedpoint/objectspell-py/blob/main/docs/limitations.md) — what this library does not do
141
+
142
+ Runnable examples live in [`examples/`](https://github.com/invertedpoint/objectspell-py/tree/main/examples).
143
+
144
+ ## License
145
+
146
+ MIT — see [LICENSE](https://github.com/invertedpoint/objectspell-py/blob/main/LICENSE).
@@ -0,0 +1,12 @@
1
+ objectspell/__init__.py,sha256=3VBfY4ru8LFvuEy28OHarY3dEtaqkIX2UTkg0plp6hg,381
2
+ objectspell/connector.py,sha256=P4pUbtBL_6c7YRlUKZc_qAhKK2xf9RYG0Ppegs0xGh4,5029
3
+ objectspell/emitter.py,sha256=Jh6bw0NxzSVBu_cm8uJ7pMqUilg0s8OYj5y9_l6GA6s,2470
4
+ objectspell/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ objectspell/receiver.py,sha256=4-J2KuNJKczvZsImhhOTFmiGA8nFWcadr8YeDK6bNQk,603
6
+ objectspell/state.py,sha256=qdW2QGjB2th_XfAipkef375ny4Rmpi7Tcu9VqDVlyGg,6446
7
+ objectspell/struct.py,sha256=xkkAsDcsKEvYxBWab7y8h2FX2iUd4hHmCcrr2XT-IoM,387
8
+ objectspell/types.py,sha256=rzyZZ94lbPY57ZJFJ7HryaG78IxR4uT-5dG2dBW-XAA,613
9
+ objectspell-0.1.0.dist-info/licenses/LICENSE,sha256=i9ERMU0hxNnGgTKMHfRhN8nzZaY_z1_edb2R_6-2Fmg,1096
10
+ objectspell-0.1.0.dist-info/WHEEL,sha256=y6e-a5KI2W-qDAJKfh9Xr81bin8NgUdfWgz3VSXXpe4,80
11
+ objectspell-0.1.0.dist-info/METADATA,sha256=dt5xRtUNUQKXOf3eKsur21gWW7SO9KGv6c6KJPhbiMY,4281
12
+ objectspell-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.9
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max Sukhorukov <signaldetect@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.