outwrite 0.1.0a1__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.
- outwrite/__init__.py +5 -0
- outwrite/_output.py +221 -0
- outwrite/py.typed +0 -0
- outwrite-0.1.0a1.dist-info/METADATA +196 -0
- outwrite-0.1.0a1.dist-info/RECORD +8 -0
- outwrite-0.1.0a1.dist-info/WHEEL +5 -0
- outwrite-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- outwrite-0.1.0a1.dist-info/top_level.txt +1 -0
outwrite/__init__.py
ADDED
outwrite/_output.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Parent rendering and worker connections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import logging
|
|
7
|
+
import multiprocessing as mp
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import traceback
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from logging.handlers import QueueHandler
|
|
14
|
+
from multiprocessing.context import BaseContext
|
|
15
|
+
from multiprocessing.queues import Queue
|
|
16
|
+
from types import TracebackType
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.live import Live
|
|
20
|
+
from rich.logging import RichHandler
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
|
|
23
|
+
_connection: OutputConnection | None = None
|
|
24
|
+
_session: _OutputSession | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _Status:
|
|
28
|
+
"""Send text to the active session's status line."""
|
|
29
|
+
|
|
30
|
+
def update(self, text: str | None) -> None:
|
|
31
|
+
"""Replace the status text. None or an empty string clears it."""
|
|
32
|
+
if text is not None and not isinstance(text, str):
|
|
33
|
+
raise TypeError("Status text must be a string or None.")
|
|
34
|
+
if _connection is None:
|
|
35
|
+
raise RuntimeError("Call setup_output() before updating status.")
|
|
36
|
+
if _connection._show_status:
|
|
37
|
+
_connection._queue.put("" if text is None else text)
|
|
38
|
+
|
|
39
|
+
def clear(self) -> None:
|
|
40
|
+
"""Clear the status line without ending the output session."""
|
|
41
|
+
self.update(None)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
status = _Status()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class OutputConnection:
|
|
49
|
+
"""Pass this handle to workers; use it as a context manager in the parent."""
|
|
50
|
+
|
|
51
|
+
_queue: Queue
|
|
52
|
+
_logger_name: str
|
|
53
|
+
_show_status: bool
|
|
54
|
+
_parent_pid: int
|
|
55
|
+
|
|
56
|
+
def __enter__(self) -> OutputConnection:
|
|
57
|
+
if os.getpid() != self._parent_pid:
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
"Only the parent owns the output context; use setup_output(output) in workers."
|
|
60
|
+
)
|
|
61
|
+
if _session is None or _session.connection is not self:
|
|
62
|
+
raise RuntimeError("This output session is no longer active.")
|
|
63
|
+
if _session.entered:
|
|
64
|
+
raise RuntimeError("An output context cannot be nested.")
|
|
65
|
+
_session.entered = True
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(
|
|
69
|
+
self,
|
|
70
|
+
exc_type: type[BaseException] | None,
|
|
71
|
+
exc_value: BaseException | None,
|
|
72
|
+
exc_traceback: TracebackType | None,
|
|
73
|
+
) -> None:
|
|
74
|
+
if _session is None or _session.connection is not self:
|
|
75
|
+
raise RuntimeError("This output session is no longer active.")
|
|
76
|
+
_session.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _OutputSession:
|
|
80
|
+
"""Own everything that must remain in the rendering process."""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
connection: OutputConnection,
|
|
85
|
+
logger: logging.Logger,
|
|
86
|
+
console: Console,
|
|
87
|
+
level: int,
|
|
88
|
+
) -> None:
|
|
89
|
+
self.connection = connection
|
|
90
|
+
self.logger = logger
|
|
91
|
+
self.console = console
|
|
92
|
+
self.previous_handlers = logger.handlers[:]
|
|
93
|
+
self.previous_level = logger.level
|
|
94
|
+
self.previous_propagate = logger.propagate
|
|
95
|
+
self.console_handler = RichHandler(console=console, level=level)
|
|
96
|
+
self.handlers = [*self.previous_handlers, self.console_handler]
|
|
97
|
+
self.queue_handler = QueueHandler(connection._queue)
|
|
98
|
+
self.live = Live(
|
|
99
|
+
console=console,
|
|
100
|
+
transient=True,
|
|
101
|
+
auto_refresh=False,
|
|
102
|
+
redirect_stdout=False,
|
|
103
|
+
redirect_stderr=False,
|
|
104
|
+
)
|
|
105
|
+
self.error: Exception | None = None
|
|
106
|
+
self.entered = False
|
|
107
|
+
self.listener = threading.Thread(
|
|
108
|
+
target=self.listen, name="outwrite-renderer", daemon=True
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def listen(self) -> None:
|
|
112
|
+
while True:
|
|
113
|
+
try:
|
|
114
|
+
message = self.connection._queue.get()
|
|
115
|
+
except (EOFError, OSError) as error:
|
|
116
|
+
self.error = error
|
|
117
|
+
traceback.print_exception(error, file=sys.stderr)
|
|
118
|
+
return
|
|
119
|
+
if message is None:
|
|
120
|
+
return
|
|
121
|
+
try:
|
|
122
|
+
if isinstance(message, logging.LogRecord):
|
|
123
|
+
for handler in self.handlers:
|
|
124
|
+
if message.levelno >= handler.level:
|
|
125
|
+
handler.handle(message)
|
|
126
|
+
elif not message:
|
|
127
|
+
self.live.stop()
|
|
128
|
+
else:
|
|
129
|
+
text = Text(
|
|
130
|
+
message, style="cyan", no_wrap=True, overflow="ellipsis"
|
|
131
|
+
)
|
|
132
|
+
text.truncate(max(1, self.console.width - 2), overflow="ellipsis")
|
|
133
|
+
self.live.update(text, refresh=self.live.is_started)
|
|
134
|
+
if not self.live.is_started:
|
|
135
|
+
self.live.start(refresh=True)
|
|
136
|
+
except Exception as error:
|
|
137
|
+
# Keep draining so producers cannot deadlock behind a failed renderer.
|
|
138
|
+
self.error = error
|
|
139
|
+
traceback.print_exception(error, file=sys.stderr)
|
|
140
|
+
|
|
141
|
+
def close(self) -> None:
|
|
142
|
+
global _connection, _session
|
|
143
|
+
self.logger.handlers = self.previous_handlers
|
|
144
|
+
self.logger.setLevel(self.previous_level)
|
|
145
|
+
self.logger.propagate = self.previous_propagate
|
|
146
|
+
_connection = None
|
|
147
|
+
try:
|
|
148
|
+
self.connection._queue.put(None)
|
|
149
|
+
self.listener.join()
|
|
150
|
+
finally:
|
|
151
|
+
self.live.stop()
|
|
152
|
+
self.connection._queue.close()
|
|
153
|
+
self.connection._queue.join_thread()
|
|
154
|
+
self.queue_handler.close()
|
|
155
|
+
self.console_handler.close()
|
|
156
|
+
_session = None
|
|
157
|
+
atexit.unregister(_cleanup_at_exit)
|
|
158
|
+
if self.error is not None:
|
|
159
|
+
raise RuntimeError(
|
|
160
|
+
"Outwrite could not render one or more messages."
|
|
161
|
+
) from self.error
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def setup_output(
|
|
165
|
+
connection: OutputConnection | None = None,
|
|
166
|
+
*,
|
|
167
|
+
logger: logging.Logger | None = None,
|
|
168
|
+
level: int = logging.INFO,
|
|
169
|
+
show_status: bool = True,
|
|
170
|
+
mp_context: BaseContext | None = None,
|
|
171
|
+
) -> OutputConnection:
|
|
172
|
+
"""Start a parent session, or connect a worker to the parent's session.
|
|
173
|
+
|
|
174
|
+
Parent usage: ``with setup_output(logger=my_logger) as output: ...``.
|
|
175
|
+
Join workers and logging threads before leaving the block.
|
|
176
|
+
Worker usage: ``setup_output(output)`` before logging or updating status.
|
|
177
|
+
Worker configuration comes from the connection, not the keyword options.
|
|
178
|
+
"""
|
|
179
|
+
global _connection, _session
|
|
180
|
+
if _connection is not None:
|
|
181
|
+
raise RuntimeError("Output is already configured in this process.")
|
|
182
|
+
if connection is not None:
|
|
183
|
+
if connection._parent_pid == os.getpid():
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
"The parent is already responsible for rendering this connection."
|
|
186
|
+
)
|
|
187
|
+
worker_logger = logging.getLogger(connection._logger_name)
|
|
188
|
+
worker_logger.handlers = [QueueHandler(connection._queue)]
|
|
189
|
+
worker_logger.setLevel(logging.DEBUG)
|
|
190
|
+
worker_logger.propagate = False
|
|
191
|
+
_connection = connection
|
|
192
|
+
return connection
|
|
193
|
+
|
|
194
|
+
if logger is None:
|
|
195
|
+
logger = logging.getLogger("outwrite")
|
|
196
|
+
if logger is logging.getLogger():
|
|
197
|
+
raise ValueError("Choose a named logger, not the root logger.")
|
|
198
|
+
if not isinstance(level, int):
|
|
199
|
+
raise TypeError("level must be an integer logging level, such as logging.INFO.")
|
|
200
|
+
context = mp_context if mp_context is not None else mp.get_context("spawn")
|
|
201
|
+
if context.get_start_method() == "fork":
|
|
202
|
+
raise ValueError("Use a spawn or forkserver multiprocessing context, not fork.")
|
|
203
|
+
|
|
204
|
+
console = Console(stderr=True)
|
|
205
|
+
connection = OutputConnection(
|
|
206
|
+
context.Queue(), logger.name, show_status and console.is_terminal, os.getpid()
|
|
207
|
+
)
|
|
208
|
+
session = _OutputSession(connection, logger, console, level)
|
|
209
|
+
logger.handlers = [session.queue_handler]
|
|
210
|
+
logger.setLevel(logging.DEBUG)
|
|
211
|
+
logger.propagate = False
|
|
212
|
+
_connection = connection
|
|
213
|
+
_session = session
|
|
214
|
+
session.listener.start()
|
|
215
|
+
atexit.register(_cleanup_at_exit)
|
|
216
|
+
return connection
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _cleanup_at_exit() -> None:
|
|
220
|
+
if _session is not None:
|
|
221
|
+
_session.close()
|
outwrite/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: outwrite
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Python logging and a live status line, across threads and processes.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Source, https://github.com/ariel-m-s/outwrite
|
|
7
|
+
Project-URL: Issues, https://github.com/ariel-m-s/outwrite/issues
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Classifier: Topic :: System :: Logging
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: rich<15,>=14
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Outwrite
|
|
23
|
+
|
|
24
|
+
**Standard Python logging and one live status line, across threads and processes.**
|
|
25
|
+
|
|
26
|
+
Outwrite connects Python's logging module to [Rich](https://github.com/Textualize/rich)
|
|
27
|
+
and relays worker output to one renderer in the parent process. Use your existing
|
|
28
|
+
processes and ordinary loggers. No queue management in application code.
|
|
29
|
+
|
|
30
|
+
This is an early alpha. APIs may change; it is not production-certified.
|
|
31
|
+
Python 3.11+ is required. Rich is the only runtime dependency.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
The first release is a prerelease:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
python -m pip install outwrite==0.1.0a1
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or use `python -m pip install --pre outwrite` to allow the latest prerelease.
|
|
42
|
+
These commands become available once the release is published to PyPI.
|
|
43
|
+
|
|
44
|
+
To install from a local checkout:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
python -m pip install .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## One process
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import logging
|
|
54
|
+
from outwrite import setup_output, status
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger("my_app")
|
|
57
|
+
|
|
58
|
+
with setup_output(logger=logger) as output:
|
|
59
|
+
logger.info("Starting")
|
|
60
|
+
status.update("Processing files")
|
|
61
|
+
# Do some work.
|
|
62
|
+
status.clear()
|
|
63
|
+
logger.info("Done")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Worker processes
|
|
67
|
+
|
|
68
|
+
Pass the connection returned by the parent to each worker. Call the **same**
|
|
69
|
+
setup function inside each worker:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import logging
|
|
73
|
+
import multiprocessing as mp
|
|
74
|
+
import signal
|
|
75
|
+
from outwrite import setup_output, status
|
|
76
|
+
|
|
77
|
+
logger = logging.getLogger("my_app")
|
|
78
|
+
|
|
79
|
+
def work(output, stop):
|
|
80
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
81
|
+
setup_output(output)
|
|
82
|
+
logger.info("Worker started")
|
|
83
|
+
for index in range(10):
|
|
84
|
+
if stop.is_set():
|
|
85
|
+
break
|
|
86
|
+
status.update(f"Processing file {index + 1}")
|
|
87
|
+
stop.wait(0.1)
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
ctx = mp.get_context("spawn")
|
|
91
|
+
stop = ctx.Event()
|
|
92
|
+
with setup_output(logger=logger, mp_context=ctx) as output:
|
|
93
|
+
process = ctx.Process(target=work, args=(output, stop))
|
|
94
|
+
process.start()
|
|
95
|
+
try:
|
|
96
|
+
process.join()
|
|
97
|
+
finally:
|
|
98
|
+
stop.set()
|
|
99
|
+
process.join()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Always join workers and logging threads **inside** the parent context. On exit,
|
|
103
|
+
Outwrite drains queued messages, clears the status, and releases its resources.
|
|
104
|
+
Exit cleanup is also registered as a fallback; it does not stop or join workers
|
|
105
|
+
for you. Forced process termination can lose messages or damage multiprocessing
|
|
106
|
+
queues, so prefer a cooperative worker stop in real applications.
|
|
107
|
+
|
|
108
|
+
See [the runnable example](https://github.com/ariel-m-s/outwrite/blob/main/examples/multiprocessing_demo.py).
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
- `setup_output(logger=logger, level=logging.INFO, show_status=True, mp_context=ctx)`
|
|
113
|
+
starts parent output and returns an `OutputConnection` context handle.
|
|
114
|
+
- `setup_output(output)` connects a worker. The parent's logger name and status
|
|
115
|
+
setting are carried by the connection; keyword options are for parent setup.
|
|
116
|
+
- `status.update(text)` replaces the status line. `None` or `""` clears it.
|
|
117
|
+
- `status.clear()` clears the line without ending the session.
|
|
118
|
+
|
|
119
|
+
Status calls are safe from threads. Only one session and one status line are
|
|
120
|
+
active per process; concurrent writers replace the same line. Status calls before
|
|
121
|
+
setup or after parent cleanup raise `RuntimeError`.
|
|
122
|
+
|
|
123
|
+
The console uses stderr. Status is automatically disabled outside terminals;
|
|
124
|
+
logs still appear. `show_status=False` disables only status, and `level` controls
|
|
125
|
+
only the Rich console handler. Use integer logging levels, such as `logging.DEBUG`.
|
|
126
|
+
|
|
127
|
+
## Logging behavior
|
|
128
|
+
|
|
129
|
+
Only the selected **named logger** and its propagating descendants are routed.
|
|
130
|
+
The root logger and unrelated loggers are not configured. Child loggers with
|
|
131
|
+
their own handlers or `propagate=False` retain their normal logging behavior.
|
|
132
|
+
|
|
133
|
+
Existing handlers on the selected logger also receive records in the parent.
|
|
134
|
+
For example, attach a standard `logging.FileHandler` before setup to retain file
|
|
135
|
+
logging. Outwrite does not close your handlers. It adds a Rich console handler,
|
|
136
|
+
so remove an existing console handler yourself if you do not want duplicate
|
|
137
|
+
console output. The logger's original handlers, level, and propagation are
|
|
138
|
+
restored when the parent context exits.
|
|
139
|
+
|
|
140
|
+
Records pass through standard `QueueHandler` preparation: exception tracebacks
|
|
141
|
+
become message text. Extra record attributes must be picklable. Cross-process
|
|
142
|
+
delivery is asynchronous; use a cooperative worker shutdown and join workers to
|
|
143
|
+
flush their messages. Ordering between different producers is not guaranteed.
|
|
144
|
+
|
|
145
|
+
The default queue context is `spawn`. If using `forkserver`, pass that same
|
|
146
|
+
context to setup and use it to create processes. `fork` is deliberately unsupported
|
|
147
|
+
because the parent has an active rendering thread. Do not send a connection to
|
|
148
|
+
an already-running pool; pass it when creating workers or through a pool initializer.
|
|
149
|
+
|
|
150
|
+
Rendering failures are reported to stderr and raised when the parent context
|
|
151
|
+
exits. Standard logging handlers retain their usual error-reporting behavior.
|
|
152
|
+
No arbitrary stdout/stderr capture is performed.
|
|
153
|
+
|
|
154
|
+
## Why not just Rich?
|
|
155
|
+
|
|
156
|
+
Rich already supplies the columns, colors, links, and live display. Outwrite adds
|
|
157
|
+
the connection between existing worker processes, standard logging, and one live
|
|
158
|
+
status line. It is not a new renderer, process pool, or progress-bar framework.
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
python -m pip install -e .
|
|
164
|
+
python examples/multiprocessing_demo.py
|
|
165
|
+
python -m pip install build
|
|
166
|
+
python -m build
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
No persisted test suite is included in this initial iteration. Add regression
|
|
170
|
+
tests before a production release, particularly for process shutdown and terminal
|
|
171
|
+
cleanup.
|
|
172
|
+
|
|
173
|
+
The initial wheel was verified on macOS ARM64 with Python 3.13.7 and 3.14.0,
|
|
174
|
+
including spawn/forkserver workers and real-terminal Ctrl+C cleanup. Python
|
|
175
|
+
3.11/3.12 and other operating systems have not yet been runtime-tested.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT. The initial implementation adapts output coordination developed in Firefly,
|
|
180
|
+
with its author's permission.
|
|
181
|
+
|
|
182
|
+
## Publishing
|
|
183
|
+
|
|
184
|
+
Maintainers use PyPI Trusted Publishing; no permanent upload token is stored in
|
|
185
|
+
GitHub. The GitHub environment `pypi` requires approval before publication.
|
|
186
|
+
|
|
187
|
+
1. Update the version in `pyproject.toml`, and commit and push the change.
|
|
188
|
+
2. Create a GitHub release with a matching tag, such as `v0.1.0a1`.
|
|
189
|
+
Mark alpha/beta/RC versions as prereleases.
|
|
190
|
+
3. Review the build job and approve the `pypi` environment deployment.
|
|
191
|
+
4. Verify the uploaded version on PyPI and install it in a fresh environment.
|
|
192
|
+
|
|
193
|
+
The workflow builds and validates the distributions before publishing. Release
|
|
194
|
+
files cannot be replaced on PyPI; use a new version to correct a release.
|
|
195
|
+
The initial pending publisher must be configured on PyPI for repository
|
|
196
|
+
`ariel-m-s/outwrite`, workflow `publish.yml`, and environment `pypi`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
outwrite/__init__.py,sha256=1vRhIi9AY8osy8im5EkW-_wXLMIRLDMywcOwD8pdY2k,186
|
|
2
|
+
outwrite/_output.py,sha256=qROH-hVEwYA8fOs9O1jm0Sb74a1mQLUB6DZLTdpceZA,7915
|
|
3
|
+
outwrite/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
outwrite-0.1.0a1.dist-info/licenses/LICENSE,sha256=lvOeGnZ0uumCPeZQu6CMF2Du-icaGTP-thzU2mXnJHE,1078
|
|
5
|
+
outwrite-0.1.0a1.dist-info/METADATA,sha256=Fxi_SDeaPGYgBHMyXzl2EdJOwXvrZfAD_05zFC-f7JU,7485
|
|
6
|
+
outwrite-0.1.0a1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
outwrite-0.1.0a1.dist-info/top_level.txt,sha256=45EdQ0LvxHSyiibXsxkslDl8Z3kHQZMSOFlOuS6ah8M,9
|
|
8
|
+
outwrite-0.1.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Outwrite contributors
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
outwrite
|