chide 3.0.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.
- chide/__init__.py +5 -0
- chide/collection.py +80 -0
- chide/factory.py +54 -0
- chide/formats.py +475 -0
- chide/set.py +71 -0
- chide/simplifiers.py +49 -0
- chide/sqlalchemy.py +57 -0
- chide/typing.py +7 -0
- chide-3.0.0.dist-info/LICENSE.txt +21 -0
- chide-3.0.0.dist-info/METADATA +94 -0
- chide-3.0.0.dist-info/RECORD +13 -0
- chide-3.0.0.dist-info/WHEEL +5 -0
- chide-3.0.0.dist-info/top_level.txt +1 -0
chide/__init__.py
ADDED
chide/collection.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import Type, Any, TypeVar, Callable
|
|
2
|
+
|
|
3
|
+
from .factory import Factory
|
|
4
|
+
from .simplifiers import ObjectSimplifier, Simplifier
|
|
5
|
+
from .typing import Attrs
|
|
6
|
+
|
|
7
|
+
T = TypeVar('T')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Collection:
|
|
11
|
+
"""
|
|
12
|
+
A collection of attributes to use to make sample objects.
|
|
13
|
+
|
|
14
|
+
:param mapping:
|
|
15
|
+
A dictionary mapping object types to a dictionary
|
|
16
|
+
of attributes to make a sample object of that type.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, mapping: dict[Type[Any], Attrs] | None = None) -> None:
|
|
21
|
+
self.mapping = mapping or {}
|
|
22
|
+
|
|
23
|
+
def _attrs(
|
|
24
|
+
self,
|
|
25
|
+
type_: Type[Any],
|
|
26
|
+
attrs: Attrs,
|
|
27
|
+
nest: Callable[[Type[T]], T]
|
|
28
|
+
) -> Attrs:
|
|
29
|
+
computed_attrs = dict(self.mapping[type_])
|
|
30
|
+
for key, value in computed_attrs.items():
|
|
31
|
+
try:
|
|
32
|
+
value_in_mapping = value in self.mapping
|
|
33
|
+
except TypeError:
|
|
34
|
+
value_in_mapping = False
|
|
35
|
+
if value_in_mapping and key not in attrs:
|
|
36
|
+
computed_attrs[key] = nest(value)
|
|
37
|
+
computed_attrs.update(attrs)
|
|
38
|
+
return computed_attrs
|
|
39
|
+
|
|
40
|
+
def add(self, obj: T, simplifier: Simplifier[T] = ObjectSimplifier()) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Add the attributes from the supplied object to this collection and
|
|
43
|
+
use them when samples of the type of that object are required.
|
|
44
|
+
|
|
45
|
+
:param obj: The sample object from which to extract attributes.
|
|
46
|
+
|
|
47
|
+
:param simplifier:
|
|
48
|
+
The :class:`~chide.simplifiers.Simplifier` to use to extract attributes
|
|
49
|
+
from ``obj``.
|
|
50
|
+
"""
|
|
51
|
+
self.mapping[type(obj)] = simplifier.one(obj)
|
|
52
|
+
|
|
53
|
+
def attributes(self, type_: Type[T], **attrs: Any) -> Attrs:
|
|
54
|
+
"""
|
|
55
|
+
Make the attributes for a sample object of the specified ``type_``
|
|
56
|
+
using the default attributes for that type in this :class:`Collection`.
|
|
57
|
+
|
|
58
|
+
The ``attrs`` mapping will be overlaid onto the sample attributes
|
|
59
|
+
and returned as a :class:`dict`.
|
|
60
|
+
"""
|
|
61
|
+
return self._attrs(type_, attrs, self.make)
|
|
62
|
+
|
|
63
|
+
def make(self, type_: Type[T], **attrs: Any) -> T:
|
|
64
|
+
"""
|
|
65
|
+
Make a sample object of the specified ``type_`` using the default
|
|
66
|
+
attributes for that type in this :class:`Collection`.
|
|
67
|
+
|
|
68
|
+
The ``attrs`` mapping will be overlaid onto the sample attributes
|
|
69
|
+
before being used with ``type_`` to instantiate and return a new
|
|
70
|
+
sample object.
|
|
71
|
+
"""
|
|
72
|
+
return type_(**self.attributes(type_, **attrs))
|
|
73
|
+
|
|
74
|
+
def bind(self, type_: Type[T], **attrs: Any) -> Factory[T]:
|
|
75
|
+
"""
|
|
76
|
+
Bind the supplied attributes into a :class:`~chide.factory.Factory` for the
|
|
77
|
+
requested ``type_`` by overlaying them onto the sample attributes
|
|
78
|
+
for that ``type_`` in this :class:`Collection`.
|
|
79
|
+
"""
|
|
80
|
+
return Factory[T](self, type_, attrs)
|
chide/factory.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import TypeVar, Generic, Any, Self, Type, TYPE_CHECKING, Callable
|
|
2
|
+
|
|
3
|
+
from .typing import Attrs
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .collection import Collection
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Factory(Generic[T]):
|
|
12
|
+
"""
|
|
13
|
+
A factory for objects of a particular type.
|
|
14
|
+
These are created by either :meth:`chide.Collection.bind` or :meth:`chide.factory.Factory.bind`.
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self, collection: 'Collection', type_: Type[T], attrs: dict[str, Any]) -> None:
|
|
17
|
+
self.collection = collection
|
|
18
|
+
self.type_ = type_
|
|
19
|
+
self.attrs = attrs
|
|
20
|
+
|
|
21
|
+
def _combine(self, attrs: Attrs) -> Attrs:
|
|
22
|
+
attrs_ = self.attrs.copy()
|
|
23
|
+
attrs_.update(attrs)
|
|
24
|
+
return attrs_
|
|
25
|
+
|
|
26
|
+
def attributes(self, **attrs: Any) -> Attrs:
|
|
27
|
+
"""
|
|
28
|
+
Make the attributes for a sample object of this factory's type
|
|
29
|
+
using the default attributes for the type in this factory's :class:`Collection`
|
|
30
|
+
overlaid with any attributes bound into the factory.
|
|
31
|
+
|
|
32
|
+
The ``attrs`` mapping will be overlaid onto those attributes
|
|
33
|
+
and returned as a :class:`dict`.
|
|
34
|
+
"""
|
|
35
|
+
return self.collection.attributes(self.type_, **self._combine(attrs))
|
|
36
|
+
|
|
37
|
+
def make(self, **attrs: Any) -> T:
|
|
38
|
+
"""
|
|
39
|
+
Make a sample object of this factory's type using the default
|
|
40
|
+
attributes for the type in this factory's :class:`Collection`
|
|
41
|
+
overlaid with any attributes bound into the factory.
|
|
42
|
+
|
|
43
|
+
The ``attrs`` mapping will be overlaid onto the sample attributes
|
|
44
|
+
before being used to instantiate and return a new
|
|
45
|
+
sample object of this factory's type.
|
|
46
|
+
"""
|
|
47
|
+
return self.collection.make(self.type_, **self._combine(attrs))
|
|
48
|
+
|
|
49
|
+
def bind(self, **attrs: Any) -> Self:
|
|
50
|
+
"""
|
|
51
|
+
Bind the supplied attributes into a new :class:`~chide.factory.Factory`
|
|
52
|
+
by overlaying them onto the sample attributes this factory would use.
|
|
53
|
+
"""
|
|
54
|
+
return type(self)(self.collection, self.type_, self._combine(attrs))
|
chide/formats.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import csv
|
|
3
|
+
import re
|
|
4
|
+
from ast import literal_eval
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from itertools import zip_longest
|
|
8
|
+
from textwrap import dedent
|
|
9
|
+
from typing import Protocol, Iterable, Type, Callable, Any, TypeVar, TypeAlias, Mapping
|
|
10
|
+
|
|
11
|
+
from .typing import Attrs
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Format(Protocol):
|
|
17
|
+
"""
|
|
18
|
+
Protocol for :doc:`formats <formats>`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def parse(self, text: str) -> list[Attrs]:
|
|
22
|
+
"""
|
|
23
|
+
Parse the supplied ``text`` into a list of :class:`~chide.typing.Attrs`.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def render(self, attrs: Iterable[Attrs]) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Render the supplied :class:`~chide.typing.Attrs` into a :class:`str`.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
#: Type of a callable to parse a value from a :class:`str`.
|
|
33
|
+
ValueParse = Callable[[str], Any]
|
|
34
|
+
#: Type of a callable to render a value into a :class:`str`.
|
|
35
|
+
ValueRender = Callable[[Any], str]
|
|
36
|
+
|
|
37
|
+
#: A mapping of column or type names to the :class:`ValueParse` to use.
|
|
38
|
+
ParseMapping: TypeAlias = dict[str, ValueParse]
|
|
39
|
+
#: A mapping of data types to the :class:`ValueRender` to use.
|
|
40
|
+
TypeRenderMapping: TypeAlias = dict[Type[Any], ValueRender]
|
|
41
|
+
#: A mapping of data types to the name to use for them when rendering a table.
|
|
42
|
+
TypeNameMapping: TypeAlias = dict[Type[Any], str | None]
|
|
43
|
+
#: A mapping of column name to the :class:`ValueRender` to use.
|
|
44
|
+
ColumnRenderMapping: TypeAlias = dict[str, ValueRender]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def default_parse(text: str) -> Any:
|
|
48
|
+
"""
|
|
49
|
+
The default :class:`ValueParse`.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
return literal_eval(text)
|
|
53
|
+
except SyntaxError:
|
|
54
|
+
return text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
NEEDS_REPR = re.compile(r'^(\s.+|.+\s)$')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def default_render(value: Any) -> str:
|
|
61
|
+
"""
|
|
62
|
+
The default :class:`ValueRender`.
|
|
63
|
+
"""
|
|
64
|
+
rendered = str(value)
|
|
65
|
+
if NEEDS_REPR.match(rendered):
|
|
66
|
+
rendered = repr(rendered)
|
|
67
|
+
return rendered
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TypeLocation(Enum):
|
|
71
|
+
#: The types are located in parentheses, after the column name, in the
|
|
72
|
+
#: row containing the column names.
|
|
73
|
+
HEADER = auto()
|
|
74
|
+
#: The types are located in their own row.
|
|
75
|
+
ROW = auto()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
#: Shortcut for :any:`TypeLocation.HEADER`.
|
|
79
|
+
HEADER = TypeLocation.HEADER
|
|
80
|
+
#: Shortcut for :any:`TypeLocation.ROW`.
|
|
81
|
+
ROW = TypeLocation.ROW
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TabularFormat(Format):
|
|
85
|
+
"""
|
|
86
|
+
A base class for tabular formats.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
header_type_pattern = re.compile(r'([^ (]+) *\((.+)\) *')
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
type_parse: ParseMapping | None = None,
|
|
94
|
+
default_type_parse: ValueParse = default_parse,
|
|
95
|
+
column_parse: ParseMapping | None = None,
|
|
96
|
+
type_render: TypeRenderMapping | None = None,
|
|
97
|
+
default_type_render: ValueRender = default_render,
|
|
98
|
+
type_names: TypeNameMapping | None = None,
|
|
99
|
+
column_render: ColumnRenderMapping | None = None,
|
|
100
|
+
types_location: TypeLocation | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
self.type_parse: ParseMapping = type_parse or {}
|
|
103
|
+
self.column_parse: ParseMapping = column_parse or {}
|
|
104
|
+
self.default_type_parse = default_type_parse
|
|
105
|
+
self.type_render: TypeRenderMapping = type_render or {}
|
|
106
|
+
self.type_names: TypeNameMapping = type_names or {}
|
|
107
|
+
self.column_render: ColumnRenderMapping = column_render or {}
|
|
108
|
+
self.default_type_render = default_type_render
|
|
109
|
+
self.types_location = types_location
|
|
110
|
+
|
|
111
|
+
def _resolve_type_names(self, type_names: dict[str, str]) -> None:
|
|
112
|
+
for column, name in type_names.items():
|
|
113
|
+
if name:
|
|
114
|
+
if name not in self.column_parse:
|
|
115
|
+
handler = self.type_parse.get(name)
|
|
116
|
+
if handler is None:
|
|
117
|
+
handler = getattr(builtins, name)
|
|
118
|
+
self.column_parse[column] = handler
|
|
119
|
+
|
|
120
|
+
def _parse(self, text: str, lexer: Callable[[str], Iterable[Iterable[str]]]) -> list[Attrs]:
|
|
121
|
+
parsed = []
|
|
122
|
+
columns: list[str] | None = None
|
|
123
|
+
types_row_handled = self.types_location is not ROW
|
|
124
|
+
types_row_next = False
|
|
125
|
+
for parts in lexer(text):
|
|
126
|
+
|
|
127
|
+
if columns is not None and not types_row_handled:
|
|
128
|
+
types_row_next = True
|
|
129
|
+
|
|
130
|
+
if columns is None:
|
|
131
|
+
columns = []
|
|
132
|
+
type_names = {}
|
|
133
|
+
for c in parts:
|
|
134
|
+
if (
|
|
135
|
+
self.types_location is HEADER
|
|
136
|
+
and (match := self.header_type_pattern.match(c))
|
|
137
|
+
):
|
|
138
|
+
column, t = match.groups()
|
|
139
|
+
type_names[column] = t
|
|
140
|
+
else:
|
|
141
|
+
column = c
|
|
142
|
+
columns.append(column)
|
|
143
|
+
self._resolve_type_names(type_names)
|
|
144
|
+
elif types_row_next:
|
|
145
|
+
self._resolve_type_names(type_names={c: t for c, t in zip(columns, parts)})
|
|
146
|
+
types_row_handled = True
|
|
147
|
+
types_row_next = False
|
|
148
|
+
else:
|
|
149
|
+
row = {}
|
|
150
|
+
for column, value in zip(columns, parts):
|
|
151
|
+
try:
|
|
152
|
+
handler = self.column_parse.get(column, self.default_type_parse)
|
|
153
|
+
value = handler(value)
|
|
154
|
+
except ValueError:
|
|
155
|
+
pass
|
|
156
|
+
row[column] = value
|
|
157
|
+
parsed.append(row)
|
|
158
|
+
return parsed
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Widths(dict[str, int]):
|
|
162
|
+
|
|
163
|
+
def handle(self, item: Mapping[str, str | int]) -> None:
|
|
164
|
+
for column, text_or_width in item.items():
|
|
165
|
+
width = text_or_width if isinstance(text_or_width, int) else len(text_or_width)
|
|
166
|
+
self[column] = max(width, self.get(column, 0))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class RenderedRows(list[dict[str, str]]):
|
|
170
|
+
columns: list[str] | None = None
|
|
171
|
+
types: dict[str, str] | None = None
|
|
172
|
+
header: dict[str, str]
|
|
173
|
+
|
|
174
|
+
def __init__(
|
|
175
|
+
self, attrs: Iterable[Attrs], format_: 'TabularFormat', columns: list[str] | None = None
|
|
176
|
+
) -> None:
|
|
177
|
+
super().__init__()
|
|
178
|
+
for attrs_ in attrs:
|
|
179
|
+
if self.columns is None:
|
|
180
|
+
attr_columns = list(attrs_.keys())
|
|
181
|
+
if columns is None:
|
|
182
|
+
self.columns = attr_columns
|
|
183
|
+
else:
|
|
184
|
+
self.columns = columns + [c for c in attr_columns if c not in columns]
|
|
185
|
+
if self.types is None:
|
|
186
|
+
self.types = {}
|
|
187
|
+
for column, value in attrs_.items():
|
|
188
|
+
type_ = type(value)
|
|
189
|
+
type_name = format_.type_names.get(type_, type(value).__name__)
|
|
190
|
+
self.types[column] = type_name or ''
|
|
191
|
+
row = {}
|
|
192
|
+
for column in self.columns:
|
|
193
|
+
value = attrs_.get(column)
|
|
194
|
+
handler = format_.column_render.get(column)
|
|
195
|
+
if handler is None:
|
|
196
|
+
handler = format_.type_render.get(type(value), format_.default_type_render)
|
|
197
|
+
text = handler(value)
|
|
198
|
+
row[column] = text
|
|
199
|
+
self.append(row)
|
|
200
|
+
|
|
201
|
+
self.header = {}
|
|
202
|
+
if self.columns is not None:
|
|
203
|
+
for column in self.columns:
|
|
204
|
+
text = column
|
|
205
|
+
if self.types is not None and (type_name := self.types.get(column)) is not None:
|
|
206
|
+
if format_.types_location is HEADER and type_name:
|
|
207
|
+
text = f'{text} ({type_name})'
|
|
208
|
+
self.header[column] = text
|
|
209
|
+
|
|
210
|
+
def update(self, widths: Widths, types_location: TypeLocation | None) -> None:
|
|
211
|
+
if self.header:
|
|
212
|
+
widths.handle(self.header)
|
|
213
|
+
if self.types is not None and types_location is ROW:
|
|
214
|
+
widths.handle(self.types)
|
|
215
|
+
for row in self:
|
|
216
|
+
widths.handle(row)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class PrettyLexer:
|
|
220
|
+
|
|
221
|
+
def __init__(self, padding: int):
|
|
222
|
+
self.widths: list[int] = []
|
|
223
|
+
self.padding = padding
|
|
224
|
+
|
|
225
|
+
def __call__(self, text: str) -> Iterable[list[str]]:
|
|
226
|
+
padding_text = ' ' * self.padding
|
|
227
|
+
padding_size = self.padding * 2
|
|
228
|
+
for line in dedent(text).splitlines():
|
|
229
|
+
line = line.strip()
|
|
230
|
+
if not line or line.startswith('+'):
|
|
231
|
+
continue
|
|
232
|
+
parts = line.split('|')[1:-1]
|
|
233
|
+
widths = []
|
|
234
|
+
for part in parts:
|
|
235
|
+
width = len(part)
|
|
236
|
+
if (
|
|
237
|
+
width >= padding_size and
|
|
238
|
+
part[:self.padding] == padding_text and
|
|
239
|
+
part[-self.padding:] == padding_text
|
|
240
|
+
):
|
|
241
|
+
width -= padding_size
|
|
242
|
+
widths.append(width)
|
|
243
|
+
if self.widths:
|
|
244
|
+
self.widths = [max(w, w_) for w, w_ in zip_longest(self.widths, widths)]
|
|
245
|
+
else:
|
|
246
|
+
self.widths = widths
|
|
247
|
+
yield [p.strip() for p in parts]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class PrettyParsed(list[Attrs]):
|
|
251
|
+
"""
|
|
252
|
+
A list of :class:`~chide.typing.Attrs` that also keeps track of the :attr:`widths`
|
|
253
|
+
required to render the columns, if they are known.
|
|
254
|
+
"""
|
|
255
|
+
def __init__(self, attrs: list[Attrs], widths: list[int]) -> None:
|
|
256
|
+
super().__init__(attrs)
|
|
257
|
+
#: The widths required for the columns needed by these `~chide.typing.Attrs`.
|
|
258
|
+
self.widths: dict[str, int] = {}
|
|
259
|
+
if attrs:
|
|
260
|
+
for columns, width in zip(attrs[0].keys(), widths):
|
|
261
|
+
self.widths[columns] = width
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class PrettyRenderedRows(list[str]):
|
|
265
|
+
|
|
266
|
+
def __init__(self, widths: Widths, padding: int) -> None:
|
|
267
|
+
super().__init__()
|
|
268
|
+
self.divider = ''.join('+'+'-'*(widths[column]+padding*2) for column in widths)+'+\n'
|
|
269
|
+
pad = padding*' '
|
|
270
|
+
self.templates = {c: f'|{pad}{{:{w}}}{pad}' for c, w in widths.items()}
|
|
271
|
+
self.widths = widths
|
|
272
|
+
|
|
273
|
+
def add_divider(self) -> None:
|
|
274
|
+
self.append(self.divider)
|
|
275
|
+
|
|
276
|
+
def add_row(self, row: dict[str, str]) -> None:
|
|
277
|
+
parts = (self.templates[column].format(value) for column, value in row.items())
|
|
278
|
+
self.append(''.join(parts) + '|\n')
|
|
279
|
+
|
|
280
|
+
def text(self) -> str:
|
|
281
|
+
return ''.join(self)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class PrettyFormat(TabularFormat):
|
|
285
|
+
"""
|
|
286
|
+
A pretty :class:`Format` for tabular data, where tables look something like::
|
|
287
|
+
|
|
288
|
+
+-----+------+
|
|
289
|
+
| x | y |
|
|
290
|
+
+-----+------+
|
|
291
|
+
|float| str |
|
|
292
|
+
+-----+------+
|
|
293
|
+
| 1 | foo |
|
|
294
|
+
| 2 | bar |
|
|
295
|
+
+-----+------+
|
|
296
|
+
|
|
297
|
+
:param type_parse:
|
|
298
|
+
A mapping of type name, as found in either a row or column heading, dependent
|
|
299
|
+
on the ``types_location``, to a function that parses the text of a cell into
|
|
300
|
+
a value.
|
|
301
|
+
|
|
302
|
+
:param default_type_parse:
|
|
303
|
+
The default function to use when parsing the text of a cell into a value.
|
|
304
|
+
|
|
305
|
+
:param column_parse:
|
|
306
|
+
A mapping of column names to functions that will be used to parse the text of cells
|
|
307
|
+
in that column into values.
|
|
308
|
+
|
|
309
|
+
:param type_render:
|
|
310
|
+
A mapping of type objects to functions that will be used to render values of that
|
|
311
|
+
type to text for cells.
|
|
312
|
+
|
|
313
|
+
:param default_type_render:
|
|
314
|
+
The default function to use when rendingering values to text for cells.
|
|
315
|
+
|
|
316
|
+
:param type_names:
|
|
317
|
+
A mapping of type objects to names to use for those types when including types
|
|
318
|
+
in either column headings or their own own. If a type is mapped to ``None``,
|
|
319
|
+
then no type name will be rendered for columns containing date of that type.
|
|
320
|
+
|
|
321
|
+
:param column_render:
|
|
322
|
+
A mapping of column names to functions that will be used to render values in that
|
|
323
|
+
column to text for cells.
|
|
324
|
+
|
|
325
|
+
:param types_location:
|
|
326
|
+
An optional location from which type information will be parsed or to which it
|
|
327
|
+
will be rendered. Must be :any:`HEADER`, :any:`ROW` or ``None``.
|
|
328
|
+
|
|
329
|
+
:param minimum_column_widths:
|
|
330
|
+
An optional mapping of column name to the minimum width to use for a column.
|
|
331
|
+
|
|
332
|
+
:param padding:
|
|
333
|
+
The number of space to put to the left and right of values of cells.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
def __init__(
|
|
337
|
+
self,
|
|
338
|
+
type_parse: ParseMapping | None = None,
|
|
339
|
+
default_type_parse: ValueParse = default_parse,
|
|
340
|
+
column_parse: ParseMapping | None = None,
|
|
341
|
+
type_render: TypeRenderMapping | None = None,
|
|
342
|
+
default_type_render: ValueRender = default_render,
|
|
343
|
+
type_names: TypeNameMapping | None = None,
|
|
344
|
+
column_render: ColumnRenderMapping | None = None,
|
|
345
|
+
types_location: TypeLocation | None = None,
|
|
346
|
+
minimum_column_widths: dict[str, int] | None = None,
|
|
347
|
+
padding: int = 1,
|
|
348
|
+
) -> None:
|
|
349
|
+
super().__init__(
|
|
350
|
+
type_parse,
|
|
351
|
+
default_type_parse,
|
|
352
|
+
column_parse,
|
|
353
|
+
type_render,
|
|
354
|
+
default_type_render,
|
|
355
|
+
type_names,
|
|
356
|
+
column_render,
|
|
357
|
+
types_location,
|
|
358
|
+
)
|
|
359
|
+
self.minimum_column_widths: dict[str, int] = minimum_column_widths or {}
|
|
360
|
+
self.padding = padding
|
|
361
|
+
|
|
362
|
+
def parse(self, text: str) -> PrettyParsed:
|
|
363
|
+
"""
|
|
364
|
+
Parse the supplied ``text`` into a :class:`PrettyParsed`.
|
|
365
|
+
"""
|
|
366
|
+
lexer = PrettyLexer(self.padding)
|
|
367
|
+
rows = self._parse(text, lexer)
|
|
368
|
+
return PrettyParsed(rows, lexer.widths)
|
|
369
|
+
|
|
370
|
+
def render(self, attrs: Iterable[Attrs], ref: list[Attrs] | PrettyParsed | None = None) -> str:
|
|
371
|
+
"""
|
|
372
|
+
Render the supplied :class:`~chide.typing.Attrs` into a :class:`str`.
|
|
373
|
+
|
|
374
|
+
If supplied, ``ref`` is used for reference to make sure:
|
|
375
|
+
|
|
376
|
+
- the reference columns are always present.
|
|
377
|
+
- columns are rendered in the order specified in the reference.
|
|
378
|
+
- columns widths will be at least as wide as those in the reference.
|
|
379
|
+
"""
|
|
380
|
+
columns = None
|
|
381
|
+
widths = Widths(self.minimum_column_widths)
|
|
382
|
+
|
|
383
|
+
if ref is not None:
|
|
384
|
+
ref_widths = getattr(ref, 'widths', None)
|
|
385
|
+
if ref_widths is None:
|
|
386
|
+
ref_rows = RenderedRows(ref, self)
|
|
387
|
+
ref_rows.update(widths, self.types_location)
|
|
388
|
+
columns = ref_rows.columns
|
|
389
|
+
else:
|
|
390
|
+
widths.handle(ref_widths)
|
|
391
|
+
columns = list(ref_widths)
|
|
392
|
+
|
|
393
|
+
rows = RenderedRows(attrs, self, columns)
|
|
394
|
+
rows.update(widths, self.types_location)
|
|
395
|
+
|
|
396
|
+
rendered = PrettyRenderedRows(widths, self.padding)
|
|
397
|
+
rendered.add_divider()
|
|
398
|
+
if rows.header:
|
|
399
|
+
rendered.add_row(rows.header)
|
|
400
|
+
rendered.add_divider()
|
|
401
|
+
if rows.types is not None and self.types_location is ROW:
|
|
402
|
+
rendered.add_row(rows.types)
|
|
403
|
+
rendered.add_divider()
|
|
404
|
+
for row in rows:
|
|
405
|
+
rendered.add_row(row)
|
|
406
|
+
rendered.add_divider()
|
|
407
|
+
|
|
408
|
+
return rendered.text()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class CSVFormat(TabularFormat):
|
|
412
|
+
"""
|
|
413
|
+
A :class:`Format` that parses and renders comma separated values.
|
|
414
|
+
|
|
415
|
+
:param type_parse:
|
|
416
|
+
A mapping of type name, as found in either a row or column heading, dependent
|
|
417
|
+
on the ``types_location``, to a function that parses the text of a cell into
|
|
418
|
+
a value.
|
|
419
|
+
|
|
420
|
+
:param default_type_parse:
|
|
421
|
+
The default function to use when parsing the text of a cell into a value.
|
|
422
|
+
|
|
423
|
+
:param column_parse:
|
|
424
|
+
A mapping of column names to functions that will be used to parse the text of cells
|
|
425
|
+
in that column into values.
|
|
426
|
+
|
|
427
|
+
:param type_render:
|
|
428
|
+
A mapping of type objects to functions that will be used to render values of that
|
|
429
|
+
type to text for cells.
|
|
430
|
+
|
|
431
|
+
:param default_type_render:
|
|
432
|
+
The default function to use when rendingering values to text for cells.
|
|
433
|
+
|
|
434
|
+
:param type_names:
|
|
435
|
+
A mapping of type objects to names to use for those types when including types
|
|
436
|
+
in either column headings or their own own. If a type is mapped to ``None``,
|
|
437
|
+
then no type name will be rendered for columns containing date of that type.
|
|
438
|
+
|
|
439
|
+
:param column_render:
|
|
440
|
+
A mapping of column names to functions that will be used to render values in that
|
|
441
|
+
column to text for cells.
|
|
442
|
+
|
|
443
|
+
:param types_location:
|
|
444
|
+
An optional location from which type information will be parsed or to which it
|
|
445
|
+
will be rendered. Must be :any:`HEADER`, :any:`ROW` or ``None``.
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
def parse(self, text: str) -> list[Attrs]:
|
|
449
|
+
return self._parse(text, lambda text: [row for row in csv.reader(StringIO(text))])
|
|
450
|
+
|
|
451
|
+
def render(self, attrs: Iterable[Attrs], ref: list[Attrs] | None = None) -> str:
|
|
452
|
+
"""
|
|
453
|
+
Render the supplied :class:`~chide.typing.Attrs` into a :class:`str`.
|
|
454
|
+
|
|
455
|
+
If supplied, ``ref`` is used for reference to make sure:
|
|
456
|
+
|
|
457
|
+
- the reference columns are always present.
|
|
458
|
+
- columns are rendered in the order specified in the reference.
|
|
459
|
+
"""
|
|
460
|
+
columns = None
|
|
461
|
+
if ref:
|
|
462
|
+
columns = list(ref[0])
|
|
463
|
+
|
|
464
|
+
rows = RenderedRows(attrs, self, columns)
|
|
465
|
+
text = StringIO()
|
|
466
|
+
writer = csv.writer(text)
|
|
467
|
+
|
|
468
|
+
if rows.header:
|
|
469
|
+
writer.writerow(rows.header.values())
|
|
470
|
+
if rows.types is not None and self.types_location is ROW:
|
|
471
|
+
writer.writerow(rows.types.values())
|
|
472
|
+
for row in rows:
|
|
473
|
+
writer.writerow(row.values())
|
|
474
|
+
|
|
475
|
+
return text.getvalue()
|
chide/set.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Any, TypeVar, Type, Hashable
|
|
2
|
+
|
|
3
|
+
from chide import Collection
|
|
4
|
+
from .typing import Identifier
|
|
5
|
+
|
|
6
|
+
T = TypeVar('T')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Set:
|
|
10
|
+
"""
|
|
11
|
+
A collection of sample objects where only one object with
|
|
12
|
+
a given identity may exist at one time.
|
|
13
|
+
|
|
14
|
+
:param collection:
|
|
15
|
+
The :class:`~chide.Collection` instance used to create sample objects
|
|
16
|
+
when necessary.
|
|
17
|
+
|
|
18
|
+
:param identify:
|
|
19
|
+
An :class:`~chide.typing.Identifier` callable that takes
|
|
20
|
+
`type_` and `attrs` parameters.
|
|
21
|
+
|
|
22
|
+
`type_`, usually a class, is the type of the
|
|
23
|
+
sample object being requested.
|
|
24
|
+
|
|
25
|
+
`attrs` is a :class:`dict` of the attributes being requested for the
|
|
26
|
+
sample object to have.
|
|
27
|
+
|
|
28
|
+
The callable should return a hashable value that indicates the identity
|
|
29
|
+
to use for the requested sample object. For each unique hashable value,
|
|
30
|
+
only one sample object will be instantiated and returned each time
|
|
31
|
+
a sample is requested where this callable returns the given identity.
|
|
32
|
+
|
|
33
|
+
``None`` may be returned to indicate that a new object should always
|
|
34
|
+
be returned for the provided parameters.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
#: You may also want to subclass :class:`Set` and implement
|
|
39
|
+
#: an :meth:`identify` method, see :class:`chide.sqlalchemy.Set`
|
|
40
|
+
#: for an example.
|
|
41
|
+
identify: Identifier
|
|
42
|
+
|
|
43
|
+
def __init__(self, collection: Collection, identify: Identifier | None = None) -> None:
|
|
44
|
+
self.collection = collection
|
|
45
|
+
identify = identify or getattr(self, 'identify', None)
|
|
46
|
+
if identify is None:
|
|
47
|
+
raise TypeError('No identify callable supplied')
|
|
48
|
+
self.identify = identify
|
|
49
|
+
self.objects: dict[Hashable, Any] = {}
|
|
50
|
+
|
|
51
|
+
def get(self, type_: Type[T], **attrs: Any) -> T:
|
|
52
|
+
"""
|
|
53
|
+
Return an appropriate sample object of the specified ``type_``.
|
|
54
|
+
|
|
55
|
+
The ``attrs`` mapping will be overlaid onto the sample attributes
|
|
56
|
+
found in this set's :class:`~chide.Collection` before checking if
|
|
57
|
+
an appropriate sample object already exists in the set.
|
|
58
|
+
|
|
59
|
+
If one exists, it is returned. If not, one is created using
|
|
60
|
+
this set's :class:`~chide.Collection`, added to the set and then
|
|
61
|
+
returned.
|
|
62
|
+
"""
|
|
63
|
+
attrs = self.collection._attrs(type_, attrs, self.get)
|
|
64
|
+
key = self.identify(type_, attrs)
|
|
65
|
+
if key is None:
|
|
66
|
+
return type_(**attrs)
|
|
67
|
+
obj = self.objects.get(key)
|
|
68
|
+
if obj is None:
|
|
69
|
+
obj = type_(**attrs)
|
|
70
|
+
self.objects[key] = obj
|
|
71
|
+
return obj
|
chide/simplifiers.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Protocol, TypeVar, Iterable
|
|
2
|
+
|
|
3
|
+
from chide.typing import Attrs
|
|
4
|
+
|
|
5
|
+
T = TypeVar('T', contravariant=True)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Simplifier(Protocol[T]):
|
|
9
|
+
"""
|
|
10
|
+
Protocol for :doc:`simplifiers <simplifiers>`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def one(self, obj: T) -> Attrs:
|
|
14
|
+
"""
|
|
15
|
+
Simplify one object into its :class:`~chide.typing.Attrs`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def many(self, objs: Iterable[T]) -> list[Attrs]:
|
|
19
|
+
"""
|
|
20
|
+
Simplify many objects into a list of their :class:`~chide.typing.Attrs`.
|
|
21
|
+
"""
|
|
22
|
+
return [self.one(obj) for obj in objs]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_MARKER = object()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ObjectSimplifier(Simplifier[object]):
|
|
29
|
+
"""
|
|
30
|
+
A simplifier that can extract attributes from :class:`object`-based
|
|
31
|
+
classes that have either a ``__dict__`` or ``__slots__``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def one(self, obj: object) -> Attrs:
|
|
35
|
+
attrs = {}
|
|
36
|
+
slots = set()
|
|
37
|
+
for class_ in type(obj).__mro__:
|
|
38
|
+
class_slots = getattr(class_, '__slots__', None)
|
|
39
|
+
if class_slots is not None:
|
|
40
|
+
slots.update(class_slots)
|
|
41
|
+
for attr in sorted(slots):
|
|
42
|
+
value = getattr(obj, attr, _MARKER)
|
|
43
|
+
if value is not _MARKER:
|
|
44
|
+
attrs[attr] = value
|
|
45
|
+
try:
|
|
46
|
+
attrs.update(vars(obj))
|
|
47
|
+
except TypeError:
|
|
48
|
+
pass
|
|
49
|
+
return attrs
|
chide/sqlalchemy.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Any, Type
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import inspect, Row
|
|
4
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
5
|
+
|
|
6
|
+
from .simplifiers import Simplifier, T, ObjectSimplifier
|
|
7
|
+
from .set import Set as BaseSet
|
|
8
|
+
from .typing import Attrs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Set(BaseSet):
|
|
12
|
+
"""
|
|
13
|
+
A specialised :class:`chide.Set` for getting sample declaratively
|
|
14
|
+
mapped objects when using SQLAlchemy.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def identify(type_: Type[Any], attrs: Attrs) -> tuple[Any, ...] | None:
|
|
19
|
+
"""
|
|
20
|
+
This method returns the primary key that will be used for the
|
|
21
|
+
returned object, meaning that only one sample object will exist
|
|
22
|
+
and be returned for each primary key in a table.
|
|
23
|
+
|
|
24
|
+
If any element of the primary key is ``None``, a new object
|
|
25
|
+
is always returned.
|
|
26
|
+
"""
|
|
27
|
+
mapper = inspect(type_)
|
|
28
|
+
key = [type_]
|
|
29
|
+
for prop in mapper._identity_key_props:
|
|
30
|
+
value = attrs.get(prop.key)
|
|
31
|
+
if value is None:
|
|
32
|
+
# no primary key, so we always get a new object...
|
|
33
|
+
return None
|
|
34
|
+
key.append(value)
|
|
35
|
+
return tuple(key)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RowSimplifier(Simplifier[Row[Any]]):
|
|
39
|
+
"""
|
|
40
|
+
A simplifier for SQLAlchemy :class:`~sqlalchemy.engine.Row` objects.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def one(self, row: Row[Any]) -> Attrs:
|
|
44
|
+
return row._asdict()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MappedSimplifier(Simplifier[DeclarativeBase]):
|
|
48
|
+
"""
|
|
49
|
+
A simplifier for SQLAlchemy ORM-mapped objects.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self._obj_simplifier = ObjectSimplifier()
|
|
54
|
+
|
|
55
|
+
def one(self, obj: DeclarativeBase) -> Attrs:
|
|
56
|
+
state = inspect(obj)
|
|
57
|
+
return {a.key: a.value for a in state.attrs}
|
chide/typing.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from typing import TypeAlias, Any, Callable, Type, Hashable
|
|
2
|
+
|
|
3
|
+
#: A dictionary of attributes that can be used to create a sample object
|
|
4
|
+
Attrs: TypeAlias = dict[str, Any]
|
|
5
|
+
|
|
6
|
+
#: A callable for uniquely identifying a sample object in a :class:`~chide.Set`
|
|
7
|
+
Identifier: TypeAlias = Callable[[Type[Any], Attrs], Hashable | None]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2016 onwards Chris Withers
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
|
4
|
+
obtaining a copy of this software and associated documentation
|
|
5
|
+
files (the "Software"), to deal in the Software without restriction,
|
|
6
|
+
including without limitation the rights to use, copy, modify, merge,
|
|
7
|
+
publish, distribute, sublicense, and/or sell copies of the Software,
|
|
8
|
+
and to permit persons to whom the Software is furnished to do so,
|
|
9
|
+
subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
16
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
18
|
+
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
19
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: chide
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Quickly create sample objects from data.
|
|
5
|
+
Home-page: https://github.com/cjw296/chide
|
|
6
|
+
Author: Chris Withers
|
|
7
|
+
Author-email: chris@withers.org
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
License-File: LICENSE.txt
|
|
15
|
+
Provides-Extra: build
|
|
16
|
+
Requires-Dist: setuptools-git ; extra == 'build'
|
|
17
|
+
Requires-Dist: twine ; extra == 'build'
|
|
18
|
+
Requires-Dist: wheel ; extra == 'build'
|
|
19
|
+
Provides-Extra: docs
|
|
20
|
+
Requires-Dist: furo ; extra == 'docs'
|
|
21
|
+
Requires-Dist: sphinx ; extra == 'docs'
|
|
22
|
+
Requires-Dist: sqlalchemy ; extra == 'docs'
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: mypy ; extra == 'test'
|
|
25
|
+
Requires-Dist: pytest ; extra == 'test'
|
|
26
|
+
Requires-Dist: pytest-cov ; extra == 'test'
|
|
27
|
+
Requires-Dist: sybil ; extra == 'test'
|
|
28
|
+
Requires-Dist: testfixtures ; extra == 'test'
|
|
29
|
+
Requires-Dist: sqlalchemy ; extra == 'test'
|
|
30
|
+
|
|
31
|
+
Chide
|
|
32
|
+
=====
|
|
33
|
+
|
|
34
|
+
|CircleCI|_ |Docs|_
|
|
35
|
+
|
|
36
|
+
.. |CircleCI| image:: https://circleci.com/gh/cjw296/chide/tree/master.svg?style=shield
|
|
37
|
+
.. _CircleCI: https://circleci.com/gh/cjw296/tree/chide
|
|
38
|
+
|
|
39
|
+
.. |Docs| image:: https://readthedocs.org/projects/chide/badge/?version=latest
|
|
40
|
+
.. _Docs: http://chide.readthedocs.org/en/latest/
|
|
41
|
+
|
|
42
|
+
Quickly create and compare sample objects.
|
|
43
|
+
|
|
44
|
+
Chide's philosophy is to give you a simple registry of parameters
|
|
45
|
+
needed to instantiate objects for your tests.
|
|
46
|
+
There's also support for simplifying objects down to mappings of their attributes
|
|
47
|
+
for easier comparison and rendering, along with parsing and rendering of formats
|
|
48
|
+
for inserting or asserting about multiple objects that are naturally tabular.
|
|
49
|
+
|
|
50
|
+
Quickstart
|
|
51
|
+
~~~~~~~~~~
|
|
52
|
+
|
|
53
|
+
Say we have two classes that each require two parameters in order to
|
|
54
|
+
be instantiated:
|
|
55
|
+
|
|
56
|
+
.. code-block:: python
|
|
57
|
+
|
|
58
|
+
from dataclasses import dataclass
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ClassOne:
|
|
62
|
+
x: int
|
|
63
|
+
y: int
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ClassTwo:
|
|
67
|
+
a: int
|
|
68
|
+
b: ClassOne
|
|
69
|
+
|
|
70
|
+
We can set up a registry of sample values as follows:
|
|
71
|
+
|
|
72
|
+
.. code-block:: python
|
|
73
|
+
|
|
74
|
+
from chide import Collection
|
|
75
|
+
|
|
76
|
+
samples = Collection({
|
|
77
|
+
ClassOne: {'x': 1, 'y': 2},
|
|
78
|
+
ClassTwo: {'a': 1, 'b': ClassOne},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
Now we can quickly make sample objects:
|
|
82
|
+
|
|
83
|
+
>>> samples.make(ClassOne)
|
|
84
|
+
ClassOne(x=1, y=2)
|
|
85
|
+
|
|
86
|
+
We can provide our own overrides if we want:
|
|
87
|
+
|
|
88
|
+
>>> samples.make(ClassOne, y=3)
|
|
89
|
+
ClassOne(x=1, y=3)
|
|
90
|
+
|
|
91
|
+
We can also create nested trees of objects:
|
|
92
|
+
|
|
93
|
+
>>> samples.make(ClassTwo)
|
|
94
|
+
ClassTwo(a=1, b=ClassOne(x=1, y=2))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
chide/__init__.py,sha256=tzfn3LuNeHzaEOQNam4RGtiGYHkNy0oJ195390MGof0,90
|
|
2
|
+
chide/collection.py,sha256=-JxfjClKy8kpjtUOv38H560mBUv4DT9vfwBKu5Bjq5w,2816
|
|
3
|
+
chide/factory.py,sha256=DvGHoKJgMSUnZderYKvoretdxwksyjJw39Bna3AcELM,1998
|
|
4
|
+
chide/formats.py,sha256=PKm9Rq1p-QQz97f_JCQeCvcyHF3B4PGC_fCj3jpUdJU,17031
|
|
5
|
+
chide/set.py,sha256=WWFJJU9TpqxUJZymDXyUG3MIYChwj3dq04u3w2DGYx0,2539
|
|
6
|
+
chide/simplifiers.py,sha256=-LRvTeyRHfSxP60mzozbIHT00MCPhh95AVNqbQC2MaA,1305
|
|
7
|
+
chide/sqlalchemy.py,sha256=QqPyAFx_0yLZXAJCJ0CdJOE79L3du3HNa4nyMd6Jduo,1659
|
|
8
|
+
chide/typing.py,sha256=HLRLFSQL975FByveENpeRoskLAQIDiNeCZI5dyaXfiE,319
|
|
9
|
+
chide-3.0.0.dist-info/LICENSE.txt,sha256=s86hR15Wsne7Yq8TE5oXlQKejwdUh4LxUKiXHtMsB78,1079
|
|
10
|
+
chide-3.0.0.dist-info/METADATA,sha256=VS3LZtgpsQbKynHfuNcfqPE73A3uBzwLmmnEdJIW67U,2527
|
|
11
|
+
chide-3.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
12
|
+
chide-3.0.0.dist-info/top_level.txt,sha256=59D5gEv53fg1VYjdlnkbkQymGB8xea5-Z56Y_7t5wyk,6
|
|
13
|
+
chide-3.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chide
|