visualdynamics.core.run_log¶
run_log
¶
The run log: what happened on the floor, in order, as it happened.
A run log is kept while a test runs, by a log keeper who is not at the controller. Each thing written down (a level reached, an abort, a clock check, a person arriving, a note) is one entry. An entry is never edited: a mistake is put right by a correction, a later entry that names the one it corrects and carries the new values, so the reader sees the corrected view and the record keeps both.
The file is JSON Lines (.vdlog): UTF-8 text, one JSON object per
line, each line written, flushed and synced to the disk before the
writer returns, so a crash loses at most the entry being written. The
first line is the header, with the format's stamp and version. A file
cut off in the middle of its last line is read up to that line, and the
torn line is named (RunLog.problems); any other line that cannot be
read is skipped and named the same way, and the rest is read.
Versions. FORMAT_VERSION is the newest version this build reads,
and the one it writes. Each version has its own reader, so a later
build reads every earlier version, and a file from a newer version is
refused with a message to update. While Visual Dynamics is an alpha
the format may still change; from the first release that is not an
alpha, an older file always opens.
Integrity. The entries of each test are chained by SHA-256: every
entry carries the hash of the entry before it in the same test and the
hash of its own canonical form, which includes that previous hash. The
entries between tests have a chain of their own. RunLog.verify checks
every chain and names the first broken link by test and entry. The
chain detects accidental damage, a file cut short and casual edits. It
is not proof against deliberate alteration: anyone who edits an
entry can compute every hash after it again, and the chain then checks.
A file cut off at the end of an entry also leaves a valid chain; it is
found only against a head hash recorded outside the file
(RunLog.head, which a report's data index can carry). Every entry
records the computer's login and machine name beside the author's
typed initials; those are recorded, not authenticated.
One writer. A log open for writing holds a lock file beside it
(<log>.lock); a second writer is refused with a message naming who
holds it, and readers open the file read-only (RunLog.read). The lock
is the operating system's, so a writer that crashes leaves no lock
behind. Each entry can also be copied to a second path (mirror, the
lab's share or the data folder); a mirror that cannot be written is
reported (RunLog.mirror_problem) and never costs the entry its place
in the log itself.
The entry kinds and the fields each may carry are one registry
(KINDS). Each kind requires as little as it can: an abort needs only
its time, the kind of stop and the author, because the first minute
after a stop is the worst moment to be refused. What is still owed of
an entry is a derived view, not a refusal.
Classes:
| Name | Description |
|---|---|
EntryKind |
One kind of entry: its name in the file, its label in words, the |
RunLogError |
A run log, or an entry for one, that cannot be read or written |
RunLogLocked |
The log is already open for writing elsewhere; the message names |
FileRef |
A file an entry names: its name, its SHA-256 as 64 lower-case hex |
Header |
A log's first line: its format version, its id, when and where it |
Entry |
One entry, as stored, or as corrected ( |
ReadProblem |
A line of the file that was not read as an entry: its number (the |
Break |
A broken link found by |
Verification |
What |
RunLog |
A run log: its header, its entries in the order written, and, when |
Functions:
| Name | Description |
|---|---|
file_ref |
A |
chain_name |
The name of the chain an entry with test reference |
identity |
(login, machine name) of this computer, each '' where the system |
canonical |
The canonical text of a stored entry (or header) that its hash is |
band_hz |
A band as (low, high) in Hz, from two frequencies or from text |
check_fields |
The fields of a new entry of |
Classes¶
EntryKind
dataclass
¶
EntryKind(name: str, label: str, required: tuple[str, ...] = (), optional: tuple[str, ...] = (), needs_test: bool = False, machine: bool = False)
One kind of entry: its name in the file, its label in words, the fields it needs and the fields it may carry.
needs_test is set for the kinds that mean nothing without a test
(a test opened or closed). machine marks the kinds a lab's own
machine log keeps: the trips, the setting changes, the clock checks,
people in and out, the restarts, and the tests opened and closed
that say what was running.
Attributes:
| Name | Type | Description |
|---|---|---|
fields |
tuple[str, ...]
|
Every field the kind may carry, the required first. |
RunLogError
¶
Bases: ValueError
A run log, or an entry for one, that cannot be read or written one way; the message names the cause.
RunLogLocked
¶
Bases: RunLogError
The log is already open for writing elsewhere; the message names who holds it.
FileRef
dataclass
¶
FileRef(name: str, sha256: str, size: int | None = None, format: str = '', holds: str = '', path: str = '')
A file an entry names: its name, its SHA-256 as 64 lower-case hex digits, and its size in bytes when known.
format, holds and path say, when known, the file's format,
what it holds (its run and channels) and where it was when it was
hashed, in the words a data index records them
(data_index.IndexEntry), so the log's files go into a report's
data index as they are. Each is left out of the file when empty.
Methods:
| Name | Description |
|---|---|
to_dict |
The file as the log stores it. |
Methods:¶
to_dict
¶
The file as the log stores it.
Source code in src/visualdynamics/core/run_log.py
Header
dataclass
¶
Header(version: int, log_id: str, created: datetime, title: str = '', login: str = '', computer: str = '')
A log's first line: its format version, its id, when and where it was created, and its title.
Entry
dataclass
¶
Entry(id: int, kind: str, event_time: datetime, entered_at: datetime, author: str, login: str = '', computer: str = '', test: int | None = None, axis: str | None = None, run: int | None = None, fields: Mapping[str, Any] = dict(), text: str = '', files: tuple[FileRef, ...] = (), corrects: int | None = None, prev: str = '', hash: str = '', corrected_by: tuple[int, ...] = (), line: int = 0)
One entry, as stored, or as corrected (RunLog.view).
event_time is when the thing happened and entered_at when it
was written down, each with its date and its UTC offset. test,
axis and run are plain references (a test and run by number, an
axis by name), any of them absent. corrects is the id of the
entry a correction corrects. prev and hash are the entry's links
in its test's chain. In the corrected view, corrected_by lists the
corrections applied, in order. line is the entry's line in the
file, the header being line 1.
Methods:
| Name | Description |
|---|---|
is_late |
Whether the entry was written down more than |
to_dict |
The entry as the log stores it, its hash included. |
Attributes:
| Name | Type | Description |
|---|---|---|
label |
str
|
The kind in words, as |
chain |
str
|
The chain the entry belongs to: 'test N', or 'log' for an |
Attributes¶
chain
property
¶
The chain the entry belongs to: 'test N', or 'log' for an entry between tests.
Methods:¶
is_late
¶
Whether the entry was written down more than after after
the event it records. Both times carry their offsets, so a
change of daylight saving time between them counts as the real
interval, not the difference of the wall clocks.
Source code in src/visualdynamics/core/run_log.py
to_dict
¶
The entry as the log stores it, its hash included.
Source code in src/visualdynamics/core/run_log.py
ReadProblem
dataclass
¶
A line of the file that was not read as an entry: its number (the header is line 1) and why.
Break
dataclass
¶
A broken link found by RunLog.verify: the chain ('test N', or
'log' for the entries between tests), the test's number, the
entry's id and line where they are known, and what was found.
Verification
dataclass
¶
Verification(breaks: tuple[Break, ...], entries: int, chains: int)
RunLog
¶
RunLog(path: str, header: Header, header_raw: Mapping[str, Any], *, clock: Callable[[], datetime] | None = None, who: tuple[str, str] | None = None)
A run log: its header, its entries in the order written, and, when opened for writing, the way to add to it.
Make one with RunLog.create (a new file), RunLog.open (an
existing file, for writing) or RunLog.read (any file, read-only).
A log open for writing holds the lock until close; use it in a
with block.
Methods:
| Name | Description |
|---|---|
create |
A new run log at |
open |
The run log at |
read |
The run log at |
parse |
A run log read from |
close |
Release the lock and the file. A read-only log has neither; |
entry |
The entry with id |
corrections_of |
The corrections of entry |
current |
Entry |
view |
The corrected view: every entry but the corrections, in the |
tests |
The test numbers the log's entries refer to, in the order |
chains |
The names of the log's chains, in the order first written: |
head |
The head of a test's chain ( |
next_number |
The number the next finding of |
clock_check_for |
The clock check in force at |
recorder_time |
|
verify |
Check every chain, and the heads recorded outside the file. |
append |
Write one entry, synced to the disk before this returns. |
correct |
Write a correction of an earlier entry, which stays as it was. |
Attributes:
| Name | Type | Description |
|---|---|---|
writable |
bool
|
Whether entries can be added: opened for writing and not yet |
version |
int
|
The format version the file is written in. |
entries |
tuple[Entry, ...]
|
Every entry as stored, corrections included, in the order |
problems |
tuple[ReadProblem, ...]
|
Every line that was not read as an entry, and why: a torn last |
Source code in src/visualdynamics/core/run_log.py
Attributes¶
writable
property
¶
Whether entries can be added: opened for writing and not yet closed.
entries
property
¶
entries: tuple[Entry, ...]
Every entry as stored, corrections included, in the order written.
problems
property
¶
problems: tuple[ReadProblem, ...]
Every line that was not read as an entry, and why: a torn last line, a line that is not JSON, a line that is not an entry.
Methods:¶
create
classmethod
¶
create(path: str | PathLike, *, title: str = '', mirror: str | PathLike | None = None, clock: Callable[[], datetime] | None = None, who: tuple[str, str] | None = None) -> RunLog
A new run log at path, open for writing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or PathLike
|
Where the log is written. Nothing may be there yet. |
required |
title
|
str
|
The log's title (the article or program it is kept for). |
''
|
mirror
|
str or PathLike
|
A second path every entry is copied to as well. |
None
|
clock
|
callable
|
Returns the time entries are entered at, with its zone; the computer's own clock unless given. |
None
|
who
|
(str, str)
|
The (login, machine name) every entry records; read from the computer unless given. |
None
|
Returns:
| Type | Description |
|---|---|
RunLog
|
The new log, holding only its header. |
Raises:
| Type | Description |
|---|---|
RunLogError
|
When a file is already at |
RunLogLocked
|
When another writer holds the lock beside |
Source code in src/visualdynamics/core/run_log.py
open
classmethod
¶
open(path: str | PathLike, *, mirror: str | PathLike | None = None, clock: Callable[[], datetime] | None = None, who: tuple[str, str] | None = None) -> RunLog
The run log at path, open for writing.
A torn last line (a crash in the middle of an entry) is named in
problems and ended, so the next entry starts on a line of its
own; nothing in the file is removed. An empty file (a crash
while it was being created) is given its header.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or PathLike
|
The log. |
required |
mirror
|
str or PathLike
|
A second path every entry is copied to as well; the whole log is copied there first. |
None
|
clock
|
callable
|
Returns the time entries are entered at, with its zone. |
None
|
who
|
(str, str)
|
The (login, machine name) every entry records. |
None
|
Returns:
| Type | Description |
|---|---|
RunLog
|
The log, with every entry read. |
Raises:
| Type | Description |
|---|---|
RunLogError
|
When the file is not a run log, is of a newer format version, or is of an older version (which opens read-only, since a log is only ever written in one version). |
RunLogLocked
|
When another writer holds the lock. |
FileNotFoundError
|
When there is no file at |
Source code in src/visualdynamics/core/run_log.py
1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 | |
read
classmethod
¶
read(path: str | PathLike) -> RunLog
The run log at path, read-only, without the lock, so it can be
read while another program writes it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or PathLike
|
The log. |
required |
Returns:
| Type | Description |
|---|---|
RunLog
|
The log, every entry that could be read in it, and the lines
that could not in |
Raises:
| Type | Description |
|---|---|
RunLogError
|
When the file is empty, is not a run log, or is of a newer format version than this build reads. |
FileNotFoundError
|
When there is no file at |
Source code in src/visualdynamics/core/run_log.py
parse
classmethod
¶
parse(data: bytes, path: str = '<memory>') -> RunLog
A run log read from data, the bytes of a .vdlog, read-only:
read without the file, for a log held in memory (a snapshot
kept in a project) or bytes read once and examined more than
one way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes
|
The log's bytes, its header line first. |
required |
path
|
str
|
What the log is called in messages, and its |
'<memory>'
|
Returns:
| Type | Description |
|---|---|
RunLog
|
The log, every entry that could be read in it, and the lines
that could not in |
Raises:
| Type | Description |
|---|---|
RunLogError
|
When |
Source code in src/visualdynamics/core/run_log.py
close
¶
Release the lock and the file. A read-only log has neither; closing it does nothing.
Source code in src/visualdynamics/core/run_log.py
current
¶
current(entry_id: int) -> Entry
Entry entry_id with its corrections applied in order, each
one's values replacing the entry's and a value of None striking
a field; corrected_by lists them. The stored entry is
unchanged (entry).
Source code in src/visualdynamics/core/run_log.py
view
¶
view() -> list[Entry]
The corrected view: every entry but the corrections, in the
order written, each with its corrections applied
(current).
Source code in src/visualdynamics/core/run_log.py
tests
¶
The test numbers the log's entries refer to, in the order first referred to.
Source code in src/visualdynamics/core/run_log.py
chains
¶
The names of the log's chains, in the order first written: 'log' for the entries between tests, 'test N' for each test.
Source code in src/visualdynamics/core/run_log.py
head
¶
The head of a test's chain (test None for the entries
between tests): the hash of its last entry, which the next entry
will carry as its previous hash. A chain with no entries yet has
its starting hash. A head recorded outside the file (printed at
the test's close, in a report's data index) is what
verify(heads=...) finds a file cut short against.
Source code in src/visualdynamics/core/run_log.py
next_number
¶
The number the next finding of kind ('deviation' or
'anomaly') takes, numbered across the whole log: D-1, D-2, A-1.
clock_check_for
¶
The clock check in force at at (an entry, by its corrected
event time, or a time with its zone): the latest clock check at
or before it, of recorder when named. None when no clock check
comes before it.
Source code in src/visualdynamics/core/run_log.py
recorder_time
¶
recorder_time(at: Entry | datetime, recorder: str | None = None) -> datetime | None
at on the recorders' clock: its time plus the offset of the
clock check in force (clock_check_for), which records the
recorders' clock less this computer's, in seconds. None when no
clock check comes before it.
Source code in src/visualdynamics/core/run_log.py
verify
¶
verify(heads: Mapping[int | None, str] | None = None, tests: Iterable[int | None] | None = None) -> Verification
Check every chain, and the heads recorded outside the file.
Each entry's previous hash must be the hash of the entry before
it in its chain (the chain's starting hash for the first), and
its hash must be the hash of its own canonical form. Entry ids
must run from 1 without a gap. A head in heads (by test number,
None for the entries between tests) must still be in its chain;
one that is not means the file was cut short or changed after
the head was recorded.
This detects accidental damage, a file cut short (mid-entry on
reading; at an entry's end only against heads) and casual
edits. It is not proof against deliberate alteration: an edit
whose author computes every following hash again is not
detected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
heads
|
mapping
|
Heads recorded outside the file, by test number. |
None
|
tests
|
iterable
|
Check only these tests' chains (None for the entries between tests), as a file holding one test's entries is checked; the ids then need not run without a gap. |
None
|
Returns:
| Type | Description |
|---|---|
Verification
|
Every break in the order of the file, and the first. |
Source code in src/visualdynamics/core/run_log.py
1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 | |
append
¶
append(kind: str, *, author: str, fields: Mapping[str, Any] | None = None, text: str = '', event_time: datetime | None = None, test: int | None = None, axis: str | None = None, run: int | None = None, files: Iterable[str | PathLike | FileRef] = ()) -> Entry
Write one entry, synced to the disk before this returns.
A deviation or anomaly opened without a number takes the next of
its kind across the log (next_number); a number given must be
of its kind and not already taken in the log. A correction is written with
correct, not here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
One of |
required |
author
|
str
|
The author's typed initials. |
required |
fields
|
mapping
|
The kind's fields ( |
None
|
text
|
str
|
Free text. |
''
|
event_time
|
datetime
|
When the thing happened, with its zone; now, if not given. |
None
|
test
|
int
|
The test and run it belongs to, by number from 1. |
None
|
run
|
int
|
The test and run it belongs to, by number from 1. |
None
|
axis
|
str
|
The axis it belongs to, by name. |
None
|
files
|
iterable
|
Files it names, each a path (hashed now) or a |
()
|
Returns:
| Type | Description |
|---|---|
Entry
|
The entry as written, with its id and hashes. |
Raises:
| Type | Description |
|---|---|
RunLogError
|
For a log not open for writing, and for anything the entry cannot hold one way, the message naming the field. |
Source code in src/visualdynamics/core/run_log.py
1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 | |
correct
¶
correct(target: int | Entry, *, author: str, fields: Mapping[str, Any] | None = None, text: str = '', event_time: datetime | None = None) -> Entry
Write a correction of an earlier entry, which stays as it was.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
int or Entry
|
The entry corrected, by id. A correction is not corrected itself; correct the original entry again. |
required |
author
|
str
|
The author's typed initials. |
required |
fields
|
mapping
|
The new values: any of the corrected entry's fields (None
strikes an optional one), and any of |
None
|
text
|
str
|
Why it is corrected. |
''
|
event_time
|
datetime
|
When the correction was made; now, if not given. |
None
|
Returns:
| Type | Description |
|---|---|
Entry
|
The correction as written. It belongs to the corrected entry's test, so it is in that test's chain. |
Raises:
| Type | Description |
|---|---|
RunLogError
|
When the entry is not in the log or is a correction, when no value is given, when a value is one the entry cannot hold, when a required field would be struck, or when a finding's number would change. |
Source code in src/visualdynamics/core/run_log.py
1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 | |
Functions:¶
file_ref
¶
file_ref(path: str | PathLike, name: str | None = None) -> FileRef
A FileRef for the file at path, hashed in chunks
(data_index.digest); name is the file's own name unless given.
Source code in src/visualdynamics/core/run_log.py
chain_name
¶
The name of the chain an entry with test reference test belongs
to: 'test N', or 'log' for none.
identity
¶
(login, machine name) of this computer, each '' where the system does not say.
Source code in src/visualdynamics/core/run_log.py
canonical
¶
The canonical text of a stored entry (or header) that its hash is
taken over: keys sorted, no spaces, UTF-8 characters as themselves,
and the entry's own hash left out. The previous hash is a key of
the entry, so it is part of what is hashed.
Source code in src/visualdynamics/core/run_log.py
band_hz
¶
A band as (low, high) in Hz, from two frequencies or from text such as '180-220 Hz', '180 to 220', '1.2-2 kHz' or '200 Hz' (one frequency, both edges).
Refused with RunLogError naming what: text that is not one of
those forms, and anything findings.band_of refuses (not two
finite, non-negative frequencies, low edge above high).
Source code in src/visualdynamics/core/run_log.py
check_fields
¶
The fields of a new entry of kind, checked against KINDS.
A field set to None or to blank text is left out, so a form can
pass every box it has. Refused with RunLogError, each naming the
kind and the field: a kind that is not in KINDS, a field the kind
does not carry, a value of the wrong type or not finite, a required
field missing, an abort's kind of stop not one of STOPS, and a
sweep's span that band_hz does not read as a band.