samplekit 1.0.0rc2__cp315-cp315-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__.pyi ADDED
@@ -0,0 +1,2218 @@
1
+ """The machine-readable contract of samplekit.
2
+
3
+ Checked against the runtime surface in both directions by the package's tests:
4
+ every public runtime attribute is declared here, and every declaration exists.
5
+ """
6
+
7
+ from collections.abc import Callable, ItemsView, Iterable, Iterator, KeysView, Mapping, Sequence
8
+ from datetime import date, datetime
9
+ from os import PathLike
10
+ from pathlib import Path
11
+ from typing import Any, Generic, Literal, Self, TypeAlias, TypeVar, overload
12
+
13
+ _T = TypeVar("_T")
14
+
15
+ # -------------------------------------------------------------- aliases
16
+
17
+ Scalar: TypeAlias = bool | int | float | str | date | datetime | None
18
+ AttributeScalar: TypeAlias = bool | int | float | str | date | datetime
19
+ AttributeList: TypeAlias = (
20
+ BoundList[bool] | BoundList[int] | BoundList[float]
21
+ | BoundList[str] | BoundList[date] | BoundList[datetime]
22
+ )
23
+ Attribute: TypeAlias = Scalar | AttributeList
24
+ IndexScalar: TypeAlias = bool | int | float | str | date | datetime
25
+ Index: TypeAlias = IndexScalar | tuple[IndexScalar, ...]
26
+ CellInput: TypeAlias = Scalar | Sequence[float] | Property
27
+ # What a table formula gives for one cell: a number, or (value, uncertainty),
28
+ # as a property's formula does; a Property carrying both is taken too.
29
+ CellOutput: TypeAlias = Scalar | tuple[Scalar, float | None] | Property
30
+ Pathish: TypeAlias = str | PathLike[str]
31
+ Precision: TypeAlias = str | tuple[str, str]
32
+ # A formula of a property, of no argument; on a table's column, of the row.
33
+ Compute: TypeAlias = Callable[[], Scalar | tuple[Scalar, float | None]] | Callable[[RowView], CellOutput]
34
+ ValueCompute: TypeAlias = Callable[[], Scalar] | Callable[[RowView], Scalar]
35
+ UncertaintyCompute: TypeAlias = Callable[[], float | None] | Callable[[RowView], float | None]
36
+ RowCompute: TypeAlias = Callable[[RowView], CellOutput | Mapping[str, CellOutput]]
37
+ ColumnCompute: TypeAlias = Callable[
38
+ [Mapping[str, ColumnView]],
39
+ Sequence[CellOutput] | Mapping[str, Sequence[CellOutput]],
40
+ ]
41
+ FieldKey: TypeAlias = str | Field
42
+ # What a formula reads: names, or by channel — {"v": [...], "u": [...]}.
43
+ Declared: TypeAlias = Sequence[str] | Mapping[str, Sequence[str]]
44
+ # One end of an axis: a number, or free — None, or "auto" as .samplekitrc writes it.
45
+ AxisBound: TypeAlias = float | Literal["auto"] | None
46
+ # What a column's cells hold, read as a list: numbers, text or dates, as its
47
+ # rows hold them, which a checker cannot tell from the column's name. Typed
48
+ # as `Scalar`, a model's arithmetic over a column — a sum, a slope, a mean —
49
+ # is refused; the code reading the list knows what its column holds.
50
+ CellValue: TypeAlias = Any
51
+
52
+ # -------------------------------------------------------------- data
53
+
54
+
55
+ __version__: str
56
+
57
+ class BoundList(list[_T]):
58
+ """A list bound to a sample: its changes are the sample's.
59
+
60
+ A sample's tags, a list attribute and a property's readings are read as
61
+ one. ``append``, ``remove``, an item assigned and every other change is
62
+ checked and made in the sample too, and a refused item leaves both
63
+ unchanged. ``list(...)`` or ``.copy()`` is a plain copy, bound to
64
+ nothing, and so is a list read before the attribute was assigned again.
65
+
66
+ Example:
67
+ >>> steel.tags.append("reference")
68
+ """
69
+
70
+
71
+ class Property:
72
+ """A property: a value, its uncertainty and its unit.
73
+
74
+ A property stands for its value in arithmetic and comparisons, so
75
+ ``steel.area * 2`` and ``steel.area > 25`` work on the number, and it prints
76
+ as the whole property, ``25.056 ± 0.053 mm²``. A model declares its properties in its
77
+ ``__init__``; a property a model does not declare is made the same way and
78
+ assigned to the sample.
79
+
80
+ A property is one of four kinds, by what is given: a value entered by hand;
81
+ readings, whose value and uncertainty a statistic gives; a value a formula
82
+ computes (``compute``, or ``compute_value`` and ``compute_uncertainty``);
83
+ or nothing yet.
84
+
85
+ A ``Table`` declares its columns with it too: there a property describes
86
+ every cell of its column, with the same keywords, and its formula is given
87
+ the row, ``f(row)``. A column takes no value nor uncertainty: each cell
88
+ holds its own, in the file.
89
+
90
+ Args:
91
+ value: The value — a number, text, a boolean, a date, ``samplekit.NA``
92
+ — or a list of readings, or a statistic from ``samplekit.stats``
93
+ that gives the value from the readings.
94
+ uncertainty: The absolute standard uncertainty, in the value's unit, or
95
+ a spread from ``samplekit.stats``.
96
+ unit: The unit, as the file writes it: ``"g"``, ``"degC"``.
97
+ symbol: Not taken here: a symbol is declared in ``.samplekitrc``,
98
+ under ``[property.NAME]``.
99
+ precision: Not taken here: a precision is declared in ``.samplekitrc``,
100
+ under ``[property.NAME]``.
101
+ compute: A function of no argument returning the property: a number,
102
+ or ``(value, uncertainty)``.
103
+ compute_value: A function of no argument returning the value alone,
104
+ beside an uncertainty entered, given by readings or computed apart.
105
+ compute_uncertainty: A function of no argument returning the
106
+ uncertainty, beside a value entered or computed apart.
107
+ compute_column: For a table's column only: a function given the
108
+ columns it reads, by name, and returning a list, one cell per row,
109
+ each a number or ``(value, uncertainty)``.
110
+ depends_on: What the formula reads: a list of names — ``["width", "thickness"]``,
111
+ ``"curve.force"`` for a column, ``"row.force"`` for a
112
+ cell of the same row — or, where the value and the uncertainty have
113
+ a formula each, ``{"v": [...], "u": [...]}``. ``[]`` says it reads
114
+ nothing of the sample. It can also be declared later with
115
+ ``Sample.set_dependencies``.
116
+
117
+ Raises:
118
+ ValueError: Two arguments give the same thing — ``value`` and a
119
+ formula, ``compute`` and ``compute_value``, ``compute_column`` and
120
+ another — or a value is not a number where a unit is given.
121
+ TypeError: A formula is not callable, or a symbol or a precision is
122
+ given.
123
+
124
+ Example:
125
+ >>> import samplekit as sk
126
+ >>> sk.Property(3.2, 0.1, unit="kg")
127
+ Property(value=3.2, uncertainty=0.1, unit='kg')
128
+ >>> width = sk.Property(unit="mm", value=sk.stats.mean,
129
+ ... uncertainty=sk.stats.standard_error)
130
+ """
131
+
132
+ def __init__(
133
+ self,
134
+ value: Scalar | Sequence[float] | Property | Statistic = None,
135
+ uncertainty: float | Statistic | None = None,
136
+ unit: str | None = None,
137
+ compute: Compute | None = None,
138
+ compute_value: ValueCompute | None = None,
139
+ compute_uncertainty: UncertaintyCompute | None = None,
140
+ compute_column: ColumnCompute | None = None,
141
+ # What this formula reads, so that a change to one of them makes it
142
+ # outdated. Optional here rather than required: a model may declare the
143
+ # same thing with `Sample.set_dependencies`, and a checker cannot see
144
+ # that from the call. A computed property that declares neither is refused once the
145
+ # model is built — `depends_on=[]` says it reads nothing.
146
+ depends_on: Declared | None = None,
147
+ ) -> None: ...
148
+ # A property's value is read as a number, so that a model's arithmetic
149
+ # type-checks: a formula runs only once its inputs hold one, and waits
150
+ # otherwise. Outside a formula it may hold text, a date, NA or nothing,
151
+ # which the code reading it checks. Assigned, it takes all of them, and
152
+ # a list of readings.
153
+ @property
154
+ def value(self) -> float:
155
+ """The value — a number, text, a boolean, a date, ``samplekit.NA``, or
156
+ ``None`` when there is none.
157
+
158
+ Assigning a value to a computed property overrides its formula: the
159
+ property reads *edited* until ``compute(force=True)`` gives it back to the
160
+ formula. Assigning a list gives the property readings. A property with a
161
+ unit takes numbers only, and raises ``ValueError`` otherwise.
162
+ """
163
+ @value.setter
164
+ def value(self, value: Scalar | NotApplicable | Sequence[float] | Property) -> None: ...
165
+ @property
166
+ def v(self) -> float:
167
+ """The value — the same as ``value``, spelt as a field path spells it
168
+ (``area.v``).
169
+ """
170
+ @v.setter
171
+ def v(self, value: Scalar | NotApplicable | Sequence[float] | Property) -> None: ...
172
+ uncertainty: float | None
173
+ """The absolute standard uncertainty, in the value's unit, or ``None``."""
174
+ u: float | None
175
+ """The uncertainty — the same as ``uncertainty``, spelt as a field path spells
176
+ it (``area.u``).
177
+ """
178
+ readings: list[float] | None
179
+ """The readings, or ``None`` for a value not measured several times.
180
+
181
+ The list is bound to the property: ``append``, ``remove`` or an item
182
+ assigned changes the property, as assigning a whole list does. Readings are
183
+ refused on a value a formula computes.
184
+ """
185
+ @property
186
+ def stats(self) -> Summary | None:
187
+ """The statistics of the readings, as a ``Summary``, or ``None`` without
188
+ readings.
189
+ """
190
+ unit: str | None
191
+ """The unit, as the file writes it — ``"g"``, ``"degC"`` — or ``None``."""
192
+ @property
193
+ def symbol(self) -> str | None:
194
+ """The property's symbol — the one this sample's file writes, else the one
195
+ the project declares in ``.samplekitrc`` under ``[property.NAME]``, else
196
+ ``None``. Read only: a symbol is the project's, declared there, never
197
+ set by a model or a script.
198
+ """
199
+ @property
200
+ def is_computed(self) -> bool:
201
+ """Whether a formula gives the value."""
202
+ @property
203
+ def is_outdated(self) -> bool:
204
+ """Whether the value is outdated — an input changed since it was computed."""
205
+ @property
206
+ def failure(self) -> str | None:
207
+ """What the formula raised when it last ran, or ``None`` if it did not fail."""
208
+ @property
209
+ def is_edited(self) -> bool:
210
+ """Whether the value is edited — typed over its formula, or a computed table
211
+ cell whose record is missing.
212
+ """
213
+ @property
214
+ def edited_upstream(self) -> list[str]:
215
+ """The edited values this one was computed from, directly or further up."""
216
+ @property
217
+ def state(self) -> str:
218
+ """Where the value stands, in one word.
219
+
220
+ One of ``"entered"`` (typed in, no formula), ``"absent"`` (no formula,
221
+ and nothing typed in), ``"current"``,
222
+ ``"outdated"`` (an input changed since it was computed), ``"waiting"``
223
+ (an input it reads was emptied since: the number is the one computed
224
+ before), ``"never_computed"``, ``"edited"`` (a value typed over its
225
+ formula) or ``"failed"`` (its formula raised): the words a filter's
226
+ ``state`` takes.
227
+ """
228
+ def compute(self, rerun: bool = False, force: bool = False) -> None:
229
+ """Compute the value, after the inputs it needs.
230
+
231
+ Only a value that is outdated or never computed runs, unless asked
232
+ otherwise. The file is not written: ``save()`` does that.
233
+
234
+ Args:
235
+ rerun: Run it even when it is current.
236
+ force: Run it even when it is edited, giving it back to its formula.
237
+
238
+ Raises:
239
+ ValueError: The value has no formula.
240
+ Exception: Whatever the formula raised, with a note naming the value.
241
+ """
242
+ def format(self, unit: bool = True) -> str:
243
+ """Format the property at its declared precision.
244
+
245
+ Args:
246
+ unit: Include the unit.
247
+
248
+ Returns:
249
+ The property as text, such as ``"25.056 ± 0.053 mm²"``. For another
250
+ precision, use a format specifier: ``f"{steel.area:.2f}"``.
251
+ """
252
+ def to_text(self, style: str | None = None) -> str:
253
+ r"""The property as a style writes it: its units, its symbols, its form
254
+ of a property — ``\SI{12.5 \pm 0.05}{\gram}`` in a style declaring
255
+ ``property = '\SI{{value} \pm {uncertainty}}{{unit}}'``.
256
+
257
+ Args:
258
+ style: A style's name: ``"plain"``, one SampleKit gives —
259
+ ``"latex"``, ``"markdown"``, ``"html"`` — or one ``.samplekitrc``
260
+ declares. Without it, ``[render] style``'s, as ``str()`` gives.
261
+
262
+ Returns:
263
+ The text, ``n/a`` where the value does not apply, and the style's
264
+ ``missing`` where there is none.
265
+
266
+ Raises:
267
+ ValueError: No style has that name.
268
+
269
+ Example:
270
+ >>> steel.area.to_text("latex")
271
+ '$25.056 \\pm 0.053$ mm²'
272
+ """
273
+ def __str__(self) -> str: ...
274
+ def __format__(self, spec: str) -> str: ...
275
+ def __repr__(self) -> str: ...
276
+ def __float__(self) -> float: ...
277
+ def __int__(self) -> int: ...
278
+ def __bool__(self) -> bool: ...
279
+ def __add__(self, other: Any) -> Any: ...
280
+ def __radd__(self, other: Any) -> Any: ...
281
+ def __sub__(self, other: Any) -> Any: ...
282
+ def __rsub__(self, other: Any) -> Any: ...
283
+ def __mul__(self, other: Any) -> Any: ...
284
+ def __rmul__(self, other: Any) -> Any: ...
285
+ def __truediv__(self, other: Any) -> Any: ...
286
+ def __rtruediv__(self, other: Any) -> Any: ...
287
+ def __floordiv__(self, other: Any) -> Any: ...
288
+ def __rfloordiv__(self, other: Any) -> Any: ...
289
+ def __mod__(self, other: Any) -> Any: ...
290
+ def __rmod__(self, other: Any) -> Any: ...
291
+ def __pow__(self, other: Any, modulo: Any = None) -> Any: ...
292
+ def __rpow__(self, other: Any, modulo: Any = None) -> Any: ...
293
+ def __neg__(self) -> Any: ...
294
+ def __pos__(self) -> Any: ...
295
+ def __abs__(self) -> Any: ...
296
+ def __round__(self, ndigits: int | None = None) -> Any: ...
297
+ def __eq__(self, other: object) -> bool: ...
298
+ def __ne__(self, other: object) -> bool: ...
299
+ def __lt__(self, other: Any) -> bool: ...
300
+ def __le__(self, other: Any) -> bool: ...
301
+ def __gt__(self, other: Any) -> bool: ...
302
+ def __ge__(self, other: Any) -> bool: ...
303
+ __hash__: None # type: ignore[assignment]
304
+
305
+
306
+ class RowView:
307
+ """One row of a table, as a column's formula receives it.
308
+
309
+ A cell is read by its column's name, as an attribute or an item:
310
+ ``row.force`` or ``row["force"]``, each a ``Property``.
311
+ """
312
+
313
+ @property
314
+ def position(self) -> int:
315
+ """The row's position in the table, from 0."""
316
+ def __getitem__(self, column: str) -> Property: ...
317
+ def __getattr__(self, column: str) -> Property: ...
318
+ def __contains__(self, column: object) -> bool: ...
319
+ def __iter__(self) -> Iterator[str]: ...
320
+ def keys(self) -> KeysView[str]:
321
+ """The names of the row's columns."""
322
+ def items(self) -> ItemsView[str, Property]:
323
+ """Each column name with its cell."""
324
+
325
+
326
+ class ColumnView:
327
+ """One column of a table, as a ``compute_column`` formula receives it, and
328
+ as ``table.column_name`` reads it.
329
+
330
+ Iterating gives each cell as a ``Property``, in row order, and an item is
331
+ the cell of a row, by its index value: ``column[3]``.
332
+ """
333
+
334
+ @property
335
+ def values(self) -> list[CellValue]:
336
+ """The value of each cell, in row order; ``None`` where a cell has none."""
337
+ @property
338
+ def uncertainties(self) -> list[float | None]:
339
+ """The uncertainty of each cell, in row order; ``None`` where a cell has none."""
340
+ def __getitem__(self, index: Index) -> Property: ...
341
+ def __iter__(self) -> Iterator[Property]: ...
342
+ def __len__(self) -> int: ...
343
+
344
+
345
+ class ComputedColumns:
346
+ """Several columns of a table filled by one formula: given each row, or the
347
+ columns it reads whole.
348
+
349
+ A column filled alone declares its formula on its ``Property``; this is for
350
+ one computation that gives several columns at once, a fit's two parameters.
351
+
352
+ Args:
353
+ columns: The columns it fills: a name, or a list of names.
354
+ compute: A function given each row, a ``RowView``, and returning a dict:
355
+ one cell for each column it fills, a number or
356
+ ``(value, uncertainty)``.
357
+ compute_column: A function given the columns it reads, by name, and
358
+ returning a dict: one list of cells for each column it fills, one
359
+ cell per row.
360
+ depends_on: What the formula reads: ``"row.force"`` for a cell of the
361
+ same row, ``"curve.force"`` for a whole column, ``"area"`` for a
362
+ property of the sample; ``[]`` for nothing.
363
+
364
+ Raises:
365
+ ValueError: Neither formula is given, or both, or ``depends_on`` is not.
366
+ TypeError: A formula is not callable.
367
+
368
+ Example:
369
+ >>> bounds = sk.ComputedColumns(
370
+ ... ["low", "high"], compute=lambda row: {"low": 1.0, "high": 2.0},
371
+ ... depends_on=[],
372
+ ... )
373
+ """
374
+
375
+ def __init__(
376
+ self,
377
+ columns: str | Sequence[str],
378
+ *,
379
+ compute: RowCompute | None = None,
380
+ compute_column: ColumnCompute | None = None,
381
+ # What the formula reads: `"row.force"`, `"curve.force"`, `"area"`.
382
+ depends_on: Sequence[str] | None = None,
383
+ ) -> None: ...
384
+
385
+
386
+ class Table:
387
+ """A table of a sample: named columns, and rows identified by their index.
388
+
389
+ The index is the column — or the columns — whose values identify a row: a
390
+ day, a temperature, a inspector. A cell is read by its index value, never by
391
+ its position: ``table.at(0.2, "force")``.
392
+
393
+ A column is declared with ``Property``: its unit, the statistics of its
394
+ cells' readings, or the formula that computes each cell, given the row
395
+ (``compute``, ``compute_value``, ``compute_uncertainty``), or the whole
396
+ column at once (``compute_column``). A formula filling several columns at
397
+ once is a ``ComputedColumns``, given in ``computed``.
398
+
399
+ Args:
400
+ columns: A dict of ``Property`` by name, in the order they are shown.
401
+ index: The name of the index column, or a list of names; always named.
402
+ title: A title for the table.
403
+ rows: The first rows: each a dict of cells by column name.
404
+ computed: The ``ComputedColumns`` of the table, each filling several
405
+ columns.
406
+
407
+ Raises:
408
+ TypeError: A column is not a ``Property``, or is given a value; the
409
+ index is not named.
410
+ ValueError: The index names no column, a row does not fit the columns,
411
+ or a column is filled by two formulas.
412
+
413
+ Example:
414
+ >>> inspection = sk.Table(
415
+ ... {"inspector": sk.Property(), "score": sk.Property()},
416
+ ... index="inspector",
417
+ ... rows=[{"inspector": "Ana", "score": 41}],
418
+ ... )
419
+ >>> inspection.at("Ana", "score").value
420
+ 41
421
+ """
422
+
423
+ def __init__(
424
+ self,
425
+ columns: Mapping[str, Property],
426
+ *,
427
+ index: str | Sequence[str],
428
+ title: str | None = None,
429
+ rows: Iterable[Mapping[str, CellInput]] | None = None,
430
+ computed: Iterable[ComputedColumns] | None = None,
431
+ ) -> None: ...
432
+ def add(self, **cells: CellInput) -> None:
433
+ """Add a row at the end.
434
+
435
+ Args:
436
+ **cells: The row's cells by column name: a value, a list of readings,
437
+ or a ``Property``. The index columns are required.
438
+
439
+ Raises:
440
+ KeyError: A column the table does not have.
441
+ ValueError: A row with that index exists already.
442
+ """
443
+ def extend(self, rows: Iterable[Mapping[str, CellInput]]) -> None:
444
+ """Add several rows at the end.
445
+
446
+ Args:
447
+ rows: An iterable of dicts, each a row's cells by column name.
448
+
449
+ Raises:
450
+ KeyError: A column the table does not have.
451
+ ValueError: A row with that index exists already.
452
+ """
453
+ def update(self, index: Index, **cells: CellInput) -> None:
454
+ """Change cells of one row.
455
+
456
+ Values computed from those cells become outdated. A computed cell changed
457
+ here is edited: kept by ``compute()`` until ``compute(force=True)``.
458
+
459
+ Args:
460
+ index: The row's index value; a tuple for an index of several columns.
461
+ **cells: The new cells, by column name.
462
+
463
+ Raises:
464
+ KeyError: No row has that index, or a column is unknown; the message
465
+ names the nearest.
466
+ """
467
+ def at(self, index: Index, column: str) -> Property:
468
+ """The cell of a row, by the row's index value.
469
+
470
+ Args:
471
+ index: The row's index value; a tuple for an index of several columns.
472
+ column: The column's name.
473
+
474
+ Returns:
475
+ The cell, as a ``Property``.
476
+
477
+ Raises:
478
+ KeyError: No row has that index, or the column is unknown; the message
479
+ names the nearest.
480
+
481
+ Example:
482
+ >>> steel.curve.at(0.2, "force").value
483
+ 9.516
484
+ """
485
+ def values(self, column: str) -> list[CellValue]:
486
+ """The values of a column, in row order.
487
+
488
+ Args:
489
+ column: The column's name.
490
+
491
+ Returns:
492
+ A list of values, ``None`` where a cell has none.
493
+
494
+ Raises:
495
+ KeyError: The column is unknown; the message lists the columns.
496
+
497
+ Example:
498
+ >>> steel.curve.values("force")[:3]
499
+ [0.0, 1.281, 2.585]
500
+ """
501
+ def uncertainties(self, column: str) -> list[float | None]:
502
+ """The uncertainties of a column, in row order.
503
+
504
+ Args:
505
+ column: The column's name.
506
+
507
+ Returns:
508
+ A list of uncertainties, ``None`` where a cell has none.
509
+
510
+ Raises:
511
+ KeyError: The column is unknown; the message lists the columns.
512
+ """
513
+ @property
514
+ def column_names(self) -> list[str]:
515
+ """The names of the columns, in the order they were declared."""
516
+ @property
517
+ def index_values(self) -> list[Index]:
518
+ """The index value of each row, in row order; a tuple each for an index of
519
+ several columns.
520
+ """
521
+ @property
522
+ def data_columns(self) -> list[str]:
523
+ """The names of the columns that are not the index."""
524
+ @property
525
+ def index(self) -> str | list[str]:
526
+ """The name of the index column, or a list of names for an index of several
527
+ columns.
528
+ """
529
+ @property
530
+ def index_unit(self) -> str | list[str | None] | None:
531
+ """The unit of the index column, or a list of units for an index of several
532
+ columns.
533
+ """
534
+ @property
535
+ def title(self) -> str | None:
536
+ """The table's title, or ``None``."""
537
+ def __getitem__(self, position: int) -> RowView: ...
538
+ def __getattr__(self, column: str) -> ColumnView: ...
539
+ def __contains__(self, index: object) -> bool: ...
540
+ def to_text(self, style: str | None = None) -> str:
541
+ """The table as ``samplekit view`` unfolds it: its columns headed with
542
+ their units, its index first, each cell in a style's forms — those of
543
+ its sample's project, or of the project the current folder is in for a
544
+ table of no sample.
545
+
546
+ Args:
547
+ style: A style's name: ``"plain"``, one SampleKit gives, or one
548
+ ``.samplekitrc`` declares. Without it, ``[render] style``'s.
549
+
550
+ Returns:
551
+ The text.
552
+
553
+ Raises:
554
+ ValueError: No style has that name.
555
+
556
+ Example:
557
+ >>> print(steel.curve.to_text())
558
+ """
559
+ def to_csv(self) -> str:
560
+ """The table as rows of CSV, as ``samplekit FILE view TABLE --csv``
561
+ prints it: its index first, each column a value and its uncertainty
562
+ headed with the unit, at the precision its columns declare.
563
+
564
+ Returns:
565
+ The text.
566
+
567
+ Raises:
568
+ ValueError: The table belongs to no sample yet.
569
+
570
+ Example:
571
+ >>> text = steel.curve.to_csv()
572
+ """
573
+ def __iter__(self) -> Iterator[RowView]: ...
574
+ def __len__(self) -> int: ...
575
+
576
+
577
+ class Sample:
578
+ """One sample, read from and saved to a Markdown file.
579
+
580
+ ``Sample(path)`` reads the file with the model its project declares;
581
+ ``Sample(name=...)`` makes a sample with no file, which ``save(path)``
582
+ gives one. A model is a subclass of ``Sample`` whose ``__init__``
583
+ declares its properties and tables, and passes ``path`` on to
584
+ ``super().__init__``.
585
+
586
+ A property, a table or an attribute is read as an attribute or an item,
587
+ ``steel.area`` or ``steel["area"]``, and any other field path as an item:
588
+ ``steel["curve.force[0.2]"]`` is that cell's value. What is assigned
589
+ decides what a name becomes: a ``Property`` or a ``Table`` is one; a
590
+ ``bool``, ``int``, ``float``, ``str``, ``date``, ``datetime``, or a list
591
+ of one of them, is an attribute; and a name with a leading ``_`` is
592
+ ordinary Python state, never saved. **Nothing assigned without a leading
593
+ ``_`` is lost on save**: it is saved, or refused.
594
+
595
+ Args:
596
+ path: The sample's file.
597
+ name: The sample's name; a name the file holds wins over it.
598
+ model: The class to read the file with, or ``False`` to read the
599
+ data alone. By default, the class the project's ``[model]``
600
+ declares.
601
+
602
+ Raises:
603
+ FileNotFoundError: The file does not exist; the message names the
604
+ nearest. ``Sample.new`` makes a sample for a new file.
605
+ IsADirectoryError: The path is a folder: ``sk.load`` reads a folder.
606
+ ValueError: The file is not a sample.
607
+ AttributeError: An unknown field, read as an attribute; the message
608
+ names the nearest.
609
+ KeyError: An unknown field, read as an item.
610
+ TypeError: A value of a kind that cannot be saved; a leading ``_``
611
+ keeps it in Python only.
612
+
613
+ Example:
614
+ >>> steel = sk.Sample("samples/steel-01.md")
615
+ >>> steel.material
616
+ 'steel'
617
+ >>> steel["curve.force[0.2]"]
618
+ 9.516
619
+ """
620
+
621
+ def __init__(
622
+ self,
623
+ path: Pathish | None = None,
624
+ name: str | None = None,
625
+ model: type[Sample] | Literal[False] | None = None,
626
+ ) -> None: ...
627
+ @classmethod
628
+ def new(
629
+ cls,
630
+ path: Pathish,
631
+ name: str | None = None,
632
+ model: type[Sample] | Literal[False] | None = None,
633
+ ) -> Self:
634
+ """Make a new sample for a file that does not exist yet.
635
+
636
+ The sample is of the model the project of that folder declares — or of
637
+ this class, when called on a model — and is named by the file, which
638
+ writes no ``name:`` unless ``name`` is given.
639
+ ``save()`` writes the file.
640
+
641
+ Args:
642
+ path: The file to create.
643
+ name: The sample's name, instead of the file's.
644
+ model: The class to make it with, or ``False`` for a plain
645
+ ``Sample``.
646
+
647
+ Returns:
648
+ The new sample, not saved yet.
649
+
650
+ Raises:
651
+ FileExistsError: The file exists already: ``Sample(path)`` reads it.
652
+ FileNotFoundError: The folder does not exist.
653
+
654
+ Example:
655
+ >>> specimen = sk.Sample.new("samples/steel-02.md")
656
+ >>> specimen.material = "steel"
657
+ >>> specimen.save()
658
+ """
659
+ @property
660
+ def path(self) -> Path | None:
661
+ """The file the sample was read from or last saved to, or ``None`` for a
662
+ sample never saved. ``save(path)`` changes it.
663
+ """
664
+ @property
665
+ def filename(self) -> str | None:
666
+ """The name of the sample's file, without its folder, or ``None``."""
667
+ @property
668
+ def project(self) -> Project:
669
+ """The project the sample's file belongs to — what its ``.samplekitrc``
670
+ declares.
671
+ """
672
+ name: str | None
673
+ """The sample's name, or ``None``."""
674
+ tags: BoundList[str]
675
+ """The sample's tags, as a list whose changes are saved with the sample.
676
+
677
+ Each tag is an identifier, and none appears twice.
678
+ """
679
+ note: str
680
+ """The note — the Markdown text below the frontmatter, kept as written."""
681
+ def to_text(self, style: str | None = None) -> str:
682
+ """The sample as ``samplekit view`` shows it — its values, then its
683
+ tables — each value in a style's forms.
684
+
685
+ Args:
686
+ style: A style's name: ``"plain"``, one SampleKit gives, or one
687
+ ``.samplekitrc`` declares. Without it, ``[render] style``'s.
688
+
689
+ Returns:
690
+ The text.
691
+
692
+ Raises:
693
+ ValueError: No style has that name.
694
+
695
+ Example:
696
+ >>> print(steel.to_text())
697
+ """
698
+ def save(self, path: Pathish | None = None, overwrite: bool = False) -> None:
699
+ """Write the sample to its file.
700
+
701
+ Nothing is overwritten unless asked: a file changed on disk since it was
702
+ read, or an existing file at a new path, is refused. Saving runs no
703
+ formula: a value never computed is written without a value.
704
+
705
+ Args:
706
+ path: Where to write it instead. The sample then belongs to that file:
707
+ a later ``save()`` writes there.
708
+ overwrite: Write even over a file changed since it was read, or over an
709
+ existing file at ``path``.
710
+
711
+ Raises:
712
+ ValueError: The sample has no file yet and no ``path`` is given.
713
+ Also: another process is saving the same file at this moment.
714
+ FileExistsError: ``path`` exists already.
715
+ FileNotFoundError: The folder does not exist, or the sample's file was
716
+ removed or moved since it was read.
717
+ IsADirectoryError: ``path`` is a folder.
718
+ OSError: The file changed on disk since it was read.
719
+ PermissionError: The file is read-only.
720
+
721
+ Example:
722
+ >>> steel.grade = "S275"
723
+ >>> steel.save()
724
+ """
725
+ def linked(self, field: str) -> Sample | Path | list[Sample | Path]:
726
+ """The sample an attribute links to, read as ``sk.load`` reads it, or
727
+ the file it leads to.
728
+
729
+ A link is an attribute written ``"[[NAME]]"``, as Obsidian writes
730
+ one: the sample of the project whose file is NAME.md, else whose
731
+ ``name`` is NAME; or a Markdown link to a file,
732
+ ``"[the second](NAME.md)"``, its path read from this sample's folder.
733
+ A sample's file gives the sample; any other file its path. A list of
734
+ links gives a list, in order.
735
+
736
+ Args:
737
+ field: The attribute holding the link, or the links.
738
+
739
+ Returns:
740
+ The sample it names, or a ``pathlib.Path`` for a file that is no
741
+ sample; for a list, a list of them, in order.
742
+
743
+ Raises:
744
+ KeyError: The sample holds no such attribute.
745
+ ValueError: The attribute is no link: text, or a web address; or
746
+ several samples answer to it.
747
+ FileNotFoundError: No sample answers to it, or nothing is at its
748
+ path.
749
+
750
+ Example:
751
+ >>> steel.linked("reference").name
752
+ 'aluminium-01'
753
+ """
754
+ def rename(self, new: str) -> Path:
755
+ """Rename the sample: its file, its own files, and every link of its
756
+ project leading to it.
757
+
758
+ The file becomes ``NEW.md`` beside the old one, its own files under
759
+ ``[collection] files`` renamed with it, its ``name`` too where it was
760
+ the file's name; each link of the project that led to it, ``[[OLD]]``
761
+ or ``[text](OLD.md)``, is written to lead to it again. The history
762
+ keeps the samples; renamed back, the files and the links go back too.
763
+
764
+ Args:
765
+ new: The new name: the file's, without its folder or ``.md``.
766
+
767
+ Returns:
768
+ The sample's new path.
769
+
770
+ Raises:
771
+ ValueError: The sample has no file, the name is no file's name, or
772
+ the script runs under ``samplekit run``, whose preview cannot
773
+ hold a rename.
774
+ FileExistsError: Another sample, or one of its files, has the name.
775
+ OSError: The file changed on disk since it was read.
776
+
777
+ Example:
778
+ >>> sk.Sample("samples/steel-01.md").rename("steel-04")
779
+ PosixPath('samples/steel-04.md')
780
+ """
781
+ ...
782
+
783
+ def remove(self) -> None:
784
+ """Delete the sample's file.
785
+
786
+ The project's history keeps it, and ``samplekit FILE restore --write``
787
+ gives it back; the sample's own files, under ``[collection] files``,
788
+ stay. The sample keeps its values: ``save()`` writes it again.
789
+
790
+ Raises:
791
+ ValueError: The sample has no file, or the script runs under
792
+ ``samplekit run``, whose preview cannot hold a removal.
793
+ FileNotFoundError: Its file was removed or moved since it was read.
794
+ OSError: The file changed on disk since it was read.
795
+
796
+ Example:
797
+ >>> sk.Sample("samples/steel-01.md").remove()
798
+ """
799
+ def files(self, pattern: str | None = None) -> list[Path]:
800
+ """The sample's own files: images, reports, anything found by the sample's
801
+ name in the folders ``[collection] files`` declares.
802
+
803
+ Args:
804
+ pattern: A glob on the files' names, such as ``"*.png"``.
805
+
806
+ Returns:
807
+ The files' paths.
808
+
809
+ Raises:
810
+ ValueError: The sample has no file, or belongs to no project.
811
+ """
812
+ def open(
813
+ self, pattern: str | None = None, navigate: bool = False, many: bool = False
814
+ ) -> list[Path]:
815
+ """Open the sample's own files with the system's application.
816
+
817
+ Args:
818
+ pattern: A glob on the files' names, such as ``"*.pdf"``.
819
+ navigate: Open the folders holding the files, in the file manager,
820
+ instead of the files.
821
+ many: Allow opening more than five.
822
+
823
+ Returns:
824
+ The paths opened.
825
+
826
+ Raises:
827
+ FileNotFoundError: No file of the sample matches.
828
+ ValueError: More than five would open and ``many`` is false.
829
+ RuntimeError: Over SSH without a display — ``DISPLAY`` and
830
+ ``WAYLAND_DISPLAY`` unset — there is nothing to open them on.
831
+ Elsewhere the system's opener is started, and what it does
832
+ without a display is its own.
833
+ """
834
+ def set_dependencies(self, output: str, *, depends_on: Declared) -> None:
835
+ """Declare what a formula reads.
836
+
837
+ A change to one of the inputs makes the value outdated, and a computation
838
+ runs the inputs first. A later declaration replaces an earlier one.
839
+
840
+ Args:
841
+ output: The value the formula gives: a property's name, or
842
+ ``table.column``.
843
+ depends_on: What it reads: a list of names, or ``{"v": [...],
844
+ "u": [...]}`` where the value and the uncertainty have a formula
845
+ each. ``[]`` says it reads nothing of the sample.
846
+
847
+ Raises:
848
+ KeyError: An unknown name, once the model is built.
849
+ ValueError: The declaration makes a cycle, or names a channel with no
850
+ formula.
851
+
852
+ Example:
853
+ >>> class Specimen(sk.Sample):
854
+ ... def __init__(self, path=None, name=None):
855
+ ... super().__init__(path, name=name)
856
+ ... self.area = sk.Property(unit="mm²", compute_value=self._area)
857
+ ... self.set_dependencies("area", depends_on=["width", "thickness"])
858
+ ...
859
+ ... def _area(self):
860
+ ... return self.width.value * self.thickness.value
861
+ """
862
+ def dependencies(self, output: str) -> list[str]:
863
+ """What a value's formula is declared to read.
864
+
865
+ Args:
866
+ output: A property's name, or ``table.column``.
867
+
868
+ Returns:
869
+ The input names, sorted.
870
+
871
+ Raises:
872
+ KeyError: An unknown name; the message names the nearest.
873
+
874
+ Example:
875
+ >>> steel.dependencies("area")
876
+ ['thickness', 'width']
877
+ """
878
+ def dependents(self, input: str) -> list[str]:
879
+ """The values whose formula reads this one directly.
880
+
881
+ Args:
882
+ input: A property's name, or ``table.column``.
883
+
884
+ Returns:
885
+ Their names, sorted.
886
+
887
+ Raises:
888
+ KeyError: An unknown name; the message names the nearest.
889
+ """
890
+ def affected_by_change(self, input: str) -> list[str]:
891
+ """Every value a change to this one would make outdated, directly or through
892
+ other values.
893
+
894
+ Args:
895
+ input: A property's name, or ``table.column``.
896
+
897
+ Returns:
898
+ Their names, sorted.
899
+
900
+ Raises:
901
+ KeyError: An unknown name; the message names the nearest.
902
+
903
+ Example:
904
+ >>> steel.affected_by_change("thickness")
905
+ ['area', 'curve.stress', 'tensile_strength', 'yield_strength', 'young_modulus']
906
+ """
907
+ def compute(self, *names: str, rerun: bool = False, force: bool = False) -> None:
908
+ """Compute the sample's values that are not current, in dependency order.
909
+
910
+ Without names, every value a formula gives is considered; only those
911
+ outdated or never computed run. A value whose inputs nobody entered waits,
912
+ with a warning saying for what, and a formula that raises is recorded as
913
+ failed while the others run. The file is not written: ``save()`` does
914
+ that.
915
+
916
+ Args:
917
+ *names: The values to compute: properties, ``table.column``, or a
918
+ table's name for all its computed columns. Their inputs run first
919
+ when needed.
920
+ rerun: Run them even when they are current.
921
+ force: Run them even when they are edited, giving them back to their
922
+ formula.
923
+
924
+ Raises:
925
+ KeyError: An unknown name; the message names the nearest.
926
+ ValueError: A named value has no formula.
927
+ Exception: The first formula that raised, with a note naming the value
928
+ and the others that failed.
929
+
930
+ Example:
931
+ >>> steel.compute()
932
+ >>> steel.save()
933
+ """
934
+ def outdated(self) -> list[str]:
935
+ """The outdated values — those an input changed under since they were
936
+ computed.
937
+
938
+ Returns:
939
+ The names of the outdated properties, and ``table.column`` for a
940
+ column with an outdated cell.
941
+ """
942
+ def edited(self) -> list[str]:
943
+ """The edited values — those typed over their formula.
944
+
945
+ Returns:
946
+ The names of the edited properties, and ``table.column`` for a column
947
+ with an edited cell.
948
+ """
949
+ def not_current(self) -> dict[str, str]:
950
+ """Every value that is not current, and why: what ``samplekit status``
951
+ reports.
952
+
953
+ ``outdated()`` and ``edited()`` answer one question each; this one also
954
+ sees values that failed, were never computed, or wait for an input.
955
+
956
+ Returns:
957
+ A dict from each name — a property, or ``table.column`` — to a reason:
958
+ ``"outdated"``, ``"formula changed"``, ``"edited"``, ``"failed"``,
959
+ ``"never computed"`` or ``"waits for notch"``.
960
+
961
+ Example:
962
+ >>> steel.not_current()
963
+ {}
964
+ """
965
+ def keys(self) -> KeysView[str]:
966
+ """The names of the sample's attributes, properties and tables, in the order
967
+ they were declared.
968
+ """
969
+ def items(self) -> ItemsView[str, Property | Table | Attribute]:
970
+ """Each name with its attribute, property or table, in the order they were
971
+ declared.
972
+ """
973
+ def __getitem__(self, name: str) -> Property | Table | Attribute | list[Scalar]: ...
974
+ # A field's type is the data's, which no checker can know from the class.
975
+ def __getattr__(self, name: str) -> Any: ...
976
+ def __setattr__(self, name: str, value: Any) -> None: ...
977
+ def __delattr__(self, name: str) -> None: ...
978
+ def __setitem__(self, name: str, value: Property | Table | Attribute) -> None: ...
979
+ def __delitem__(self, name: str) -> None: ...
980
+ def __contains__(self, name: object) -> bool: ...
981
+ def __iter__(self) -> Iterator[str]: ...
982
+
983
+
984
+ class SampleList(Sequence[Sample]):
985
+ """An ordered list of samples, read like a Python list.
986
+
987
+ A list is read from a folder, or built from samples already read. An item
988
+ is a sample, by position or by name — ``specimens["steel-01"]`` — and a slice
989
+ is a list. Lists join with ``+``. ``filter``, ``query``, ``sorted`` and
990
+ ``group_by`` make new lists; the samples in them are the same objects.
991
+
992
+ Args:
993
+ source: A folder, whose samples are read as the project declares, or
994
+ an iterable of samples.
995
+ pattern: A glob choosing the folder's files, instead of what
996
+ ``[collection]`` declares.
997
+ model: The model class to read the samples with, or ``False`` to read
998
+ the data alone. By default, the project's model.
999
+
1000
+ Raises:
1001
+ TypeError: The iterable holds something other than samples, such as
1002
+ paths: ``sk.load`` reads paths.
1003
+ ValueError: A sample appears twice, or ``pattern`` or ``model`` is
1004
+ given without a folder.
1005
+ FileNotFoundError: The folder does not exist.
1006
+
1007
+ Example:
1008
+ >>> specimens = sk.SampleList("samples")
1009
+ >>> len(specimens)
1010
+ 3
1011
+ >>> specimens["steel-01"].material
1012
+ 'steel'
1013
+ """
1014
+
1015
+ def __init__(
1016
+ self,
1017
+ source: Pathish | Iterable[Sample] | None = None,
1018
+ pattern: str | None = None,
1019
+ model: type[Sample] | Literal[False] | None = None,
1020
+ ) -> None: ...
1021
+ def __len__(self) -> int: ...
1022
+ @overload
1023
+ def __getitem__(self, key: int) -> Sample: ...
1024
+ @overload
1025
+ def __getitem__(self, key: slice) -> SampleList: ...
1026
+ @overload
1027
+ def __getitem__(self, key: str) -> Sample: ...
1028
+ def __contains__(self, item: object) -> bool: ...
1029
+ def __iter__(self) -> Iterator[Sample]: ...
1030
+ def __reversed__(self) -> Iterator[Sample]: ...
1031
+ def __add__(self, other: SampleList) -> SampleList: ...
1032
+ def index(self, value: object, start: int = 0, stop: int = ...) -> int:
1033
+ """The position of a sample in the list.
1034
+
1035
+ Args:
1036
+ value: A sample, or a sample's name.
1037
+ start: Where to start looking.
1038
+ stop: Where to stop looking.
1039
+
1040
+ Raises:
1041
+ ValueError: It is not in the list.
1042
+ """
1043
+ def count(self, value: object) -> int:
1044
+ """How many times a sample is in the list: 0 or 1 for a sample, and, for a
1045
+ name, the number of samples of that name.
1046
+ """
1047
+ def filter(self, predicate: str | Callable[[Sample], bool]) -> SampleList:
1048
+ """The samples an expression selects, as ``-f`` selects them.
1049
+
1050
+ Args:
1051
+ predicate: A filter expression — ``"elongation > 20 && material == steel"`` — or a
1052
+ function taking a sample and returning whether to keep it.
1053
+
1054
+ Returns:
1055
+ A new list, in this list's order.
1056
+
1057
+ Raises:
1058
+ KeyError: The expression names an unknown field; the message names
1059
+ the nearest.
1060
+ ValueError: The expression does not parse, or names a tag no sample
1061
+ carries.
1062
+ TypeError: The expression compares a field in a way its values do not
1063
+ allow.
1064
+
1065
+ Example:
1066
+ >>> [specimen.name for specimen in specimens.filter("tensile_strength > 300")]
1067
+ ['aluminium-01', 'steel-01']
1068
+ """
1069
+ def sort(self, key: FieldKey | Sequence[FieldKey] | Callable[[Sample], Any]) -> None:
1070
+ """Sort the list in place, and return ``None``.
1071
+
1072
+ Args:
1073
+ key: A field — ``"area"``, or ``"-area"`` for descending — several in a
1074
+ list or comma-separated, or a function of a sample as ``sorted``
1075
+ takes one.
1076
+
1077
+ Raises:
1078
+ KeyError: An unknown field; the message names the nearest.
1079
+ TypeError: A sort by ``state``, which a sample holds several of.
1080
+ """
1081
+ def sorted(self, key: FieldKey | Sequence[FieldKey] | Callable[[Sample], Any]) -> SampleList:
1082
+ """A new list in sorted order; this one is unchanged.
1083
+
1084
+ Args:
1085
+ key: A field — ``"area"``, or ``"-area"`` for descending — several in a
1086
+ list or comma-separated, or a function of a sample as ``sorted``
1087
+ takes one.
1088
+
1089
+ Returns:
1090
+ A new list.
1091
+
1092
+ Raises:
1093
+ KeyError: An unknown field; the message names the nearest.
1094
+ TypeError: A sort by ``state``, which a sample holds several of.
1095
+
1096
+ Example:
1097
+ >>> specimens.sorted("-tensile_strength")[0].name
1098
+ 'steel-01'
1099
+ """
1100
+ def query(self, name: str | Query) -> SampleList:
1101
+ """The samples a saved query selects, as ``-Q`` selects them.
1102
+
1103
+ Args:
1104
+ name: The query's name in ``.samplekitrc``, or a ``Query``.
1105
+
1106
+ Returns:
1107
+ A new list, in this list's order.
1108
+
1109
+ Raises:
1110
+ KeyError: No query has that name; the message names the nearest.
1111
+ ValueError: The list spans several projects that declare it
1112
+ differently.
1113
+
1114
+ Example:
1115
+ >>> len(specimens.query("ductile"))
1116
+ 2
1117
+ """
1118
+ def stats(self, field: FieldKey) -> Summary:
1119
+ """The statistics of one field over the list, as ``--summary`` gives them.
1120
+
1121
+ Args:
1122
+ field: A field: ``"area"``, ``"area.u"``, ``"curve.force[0.2]"``.
1123
+
1124
+ Returns:
1125
+ A ``Summary``. Its mean is weighted by 1/u² where every value has an
1126
+ uncertainty above zero.
1127
+
1128
+ Raises:
1129
+ KeyError: An unknown field; the message names the nearest.
1130
+ ValueError: No sample of the list holds a number there.
1131
+
1132
+ Example:
1133
+ >>> round(specimens.stats("tensile_strength").mean, 1)
1134
+ 352.3
1135
+ """
1136
+ def group_by(self, fields: FieldKey | Sequence[FieldKey]) -> dict[Any, SampleList]:
1137
+ """Split the list into groups by the values of fields, as ``--group`` does.
1138
+
1139
+ Args:
1140
+ fields: A field, or several — a list, or comma-separated as
1141
+ ``--group`` writes them.
1142
+
1143
+ Returns:
1144
+ A dict from each value — a tuple of values for several fields — to
1145
+ the list of samples holding it. The groups come in the order their
1146
+ values sort; for a sorted list, in the order of their first samples.
1147
+
1148
+ Raises:
1149
+ KeyError: An unknown field; the message names the nearest.
1150
+ ValueError: The field is a whole table.
1151
+ RuntimeError: The field is ``state``, which a sample holds several of.
1152
+
1153
+ Example:
1154
+ >>> list(specimens.group_by("material"))
1155
+ ['aluminium', 'copper', 'steel']
1156
+ """
1157
+ def save_all(self, directory: Pathish | None = None, overwrite: bool = False) -> list[Path]:
1158
+ """Write every sample of the list: back where each was read, or into a folder.
1159
+
1160
+ Without a folder, each sample is written to its own file, as ``save()``
1161
+ writes it: a file changed since it was read — by another object, or an
1162
+ editor — is refused rather than written over. Into a folder, each is
1163
+ written under its file's name, or its name, and nothing is written if
1164
+ any file is refused.
1165
+
1166
+ Args:
1167
+ directory: The folder, which must exist; each sample's own file when
1168
+ left out.
1169
+ overwrite: Replace existing files in the folder.
1170
+
1171
+ Returns:
1172
+ The paths written.
1173
+
1174
+ Raises:
1175
+ FileNotFoundError: The folder does not exist.
1176
+ NotADirectoryError: It is a file.
1177
+ FileExistsError: A file exists and ``overwrite`` is false.
1178
+ ValueError: Two samples would be written to the same file, or,
1179
+ without a folder, a sample has no file of its own.
1180
+ OSError: Without a folder, a file changed on disk since it was read.
1181
+ """
1182
+ def compute(self, *names: str, rerun: bool = False, force: bool = False) -> None:
1183
+ """Compute every sample of the list, as ``Sample.compute`` does.
1184
+
1185
+ Every sample is computed even when one fails; the failures are raised at
1186
+ the end. A ``KeyboardInterrupt`` stops at once.
1187
+
1188
+ Args:
1189
+ *names: The values to compute; every value that is not current by
1190
+ default.
1191
+ rerun: Run them even when they are current.
1192
+ force: Run them even when they are edited.
1193
+
1194
+ Raises:
1195
+ Exception: The one failure, with a note naming the sample and the
1196
+ value.
1197
+ ExceptionGroup: Several failures, in the order they happened.
1198
+ """
1199
+ @property
1200
+ def fields(self) -> Names[Field]:
1201
+ """Every field the samples of the list can be addressed by, as
1202
+ ``samplekit list fields`` lists them.
1203
+ """
1204
+ @property
1205
+ def project(self) -> Project:
1206
+ """The project of the list's samples — what their ``.samplekitrc`` declares.
1207
+
1208
+ Raises:
1209
+ ValueError: The samples belong to several projects.
1210
+ """
1211
+ def to_dict(
1212
+ self,
1213
+ columns: str | Sequence[FieldKey] | None = None,
1214
+ profile: str | Profile | None = None,
1215
+ ) -> dict[str, list[Scalar | list[AttributeScalar]]]:
1216
+ """The list as a dict of columns, one list of values per column heading.
1217
+
1218
+ Values are the stored numbers, unrounded.
1219
+
1220
+ Args:
1221
+ columns: The fields to include: a list, or comma-separated as ``-c``
1222
+ writes them, each ``FIELD[:PRECISION][=LABEL]``.
1223
+ profile: A profile's name in ``.samplekitrc``, or a ``Profile``,
1224
+ instead of ``columns``.
1225
+
1226
+ Returns:
1227
+ A dict from each heading to its values, one per sample.
1228
+
1229
+ Raises:
1230
+ ValueError: Neither ``columns`` nor ``profile`` is given, or both.
1231
+ KeyError: An unknown field or profile; the message names the nearest.
1232
+
1233
+ Example:
1234
+ >>> columns = specimens.to_dict(columns="name,area")
1235
+ >>> list(columns)
1236
+ ['name', 'area_value', 'area_uncertainty']
1237
+ """
1238
+ # Without a path the text is returned; with one it is written, and
1239
+ # nothing is returned.
1240
+ @overload
1241
+ def to_csv(
1242
+ self,
1243
+ path: None = None,
1244
+ columns: str | Sequence[FieldKey] | None = None,
1245
+ profile: str | Profile | None = None,
1246
+ overwrite: bool = False,
1247
+ ) -> str:
1248
+ """The list as CSV, as the command line exports it.
1249
+
1250
+ Args:
1251
+ path: The file to write. Without it, the text is returned.
1252
+ columns: The fields to include: a list, or comma-separated as ``-c``
1253
+ writes them, each ``FIELD[:PRECISION][=LABEL]``.
1254
+ profile: A profile's name in ``.samplekitrc``, or a ``Profile``,
1255
+ instead of ``columns``.
1256
+ overwrite: Replace an existing file.
1257
+
1258
+ Returns:
1259
+ The text, when no ``path`` is given; otherwise ``None``.
1260
+
1261
+ Raises:
1262
+ ValueError: Neither ``columns`` nor ``profile`` is given, or both.
1263
+ KeyError: An unknown field or profile; the message names the nearest.
1264
+ FileExistsError: ``path`` exists and ``overwrite`` is false.
1265
+
1266
+ Example:
1267
+ >>> text = specimens.to_csv(columns="name,area")
1268
+ """
1269
+ @overload
1270
+ def to_csv(
1271
+ self,
1272
+ path: Pathish,
1273
+ columns: str | Sequence[FieldKey] | None = None,
1274
+ profile: str | Profile | None = None,
1275
+ overwrite: bool = False,
1276
+ ) -> None:
1277
+ """The list as CSV, as the command line exports it.
1278
+
1279
+ Args:
1280
+ path: The file to write. Without it, the text is returned.
1281
+ columns: The fields to include: a list, or comma-separated as ``-c``
1282
+ writes them, each ``FIELD[:PRECISION][=LABEL]``.
1283
+ profile: A profile's name in ``.samplekitrc``, or a ``Profile``,
1284
+ instead of ``columns``.
1285
+ overwrite: Replace an existing file.
1286
+
1287
+ Returns:
1288
+ The text, when no ``path`` is given; otherwise ``None``.
1289
+
1290
+ Raises:
1291
+ ValueError: Neither ``columns`` nor ``profile`` is given, or both.
1292
+ KeyError: An unknown field or profile; the message names the nearest.
1293
+ FileExistsError: ``path`` exists and ``overwrite`` is false.
1294
+
1295
+ Example:
1296
+ >>> text = specimens.to_csv(columns="name,area")
1297
+ """
1298
+ @overload
1299
+ def to_tsv(
1300
+ self,
1301
+ path: None = None,
1302
+ columns: str | Sequence[FieldKey] | None = None,
1303
+ profile: str | Profile | None = None,
1304
+ overwrite: bool = False,
1305
+ ) -> str:
1306
+ """The list as TSV, as the command line exports it.
1307
+
1308
+ Args:
1309
+ path: The file to write. Without it, the text is returned.
1310
+ columns: The fields to include: a list, or comma-separated as ``-c``
1311
+ writes them, each ``FIELD[:PRECISION][=LABEL]``.
1312
+ profile: A profile's name in ``.samplekitrc``, or a ``Profile``,
1313
+ instead of ``columns``.
1314
+ overwrite: Replace an existing file.
1315
+
1316
+ Returns:
1317
+ The text, when no ``path`` is given; otherwise ``None``.
1318
+
1319
+ Raises:
1320
+ ValueError: Neither ``columns`` nor ``profile`` is given, or both.
1321
+ KeyError: An unknown field or profile; the message names the nearest.
1322
+ FileExistsError: ``path`` exists and ``overwrite`` is false.
1323
+
1324
+ Example:
1325
+ >>> text = specimens.to_tsv(columns="name,area")
1326
+ """
1327
+ @overload
1328
+ def to_tsv(
1329
+ self,
1330
+ path: Pathish,
1331
+ columns: str | Sequence[FieldKey] | None = None,
1332
+ profile: str | Profile | None = None,
1333
+ overwrite: bool = False,
1334
+ ) -> None:
1335
+ """The list as TSV, as the command line exports it.
1336
+
1337
+ Args:
1338
+ path: The file to write. Without it, the text is returned.
1339
+ columns: The fields to include: a list, or comma-separated as ``-c``
1340
+ writes them, each ``FIELD[:PRECISION][=LABEL]``.
1341
+ profile: A profile's name in ``.samplekitrc``, or a ``Profile``,
1342
+ instead of ``columns``.
1343
+ overwrite: Replace an existing file.
1344
+
1345
+ Returns:
1346
+ The text, when no ``path`` is given; otherwise ``None``.
1347
+
1348
+ Raises:
1349
+ ValueError: Neither ``columns`` nor ``profile`` is given, or both.
1350
+ KeyError: An unknown field or profile; the message names the nearest.
1351
+ FileExistsError: ``path`` exists and ``overwrite`` is false.
1352
+
1353
+ Example:
1354
+ >>> text = specimens.to_tsv(columns="name,area")
1355
+ """
1356
+ @overload
1357
+ def to_text(
1358
+ self,
1359
+ style: str,
1360
+ path: None = None,
1361
+ columns: str | Sequence[FieldKey] | None = None,
1362
+ profile: str | Profile | None = None,
1363
+ overwrite: bool = False,
1364
+ ) -> str:
1365
+ """The list as a document in a style: ``"latex"``, ``"markdown"``,
1366
+ ``"html"``, or one ``[style.NAME]`` declares, as ``--style NAME -o -``
1367
+ writes it.
1368
+
1369
+ Args:
1370
+ style: The style's name.
1371
+ path: The file to write. Without it, the text is returned.
1372
+ columns: The fields to include: a list, or comma-separated as ``-c``
1373
+ writes them, each ``FIELD[:PRECISION][=LABEL]``.
1374
+ profile: A profile's name in ``.samplekitrc``, or a ``Profile``,
1375
+ instead of ``columns``.
1376
+ overwrite: Replace an existing file.
1377
+
1378
+ Returns:
1379
+ The text, when no ``path`` is given; otherwise ``None``.
1380
+
1381
+ Raises:
1382
+ ValueError: No style writing documents has that name, or neither
1383
+ ``columns`` nor ``profile`` is given, or both.
1384
+ KeyError: An unknown field or profile; the message names the nearest.
1385
+ FileExistsError: ``path`` exists and ``overwrite`` is false.
1386
+
1387
+ Example:
1388
+ >>> table = specimens.to_text("latex", columns="name,area")
1389
+ """
1390
+ @overload
1391
+ def to_text(
1392
+ self,
1393
+ style: str,
1394
+ path: Pathish,
1395
+ columns: str | Sequence[FieldKey] | None = None,
1396
+ profile: str | Profile | None = None,
1397
+ overwrite: bool = False,
1398
+ ) -> None:
1399
+ """The list as a document in a style: ``"latex"``, ``"markdown"``,
1400
+ ``"html"``, or one ``[style.NAME]`` declares, as ``--style NAME -o -``
1401
+ writes it.
1402
+
1403
+ Args:
1404
+ style: The style's name.
1405
+ path: The file to write. Without it, the text is returned.
1406
+ columns: The fields to include: a list, or comma-separated as ``-c``
1407
+ writes them, each ``FIELD[:PRECISION][=LABEL]``.
1408
+ profile: A profile's name in ``.samplekitrc``, or a ``Profile``,
1409
+ instead of ``columns``.
1410
+ overwrite: Replace an existing file.
1411
+
1412
+ Returns:
1413
+ The text, when no ``path`` is given; otherwise ``None``.
1414
+
1415
+ Raises:
1416
+ ValueError: No style writing documents has that name, or neither
1417
+ ``columns`` nor ``profile`` is given, or both.
1418
+ KeyError: An unknown field or profile; the message names the nearest.
1419
+ FileExistsError: ``path`` exists and ``overwrite`` is false.
1420
+
1421
+ Example:
1422
+ >>> table = specimens.to_text("latex", columns="name,area")
1423
+ """
1424
+ @overload
1425
+ def to_json(
1426
+ self,
1427
+ path: None = None,
1428
+ columns: str | Sequence[FieldKey] | None = None,
1429
+ profile: str | Profile | None = None,
1430
+ overwrite: bool = False,
1431
+ ) -> str:
1432
+ """The list as JSON, as the command line exports it.
1433
+
1434
+ Args:
1435
+ path: The file to write. Without it, the text is returned.
1436
+ columns: The fields to include: a list, or comma-separated as ``-c``
1437
+ writes them, each ``FIELD[:PRECISION][=LABEL]``.
1438
+ profile: A profile's name in ``.samplekitrc``, or a ``Profile``,
1439
+ instead of ``columns``.
1440
+ overwrite: Replace an existing file.
1441
+
1442
+ Returns:
1443
+ The text, when no ``path`` is given; otherwise ``None``.
1444
+
1445
+ Raises:
1446
+ ValueError: Neither ``columns`` nor ``profile`` is given, or both.
1447
+ KeyError: An unknown field or profile; the message names the nearest.
1448
+ FileExistsError: ``path`` exists and ``overwrite`` is false.
1449
+
1450
+ Example:
1451
+ >>> text = specimens.to_json(columns="name,area")
1452
+ """
1453
+ @overload
1454
+ def to_json(
1455
+ self,
1456
+ path: Pathish,
1457
+ columns: str | Sequence[FieldKey] | None = None,
1458
+ profile: str | Profile | None = None,
1459
+ overwrite: bool = False,
1460
+ ) -> None:
1461
+ """The list as JSON, as the command line exports it.
1462
+
1463
+ Args:
1464
+ path: The file to write. Without it, the text is returned.
1465
+ columns: The fields to include: a list, or comma-separated as ``-c``
1466
+ writes them, each ``FIELD[:PRECISION][=LABEL]``.
1467
+ profile: A profile's name in ``.samplekitrc``, or a ``Profile``,
1468
+ instead of ``columns``.
1469
+ overwrite: Replace an existing file.
1470
+
1471
+ Returns:
1472
+ The text, when no ``path`` is given; otherwise ``None``.
1473
+
1474
+ Raises:
1475
+ ValueError: Neither ``columns`` nor ``profile`` is given, or both.
1476
+ KeyError: An unknown field or profile; the message names the nearest.
1477
+ FileExistsError: ``path`` exists and ``overwrite`` is false.
1478
+
1479
+ Example:
1480
+ >>> text = specimens.to_json(columns="name,area")
1481
+ """
1482
+ def export(
1483
+ self, export: str | Export, output: Pathish | None = None, overwrite: bool = False
1484
+ ) -> Path:
1485
+ """Write an export declared in ``.samplekitrc``, as ``samplekit export
1486
+ --write`` does.
1487
+
1488
+ The export's own query, if it declares one, narrows the list first. An
1489
+ export of a figure draws it as ``sk.plot`` does, to its file.
1490
+
1491
+ Args:
1492
+ export: The export's name, or an ``Export``.
1493
+ output: Write here instead of the declared file.
1494
+ overwrite: Replace an existing file.
1495
+
1496
+ Returns:
1497
+ The path written, absolute.
1498
+
1499
+ Raises:
1500
+ KeyError: No export has that name, or its profile or figure is not
1501
+ declared.
1502
+ FileExistsError: The file exists and ``overwrite`` is false.
1503
+ ValueError: ``output``'s extension does not match the export's format.
1504
+
1505
+ Example:
1506
+ >>> specimens.export("paper", output="paper/results.tex").name
1507
+ 'results.tex'
1508
+ """
1509
+
1510
+
1511
+ class Summary:
1512
+ """The statistics of a set of values: readings, or a field over a list.
1513
+
1514
+ Given by ``Property.stats`` and ``SampleList.stats``.
1515
+ """
1516
+
1517
+ def to_text(self, style: str | None = None) -> str:
1518
+ """The summary as ``--summary`` writes a row of it: the count, the
1519
+ mean, the deviation ``s``, its standard error ``sem``, the median and
1520
+ the extremes.
1521
+
1522
+ Args:
1523
+ style: Accepted for every ``to_text``; a summary's numbers are
1524
+ written alike in every style.
1525
+
1526
+ Returns:
1527
+ The text.
1528
+ """
1529
+ @property
1530
+ def count(self) -> int:
1531
+ """How many values were summarised."""
1532
+ @property
1533
+ def considered(self) -> int | None:
1534
+ """How many samples the value applies to, those holding no number
1535
+ included and those saying *not applicable* left out: the denominator of
1536
+ ``count``, as ``--summary`` writes ``3/4``. ``None`` for readings.
1537
+ """
1538
+ @property
1539
+ def not_applicable(self) -> int:
1540
+ """How many samples say the value is not applicable, counted apart —
1541
+ neither summarised nor missing.
1542
+ """
1543
+ @property
1544
+ def minimum(self) -> float:
1545
+ """The smallest value."""
1546
+ @property
1547
+ def maximum(self) -> float:
1548
+ """The largest value."""
1549
+ @property
1550
+ def mean(self) -> float:
1551
+ """The mean — weighted by 1/u² where every value has an uncertainty above zero,
1552
+ the arithmetic mean otherwise. ``weighted`` says which.
1553
+ """
1554
+ @property
1555
+ def weighted(self) -> bool:
1556
+ """Whether ``mean`` is weighted by 1/u², as the command line's summary heads
1557
+ it ``mean (1/u²)``.
1558
+ """
1559
+ @property
1560
+ def median(self) -> float:
1561
+ """The median."""
1562
+ @property
1563
+ def first_quartile(self) -> float:
1564
+ """The first quartile."""
1565
+ @property
1566
+ def third_quartile(self) -> float:
1567
+ """The third quartile."""
1568
+ @property
1569
+ def sample_stdev(self) -> float | None:
1570
+ """The sample standard deviation, dividing by n − 1; ``None`` for one value."""
1571
+ @property
1572
+ def population_stdev(self) -> float:
1573
+ """The population standard deviation, dividing by n."""
1574
+ @property
1575
+ def standard_error(self) -> float | None:
1576
+ """The standard error of the mean — 1/√Σw beside a weighted mean, the
1577
+ sample standard deviation over √n beside an arithmetic one; ``None``
1578
+ for one value unweighted.
1579
+ """
1580
+
1581
+
1582
+ class Field:
1583
+ """A field of a list, as ``SampleList.fields`` gives it, and as ``-c`` and
1584
+ ``-s`` take it.
1585
+
1586
+ ``str(field)`` is its path, and a field is accepted wherever a field's name
1587
+ is: ``specimens.sorted(specimens.fields.area)``.
1588
+ """
1589
+
1590
+ @property
1591
+ def path(self) -> str:
1592
+ """The field's path — ``"area"``, ``"curve.force[0.2]"``."""
1593
+ def __str__(self) -> str: ...
1594
+
1595
+
1596
+ # ------------------------------------------------------------ project
1597
+
1598
+
1599
+ class Names(Generic[_T]):
1600
+ """The entries a project declares, read-only, by name.
1601
+
1602
+ An entry is read as an attribute or an item — ``project.queries.ductile`` or
1603
+ ``project.queries["ductile"]`` — and iterating gives the names. An unknown
1604
+ name raises ``AttributeError`` or ``KeyError`` naming the nearest.
1605
+ """
1606
+
1607
+ def __getattr__(self, name: str) -> _T: ...
1608
+ def __getitem__(self, name: str) -> _T: ...
1609
+ def __iter__(self) -> Iterator[str]: ...
1610
+ def __len__(self) -> int: ...
1611
+ def __contains__(self, name: object) -> bool: ...
1612
+
1613
+
1614
+ class Project:
1615
+ """A project: what its ``.samplekitrc`` declares, read-only.
1616
+
1617
+ Read from a sample or a list — ``steel.project``, ``specimens.project``. Outside
1618
+ any project every collection of entries is empty.
1619
+
1620
+ Example:
1621
+ >>> list(specimens.project.queries)
1622
+ ['ductile']
1623
+ """
1624
+
1625
+ @property
1626
+ def root(self) -> Path | None:
1627
+ """The folder holding ``.samplekitrc``, or ``None`` outside a project."""
1628
+ @property
1629
+ def queries(self) -> Names[Query]:
1630
+ """The saved queries, ``[query.*]``, by name, each a ``Query``."""
1631
+ @property
1632
+ def profiles(self) -> Names[Profile]:
1633
+ """The profiles, ``[profile.*]``, by name, each a ``Profile``."""
1634
+ @property
1635
+ def exports(self) -> Names[Export]:
1636
+ """The exports, ``[export.*]``, by name, each an ``Export``."""
1637
+ @property
1638
+ def figures(self) -> Names[FigureDeclaration]:
1639
+ """The figures, ``[figure.*]``, by name, each a ``FigureDeclaration``."""
1640
+ @property
1641
+ def matplotlib(self) -> dict[str, object]:
1642
+ """The matplotlib settings every figure is drawn with, ``[matplotlib]`` —
1643
+ a dict by matplotlib's own dotted names.
1644
+ """
1645
+ @property
1646
+ def properties(self) -> Names[PropertyDeclaration]:
1647
+ """The declared properties, ``[property.*]``, by name, each a
1648
+ ``PropertyDeclaration``.
1649
+ """
1650
+ @property
1651
+ def units(self) -> Names[Unit]:
1652
+ """The declared units, ``[unit.*]``, by spelling, each a ``Unit``."""
1653
+ @property
1654
+ def render(self) -> Render:
1655
+ """How tables and figures are rendered, ``[render]``, as a ``Render``."""
1656
+ @property
1657
+ def model(self) -> Model | None:
1658
+ """The model, ``[model]``, or ``None`` when the project declares none."""
1659
+ @property
1660
+ def collection(self) -> Collection:
1661
+ """Which files are samples, ``[collection]``, as a ``Collection``."""
1662
+
1663
+
1664
+ class Query:
1665
+ """A saved query, ``[query.*]``: a filter under a name."""
1666
+
1667
+ @property
1668
+ def name(self) -> str:
1669
+ """The query's name."""
1670
+ @property
1671
+ def filter(self) -> str:
1672
+ """The filter expression, as ``-f`` takes it."""
1673
+ @property
1674
+ def directory(self) -> Path | None:
1675
+ """The folder the query searches by default, or ``None`` for the project's."""
1676
+
1677
+
1678
+ class ColumnSpec:
1679
+ """One column of a profile: its field, its headings and its precision."""
1680
+
1681
+ @property
1682
+ def field(self) -> str:
1683
+ """The field the column shows."""
1684
+ @property
1685
+ def label(self) -> str | None:
1686
+ """The heading of the column in a terminal table, or ``None`` for the field."""
1687
+ @property
1688
+ def header(self) -> str | None:
1689
+ """The heading of the column in an exported file, or ``None``."""
1690
+ @property
1691
+ def precision(self) -> Precision | None:
1692
+ """The column's precision — ``".3f"``, or a pair for the value and the
1693
+ uncertainty — or ``None``.
1694
+ """
1695
+ @property
1696
+ def template(self) -> str | None:
1697
+ """The text each cell is written as, such as ``"{value:.3f}"``, or ``None``."""
1698
+
1699
+
1700
+ class Profile:
1701
+ """A profile, ``[profile.*]``: the columns of a table, and the order of its
1702
+ rows.
1703
+ """
1704
+
1705
+ @property
1706
+ def name(self) -> str:
1707
+ """The profile's name."""
1708
+ @property
1709
+ def columns(self) -> tuple[ColumnSpec, ...]:
1710
+ """The columns, each a ``ColumnSpec``, in order."""
1711
+ @property
1712
+ def sort(self) -> tuple[str, ...]:
1713
+ """The sort keys, in order of precedence; a leading ``-`` sorts descending."""
1714
+
1715
+
1716
+ class FigureDeclaration:
1717
+ """A figure declared in ``.samplekitrc``, ``[figure.*]``: what it draws, and
1718
+ how its axes are set.
1719
+ """
1720
+
1721
+ @property
1722
+ def name(self) -> str:
1723
+ """The figure's name."""
1724
+ @property
1725
+ def kind(self) -> Literal["scatter", "line", "step", "bar", "box"]:
1726
+ """``"scatter"``, ``"line"``, ``"step"``, ``"bar"`` or ``"box"``."""
1727
+ @property
1728
+ def x(self) -> str:
1729
+ """The field on the x axis."""
1730
+ @property
1731
+ def y(self) -> str:
1732
+ """The field on the y axis."""
1733
+ @property
1734
+ def group(self) -> str | None:
1735
+ """The field, or comma-separated fields, whose values split the samples into
1736
+ series; ``None`` for one series.
1737
+ """
1738
+ @property
1739
+ def query(self) -> str | None:
1740
+ """The saved query that selects its samples, or ``None``."""
1741
+ @property
1742
+ def title(self) -> str | None:
1743
+ """The title, or ``None``."""
1744
+ @property
1745
+ def x_label(self) -> str | None:
1746
+ """The x axis label, or ``None`` for the field's symbol and unit."""
1747
+ @property
1748
+ def y_label(self) -> str | None:
1749
+ """The y axis label, or ``None`` for the field's symbol and unit."""
1750
+ @property
1751
+ def style(self) -> str | None:
1752
+ """The style whose symbols and units label the axes, or ``None``."""
1753
+ @property
1754
+ def x_limits(self) -> tuple[float | None, float | None] | None:
1755
+ """The x axis bounds, ``None`` for a free one; ``None`` when not declared."""
1756
+ @property
1757
+ def y_limits(self) -> tuple[float | None, float | None] | None:
1758
+ """The y axis bounds, ``None`` for a free one; ``None`` when not declared."""
1759
+ @property
1760
+ def x_scale(self) -> str | None:
1761
+ """``"linear"``, ``"log"`` or ``"symlog"``, or ``None``."""
1762
+ @property
1763
+ def y_scale(self) -> str | None:
1764
+ """``"linear"``, ``"log"`` or ``"symlog"``, or ``None``."""
1765
+ @property
1766
+ def aspect(self) -> str | None:
1767
+ """``"equal"`` or ``"auto"``, or ``None``."""
1768
+ @property
1769
+ def legend(self) -> str | None:
1770
+ """Where the legend goes — ``"best"``, ``"outside"``, ``"none"`` or one of
1771
+ matplotlib's places — or ``None``.
1772
+ """
1773
+ @property
1774
+ def figsize(self) -> tuple[float, float] | None:
1775
+ """The figure's width and height in centimetres, or ``None``."""
1776
+ @property
1777
+ def matplotlib(self) -> dict[str, Any]:
1778
+ """Its own matplotlib settings, over the project's ``[matplotlib]``, by
1779
+ matplotlib's dotted names.
1780
+ """
1781
+
1782
+
1783
+ class Export:
1784
+ """An export, ``[export.*]``: a profile written to a file in a format, or
1785
+ a figure drawn to a file.
1786
+ """
1787
+
1788
+ @property
1789
+ def name(self) -> str:
1790
+ """The export's name."""
1791
+ @property
1792
+ def profile(self) -> str | None:
1793
+ """The name of the profile it writes, or ``None`` for a figure's export."""
1794
+ @property
1795
+ def figure(self) -> str | None:
1796
+ """The name of the figure it draws, or ``None`` for a table's export."""
1797
+ @property
1798
+ def format(self) -> Literal["csv", "tsv", "json"] | None:
1799
+ """The data format — ``"csv"``, ``"tsv"``, ``"json"`` — or ``None`` for
1800
+ a document its style writes, or a figure.
1801
+ """
1802
+ @property
1803
+ def output(self) -> Path:
1804
+ """The file it writes."""
1805
+ @property
1806
+ def filename(self) -> bool:
1807
+ """Whether the export adds a column with each sample's file name."""
1808
+ @property
1809
+ def path(self) -> bool:
1810
+ """Whether the export adds a column with each sample's path."""
1811
+ @property
1812
+ def query(self) -> str | None:
1813
+ """The saved query that selects its samples, or ``None``."""
1814
+
1815
+
1816
+ class Statistic:
1817
+ """A statistic of a property's readings, which a model declares to give the
1818
+ value or the uncertainty.
1819
+
1820
+ The statistics are in ``samplekit.stats``.
1821
+
1822
+ Example:
1823
+ >>> sk.Property(value=sk.stats.median, uncertainty=sk.stats.standard_error)
1824
+ Property(value=None)
1825
+ """
1826
+
1827
+ @property
1828
+ def name(self) -> str:
1829
+ """Its name, as ``Summary`` spells it — ``"median"``."""
1830
+
1831
+
1832
+ class stats:
1833
+ """The statistics a model can declare for readings.
1834
+
1835
+ Six give a value — ``mean``, ``median``, ``minimum``, ``maximum``,
1836
+ ``first_quartile``, ``third_quartile`` — and three give an uncertainty —
1837
+ ``standard_error``, ``sample_stdev``, ``population_stdev``.
1838
+
1839
+ Example:
1840
+ >>> width = sk.Property(unit="mm", value=sk.stats.mean,
1841
+ ... uncertainty=sk.stats.standard_error)
1842
+ """
1843
+
1844
+ mean: Statistic
1845
+ median: Statistic
1846
+ minimum: Statistic
1847
+ maximum: Statistic
1848
+ first_quartile: Statistic
1849
+ third_quartile: Statistic
1850
+ standard_error: Statistic
1851
+ sample_stdev: Statistic
1852
+ population_stdev: Statistic
1853
+
1854
+
1855
+ class PropertyDeclaration:
1856
+ """A property declared in ``.samplekitrc``, ``[property.*]``: its unit, symbol
1857
+ and precision, for every sample of the project.
1858
+ """
1859
+
1860
+ @property
1861
+ def unit(self) -> str | None:
1862
+ """The unit, or ``None``."""
1863
+ @property
1864
+ def symbol(self) -> str | None:
1865
+ """The symbol, or ``None``."""
1866
+ @property
1867
+ def symbol_variants(self) -> Mapping[str, str]:
1868
+ r"""The symbol in each style, by style name:
1869
+ ``{"figure": "$\\sigma$"}``.
1870
+ """
1871
+ @property
1872
+ def precision(self) -> Precision | None:
1873
+ """The precision — ``".3f"``, or a pair for the value and the uncertainty —
1874
+ or ``None``.
1875
+ """
1876
+
1877
+
1878
+ class Unit:
1879
+ """A unit declared in ``.samplekitrc``, ``[unit.*]``: how it is written in
1880
+ each style.
1881
+ """
1882
+
1883
+ @property
1884
+ def spelling(self) -> str:
1885
+ """The unit as the files write it — ``"degC"``."""
1886
+ @property
1887
+ def variants(self) -> Mapping[str, str]:
1888
+ """The unit in each style, by style name — ``{"plain": "°C"}``."""
1889
+
1890
+
1891
+ class Render:
1892
+ """How the project renders tables and figures, ``[render]``."""
1893
+
1894
+ @property
1895
+ def style(self) -> str | None:
1896
+ """The style tables are rendered in, or ``None``."""
1897
+ @property
1898
+ def precision(self) -> Precision | None:
1899
+ """The precision of every number without its own, or ``None``."""
1900
+ @property
1901
+ def table(self) -> str | None:
1902
+ """How a table is drawn — ``"plain"`` or ``"boxed"`` — or ``None``."""
1903
+ @property
1904
+ def figure_style(self) -> str | None:
1905
+ """The style figures label their axes in when they name none, or ``None``."""
1906
+
1907
+
1908
+ class Model:
1909
+ """The project's model, ``[model]``: the Python file and class its samples are
1910
+ read with.
1911
+ """
1912
+
1913
+ @property
1914
+ def path(self) -> Path:
1915
+ """The model's Python file."""
1916
+ @property
1917
+ def class_name(self) -> str | None:
1918
+ """The class in that file, or ``None`` when the file declares only one."""
1919
+
1920
+
1921
+ class Collection:
1922
+ """Which files of the project are samples, ``[collection]``."""
1923
+
1924
+ @property
1925
+ def recursive(self) -> bool:
1926
+ """Whether subfolders are searched."""
1927
+ @property
1928
+ def include(self) -> tuple[str, ...]:
1929
+ """The globs a sample's file must match."""
1930
+ @property
1931
+ def exclude(self) -> tuple[str, ...]:
1932
+ """The globs of files that are not samples."""
1933
+ @property
1934
+ def files(self) -> tuple[str, ...]:
1935
+ """Where a sample's own files are looked for, ``[collection] files`` —
1936
+ the folders searched by the sample's name, and the patterns naming it,
1937
+ as written.
1938
+ """
1939
+
1940
+
1941
+ class NotApplicable:
1942
+ """A value that does not apply to a sample — ``yield_strength: n/a`` in its file.
1943
+
1944
+ There is one, ``samplekit.NA``. It is false in a test and prints as
1945
+ ``n/a``; a property set to it is saved as ``n/a``, and a formula reading
1946
+ it gives ``NA`` too, without running.
1947
+
1948
+ Example:
1949
+ >>> steel.yield_strength.value = sk.NA
1950
+ """
1951
+
1952
+ def __bool__(self) -> bool: ...
1953
+
1954
+ NA: NotApplicable
1955
+
1956
+ # One path is a sample for a file and a list for a folder, which a type
1957
+ # cannot tell apart: the script knows which it named, and a union would
1958
+ # refuse `for specimen in sk.load("samples")` until narrowed. Several paths are
1959
+ # always one list.
1960
+ @overload
1961
+ def load(path: Pathish, /) -> Any:
1962
+ """Read samples as the command line reads its targets.
1963
+
1964
+ A folder is read as a ``SampleList`` of its samples, as its project
1965
+ declares them, and a file as one ``Sample``; each is read with its
1966
+ project's model. Several paths are one ``SampleList``, in the order given,
1967
+ each sample once.
1968
+
1969
+ Args:
1970
+ *paths: Folders and sample files.
1971
+
1972
+ Returns:
1973
+ A ``Sample`` for one file, otherwise a ``SampleList``.
1974
+
1975
+ Raises:
1976
+ FileNotFoundError: A path does not exist; the message names the
1977
+ nearest.
1978
+ ValueError: A file is not a sample.
1979
+
1980
+ Example:
1981
+ >>> specimens = sk.load("samples")
1982
+ >>> steel = sk.load("samples/steel-01.md")
1983
+ >>> steel.area
1984
+ Property(value=25.05586444444445, uncertainty=0.05217296159913354, unit='mm²')
1985
+ """
1986
+ @overload
1987
+ def load(first: Pathish, second: Pathish, /, *paths: Pathish) -> SampleList:
1988
+ """Read samples as the command line reads its targets.
1989
+
1990
+ A folder is read as a ``SampleList`` of its samples, as its project
1991
+ declares them, and a file as one ``Sample``; each is read with its
1992
+ project's model. Several paths are one ``SampleList``, in the order given,
1993
+ each sample once.
1994
+
1995
+ Args:
1996
+ *paths: Folders and sample files.
1997
+
1998
+ Returns:
1999
+ A ``Sample`` for one file, otherwise a ``SampleList``.
2000
+
2001
+ Raises:
2002
+ FileNotFoundError: A path does not exist; the message names the
2003
+ nearest.
2004
+ ValueError: A file is not a sample.
2005
+
2006
+ Example:
2007
+ >>> specimens = sk.load("samples")
2008
+ >>> steel = sk.load("samples/steel-01.md")
2009
+ >>> steel.area
2010
+ Property(value=25.05586444444445, uncertainty=0.05217296159913354, unit='mm²')
2011
+ """
2012
+ def keep(message: str | None = None) -> None:
2013
+ """Keep what the script has saved so far as one snapshot of the history.
2014
+
2015
+ A script's saves are kept as one snapshot when it ends, with the
2016
+ script's source beside it; ``keep`` takes that snapshot now, so that the
2017
+ history shows the script's steps apart.
2018
+
2019
+ Args:
2020
+ message: What the snapshot says; the script's command line when not
2021
+ given. A blank message says nothing, and several lines are kept
2022
+ on one.
2023
+
2024
+ Warns:
2025
+ UserWarning: Nothing was saved in a project since the last snapshot,
2026
+ so nothing is kept.
2027
+
2028
+ Raises:
2029
+ TypeError: The message is not text.
2030
+
2031
+ Example:
2032
+ >>> steel.save()
2033
+ >>> sk.keep("thickness measured again")
2034
+ """
2035
+
2036
+
2037
+ # A figure is matplotlib's. `figure` marks a model's method — an
2038
+ # instance method draws one sample, a class method a collection — and `plot`
2039
+ # draws it, a declared one, or the axes given.
2040
+ _Method = TypeVar("_Method")
2041
+
2042
+
2043
+ @overload
2044
+ def figure(method: _Method) -> _Method:
2045
+ """Mark a method of a model as a figure.
2046
+
2047
+ The method draws into the matplotlib axes it receives, and returns
2048
+ nothing; ``samplekit.plot`` and ``samplekit plot`` open it in a window or
2049
+ write it to a file. A method draws one sample; placed above
2050
+ ``@classmethod``, it draws a list of them.
2051
+
2052
+ Args:
2053
+ method: The method, when used without arguments: ``@sk.figure``.
2054
+ subplots: ``(rows, columns)``: the method receives the array of axes
2055
+ ``plt.subplots`` makes, instead of one.
2056
+ figsize: The figure's width and height, in centimetres.
2057
+ matplotlib: matplotlib's settings for this figure alone, by their
2058
+ names, over the project's ``[matplotlib]``:
2059
+ ``{"lines.marker": "None"}``.
2060
+
2061
+ Returns:
2062
+ The method, marked.
2063
+
2064
+ Raises:
2065
+ ValueError: ``subplots`` is not two positive integers.
2066
+
2067
+ Example:
2068
+ >>> class Specimen(sk.Sample):
2069
+ ... @sk.figure
2070
+ ... def stress_strain(self, ax):
2071
+ ... ax.plot(self.curve.values("strain"),
2072
+ ... self.curve.values("stress"))
2073
+ """
2074
+ @overload
2075
+ def figure(
2076
+ method: None = None,
2077
+ *,
2078
+ subplots: tuple[int, int] | None = None,
2079
+ figsize: tuple[float, float] | None = None,
2080
+ matplotlib: dict[str, Any] | None = None,
2081
+ ) -> Callable[[_Method], _Method]:
2082
+ """Mark a method of a model as a figure.
2083
+
2084
+ The method draws into the matplotlib axes it receives, and returns
2085
+ nothing; ``samplekit.plot`` and ``samplekit plot`` open it in a window or
2086
+ write it to a file. A method draws one sample; placed above
2087
+ ``@classmethod``, it draws a list of them.
2088
+
2089
+ Args:
2090
+ method: The method, when used without arguments: ``@sk.figure``.
2091
+ subplots: ``(rows, columns)``: the method receives the array of axes
2092
+ ``plt.subplots`` makes, instead of one.
2093
+ figsize: The figure's width and height, in centimetres.
2094
+ matplotlib: matplotlib's settings for this figure alone, by their
2095
+ names, over the project's ``[matplotlib]``:
2096
+ ``{"lines.marker": "None"}``.
2097
+
2098
+ Returns:
2099
+ The method, marked.
2100
+
2101
+ Raises:
2102
+ ValueError: ``subplots`` is not two positive integers.
2103
+
2104
+ Example:
2105
+ >>> class Specimen(sk.Sample):
2106
+ ... @sk.figure
2107
+ ... def stress_strain(self, ax):
2108
+ ... ax.plot(self.curve.values("strain"),
2109
+ ... self.curve.values("stress"))
2110
+ """
2111
+ # A script is the model's. `script` marks a method — an instance method acts
2112
+ # on one sample, a class method on the selection — and `samplekit run` runs it.
2113
+ def script(method: _Method) -> _Method:
2114
+ """Mark a method of a model as a script.
2115
+
2116
+ ``samplekit run NAME`` runs it, and ``r`` in the workbench: a method acts
2117
+ on one sample, each selected in turn; placed above ``@classmethod``, it
2118
+ receives the selection. Its keyword parameters are the command's options,
2119
+ ``--set offset=0.002``, read as their annotation or their default says;
2120
+ its docstring is its description. What it changes in the samples is
2121
+ shown, and written once the change is accepted; what it returns is shown.
2122
+
2123
+ Args:
2124
+ method: The method.
2125
+
2126
+ Returns:
2127
+ The method, marked.
2128
+
2129
+ Raises:
2130
+ TypeError: The method is a static method, or takes ``*args`` or
2131
+ ``**kwargs``.
2132
+
2133
+ Example:
2134
+ >>> class Specimen(sk.Sample):
2135
+ ... @sk.script
2136
+ ... def classify(self, limit: float = 20.0):
2137
+ ... "Class the specimen's ductility by its elongation at break."
2138
+ ... self.ductility = "high" if self.elongation.value >= limit else "low"
2139
+ """
2140
+ def plot(
2141
+ target: Sample | SampleList | Iterable[Sample],
2142
+ figure: str | None = None,
2143
+ *,
2144
+ x: str | None = None,
2145
+ y: str | None = None,
2146
+ # A field, or several whose combinations are the groups.
2147
+ group: str | Sequence[str] | None = None,
2148
+ kind: Literal["scatter", "line", "step", "bar", "box"] | None = None,
2149
+ title: str | None = None,
2150
+ x_label: str | None = None,
2151
+ y_label: str | None = None,
2152
+ style: str | None = None,
2153
+ x_limits: tuple[AxisBound, AxisBound] | None = None,
2154
+ y_limits: tuple[AxisBound, AxisBound] | None = None,
2155
+ x_scale: Literal["linear", "log", "symlog"] | None = None,
2156
+ y_scale: Literal["linear", "log", "symlog"] | None = None,
2157
+ aspect: Literal["equal", "auto"] | None = None,
2158
+ legend: str | None = None,
2159
+ figsize: tuple[float, float] | None = None,
2160
+ output: Pathish | None = None,
2161
+ overwrite: bool = False,
2162
+ project_style: bool = True,
2163
+ ) -> list[Any]:
2164
+ """Draw a figure of a sample or a list of samples, with matplotlib.
2165
+
2166
+ The figure is a model's figure, one declared in ``.samplekitrc``, or
2167
+ axes given here: ``x`` and ``y``, drawn as a declared figure would be.
2168
+ It opens in a window, or is written to ``output``. The project's
2169
+ ``[matplotlib]`` settings apply to every figure.
2170
+
2171
+ Args:
2172
+ target: A sample, a ``SampleList``, or an iterable of samples.
2173
+ figure: The name of a model's figure or of a declared one.
2174
+ x: The field on the x axis, for a figure not declared.
2175
+ y: The field on the y axis, for a figure not declared.
2176
+ group: A field, or several, whose values split the samples into
2177
+ series, each in its own colour.
2178
+ kind: ``"scatter"`` (the default), ``"line"``, ``"step"``, ``"bar"``
2179
+ or ``"box"``.
2180
+ title: The title; ``""`` draws none.
2181
+ x_label: The x axis label, instead of the field's symbol and unit.
2182
+ y_label: The y axis label, instead of the field's symbol and unit.
2183
+ style: The declared style whose symbols and units label the axes;
2184
+ ``[render] figure_style`` by default.
2185
+ x_limits: The x axis bounds, ``(low, high)``; ``None`` or ``"auto"``
2186
+ leaves one free.
2187
+ y_limits: The y axis bounds, ``(low, high)``; ``None`` or ``"auto"``
2188
+ leaves one free.
2189
+ x_scale: ``"linear"``, ``"log"`` or ``"symlog"``.
2190
+ y_scale: ``"linear"``, ``"log"`` or ``"symlog"``.
2191
+ aspect: ``"equal"`` or ``"auto"``.
2192
+ legend: Where the legend goes: ``"best"``, ``"outside"``, ``"none"``
2193
+ or one of matplotlib's places.
2194
+ figsize: The figure's width and height, in centimetres.
2195
+ output: The file to write, in the format its extension names:
2196
+ ``.png``, ``.pdf``, ``.svg``… ``{name}`` in it writes one file per
2197
+ sample, for a figure of one sample. A folder it names that does
2198
+ not exist is made.
2199
+ overwrite: Replace an existing file.
2200
+ project_style: Apply the project's ``[matplotlib]`` settings.
2201
+
2202
+ Returns:
2203
+ The matplotlib figures drawn.
2204
+
2205
+ Raises:
2206
+ TypeError: Neither ``figure`` nor ``x`` and ``y`` is given, or both
2207
+ are, or axes options are given to a model's figure.
2208
+ KeyError: An unknown figure or field; the message names the nearest.
2209
+ ValueError: No sample to draw, or an output format matplotlib
2210
+ cannot write.
2211
+ FileExistsError: ``output`` exists and ``overwrite`` is false.
2212
+ RuntimeError: No ``output`` is given and there is no display.
2213
+
2214
+ Example:
2215
+ >>> figures = sk.plot(specimens, "curves", output="paper/curves.pdf")
2216
+ >>> figures = sk.plot(specimens, x="yield_strength", y="tensile_strength",
2217
+ ... group="material", output="strengths.png")
2218
+ """