samplekit 1.0.0rc1__cp313-cp313-win_amd64.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.
- samplekit/__init__.py +350 -0
- samplekit/__init__.pyi +1913 -0
- samplekit/_figure.py +146 -0
- samplekit/_figures.py +1813 -0
- samplekit/_native.cp313-win_amd64.pyd +0 -0
- samplekit/_worker.py +1102 -0
- samplekit/py.typed +0 -0
- samplekit-1.0.0rc1.dist-info/METADATA +116 -0
- samplekit-1.0.0rc1.dist-info/RECORD +13 -0
- samplekit-1.0.0rc1.dist-info/WHEEL +4 -0
- samplekit-1.0.0rc1.dist-info/licenses/LICENSE +21 -0
- samplekit-1.0.0rc1.dist-info/licenses/THIRD-PARTY-LICENSES.md +2493 -0
- samplekit-1.0.0rc1.dist-info/sboms/samplekit.cyclonedx.json +10774 -0
samplekit/__init__.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""SampleKit: sample data in plain Markdown — computed, current, tracked.
|
|
2
|
+
|
|
3
|
+
A sample is a Markdown file whose frontmatter holds its values. In Python a
|
|
4
|
+
sample is read from its file, read and changed through its attributes, and
|
|
5
|
+
saved back::
|
|
6
|
+
|
|
7
|
+
import samplekit as sk
|
|
8
|
+
|
|
9
|
+
brew = sk.load("brews/citra-ipa.md")
|
|
10
|
+
brew.volume.value = 21
|
|
11
|
+
brew.compute()
|
|
12
|
+
brew.save()
|
|
13
|
+
|
|
14
|
+
``load`` reads a folder as a ``SampleList``, which filters, sorts, groups and
|
|
15
|
+
exports as the command line does.
|
|
16
|
+
|
|
17
|
+
The examples of this package run in the demo's ``08-python`` folder, after::
|
|
18
|
+
|
|
19
|
+
import samplekit as sk
|
|
20
|
+
|
|
21
|
+
brews = sk.load("brews")
|
|
22
|
+
ipa = brews["citra-ipa"]
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# This file is the package's Python, and it writes only what the native module
|
|
26
|
+
# cannot: BoundList, a real list bound to a sample, and the class of Sample,
|
|
27
|
+
# which loads a file after the class's __init__ returns. Every rule either
|
|
28
|
+
# applies is a call into samplekit._native.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NotApplicable:
|
|
32
|
+
"""A value that does not apply to a sample: ``fg: n/a`` in its file.
|
|
33
|
+
|
|
34
|
+
There is one, ``samplekit.NA``. It is false in a test and prints as
|
|
35
|
+
``n/a``; a property set to it is saved as ``n/a``, and a formula reading
|
|
36
|
+
it gives ``NA`` too, without running.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> ipa.fg.value = sk.NA
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_one = None
|
|
43
|
+
|
|
44
|
+
def __new__(cls):
|
|
45
|
+
if cls._one is None:
|
|
46
|
+
cls._one = super().__new__(cls)
|
|
47
|
+
return cls._one
|
|
48
|
+
|
|
49
|
+
def __repr__(self):
|
|
50
|
+
return "samplekit.NA"
|
|
51
|
+
|
|
52
|
+
def __str__(self):
|
|
53
|
+
return "n/a"
|
|
54
|
+
|
|
55
|
+
def __bool__(self):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def __reduce__(self):
|
|
59
|
+
return (NotApplicable, ())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
NA = NotApplicable()
|
|
63
|
+
|
|
64
|
+
from samplekit import _native # noqa: E402
|
|
65
|
+
from samplekit._native import (
|
|
66
|
+
Collection,
|
|
67
|
+
Column,
|
|
68
|
+
ColumnSpec,
|
|
69
|
+
ColumnView,
|
|
70
|
+
Export,
|
|
71
|
+
Field,
|
|
72
|
+
FigureDeclaration,
|
|
73
|
+
Model,
|
|
74
|
+
Names,
|
|
75
|
+
Profile,
|
|
76
|
+
Project,
|
|
77
|
+
Property,
|
|
78
|
+
PropertyDeclaration,
|
|
79
|
+
Query,
|
|
80
|
+
Render,
|
|
81
|
+
RowView,
|
|
82
|
+
SampleList,
|
|
83
|
+
Statistic,
|
|
84
|
+
Summary,
|
|
85
|
+
Table,
|
|
86
|
+
Unit,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
from samplekit._native import load, stats
|
|
90
|
+
from samplekit._figures import figure, plot
|
|
91
|
+
|
|
92
|
+
__version__ = _native.__version__
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def keep(message=None):
|
|
96
|
+
"""Keep what the script has saved so far as one snapshot of the history.
|
|
97
|
+
|
|
98
|
+
A script's saves are kept as one snapshot when it ends, with the
|
|
99
|
+
script's source beside it; ``keep`` takes that snapshot now, so that the
|
|
100
|
+
history shows the script's steps apart.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
message: What the snapshot says; the script's command line when not
|
|
104
|
+
given. A blank message says nothing, and several lines are kept
|
|
105
|
+
on one.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
TypeError: The message is not text.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> ipa.save()
|
|
112
|
+
>>> sk.keep("volumes corrected")
|
|
113
|
+
"""
|
|
114
|
+
if message is not None and not isinstance(message, str):
|
|
115
|
+
raise TypeError(f"keep() takes a message as text, not {type(message).__name__}")
|
|
116
|
+
if message is not None:
|
|
117
|
+
# One line: the history's log shows a message a row each.
|
|
118
|
+
message = " ".join(message.split()) or None
|
|
119
|
+
_native._history_flush(message)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# One snapshot for the whole script, when the interpreter ends.
|
|
123
|
+
import atexit as _atexit # noqa: E402
|
|
124
|
+
|
|
125
|
+
_atexit.register(_native._history_flush)
|
|
126
|
+
|
|
127
|
+
__all__ = [
|
|
128
|
+
"BoundList",
|
|
129
|
+
"Collection",
|
|
130
|
+
"Column",
|
|
131
|
+
"ColumnSpec",
|
|
132
|
+
"ColumnView",
|
|
133
|
+
"Export",
|
|
134
|
+
"Field",
|
|
135
|
+
"FigureDeclaration",
|
|
136
|
+
"Model",
|
|
137
|
+
"NA",
|
|
138
|
+
"Names",
|
|
139
|
+
"NotApplicable",
|
|
140
|
+
"Profile",
|
|
141
|
+
"Project",
|
|
142
|
+
"Property",
|
|
143
|
+
"PropertyDeclaration",
|
|
144
|
+
"Query",
|
|
145
|
+
"Render",
|
|
146
|
+
"RowView",
|
|
147
|
+
"Sample",
|
|
148
|
+
"SampleList",
|
|
149
|
+
"Statistic",
|
|
150
|
+
"Summary",
|
|
151
|
+
"Table",
|
|
152
|
+
"Unit",
|
|
153
|
+
"figure",
|
|
154
|
+
"keep",
|
|
155
|
+
"load",
|
|
156
|
+
"plot",
|
|
157
|
+
"stats",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class BoundList(list):
|
|
162
|
+
"""A list bound to a sample: its changes are the sample's.
|
|
163
|
+
|
|
164
|
+
A sample's tags, a list attribute and a property's readings are read as
|
|
165
|
+
one. ``append``, ``remove``, an item assigned and every other change is
|
|
166
|
+
checked and made in the sample too, and a refused item leaves both
|
|
167
|
+
unchanged. ``list(...)`` or ``.copy()`` is a plain copy, bound to
|
|
168
|
+
nothing, and so is a list read before the attribute was assigned again.
|
|
169
|
+
|
|
170
|
+
Example:
|
|
171
|
+
>>> ipa.tags.append("medal")
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
__slots__ = ("_owner", "_name", "_epoch", "__weakref__")
|
|
175
|
+
|
|
176
|
+
def _change(self, mutate):
|
|
177
|
+
candidate = list(self)
|
|
178
|
+
result = mutate(candidate)
|
|
179
|
+
committed = self._owner._commit_list(self._name, self._epoch, candidate)
|
|
180
|
+
list.__setitem__(self, slice(None), candidate if committed is None else committed)
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
def append(self, item):
|
|
184
|
+
self._change(lambda items: items.append(item))
|
|
185
|
+
|
|
186
|
+
def extend(self, items):
|
|
187
|
+
self._change(lambda current: current.extend(items))
|
|
188
|
+
|
|
189
|
+
def insert(self, index, item):
|
|
190
|
+
self._change(lambda items: items.insert(index, item))
|
|
191
|
+
|
|
192
|
+
def pop(self, index=-1):
|
|
193
|
+
return self._change(lambda items: items.pop(index))
|
|
194
|
+
|
|
195
|
+
def remove(self, item):
|
|
196
|
+
self._change(lambda items: items.remove(item))
|
|
197
|
+
|
|
198
|
+
def clear(self):
|
|
199
|
+
self._change(lambda items: items.clear())
|
|
200
|
+
|
|
201
|
+
def sort(self, *, key=None, reverse=False):
|
|
202
|
+
self._change(lambda items: items.sort(key=key, reverse=reverse))
|
|
203
|
+
|
|
204
|
+
def reverse(self):
|
|
205
|
+
self._change(lambda items: items.reverse())
|
|
206
|
+
|
|
207
|
+
def __setitem__(self, index, value):
|
|
208
|
+
self._change(lambda items: items.__setitem__(index, value))
|
|
209
|
+
|
|
210
|
+
def __delitem__(self, index):
|
|
211
|
+
self._change(lambda items: items.__delitem__(index))
|
|
212
|
+
|
|
213
|
+
def __iadd__(self, other):
|
|
214
|
+
self._change(lambda items: items.__iadd__(other))
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def __imul__(self, count):
|
|
218
|
+
self._change(lambda items: items.__imul__(count))
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
def copy(self):
|
|
222
|
+
return list(self)
|
|
223
|
+
|
|
224
|
+
def __reduce_ex__(self, protocol):
|
|
225
|
+
return (list, (list(self),))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _bind(owner, name, epoch, items):
|
|
229
|
+
bound = BoundList(items)
|
|
230
|
+
bound._owner = owner
|
|
231
|
+
bound._name = name
|
|
232
|
+
bound._epoch = epoch
|
|
233
|
+
return bound
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _arguments(path=None, name=None, model=None):
|
|
237
|
+
return path, name, model
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class _Loading(type(_native.Sample)):
|
|
241
|
+
"""Constructs a sample, then loads its file.
|
|
242
|
+
|
|
243
|
+
A file is read after the class's ``__init__`` returns, so that the
|
|
244
|
+
``Property`` a model declares is filled by the file rather than overwriting
|
|
245
|
+
what was read; and ``Sample(path)`` with a project's model constructs that
|
|
246
|
+
model instead. Only a metaclass runs at both moments.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __call__(cls, *args, **kwargs):
|
|
250
|
+
if cls is Sample:
|
|
251
|
+
path, name, model = _arguments(*args, **kwargs)
|
|
252
|
+
chosen = _native._model_for(path, model)
|
|
253
|
+
if chosen is not None:
|
|
254
|
+
sample = chosen(path)
|
|
255
|
+
sample._declare_name(name)
|
|
256
|
+
return sample
|
|
257
|
+
sample = super().__call__(*args, **kwargs)
|
|
258
|
+
sample._load(args, kwargs)
|
|
259
|
+
return sample
|
|
260
|
+
|
|
261
|
+
def __getattr__(cls, name):
|
|
262
|
+
# `name=None`: the message names the replacement, and Python adding a
|
|
263
|
+
# nearest name of its own wrote a second suggestion after it.
|
|
264
|
+
raise AttributeError(_native._missing_class_member(cls, name), name=None)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class Sample(_native.Sample, metaclass=_Loading):
|
|
268
|
+
"""One sample, read from and saved to a Markdown file.
|
|
269
|
+
|
|
270
|
+
``Sample(path)`` reads the file with the model its project declares;
|
|
271
|
+
``Sample(name=...)`` makes a sample with no file, which ``save(path)``
|
|
272
|
+
gives one. A model is a subclass of ``Sample`` whose ``__init__``
|
|
273
|
+
declares its properties and tables, and passes ``path`` on to
|
|
274
|
+
``super().__init__``.
|
|
275
|
+
|
|
276
|
+
A property, a table or an attribute is read as an attribute or an item,
|
|
277
|
+
``brew.abv`` or ``brew["abv"]``, and any other field path as an item:
|
|
278
|
+
``brew["fermentation.gravity[2]"]`` is that cell's value. What is assigned
|
|
279
|
+
decides what a name becomes: a ``Property`` or a ``Table`` is one; a
|
|
280
|
+
``bool``, ``int``, ``float``, ``str``, ``date``, ``datetime``, or a list
|
|
281
|
+
of one of them, is an attribute; and a name with a leading ``_`` is
|
|
282
|
+
ordinary Python state, never saved. **Nothing assigned without a leading
|
|
283
|
+
``_`` is lost on save**: it is saved, or refused.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
path: The sample's file.
|
|
287
|
+
name: The sample's name; a name the file holds wins over it.
|
|
288
|
+
model: The class to read the file with, or ``False`` to read the
|
|
289
|
+
data alone. By default, the class the project's ``[model]``
|
|
290
|
+
declares.
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
FileNotFoundError: The file does not exist; the message names the
|
|
294
|
+
nearest. ``Sample.new`` makes a sample for a new file.
|
|
295
|
+
IsADirectoryError: The path is a folder: ``sk.load`` reads a folder.
|
|
296
|
+
ValueError: The file is not a sample.
|
|
297
|
+
AttributeError: An unknown field, read as an attribute; the message
|
|
298
|
+
names the nearest.
|
|
299
|
+
KeyError: An unknown field, read as an item.
|
|
300
|
+
TypeError: A value of a kind that cannot be saved; a leading ``_``
|
|
301
|
+
keeps it in Python only.
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
>>> brew = sk.Sample("brews/citra-ipa.md")
|
|
305
|
+
>>> brew.style
|
|
306
|
+
'ipa'
|
|
307
|
+
>>> brew["fermentation.gravity[2]"]
|
|
308
|
+
1.029
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def new(cls, path, name=None, model=None):
|
|
313
|
+
"""Make a new sample for a file that does not exist yet.
|
|
314
|
+
|
|
315
|
+
The sample is of the model the project of that folder declares — or of
|
|
316
|
+
this class, when called on a model — and is named by the file, which
|
|
317
|
+
writes no ``name:`` unless ``name`` is given.
|
|
318
|
+
``save()`` writes the file.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
path: The file to create.
|
|
322
|
+
name: The sample's name, instead of the file's.
|
|
323
|
+
model: The class to make it with, or ``False`` for a plain
|
|
324
|
+
``Sample``.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
The new sample, not saved yet.
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
FileExistsError: The file exists already: ``Sample(path)`` reads it.
|
|
331
|
+
FileNotFoundError: The folder does not exist.
|
|
332
|
+
|
|
333
|
+
Example:
|
|
334
|
+
>>> brew = sk.Sample.new("brews/red-ale.md")
|
|
335
|
+
>>> brew.style = "red_ale"
|
|
336
|
+
>>> brew.save()
|
|
337
|
+
"""
|
|
338
|
+
_native._check_new(path)
|
|
339
|
+
chosen = cls
|
|
340
|
+
if cls is Sample:
|
|
341
|
+
chosen = _native._model_for(path, model) or Sample
|
|
342
|
+
sample = chosen()
|
|
343
|
+
sample._create_at(path)
|
|
344
|
+
# Named by its file, and writing no `name:` unless one is given.
|
|
345
|
+
if name is not None:
|
|
346
|
+
sample._declare_name(name)
|
|
347
|
+
return sample
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
_native._register(Sample, _bind)
|