omlish 0.0.0.dev473__py3-none-any.whl → 0.0.0.dev474__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.
Potentially problematic release.
This version of omlish might be problematic. Click here for more details.
- omlish/__about__.py +3 -3
- omlish/diag/cmds/__init__.py +0 -0
- omlish/diag/{lslocks.py → cmds/lslocks.py} +6 -6
- omlish/diag/{lsof.py → cmds/lsof.py} +6 -6
- omlish/diag/{ps.py → cmds/ps.py} +6 -6
- omlish/http/clients/coro/sync.py +1 -1
- omlish/http/clients/default.py +2 -2
- omlish/http/clients/executor.py +2 -2
- omlish/http/clients/httpx.py +11 -23
- omlish/http/clients/syncasync.py +2 -2
- omlish/io/buffers.py +2 -2
- omlish/io/readers.py +4 -4
- omlish/os/pidfiles/pinning.py +2 -2
- omlish/text/docwrap/__init__.py +3 -0
- omlish/text/docwrap/api.py +77 -0
- omlish/text/docwrap/groups.py +84 -0
- omlish/text/docwrap/lists.py +167 -0
- omlish/text/docwrap/parts.py +139 -0
- omlish/text/docwrap/reflowing.py +103 -0
- omlish/text/docwrap/rendering.py +142 -0
- omlish/text/docwrap/utils.py +11 -0
- omlish/text/docwrap/wrapping.py +59 -0
- omlish/text/textwrap.py +51 -0
- {omlish-0.0.0.dev473.dist-info → omlish-0.0.0.dev474.dist-info}/METADATA +7 -6
- {omlish-0.0.0.dev473.dist-info → omlish-0.0.0.dev474.dist-info}/RECORD +29 -18
- {omlish-0.0.0.dev473.dist-info → omlish-0.0.0.dev474.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev473.dist-info → omlish-0.0.0.dev474.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev473.dist-info → omlish-0.0.0.dev474.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev473.dist-info → omlish-0.0.0.dev474.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
__version__ = '0.0.0.
|
|
2
|
-
__revision__ = '
|
|
1
|
+
__version__ = '0.0.0.dev474'
|
|
2
|
+
__revision__ = '5a7b02bc5884a6c8e41f2e2afd5aaf7f495cc28e'
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
#
|
|
@@ -41,7 +41,7 @@ class Project(ProjectBase):
|
|
|
41
41
|
|
|
42
42
|
'greenlet ~= 3.2',
|
|
43
43
|
|
|
44
|
-
'trio ~= 0.
|
|
44
|
+
'trio ~= 0.32',
|
|
45
45
|
'trio-asyncio ~= 0.15',
|
|
46
46
|
],
|
|
47
47
|
|
|
File without changes
|
|
@@ -7,12 +7,12 @@ import dataclasses as dc
|
|
|
7
7
|
import json
|
|
8
8
|
import typing as ta
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
from
|
|
10
|
+
from ...lite.check import check
|
|
11
|
+
from ...lite.marshal import OBJ_MARSHALER_FIELD_KEY
|
|
12
|
+
from ...lite.marshal import unmarshal_obj
|
|
13
|
+
from ...subprocesses.run import SubprocessRun
|
|
14
|
+
from ...subprocesses.run import SubprocessRunnable
|
|
15
|
+
from ...subprocesses.run import SubprocessRunOutput
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
##
|
|
@@ -7,12 +7,12 @@ import dataclasses as dc
|
|
|
7
7
|
import enum
|
|
8
8
|
import typing as ta
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
from
|
|
10
|
+
from ...lite.check import check
|
|
11
|
+
from ...lite.dataclasses import dataclass_repr_omit_falsey
|
|
12
|
+
from ...lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
|
|
13
|
+
from ...subprocesses.run import SubprocessRun
|
|
14
|
+
from ...subprocesses.run import SubprocessRunnable
|
|
15
|
+
from ...subprocesses.run import SubprocessRunOutput
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
##
|
omlish/diag/{ps.py → cmds/ps.py}
RENAMED
|
@@ -4,12 +4,12 @@ import dataclasses as dc
|
|
|
4
4
|
import os
|
|
5
5
|
import typing as ta
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
7
|
+
from ...lite.check import check
|
|
8
|
+
from ...lite.timeouts import Timeout
|
|
9
|
+
from ...subprocesses.run import SubprocessRun
|
|
10
|
+
from ...subprocesses.run import SubprocessRunnable
|
|
11
|
+
from ...subprocesses.run import SubprocessRunOutput
|
|
12
|
+
from ...subprocesses.sync import subprocesses
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
##
|
omlish/http/clients/coro/sync.py
CHANGED
|
@@ -157,7 +157,7 @@ class CoroHttpClient(HttpClient):
|
|
|
157
157
|
else:
|
|
158
158
|
raise TypeError(o)
|
|
159
159
|
|
|
160
|
-
def read1(self,
|
|
160
|
+
def read1(self, n: int = -1, /) -> bytes:
|
|
161
161
|
return self._run_coro(check.not_none(self._resp).read(n if n >= 0 else None))
|
|
162
162
|
|
|
163
163
|
def close(self) -> None:
|
omlish/http/clients/default.py
CHANGED
|
@@ -83,7 +83,7 @@ def client() -> HttpClient:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
@contextlib.contextmanager
|
|
86
|
-
def manage_client(client: HttpClient | None) -> ta.Generator[HttpClient]: # noqa
|
|
86
|
+
def manage_client(client: HttpClient | None = None) -> ta.Generator[HttpClient]: # noqa
|
|
87
87
|
if client is not None:
|
|
88
88
|
yield client
|
|
89
89
|
|
|
@@ -167,7 +167,7 @@ def async_client() -> AsyncHttpClient:
|
|
|
167
167
|
|
|
168
168
|
|
|
169
169
|
@contextlib.asynccontextmanager
|
|
170
|
-
async def manage_async_client(client: AsyncHttpClient | None) -> ta.AsyncGenerator[AsyncHttpClient]: # noqa
|
|
170
|
+
async def manage_async_client(client: AsyncHttpClient | None = None) -> ta.AsyncGenerator[AsyncHttpClient]: # noqa
|
|
171
171
|
if client is not None:
|
|
172
172
|
yield client
|
|
173
173
|
|
omlish/http/clients/executor.py
CHANGED
|
@@ -30,10 +30,10 @@ class ExecutorAsyncHttpClient(AsyncHttpClient):
|
|
|
30
30
|
owner: 'ExecutorAsyncHttpClient'
|
|
31
31
|
resp: StreamHttpResponse
|
|
32
32
|
|
|
33
|
-
async def read1(self,
|
|
33
|
+
async def read1(self, n: int = -1, /) -> bytes:
|
|
34
34
|
return await self.owner._run_in_executor(self.resp.stream.read1, n) # noqa
|
|
35
35
|
|
|
36
|
-
async def read(self,
|
|
36
|
+
async def read(self, n: int = -1, /) -> bytes:
|
|
37
37
|
return await self.owner._run_in_executor(self.resp.stream.read, n) # noqa
|
|
38
38
|
|
|
39
39
|
async def readall(self) -> bytes:
|
omlish/http/clients/httpx.py
CHANGED
|
@@ -33,7 +33,7 @@ class HttpxHttpClient(HttpClient):
|
|
|
33
33
|
class _StreamAdapter:
|
|
34
34
|
it: ta.Iterator[bytes]
|
|
35
35
|
|
|
36
|
-
def read1(self,
|
|
36
|
+
def read1(self, n: int = -1, /) -> bytes:
|
|
37
37
|
try:
|
|
38
38
|
return next(self.it)
|
|
39
39
|
except StopIteration:
|
|
@@ -82,7 +82,7 @@ class HttpxAsyncHttpClient(AsyncHttpClient):
|
|
|
82
82
|
class _StreamAdapter:
|
|
83
83
|
it: ta.AsyncIterator[bytes]
|
|
84
84
|
|
|
85
|
-
async def read1(self,
|
|
85
|
+
async def read1(self, n: int = -1, /) -> bytes:
|
|
86
86
|
try:
|
|
87
87
|
return await anext(self.it)
|
|
88
88
|
except StopAsyncIteration:
|
|
@@ -105,27 +105,15 @@ class HttpxAsyncHttpClient(AsyncHttpClient):
|
|
|
105
105
|
it = resp.aiter_bytes()
|
|
106
106
|
|
|
107
107
|
# FIXME:
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
# -
|
|
111
|
-
#
|
|
112
|
-
# -
|
|
113
|
-
# -
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
@es.push_async_callback
|
|
118
|
-
async def close_it() -> None:
|
|
119
|
-
try:
|
|
120
|
-
# print(f'close_it.begin: {it=}', file=sys.stderr)
|
|
121
|
-
await it.aclose() # type: ignore[attr-defined]
|
|
122
|
-
# print(f'close_it.end: {it=}', file=sys.stderr)
|
|
123
|
-
except BaseException as be: # noqa
|
|
124
|
-
# print(f'close_it.__exit__: {it=} {be=}', file=sys.stderr)
|
|
125
|
-
raise
|
|
126
|
-
finally:
|
|
127
|
-
# print(f'close_it.finally: {it=}', file=sys.stderr)
|
|
128
|
-
pass
|
|
108
|
+
# this has a tendency to raise `RuntimeError: async generator ignored GeneratorExit` when all of the
|
|
109
|
+
# following conditions are met:
|
|
110
|
+
# - stopped iterating midway through
|
|
111
|
+
# - shutting down the event loop
|
|
112
|
+
# - debugging under pycharm / pydevd
|
|
113
|
+
# - running under asyncio
|
|
114
|
+
# it does not seem to happen unless all of these conditions are met. see:
|
|
115
|
+
# https://gist.github.com/wrmsr/a0578ee5d5371b53804cfb56aeb84cdf .
|
|
116
|
+
es.push_async_callback(it.aclose) # type: ignore[attr-defined]
|
|
129
117
|
|
|
130
118
|
return AsyncStreamHttpResponse(
|
|
131
119
|
status=resp.status_code,
|
omlish/http/clients/syncasync.py
CHANGED
|
@@ -23,10 +23,10 @@ class SyncAsyncHttpClient(AsyncHttpClient):
|
|
|
23
23
|
class _StreamAdapter:
|
|
24
24
|
ul: StreamHttpResponse
|
|
25
25
|
|
|
26
|
-
async def read1(self,
|
|
26
|
+
async def read1(self, n: int = -1, /) -> bytes:
|
|
27
27
|
return self.ul.stream.read1(n)
|
|
28
28
|
|
|
29
|
-
async def read(self,
|
|
29
|
+
async def read(self, n: int = -1, /) -> bytes:
|
|
30
30
|
return self.ul.stream.read(n)
|
|
31
31
|
|
|
32
32
|
async def readall(self) -> bytes:
|
omlish/io/buffers.py
CHANGED
|
@@ -277,7 +277,7 @@ class ReadableListBuffer:
|
|
|
277
277
|
self._buf = buf
|
|
278
278
|
self._chunk_size = chunk_size or ReadableListBuffer.DEFAULT_BUFFERED_READER_CHUNK_SIZE
|
|
279
279
|
|
|
280
|
-
def read1(self,
|
|
280
|
+
def read1(self, n: int = -1, /) -> bytes:
|
|
281
281
|
if n < 0:
|
|
282
282
|
n = self._chunk_size
|
|
283
283
|
if not n:
|
|
@@ -327,7 +327,7 @@ class ReadableListBuffer:
|
|
|
327
327
|
self._buf = buf
|
|
328
328
|
self._chunk_size = chunk_size or ReadableListBuffer.DEFAULT_BUFFERED_READER_CHUNK_SIZE
|
|
329
329
|
|
|
330
|
-
async def read1(self,
|
|
330
|
+
async def read1(self, n: int = -1, /) -> bytes:
|
|
331
331
|
if n < 0:
|
|
332
332
|
n = self._chunk_size
|
|
333
333
|
if not n:
|
omlish/io/readers.py
CHANGED
|
@@ -7,11 +7,11 @@ import typing as ta
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class RawBytesReader(ta.Protocol):
|
|
10
|
-
def read1(self,
|
|
10
|
+
def read1(self, n: int = -1, /) -> bytes: ...
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class BufferedBytesReader(RawBytesReader, ta.Protocol):
|
|
14
|
-
def read(self,
|
|
14
|
+
def read(self, n: int = -1, /) -> bytes: ...
|
|
15
15
|
|
|
16
16
|
def readall(self) -> bytes: ...
|
|
17
17
|
|
|
@@ -20,10 +20,10 @@ class BufferedBytesReader(RawBytesReader, ta.Protocol):
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class AsyncRawBytesReader(ta.Protocol):
|
|
23
|
-
def read1(self,
|
|
23
|
+
def read1(self, n: int = -1, /) -> ta.Awaitable[bytes]: ...
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class AsyncBufferedBytesReader(AsyncRawBytesReader, ta.Protocol):
|
|
27
|
-
def read(self,
|
|
27
|
+
def read(self, n: int = -1, /) -> ta.Awaitable[bytes]: ...
|
|
28
28
|
|
|
29
29
|
def readall(self) -> ta.Awaitable[bytes]: ...
|
omlish/os/pidfiles/pinning.py
CHANGED
|
@@ -25,8 +25,8 @@ import sys
|
|
|
25
25
|
import time
|
|
26
26
|
import typing as ta
|
|
27
27
|
|
|
28
|
-
from ...diag.lslocks import LslocksCommand
|
|
29
|
-
from ...diag.lsof import LsofCommand
|
|
28
|
+
from ...diag.cmds.lslocks import LslocksCommand
|
|
29
|
+
from ...diag.cmds.lsof import LsofCommand
|
|
30
30
|
from ...lite.abstract import Abstract
|
|
31
31
|
from ...lite.check import check
|
|
32
32
|
from ...lite.timeouts import Timeout
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from ..textwrap import TextwrapOpts
|
|
5
|
+
from .groups import group_indents
|
|
6
|
+
from .lists import ListBuilder
|
|
7
|
+
from .parts import Part
|
|
8
|
+
from .parts import build_root
|
|
9
|
+
from .reflowing import TextwrapReflower
|
|
10
|
+
from .reflowing import reflow_block_text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEFAULT_TAB_WIDTH: int = 4
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def replace_tabs(s: str, tab_width: int | None = None) -> str:
|
|
20
|
+
if tab_width is None:
|
|
21
|
+
tab_width = DEFAULT_TAB_WIDTH
|
|
22
|
+
return s.replace('\t', ' ' * tab_width)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse(
|
|
29
|
+
s: str,
|
|
30
|
+
*,
|
|
31
|
+
tab_width: int | None = None,
|
|
32
|
+
allow_improper_list_children: bool | ta.Literal['lists_only'] | None = None,
|
|
33
|
+
) -> Part:
|
|
34
|
+
s = replace_tabs(
|
|
35
|
+
s,
|
|
36
|
+
tab_width=tab_width,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
root = build_root(s)
|
|
40
|
+
|
|
41
|
+
root = group_indents(root)
|
|
42
|
+
|
|
43
|
+
root = ListBuilder(
|
|
44
|
+
allow_improper_children=allow_improper_list_children,
|
|
45
|
+
).build_lists(root)
|
|
46
|
+
|
|
47
|
+
return root
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def docwrap(
|
|
54
|
+
s: str,
|
|
55
|
+
*,
|
|
56
|
+
width: int | None = None,
|
|
57
|
+
textwrap: TextwrapOpts | ta.Mapping[str, ta.Any] | None = None,
|
|
58
|
+
allow_improper_list_children: bool | ta.Literal['lists_only'] = False,
|
|
59
|
+
) -> Part:
|
|
60
|
+
if isinstance(textwrap, ta.Mapping):
|
|
61
|
+
textwrap = TextwrapOpts(**textwrap)
|
|
62
|
+
elif textwrap is None:
|
|
63
|
+
textwrap = TextwrapOpts()
|
|
64
|
+
if width is not None:
|
|
65
|
+
textwrap = dc.replace(textwrap, width=width)
|
|
66
|
+
|
|
67
|
+
root = parse(
|
|
68
|
+
s,
|
|
69
|
+
allow_improper_list_children=allow_improper_list_children,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
root = reflow_block_text(
|
|
73
|
+
root,
|
|
74
|
+
TextwrapReflower(opts=textwrap),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return root
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import typing as ta
|
|
2
|
+
|
|
3
|
+
from ... import check
|
|
4
|
+
from ... import dataclasses as dc
|
|
5
|
+
from .parts import Blank
|
|
6
|
+
from .parts import Block
|
|
7
|
+
from .parts import Indent
|
|
8
|
+
from .parts import Part
|
|
9
|
+
from .parts import Text
|
|
10
|
+
from .parts import blockify
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dc.dataclass()
|
|
17
|
+
@dc.extra_class_params(default_repr_fn=dc.truthy_repr)
|
|
18
|
+
class _IndentGroup:
|
|
19
|
+
n: int
|
|
20
|
+
cs: list[ta.Union[Blank, Text, '_IndentGroup']] = dc.field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def group_indents(root: Part) -> Part:
|
|
24
|
+
rg = _IndentGroup(0)
|
|
25
|
+
stk: list[_IndentGroup] = [rg]
|
|
26
|
+
|
|
27
|
+
for p in (root.ps if isinstance(root, Block) else [root]):
|
|
28
|
+
if isinstance(p, Blank):
|
|
29
|
+
stk[-1].cs.append(p)
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
n: int
|
|
33
|
+
t: Text
|
|
34
|
+
if isinstance(p, Text):
|
|
35
|
+
n, t = 0, p
|
|
36
|
+
elif isinstance(p, Indent):
|
|
37
|
+
n = p.n
|
|
38
|
+
t = check.isinstance(p.p, Text)
|
|
39
|
+
else:
|
|
40
|
+
raise TypeError(p)
|
|
41
|
+
|
|
42
|
+
while n < stk[-1].n:
|
|
43
|
+
stk.pop()
|
|
44
|
+
|
|
45
|
+
if n > stk[-1].n:
|
|
46
|
+
nxt = _IndentGroup(n=n, cs=[t])
|
|
47
|
+
stk[-1].cs.append(nxt)
|
|
48
|
+
stk.append(nxt)
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
check.state(stk[-1].n == n)
|
|
52
|
+
stk[-1].cs.append(t)
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
|
|
56
|
+
def relativize(g: '_IndentGroup') -> None:
|
|
57
|
+
for c in g.cs:
|
|
58
|
+
if isinstance(c, _IndentGroup):
|
|
59
|
+
check.state(c.n > g.n)
|
|
60
|
+
relativize(c)
|
|
61
|
+
c.n -= g.n
|
|
62
|
+
|
|
63
|
+
relativize(rg)
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
|
|
67
|
+
def convert(g: '_IndentGroup') -> Part:
|
|
68
|
+
if g.n < 1:
|
|
69
|
+
check.state(g is rg)
|
|
70
|
+
|
|
71
|
+
lst: list[Part] = []
|
|
72
|
+
for c in g.cs:
|
|
73
|
+
if isinstance(c, (Blank, Text)):
|
|
74
|
+
lst.append(c)
|
|
75
|
+
|
|
76
|
+
elif isinstance(c, _IndentGroup):
|
|
77
|
+
lst.append(Indent(c.n, convert(c))) # type: ignore[arg-type]
|
|
78
|
+
|
|
79
|
+
else:
|
|
80
|
+
raise TypeError(c)
|
|
81
|
+
|
|
82
|
+
return blockify(*lst)
|
|
83
|
+
|
|
84
|
+
return convert(rg)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- numeric lettered lists (even unordered) (with separator - `1)` / `1:` / ...)
|
|
4
|
+
"""
|
|
5
|
+
import typing as ta
|
|
6
|
+
|
|
7
|
+
from ... import check
|
|
8
|
+
from .parts import Blank
|
|
9
|
+
from .parts import Block
|
|
10
|
+
from .parts import Indent
|
|
11
|
+
from .parts import List
|
|
12
|
+
from .parts import Part
|
|
13
|
+
from .parts import Text
|
|
14
|
+
from .parts import blockify
|
|
15
|
+
from .utils import all_same
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ListBuilder:
|
|
22
|
+
DEFAULT_ALLOW_IMPROPER_CHILDREN: ta.ClassVar[bool | ta.Literal['lists_only']] = False
|
|
23
|
+
DEFAULT_LIST_PREFIXES: ta.ClassVar[ta.Sequence[str]] = ['*', '-']
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
list_prefixes: ta.Iterable[str] | None = None,
|
|
29
|
+
allow_improper_children: bool | ta.Literal['lists_only'] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
|
|
33
|
+
if list_prefixes is None:
|
|
34
|
+
list_prefixes = self.DEFAULT_LIST_PREFIXES
|
|
35
|
+
self._list_prefixes = set(check.not_isinstance(list_prefixes, str))
|
|
36
|
+
if allow_improper_children is None:
|
|
37
|
+
allow_improper_children = self.DEFAULT_ALLOW_IMPROPER_CHILDREN
|
|
38
|
+
self._allow_improper_children = allow_improper_children
|
|
39
|
+
|
|
40
|
+
self._len_sorted_list_prefixes = sorted(self._list_prefixes, key=len, reverse=True)
|
|
41
|
+
|
|
42
|
+
#
|
|
43
|
+
|
|
44
|
+
def _should_promote_indent_child(self, p: Indent) -> bool:
|
|
45
|
+
ac = self._allow_improper_children
|
|
46
|
+
if isinstance(ac, bool):
|
|
47
|
+
return ac
|
|
48
|
+
elif ac == 'lists_only':
|
|
49
|
+
return isinstance(p.p, List)
|
|
50
|
+
else:
|
|
51
|
+
raise TypeError(ac)
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
|
|
55
|
+
class _DetectedList(ta.NamedTuple):
|
|
56
|
+
pfx: str
|
|
57
|
+
ofs: int
|
|
58
|
+
len: int
|
|
59
|
+
|
|
60
|
+
def _detect_list(self, ps: ta.Sequence[Part], st: int = 0) -> _DetectedList | None:
|
|
61
|
+
if not ps:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
for lp in self._len_sorted_list_prefixes:
|
|
65
|
+
sp = lp + ' '
|
|
66
|
+
|
|
67
|
+
mo = -1
|
|
68
|
+
n = st
|
|
69
|
+
while n < len(ps):
|
|
70
|
+
p = ps[n]
|
|
71
|
+
|
|
72
|
+
if isinstance(p, (Blank, Text)):
|
|
73
|
+
if isinstance(p, Text):
|
|
74
|
+
if p.s.startswith(sp):
|
|
75
|
+
if mo < 0:
|
|
76
|
+
mo = n
|
|
77
|
+
elif mo >= 0:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
elif isinstance(p, Indent):
|
|
81
|
+
if mo >= 0 and p.n < len(sp):
|
|
82
|
+
if not self._should_promote_indent_child(p):
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
elif isinstance(p, List):
|
|
86
|
+
if mo >= 0:
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
else:
|
|
90
|
+
raise TypeError(p)
|
|
91
|
+
|
|
92
|
+
n += 1
|
|
93
|
+
|
|
94
|
+
if mo >= 0:
|
|
95
|
+
return ListBuilder._DetectedList(lp, mo, n - mo)
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def _build_list(self, lp: str, ps: ta.Sequence[Part]) -> List:
|
|
100
|
+
sp = lp + ' '
|
|
101
|
+
|
|
102
|
+
new: list[list[Part]] = []
|
|
103
|
+
|
|
104
|
+
f = check.isinstance(ps[0], Text)
|
|
105
|
+
check.state(f.s.startswith(sp))
|
|
106
|
+
new.append([Text(f.s[len(sp):])])
|
|
107
|
+
del f
|
|
108
|
+
|
|
109
|
+
for i in range(1, len(ps)):
|
|
110
|
+
p = ps[i]
|
|
111
|
+
|
|
112
|
+
if isinstance(p, Blank):
|
|
113
|
+
new[-1].append(p)
|
|
114
|
+
|
|
115
|
+
elif isinstance(p, Text):
|
|
116
|
+
check.state(p.s.startswith(sp))
|
|
117
|
+
new.append([Text(p.s[len(sp):])])
|
|
118
|
+
|
|
119
|
+
elif isinstance(p, Indent):
|
|
120
|
+
if p.n < len(sp):
|
|
121
|
+
check.state(self._should_promote_indent_child(p))
|
|
122
|
+
p = Indent(len(sp), p.p)
|
|
123
|
+
|
|
124
|
+
if p.n == len(sp):
|
|
125
|
+
new[-1].append(p.p)
|
|
126
|
+
|
|
127
|
+
else:
|
|
128
|
+
raise NotImplementedError
|
|
129
|
+
|
|
130
|
+
else:
|
|
131
|
+
raise TypeError(p)
|
|
132
|
+
|
|
133
|
+
#
|
|
134
|
+
|
|
135
|
+
return List(lp, [blockify(*x) for x in new])
|
|
136
|
+
|
|
137
|
+
def build_lists(self, root: Part) -> Part:
|
|
138
|
+
def rec(p: Part) -> Part: # noqa
|
|
139
|
+
if isinstance(p, Block):
|
|
140
|
+
new = [rec(c) for c in p.ps]
|
|
141
|
+
if not all_same(new, p.ps):
|
|
142
|
+
return rec(blockify(*new))
|
|
143
|
+
|
|
144
|
+
st = 0
|
|
145
|
+
diff = False
|
|
146
|
+
while (dl := self._detect_list(new, st)) is not None:
|
|
147
|
+
diff = True
|
|
148
|
+
ln = self._build_list(dl.pfx, new[dl.ofs:dl.ofs + dl.len])
|
|
149
|
+
new[dl.ofs:dl.ofs + dl.len] = [ln]
|
|
150
|
+
st = dl.ofs + 1
|
|
151
|
+
|
|
152
|
+
if diff:
|
|
153
|
+
p = blockify(*new)
|
|
154
|
+
return p
|
|
155
|
+
|
|
156
|
+
elif isinstance(p, Indent):
|
|
157
|
+
if (n := rec(p.p)) is not p.p:
|
|
158
|
+
p = Indent(p.n, n) # type: ignore[arg-type]
|
|
159
|
+
return p
|
|
160
|
+
|
|
161
|
+
elif isinstance(p, (Blank, Text, List)):
|
|
162
|
+
return p
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
raise TypeError(p)
|
|
166
|
+
|
|
167
|
+
return rec(root)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import typing as ta
|
|
2
|
+
|
|
3
|
+
from ... import check
|
|
4
|
+
from ... import dataclasses as dc
|
|
5
|
+
from ... import lang
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dc.dataclass(frozen=True)
|
|
12
|
+
@dc.extra_class_params(terse_repr=True)
|
|
13
|
+
class Part(lang.Abstract, lang.Sealed):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dc.dataclass(frozen=True)
|
|
18
|
+
@dc.extra_class_params(terse_repr=True)
|
|
19
|
+
class Text(Part, lang.Final):
|
|
20
|
+
s: str
|
|
21
|
+
|
|
22
|
+
@dc.init
|
|
23
|
+
def _check_s(self) -> None:
|
|
24
|
+
check.non_empty_str(self.s)
|
|
25
|
+
check.state(self.s == self.s.strip())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dc.dataclass(frozen=True)
|
|
29
|
+
@dc.extra_class_params(terse_repr=True)
|
|
30
|
+
class Blank(Part, lang.Final):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dc.dataclass(frozen=True)
|
|
35
|
+
@dc.extra_class_params(terse_repr=True)
|
|
36
|
+
class Indent(Part, lang.Final):
|
|
37
|
+
n: int = dc.xfield(validate=lambda n: n > 0)
|
|
38
|
+
p: ta.Union[Text, 'Block', 'List'] = dc.xfield(coerce=lambda p: check.isinstance(p, (Text, Block, List)))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dc.dataclass(frozen=True)
|
|
42
|
+
@dc.extra_class_params(terse_repr=True)
|
|
43
|
+
class Block(Part, lang.Final):
|
|
44
|
+
ps: ta.Sequence[Part]
|
|
45
|
+
|
|
46
|
+
@dc.init
|
|
47
|
+
def _check_ps(self) -> None:
|
|
48
|
+
check.state(len(self.ps) > 1)
|
|
49
|
+
for i, p in enumerate(self.ps):
|
|
50
|
+
check.isinstance(p, Part)
|
|
51
|
+
if i and isinstance(p, Block):
|
|
52
|
+
check.not_isinstance(self.ps[i - 1], Block)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dc.dataclass(frozen=True)
|
|
56
|
+
@dc.extra_class_params(terse_repr=True)
|
|
57
|
+
class List(Part, lang.Final):
|
|
58
|
+
d: str = dc.xfield(coerce=check.non_empty_str)
|
|
59
|
+
es: ta.Sequence[Part] = dc.xfield()
|
|
60
|
+
|
|
61
|
+
@dc.init
|
|
62
|
+
def _check_es(self) -> None:
|
|
63
|
+
check.not_empty(self.es)
|
|
64
|
+
for e in self.es:
|
|
65
|
+
check.isinstance(e, Part)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _squish(ps: ta.Sequence[Part]) -> ta.Sequence[Part]:
|
|
72
|
+
for p in ps:
|
|
73
|
+
check.isinstance(p, Part)
|
|
74
|
+
|
|
75
|
+
if len(ps) < 2:
|
|
76
|
+
return ps
|
|
77
|
+
|
|
78
|
+
while True:
|
|
79
|
+
if any(isinstance(p, Block) for p in ps):
|
|
80
|
+
ps = list(lang.flatmap(lambda p: p.ps if isinstance(p, Block) else [p], ps))
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if any(
|
|
84
|
+
isinstance(ps[i], Indent) and
|
|
85
|
+
isinstance(ps[i + 1], Indent) and
|
|
86
|
+
ps[i].n == ps[i + 1].n # type: ignore[attr-defined]
|
|
87
|
+
for i in range(len(ps) - 1)
|
|
88
|
+
):
|
|
89
|
+
new: list[Part | tuple[int, list[Part]]] = []
|
|
90
|
+
for p in ps:
|
|
91
|
+
if isinstance(p, Indent):
|
|
92
|
+
if new and isinstance(y := new[-1], tuple) and p.n == y[0]:
|
|
93
|
+
y[1].append(p.p)
|
|
94
|
+
else:
|
|
95
|
+
new.append((p.n, [p.p]))
|
|
96
|
+
else:
|
|
97
|
+
new.append(p)
|
|
98
|
+
ps = [
|
|
99
|
+
Indent(x[0], blockify(*x[1])) if isinstance(x, tuple) else x # type: ignore[arg-type]
|
|
100
|
+
for x in new
|
|
101
|
+
]
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
return ps
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def blockify(*ps: Part) -> Part:
|
|
110
|
+
check.not_empty(ps)
|
|
111
|
+
ps = _squish(ps) # type: ignore[assignment]
|
|
112
|
+
if len(ps) == 1:
|
|
113
|
+
return ps[0]
|
|
114
|
+
return Block(ps)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_root(s: str) -> Part:
|
|
121
|
+
lst: list[Part] = []
|
|
122
|
+
|
|
123
|
+
for l in s.splitlines():
|
|
124
|
+
if not (sl := l.strip()):
|
|
125
|
+
lst.append(Blank())
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
p: Part = Text(sl)
|
|
129
|
+
|
|
130
|
+
n = next((i for i, c in enumerate(l) if not c.isspace()), 0)
|
|
131
|
+
if n:
|
|
132
|
+
p = Indent(n, p) # type: ignore[arg-type]
|
|
133
|
+
|
|
134
|
+
lst.append(p)
|
|
135
|
+
|
|
136
|
+
if len(lst) == 1:
|
|
137
|
+
return lst[0]
|
|
138
|
+
else:
|
|
139
|
+
return Block(lst)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FIXME:
|
|
3
|
+
- use wrapping.py with all its todos
|
|
4
|
+
"""
|
|
5
|
+
import dataclasses as dc
|
|
6
|
+
import textwrap
|
|
7
|
+
import typing as ta
|
|
8
|
+
|
|
9
|
+
from ... import lang
|
|
10
|
+
from ..textwrap import TextwrapOpts
|
|
11
|
+
from .parts import Blank
|
|
12
|
+
from .parts import Block
|
|
13
|
+
from .parts import Indent
|
|
14
|
+
from .parts import List
|
|
15
|
+
from .parts import Part
|
|
16
|
+
from .parts import Text
|
|
17
|
+
from .parts import blockify
|
|
18
|
+
from .utils import all_same
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TextReflower(ta.Protocol):
|
|
25
|
+
def __call__(self, strs: ta.Sequence[str], current_indent: int) -> ta.Sequence[str]: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NopReflower:
|
|
29
|
+
def __call__(self, strs: ta.Sequence[str], current_indent: int = 0) -> ta.Sequence[str]:
|
|
30
|
+
return strs
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dc.dataclass(frozen=True)
|
|
34
|
+
class JoiningReflower:
|
|
35
|
+
sep: str = ' '
|
|
36
|
+
|
|
37
|
+
def __call__(self, strs: ta.Sequence[str], current_indent: int = 0) -> ta.Sequence[str]:
|
|
38
|
+
return [self.sep.join(strs)]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dc.dataclass(frozen=True)
|
|
42
|
+
class TextwrapReflower:
|
|
43
|
+
sep: str = ' '
|
|
44
|
+
opts: TextwrapOpts = TextwrapOpts()
|
|
45
|
+
|
|
46
|
+
def __call__(self, strs: ta.Sequence[str], current_indent: int = 0) -> ta.Sequence[str]:
|
|
47
|
+
return textwrap.wrap(
|
|
48
|
+
self.sep.join(strs),
|
|
49
|
+
**dc.asdict(dc.replace(
|
|
50
|
+
self.opts,
|
|
51
|
+
width=self.opts.width - current_indent,
|
|
52
|
+
)),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def reflow_block_text(
|
|
60
|
+
root: Part,
|
|
61
|
+
reflower: TextReflower = JoiningReflower(),
|
|
62
|
+
) -> Part:
|
|
63
|
+
def rec(p: Part, ci: int) -> Part:
|
|
64
|
+
if isinstance(p, Blank):
|
|
65
|
+
return p
|
|
66
|
+
|
|
67
|
+
elif isinstance(p, Text):
|
|
68
|
+
if (rf := reflower([p.s], ci)) != [p.s]:
|
|
69
|
+
p = blockify(*map(Text, rf)) # noqa
|
|
70
|
+
return p
|
|
71
|
+
|
|
72
|
+
elif isinstance(p, Indent):
|
|
73
|
+
if (np := rec(p.p, ci + p.n)) is not p.p:
|
|
74
|
+
p = Indent(p.n, np) # type: ignore[arg-type]
|
|
75
|
+
return p
|
|
76
|
+
|
|
77
|
+
elif isinstance(p, List):
|
|
78
|
+
ne = [rec(e, ci + len(p.d) + 1) for e in p.es]
|
|
79
|
+
if not all_same(ne, p.es):
|
|
80
|
+
p = List(p.d, ne)
|
|
81
|
+
return p
|
|
82
|
+
|
|
83
|
+
elif not isinstance(p, Block):
|
|
84
|
+
raise TypeError(p)
|
|
85
|
+
|
|
86
|
+
ps = [rec(c, ci) for c in p.ps]
|
|
87
|
+
|
|
88
|
+
new: list[Part | list[str]] = []
|
|
89
|
+
for c in ps:
|
|
90
|
+
if isinstance(c, Text):
|
|
91
|
+
if new and isinstance(x := new[-1], list):
|
|
92
|
+
x.append(c.s)
|
|
93
|
+
else:
|
|
94
|
+
new.append([c.s])
|
|
95
|
+
else:
|
|
96
|
+
new.append(c)
|
|
97
|
+
|
|
98
|
+
return blockify(*lang.flatmap(
|
|
99
|
+
lambda x: map(Text, reflower(x, ci)) if isinstance(x, list) else [x], # noqa
|
|
100
|
+
new,
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
return rec(root, 0)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import io
|
|
3
|
+
import typing as ta
|
|
4
|
+
|
|
5
|
+
from ... import check
|
|
6
|
+
from .parts import Blank
|
|
7
|
+
from .parts import Block
|
|
8
|
+
from .parts import Indent
|
|
9
|
+
from .parts import List
|
|
10
|
+
from .parts import Part
|
|
11
|
+
from .parts import Text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@functools.lru_cache
|
|
18
|
+
def _indent_str(n: int) -> str:
|
|
19
|
+
return ' ' * n
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def render_to(root: Part, out: ta.Callable[[str], ta.Any]) -> None:
|
|
26
|
+
i_stk: list[int] = [0]
|
|
27
|
+
ci = 0
|
|
28
|
+
|
|
29
|
+
def write(s: str, ti: int | None = None) -> None:
|
|
30
|
+
if (nl := '\n' in s) and s != '\n':
|
|
31
|
+
check.state(s[-1] == '\n')
|
|
32
|
+
check.state(s.count('\n') == 1)
|
|
33
|
+
|
|
34
|
+
if ti is None:
|
|
35
|
+
ti = i_stk[-1]
|
|
36
|
+
nonlocal ci
|
|
37
|
+
if ci < ti:
|
|
38
|
+
out(_indent_str(ti - ci))
|
|
39
|
+
ci = ti
|
|
40
|
+
|
|
41
|
+
out(s)
|
|
42
|
+
|
|
43
|
+
if nl:
|
|
44
|
+
ci = 0
|
|
45
|
+
|
|
46
|
+
def rec(p: Part) -> None:
|
|
47
|
+
nonlocal ci
|
|
48
|
+
|
|
49
|
+
if isinstance(p, Blank):
|
|
50
|
+
check.state(ci == 0)
|
|
51
|
+
out('\n')
|
|
52
|
+
|
|
53
|
+
elif isinstance(p, Text):
|
|
54
|
+
write(p.s)
|
|
55
|
+
write('\n')
|
|
56
|
+
|
|
57
|
+
elif isinstance(p, Block):
|
|
58
|
+
for c in p.ps:
|
|
59
|
+
rec(c)
|
|
60
|
+
|
|
61
|
+
elif isinstance(p, Indent):
|
|
62
|
+
i_stk.append(i_stk[-1] + p.n)
|
|
63
|
+
rec(p.p)
|
|
64
|
+
i_stk.pop()
|
|
65
|
+
|
|
66
|
+
elif isinstance(p, List):
|
|
67
|
+
i_stk.append((li := i_stk[-1]) + len(p.d) + 1)
|
|
68
|
+
sd = p.d + ' '
|
|
69
|
+
for e in p.es:
|
|
70
|
+
if isinstance(e, Blank):
|
|
71
|
+
rec(e)
|
|
72
|
+
else:
|
|
73
|
+
check.state(ci == 0)
|
|
74
|
+
write(sd, li)
|
|
75
|
+
ci += len(sd)
|
|
76
|
+
rec(e)
|
|
77
|
+
i_stk.pop()
|
|
78
|
+
|
|
79
|
+
else:
|
|
80
|
+
raise TypeError(p)
|
|
81
|
+
|
|
82
|
+
rec(root)
|
|
83
|
+
check.state(i_stk == [0])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def render(root: Part) -> str:
|
|
87
|
+
buf = io.StringIO()
|
|
88
|
+
render_to(root, buf.write)
|
|
89
|
+
return buf.getvalue()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def dump_to(root: Part, out: ta.Callable[[str], ta.Any]) -> None:
|
|
96
|
+
i = 0
|
|
97
|
+
|
|
98
|
+
def write(s: str) -> None:
|
|
99
|
+
out(_indent_str(i * 2))
|
|
100
|
+
out(s)
|
|
101
|
+
out('\n')
|
|
102
|
+
|
|
103
|
+
def rec(p: Part) -> None:
|
|
104
|
+
nonlocal i
|
|
105
|
+
|
|
106
|
+
if isinstance(p, Blank):
|
|
107
|
+
write('blank')
|
|
108
|
+
|
|
109
|
+
elif isinstance(p, Text):
|
|
110
|
+
write(f'text:{p.s!r}')
|
|
111
|
+
|
|
112
|
+
elif isinstance(p, Block):
|
|
113
|
+
write(f'block/{len(p.ps)}')
|
|
114
|
+
i += 1
|
|
115
|
+
for c in p.ps:
|
|
116
|
+
rec(c)
|
|
117
|
+
i -= 1
|
|
118
|
+
|
|
119
|
+
elif isinstance(p, Indent):
|
|
120
|
+
write(f'indent:{p.n}')
|
|
121
|
+
i += 1
|
|
122
|
+
rec(p.p)
|
|
123
|
+
i -= 1
|
|
124
|
+
|
|
125
|
+
elif isinstance(p, List):
|
|
126
|
+
write(f'list/{len(p.es)}:{p.d!r}')
|
|
127
|
+
i += 1
|
|
128
|
+
for e in p.es:
|
|
129
|
+
rec(e)
|
|
130
|
+
i -= 1
|
|
131
|
+
|
|
132
|
+
else:
|
|
133
|
+
raise TypeError(p)
|
|
134
|
+
|
|
135
|
+
rec(root)
|
|
136
|
+
check.state(i == 0)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def dump(root: Part) -> str:
|
|
140
|
+
buf = io.StringIO()
|
|
141
|
+
dump_to(root, buf.write)
|
|
142
|
+
return buf.getvalue()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- a reflowing.TextReflower
|
|
4
|
+
- import textwrap lol
|
|
5
|
+
- obviously handle hyphens, underscores, etc
|
|
6
|
+
- optionally preserve/normalize inter-sentence spaces - '. ' vs '. '
|
|
7
|
+
- detect if 'intentionally' smaller than current remaining line width, if so do not merge.
|
|
8
|
+
- maybe if only ending with punctuation?
|
|
9
|
+
- detect 'matched pairs' of 'quotes'? to preserve whitespace? `foo bar` ...
|
|
10
|
+
- how to escape it lol - if we see any \\` do we give up?
|
|
11
|
+
"""
|
|
12
|
+
import re
|
|
13
|
+
import typing as ta
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SpanKind: ta.TypeAlias = ta.Literal[
|
|
17
|
+
'word',
|
|
18
|
+
'space',
|
|
19
|
+
'symbol',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Span(ta.NamedTuple):
|
|
27
|
+
k: SpanKind
|
|
28
|
+
s: str
|
|
29
|
+
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
return f'{self.k}:{self.s!r}'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_SPAN_PAT = re.compile(r'(\w+)|(\s+)')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def split_line_spans(s: str) -> list[Span]:
|
|
38
|
+
spans: list[Span] = []
|
|
39
|
+
p = 0
|
|
40
|
+
for m in _SPAN_PAT.finditer(s):
|
|
41
|
+
l, r = m.span()
|
|
42
|
+
if p < l:
|
|
43
|
+
spans.append(Span('symbol', s[p:l]))
|
|
44
|
+
if m.group(1):
|
|
45
|
+
spans.append(Span('word', m.group(1)))
|
|
46
|
+
else:
|
|
47
|
+
spans.append(Span('space', m.group(2)))
|
|
48
|
+
p = r
|
|
49
|
+
if p < len(s):
|
|
50
|
+
spans.append(Span('symbol', s[p:]))
|
|
51
|
+
return spans
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _main() -> None:
|
|
55
|
+
print(split_line_spans('hi i am a string! this has a hy-phen.'))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == '__main__':
|
|
59
|
+
_main()
|
omlish/text/textwrap.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
##
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dc.dataclass(frozen=True)
|
|
8
|
+
class TextwrapOpts:
|
|
9
|
+
# The maximum width of wrapped lines (unless break_long_words is false).
|
|
10
|
+
width: int = 70 # noqa
|
|
11
|
+
|
|
12
|
+
_: dc.KW_ONLY
|
|
13
|
+
|
|
14
|
+
# String that will be prepended to the first line of wrapped output. Counts towards the line's width.
|
|
15
|
+
initial_indent: str = ''
|
|
16
|
+
|
|
17
|
+
# String that will be prepended to all lines save the first of wrapped output; also counts towards each line's
|
|
18
|
+
# width.
|
|
19
|
+
subsequent_indent: str = ''
|
|
20
|
+
|
|
21
|
+
# Expand tabs in input text to spaces before further processing. Each tab will become 0 .. 'tabsize' spaces,
|
|
22
|
+
# depending on its position in its line. If false, each tab is treated as a single character.
|
|
23
|
+
expand_tabs: bool = True
|
|
24
|
+
|
|
25
|
+
# Expand tabs in input text to 0 .. 'tabsize' spaces, unless 'expand_tabs' is false.
|
|
26
|
+
tabsize: int = 8
|
|
27
|
+
|
|
28
|
+
# Replace all whitespace characters in the input text by spaces after tab expansion. Note that if expand_tabs is
|
|
29
|
+
# false and replace_whitespace is true, every tab will be converted to a single space!
|
|
30
|
+
replace_whitespace: bool = True
|
|
31
|
+
|
|
32
|
+
# Ensure that sentence-ending punctuation is always followed by two spaces. Off by default because the algorithm is
|
|
33
|
+
# (unavoidably) imperfect.
|
|
34
|
+
fix_sentence_endings: bool = False
|
|
35
|
+
|
|
36
|
+
# Break words longer than 'width'. If false, those words will not be broken, and some lines might be longer than
|
|
37
|
+
# 'width'.
|
|
38
|
+
break_long_words: bool = True
|
|
39
|
+
|
|
40
|
+
# Allow breaking hyphenated words. If true, wrapping will occur preferably on whitespaces and right after hyphens
|
|
41
|
+
# part of compound words.
|
|
42
|
+
break_on_hyphens: bool = True
|
|
43
|
+
|
|
44
|
+
# Drop leading and trailing whitespace from lines.
|
|
45
|
+
drop_whitespace: bool = True
|
|
46
|
+
|
|
47
|
+
# Truncate wrapped lines.
|
|
48
|
+
max_lines: int | None = None
|
|
49
|
+
|
|
50
|
+
# Append to the last line of truncated text.
|
|
51
|
+
placeholder: str = ' [...]'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: omlish
|
|
3
|
-
Version: 0.0.0.
|
|
3
|
+
Version: 0.0.0.dev474
|
|
4
4
|
Summary: omlish
|
|
5
5
|
Author: wrmsr
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -18,7 +18,7 @@ Provides-Extra: all
|
|
|
18
18
|
Requires-Dist: anyio~=4.11; extra == "all"
|
|
19
19
|
Requires-Dist: sniffio~=1.3; extra == "all"
|
|
20
20
|
Requires-Dist: greenlet~=3.2; extra == "all"
|
|
21
|
-
Requires-Dist: trio~=0.
|
|
21
|
+
Requires-Dist: trio~=0.32; extra == "all"
|
|
22
22
|
Requires-Dist: trio-asyncio~=0.15; extra == "all"
|
|
23
23
|
Requires-Dist: lz4~=4.4; extra == "all"
|
|
24
24
|
Requires-Dist: python-snappy~=0.7; extra == "all"
|
|
@@ -58,7 +58,7 @@ Provides-Extra: async
|
|
|
58
58
|
Requires-Dist: anyio~=4.11; extra == "async"
|
|
59
59
|
Requires-Dist: sniffio~=1.3; extra == "async"
|
|
60
60
|
Requires-Dist: greenlet~=3.2; extra == "async"
|
|
61
|
-
Requires-Dist: trio~=0.
|
|
61
|
+
Requires-Dist: trio~=0.32; extra == "async"
|
|
62
62
|
Requires-Dist: trio-asyncio~=0.15; extra == "async"
|
|
63
63
|
Provides-Extra: compress
|
|
64
64
|
Requires-Dist: lz4~=4.4; extra == "compress"
|
|
@@ -234,7 +234,8 @@ dependencies of any kind**.
|
|
|
234
234
|
- **[queries](https://github.com/wrmsr/omlish/blob/master/omlish/sql/queries)** - A SQL query builder with a fluent
|
|
235
235
|
interface.
|
|
236
236
|
- **[alchemy](https://github.com/wrmsr/omlish/blob/master/omlish/sql/alchemy)** - SQLAlchemy utilities. The codebase
|
|
237
|
-
|
|
237
|
+
has moved away from SQLAlchemy in favor of its own internal SQL api, but it will likely still remain as an optional
|
|
238
|
+
dep for the api adapter.
|
|
238
239
|
|
|
239
240
|
- **[testing](https://github.com/wrmsr/omlish/blob/master/omlish/testing)** - Test - primarily pytest - helpers,
|
|
240
241
|
including:
|
|
@@ -294,8 +295,8 @@ examples are:
|
|
|
294
295
|
- **pytest** - What is used for all standard testing - as lite code has no dependencies of any kind its testing uses
|
|
295
296
|
stdlib's [unittest](https://docs.python.org/3/library/unittest.html).
|
|
296
297
|
- **wrapt** - For (optionally-enabled) injector circular proxies.
|
|
297
|
-
- **sqlalchemy** -
|
|
298
|
-
|
|
298
|
+
- **sqlalchemy** - The codebase has migrated away from SQLAlchemy in favor of the internal api but it retains it as an
|
|
299
|
+
optional dep to support adapting the internal api to it.
|
|
299
300
|
|
|
300
301
|
Additionally, some catchall dep categories include:
|
|
301
302
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
omlish/.omlish-manifests.json,sha256=FLw7xkPiSXuImZgqSP8BwrEib2R1doSzUPLUkc-QUIA,8410
|
|
2
|
-
omlish/__about__.py,sha256=
|
|
2
|
+
omlish/__about__.py,sha256=35_ul-npqd8WV5dBJiHcOowqXVDo-ayDrD_0zvxyImg,3611
|
|
3
3
|
omlish/__init__.py,sha256=SsyiITTuK0v74XpKV8dqNaCmjOlan1JZKrHQv5rWKPA,253
|
|
4
4
|
omlish/c3.py,sha256=ZNIMl1kwg3qdei4DiUrJPQe5M81S1e76N-GuNSwLBAE,8683
|
|
5
5
|
omlish/cached.py,sha256=MLap_p0rdGoDIMVhXVHm1tsbcWobJF0OanoodV03Ju8,542
|
|
@@ -202,17 +202,18 @@ omlish/dataclasses/tools/static.py,sha256=byDnPc0W15s3YPQNC9oyCASKVNxCytrHGLvDky
|
|
|
202
202
|
omlish/diag/__init__.py,sha256=c1q8vuapGH1YiYdU300FIJXMI1MOcnLNBZXr-zc8BXk,1181
|
|
203
203
|
omlish/diag/asts.py,sha256=MWh9XAG3m9L10FIJCyoNT2aU4Eft6tun_x9K0riq6Dk,3332
|
|
204
204
|
omlish/diag/debug.py,sha256=ClED7kKXeVMyKrjGIxcq14kXk9kvUJfytBQwK9y7c4Q,1637
|
|
205
|
-
omlish/diag/lslocks.py,sha256=VuA4MNNqXTcnHWsJvulGM6pNJRKmlXFXUZTfXpY0V6g,1750
|
|
206
|
-
omlish/diag/lsof.py,sha256=5N5aZQ7UqEBgV-hj3_a8QcvALOeLlVb8otqF2hvucxY,9107
|
|
207
205
|
omlish/diag/procfs.py,sha256=eeB3L9UpNBpAfsax3U6OczayYboPlFzOGplqlQ4gBNY,9700
|
|
208
206
|
omlish/diag/procstats.py,sha256=EJEe2Zc58ykBoTfqMXro7H52aQa_pd6uC2hsIPFceso,825
|
|
209
|
-
omlish/diag/ps.py,sha256=MEpMU6fbkh0bSWrOHh_okOa0JDTUSUQUVSYBdh1TGvE,1672
|
|
210
207
|
omlish/diag/pycharm.py,sha256=_WVmPm1E66cBtR4ukgUAaApe_3rX9Cv3sQRP5PL37P8,5013
|
|
211
208
|
omlish/diag/pydevd.py,sha256=JEHC0hiSJxL-vVtuxKUi1BNkgUKnRC1N4alDg6JP5WQ,8363
|
|
212
209
|
omlish/diag/threads.py,sha256=sjtlTl41wxssoVCDkBB6xeLF-9kJEK3eA6hmSFWJSQA,3643
|
|
213
210
|
omlish/diag/timers.py,sha256=cxX3GgjTIjBx9DI4pzCCO5Hfqb1TM3uo22yim7kjfRU,3831
|
|
214
211
|
omlish/diag/_pycharm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
215
212
|
omlish/diag/_pycharm/runhack.py,sha256=IxawPyqrPfY1fpV1D_9dZXxny89fVSR3RlmqlbaRctw,37869
|
|
213
|
+
omlish/diag/cmds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
214
|
+
omlish/diag/cmds/lslocks.py,sha256=iEYe3OqcbMCKg1NbtO2EsYsnr2GnDYnNdi6Cbb_zEKM,1756
|
|
215
|
+
omlish/diag/cmds/lsof.py,sha256=hmrEZJNY0rQxa42t2Y3e3w5ZVe8rJqO2eI7_8HmzZEQ,9113
|
|
216
|
+
omlish/diag/cmds/ps.py,sha256=Ff0JusuXhJqpZ4w5cpOwovoRftQUtaKjT5YTtHfazl4,1678
|
|
216
217
|
omlish/diag/replserver/__init__.py,sha256=uLo6V2aQ29v9z3IMELlPDSlG3_2iOT4-_X8VniF-EgE,235
|
|
217
218
|
omlish/diag/replserver/__main__.py,sha256=LmU41lQ58bm1h4Mx7S8zhE_uEBSC6kPcp9mn5JRpulA,32
|
|
218
219
|
omlish/diag/replserver/console.py,sha256=MxDLslAaYfC-l0rfGO-SnYAJ__8RWBJQ5FxGi29czYU,7438
|
|
@@ -325,15 +326,15 @@ omlish/http/wsgi.py,sha256=1JpfrY2JrQ0wrEVE0oLdQMWZw8Zcx0b4_9f3VmH4JKA,1070
|
|
|
325
326
|
omlish/http/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
326
327
|
omlish/http/clients/asyncs.py,sha256=QrbavgXv4BuQFW0bRojy-dxa_jpxjPVLA7eyfo1zzns,4230
|
|
327
328
|
omlish/http/clients/base.py,sha256=bm3Oy2wuunSO3Rd2dxCmhHoZcQ8X9B5pkzaudbjZKvI,2810
|
|
328
|
-
omlish/http/clients/default.py,sha256=
|
|
329
|
-
omlish/http/clients/executor.py,sha256=
|
|
330
|
-
omlish/http/clients/httpx.py,sha256=
|
|
329
|
+
omlish/http/clients/default.py,sha256=yQ7kOxlU-Dds8NdIJszeHkgnxtiJd6W-d0oUhKXTaYI,5653
|
|
330
|
+
omlish/http/clients/executor.py,sha256=r66xoGWdRsaSpfGtx8ovf3pOMG3k4UONlZEQZ58KAS0,1854
|
|
331
|
+
omlish/http/clients/httpx.py,sha256=T1Pa9Of6GylTmt9AGp8D6Uni3HxOtP7OPsImo35Kc1o,4029
|
|
331
332
|
omlish/http/clients/middleware.py,sha256=xnXlNN1MU4sUKtkhPzdpRQsRh9wpLf1r7OGKjqGzLWg,5058
|
|
332
333
|
omlish/http/clients/sync.py,sha256=jVbqbUlruuaNFKUYQr55kVF6bwcIZgeaLyLclXM_T5w,3891
|
|
333
|
-
omlish/http/clients/syncasync.py,sha256=
|
|
334
|
+
omlish/http/clients/syncasync.py,sha256=nhQlcds4BWib_gphhGElUdA_0uJ0tD1lV1At-fr13Q0,1401
|
|
334
335
|
omlish/http/clients/urllib.py,sha256=c8TQITZYRo5RrdxaNhsE2QoeDw4mZW0wLzoIsWYTYwU,2852
|
|
335
336
|
omlish/http/clients/coro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
336
|
-
omlish/http/clients/coro/sync.py,sha256=
|
|
337
|
+
omlish/http/clients/coro/sync.py,sha256=Fk1KdJf9q6ba2-7HWzj7Tb-ibBuj5VIV5f4SCziP-C8,5762
|
|
337
338
|
omlish/http/coro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
338
339
|
omlish/http/coro/io.py,sha256=d97Oa7KlgMP7BQWdhUbOv3sfA96uWyMtjJF7lJFwZC0,1090
|
|
339
340
|
omlish/http/coro/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -390,10 +391,10 @@ omlish/inject/impl/scopes.py,sha256=sqtjHe6g0pAEOHqIARXGiIE9mKmGmlqh1FDpskV4TIw,
|
|
|
390
391
|
omlish/inject/impl/sync.py,sha256=_aLaC-uVNHOePcfE1as61Ni7fuyHZpjEafIWBc7FvC0,1049
|
|
391
392
|
omlish/io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
392
393
|
omlish/io/abc.py,sha256=M40QB2udYpCEqmlxCcHv6FlJYJY6ymmJQBlaklYv0U8,1256
|
|
393
|
-
omlish/io/buffers.py,sha256=
|
|
394
|
+
omlish/io/buffers.py,sha256=eeDSG0mhwx1IAYPu3pSe-ce3LG5WpQz924OsRyLu0mI,10935
|
|
394
395
|
omlish/io/fileno.py,sha256=_W3qxkIKpnabn1_7kgmKdx0IsPF3R334xWnF_TtkEj4,185
|
|
395
396
|
omlish/io/pyio.py,sha256=xmHTV-sn7QZThWCVBo6lTM7dhgsQn7m2L0DqRwdF2-8,94509
|
|
396
|
-
omlish/io/readers.py,sha256=
|
|
397
|
+
omlish/io/readers.py,sha256=w67uZQbWzed4Ton2kyA_WRrRz-IRXmFip8EJtpAxLYU,583
|
|
397
398
|
omlish/io/compress/__init__.py,sha256=fJFPT4ONfqxmsA4jR6qbMt2woIyyEgnc_qOWK9o1kII,247
|
|
398
399
|
omlish/io/compress/abc.py,sha256=P9YoQX8XYoq2UfBsinKLUuwwqV1ODUIJzjTraRWGF1M,3090
|
|
399
400
|
omlish/io/compress/adapters.py,sha256=LJHhjwMHXstoLyX_q0QhGoBAcqyYGWfzhzQbGBXHzHY,6148
|
|
@@ -635,7 +636,7 @@ omlish/os/pidfiles/__main__.py,sha256=nfjVxFY9I2I7hfb81rCxX6zm2Rc3P87ySpUouy33vG
|
|
|
635
636
|
omlish/os/pidfiles/cli.py,sha256=dAKukx4mrarH8t7KZM4n_XSfTk3ycShGAFqRrirK5dw,2079
|
|
636
637
|
omlish/os/pidfiles/manager.py,sha256=oOnL90ttOG5ba_k9XPJ18reP7M5S-_30ln4OAfYV5W8,2357
|
|
637
638
|
omlish/os/pidfiles/pidfile.py,sha256=KlqrW7I-lYVDNJspFHE89i_A0HTTteT5B99b7vpTfNs,4400
|
|
638
|
-
omlish/os/pidfiles/pinning.py,sha256=
|
|
639
|
+
omlish/os/pidfiles/pinning.py,sha256=JJBpW-9cM9Jv_2l0LP4vXQhpx8EnjFaLUnt1FnxrKhw,6676
|
|
639
640
|
omlish/reflect/__init__.py,sha256=omD3VLFQtYTwPxrTH6gLATQIax9sTGGKc-7ps96-q0g,1202
|
|
640
641
|
omlish/reflect/inspect.py,sha256=dUrVz8VfAdLVFtFpW416DxzVC17D80cvFb_g2Odzgq8,1823
|
|
641
642
|
omlish/reflect/ops.py,sha256=4mGvFMw5D6XGbhDhdCqZ1dZsiqezUTenDv63FIDohpY,3029
|
|
@@ -831,6 +832,16 @@ omlish/text/minja.py,sha256=7UKNalkWpTG_364OIo7p5ym--uiNPR2RFBW_W8rrO4I,9194
|
|
|
831
832
|
omlish/text/parts.py,sha256=MFi-ANxXlBiOMqW2y2S0BRg6xDwPYxSXSfrYPM3rtcE,6669
|
|
832
833
|
omlish/text/random.py,sha256=8feS5JE_tSjYlMl-lp0j93kCfzBae9AM2cXlRLebXMA,199
|
|
833
834
|
omlish/text/templating.py,sha256=i-HU7W-GtKdnBhEG3mnSey5ig7vV0q02D3pVzMoOzJY,3318
|
|
835
|
+
omlish/text/textwrap.py,sha256=fTe04E4T5MpMGLz5QOTfT8T93FdiIT1tohEpzL28_zI,1874
|
|
836
|
+
omlish/text/docwrap/__init__.py,sha256=NQh0gVyJSxUOAX7nnwsAOwHVPeuZ8abVaN5topQI6gE,42
|
|
837
|
+
omlish/text/docwrap/api.py,sha256=cePtl2fLb1FwWN0z6dL5CquAt1AiH0aEEiGp7ijF-Wo,1602
|
|
838
|
+
omlish/text/docwrap/groups.py,sha256=8__fk1q3KX7tRBcWQ2RprpFS2p4QYnRtCNOxbUBaOVI,1887
|
|
839
|
+
omlish/text/docwrap/lists.py,sha256=8ZTfXV-hJaTZTf0jAkODke0Z87kZDS5w-xGqNlc0U_c,4693
|
|
840
|
+
omlish/text/docwrap/parts.py,sha256=IaW_qRvIiP3e12OhFgTIa_qKamQX4MAps-BG5CT_dDw,3338
|
|
841
|
+
omlish/text/docwrap/reflowing.py,sha256=F1mrGpNvTPfII8LpnZGCU4aoUxEvh91iL1mfswHB98U,2584
|
|
842
|
+
omlish/text/docwrap/rendering.py,sha256=SmnkLdePgK4qH0f3lGRY94NfRKJNdTlUaTgzo6RjEnM,2848
|
|
843
|
+
omlish/text/docwrap/utils.py,sha256=n4oFWgA9PBC_77vYrH8H7uFR2s8bcFqDtrCWTcyhGFw,188
|
|
844
|
+
omlish/text/docwrap/wrapping.py,sha256=VyECcYTpjPz7SidT8PJi7mLTg4bGLzaME1H3G5Ub0Yg,1314
|
|
834
845
|
omlish/text/go/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
835
846
|
omlish/text/go/quoting.py,sha256=zbcPEDWsdj7GAemtu0x8nwMqpIu2C_5iInSwn6mMWlE,9142
|
|
836
847
|
omlish/typedvalues/__init__.py,sha256=dQpM8VaON8S7dUv1LBwhyBDjUVo7EW475a9DpnDQz1E,936
|
|
@@ -843,9 +854,9 @@ omlish/typedvalues/marshal.py,sha256=2xqX6JllhtGpmeYkU7C-qzgU__0x-vd6CzYbAsocQlc
|
|
|
843
854
|
omlish/typedvalues/of_.py,sha256=UXkxSj504WI2UrFlqdZJbu2hyDwBhL7XVrc2qdR02GQ,1309
|
|
844
855
|
omlish/typedvalues/reflect.py,sha256=PAvKW6T4cW7u--iX80w3HWwZUS3SmIZ2_lQjT65uAyk,1026
|
|
845
856
|
omlish/typedvalues/values.py,sha256=ym46I-q2QJ_6l4UlERqv3yj87R-kp8nCKMRph0xQ3UA,1307
|
|
846
|
-
omlish-0.0.0.
|
|
847
|
-
omlish-0.0.0.
|
|
848
|
-
omlish-0.0.0.
|
|
849
|
-
omlish-0.0.0.
|
|
850
|
-
omlish-0.0.0.
|
|
851
|
-
omlish-0.0.0.
|
|
857
|
+
omlish-0.0.0.dev474.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
|
858
|
+
omlish-0.0.0.dev474.dist-info/METADATA,sha256=sTrBg5y3IV8XQuez3bmFN_mIxOqGGArfERXAKffXCFw,19032
|
|
859
|
+
omlish-0.0.0.dev474.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
860
|
+
omlish-0.0.0.dev474.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
|
|
861
|
+
omlish-0.0.0.dev474.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
|
|
862
|
+
omlish-0.0.0.dev474.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|