c0data-py 0.3.2__tar.gz → 1.0.0__tar.gz
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.
- c0data_py-1.0.0/MANIFEST.in +6 -0
- c0data_py-1.0.0/PKG-INFO +234 -0
- c0data_py-1.0.0/README.md +221 -0
- {c0data_py-0.3.2 → c0data_py-1.0.0}/c0-c/c0.h +2309 -1509
- c0data_py-1.0.0/c0-c/c0_diff.c +551 -0
- c0data_py-1.0.0/c0-c/c0_diff.h +176 -0
- c0data_py-1.0.0/c0-c/c0_strbuf.h +49 -0
- {c0data_py-0.3.2 → c0data_py-1.0.0}/pyproject.toml +1 -1
- {c0data_py-0.3.2 → c0data_py-1.0.0}/setup.py +1 -1
- {c0data_py-0.3.2 → c0data_py-1.0.0}/src/c0/__init__.py +412 -115
- {c0data_py-0.3.2 → c0data_py-1.0.0}/src/c0/_c0module.c +426 -45
- c0data_py-1.0.0/src/c0/diff.py +314 -0
- {c0data_py-0.3.2 → c0data_py-1.0.0}/src/c0/errors.py +17 -0
- c0data_py-1.0.0/src/c0data_py.egg-info/PKG-INFO +234 -0
- {c0data_py-0.3.2 → c0data_py-1.0.0}/src/c0data_py.egg-info/SOURCES.txt +6 -1
- {c0data_py-0.3.2 → c0data_py-1.0.0}/tests/test_basic.py +89 -13
- {c0data_py-0.3.2 → c0data_py-1.0.0}/tests/test_conformance.py +64 -13
- {c0data_py-0.3.2 → c0data_py-1.0.0}/tests/test_primitives.py +31 -13
- c0data_py-1.0.0/tests/test_redesign.py +262 -0
- c0data_py-0.3.2/MANIFEST.in +0 -3
- c0data_py-0.3.2/PKG-INFO +0 -159
- c0data_py-0.3.2/README.md +0 -146
- c0data_py-0.3.2/src/c0data_py.egg-info/PKG-INFO +0 -159
- {c0data_py-0.3.2 → c0data_py-1.0.0}/LICENSE +0 -0
- {c0data_py-0.3.2 → c0data_py-1.0.0}/setup.cfg +0 -0
- {c0data_py-0.3.2 → c0data_py-1.0.0}/src/c0data_py.egg-info/dependency_links.txt +0 -0
- {c0data_py-0.3.2 → c0data_py-1.0.0}/src/c0data_py.egg-info/top_level.txt +0 -0
c0data_py-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: c0data-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: C0DATA 1.0 — structured data using ASCII C0 control codes (Python binding over the c0 C core)
|
|
5
|
+
Author-email: Thomas Sawyer <transfire@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/c0data/c0-py
|
|
8
|
+
Keywords: c0,c0data,data-format,control-codes
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# c0
|
|
15
|
+
|
|
16
|
+
A Python implementation of [C0DATA](https://github.com/c0data) 1.0 — structured
|
|
17
|
+
data built on ASCII C0 control codes.
|
|
18
|
+
|
|
19
|
+
This is **not** a reimplementation: it's a thin, fast binding over the
|
|
20
|
+
[c0 C core](https://github.com/c0data/c0-c) (a CPython extension, no third-party
|
|
21
|
+
dependencies), so the scan-heavy work runs as native C while the Python layer
|
|
22
|
+
provides an idiomatic API. The read path is **zero-copy** — raw accessors return
|
|
23
|
+
`memoryview` slices into the input buffer; logical values come back as `bytes`,
|
|
24
|
+
with `str` conveniences beside them.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import c0
|
|
30
|
+
|
|
31
|
+
# Write: SYN heading, a group with a header row (␁␞), records, EOT
|
|
32
|
+
buf = c0.build(lambda b: (
|
|
33
|
+
b.syn(schema="ledger"),
|
|
34
|
+
b.group("users", ["name", "amount"]),
|
|
35
|
+
b.record("Alice", "100"),
|
|
36
|
+
b.record("Bob", "200"),
|
|
37
|
+
b.eot(),
|
|
38
|
+
))
|
|
39
|
+
# ␖␁schema␟ledger␝users␁␞name␟amount␞Alice␟100␞Bob␟200␄
|
|
40
|
+
|
|
41
|
+
# Read (parse is validate: malformed input raises c0.C0SyntaxError)
|
|
42
|
+
doc = c0.Document(buf) # or c0.Container(buf) for several documents / an EM tail
|
|
43
|
+
doc.heading.schema # b'ledger'
|
|
44
|
+
users = doc.group("users") # by name or index; doc["users"] too
|
|
45
|
+
users.headers # [b'name', b'amount']
|
|
46
|
+
for rec in users:
|
|
47
|
+
rec.values # [b'Alice', b'100'] — plain trimmed, text decoded
|
|
48
|
+
rec.string("amount") # '100', by header name or index
|
|
49
|
+
rec.field(0) # memoryview of the raw unit (zero-copy)
|
|
50
|
+
|
|
51
|
+
# The body is a canonical span — hashable for content addressing
|
|
52
|
+
assert c0.canonical(c0.build(lambda b: (b.group("users", ["name", "amount"]),
|
|
53
|
+
b.record("Alice", "100"), b.record("Bob", "200"))))
|
|
54
|
+
c0.validate(buf) # the same grammar check on a buffer, without building a model
|
|
55
|
+
|
|
56
|
+
# Streams and pretty form
|
|
57
|
+
log = c0.StreamReader(open("claims.c0", "rb").read()) # .torn, .blocks, .damaged, .records
|
|
58
|
+
print(c0.pretty_format(buf)) # Unicode Control Pictures
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The model is the **result**: a dead element (`␇␞…`) is parsed and dropped
|
|
62
|
+
with everything it holds (a dead group takes its header row, records and
|
|
63
|
+
subsections, a dead file its groups). SUB makes the element after it a
|
|
64
|
+
child of the element before, at that code's grain: `␚␝s` is a subsection
|
|
65
|
+
at depth two (`g.depth`; a run of SUB is the depth, never skipped), and
|
|
66
|
+
`␞a␟b␚␟c` is two units with `c` a sub-unit of `b`, not a unit of the
|
|
67
|
+
record (`field_count`, `values`, `pairs` see units only). `rec.unit(n)` and
|
|
68
|
+
`rec.unit(key)` give a `Unit` with its `subs`, each a `Unit` in turn;
|
|
69
|
+
`rec.units` lists them all. `␚␞`, `␚␜` and `␚␁` are reserved and refused.
|
|
70
|
+
A SUB prefix is structure: canonical form keeps it. Canonical checking is
|
|
71
|
+
opt-in (`c0.canonical`) and never fails parsing.
|
|
72
|
+
|
|
73
|
+
Values are written in their one spelling: plain when they can be, text
|
|
74
|
+
(`␂…␃`) when they hold a control byte or edge whitespace. Keyed records
|
|
75
|
+
(`␞␁host␟localhost␁port␟5432`) are maps that keep their order:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
buf = c0.build(lambda b: (b.group("database"), b.map(host="localhost", port="5432")))
|
|
79
|
+
rec = c0.Body(buf).group(0).record(0)
|
|
80
|
+
rec.keyed, rec.keys, rec["port"] # True, [b'host', b'port'], b'5432'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Nested levels, lists and references
|
|
84
|
+
|
|
85
|
+
A unit may be a nested level, `␐…␘`, whose contents are a whole body. A
|
|
86
|
+
level holding one record is a list; `Builder.list` writes one and
|
|
87
|
+
`Record.list` reads it back.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
buf = c0.build(lambda b: (
|
|
91
|
+
b.group("users", ["name", "roles", "address"]),
|
|
92
|
+
b.record("Alice"),
|
|
93
|
+
b.list(["Admin", "Editor"]), # ␐␞Admin␟Editor␘
|
|
94
|
+
b.level(lambda l: (l.header("street", "city"), # ␐␁␞street␟city␞1 Main␟Springfield␘
|
|
95
|
+
l.record("1 Main", "Springfield"))),
|
|
96
|
+
))
|
|
97
|
+
rec = c0.Body(buf).group(0).record(0)
|
|
98
|
+
rec.list(1) # [b'Admin', b'Editor']
|
|
99
|
+
rec.level(2).record(0).string("city") # 'Springfield'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`level()` and `key_level(name)` also work as context managers (`with b.level(): …`).
|
|
103
|
+
|
|
104
|
+
A reference is a group name written plain, `b.ref("tags")` (`␅tags`), or a
|
|
105
|
+
**pattern**: a level that is one branch of the target with its values left
|
|
106
|
+
out, `b.ref("tags", "001", "label")` (`␅␐␝tags␞001␟label␘`) or any pattern
|
|
107
|
+
built in a callable, `b.ref(lambda: b.gs("s").rs().soh("host").us())`
|
|
108
|
+
(`␅␐␝s␞␁host␟␘`). `Record.ref` reads one back as a `Reference` with a
|
|
109
|
+
`name` or a `pattern` (the level as a `Body`); `ref.path` linearises it and
|
|
110
|
+
`ref.is_group` says whether it names a group as a whole. `b.sub_unit("new")`
|
|
111
|
+
writes `␚␟new`, a sub-unit of the unit just written (`depth=2` one deeper);
|
|
112
|
+
`b.group("intro", depth=2)` writes `␚␝intro`, a subsection.
|
|
113
|
+
|
|
114
|
+
The builder also has `file`, `section`, `header`, `field`, `key`, `note`,
|
|
115
|
+
`etb(payload)`, `em(tail)`, `block` and `item`, matching the Crystal reference.
|
|
116
|
+
Every error derives from `c0.C0Error`.
|
|
117
|
+
|
|
118
|
+
### Primitives
|
|
119
|
+
|
|
120
|
+
Underneath, the builder has one method per code: `fs`, `gs`, `rs`, `us`,
|
|
121
|
+
`sub`, `soh`, `dle`, `can`, `bel`, `enq`, `syn`, `eot`, `etb`, `em`. Each
|
|
122
|
+
writes its code and, when given, what follows it, so a chain reads like the
|
|
123
|
+
bytes, and anything the grammar allows can be written. The argument is
|
|
124
|
+
optional where the grammar allows the code alone (`gs()`, `rs()`, `us()`,
|
|
125
|
+
`soh()`, `bel()`, `etb()`); `sub()` is bare; `enq` takes a group name or a
|
|
126
|
+
callable that writes a pattern:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
b.rs("Alice").us("1502.30") # ␞Alice␟1502.30
|
|
130
|
+
b.rs().soh("host").us("localhost").soh("port").us("5432") # ␞␁host␟localhost␁port␟5432
|
|
131
|
+
b.gs("users").soh().rs("name").us("amount") # ␝users␁␞name␟amount: a header row
|
|
132
|
+
b.rs("cfg").us().dle().rs().soh("b").us("2").can() # ␞cfg␟␐␞␁b␟2␘
|
|
133
|
+
b.bel("checked").rs("x") # ␇checked␞x: a note
|
|
134
|
+
b.bel().gs("old") # ␇␝old: a dead group
|
|
135
|
+
b.rs("old").sub().us("new") # ␞old␚␟new: new is a sub-unit of old
|
|
136
|
+
b.gs("top").gs("sub", depth=2) # ␝top␚␝sub: a subsection
|
|
137
|
+
b.gs("sub").rs().enq("users").us().enq(lambda: b.gs("users").rs("1")) # ␝sub␞␅users␟␅␐␝users␞1␘
|
|
138
|
+
b.syn().soh("version").us("1").rs("a").eot() # ␖␁version␟1␞a␄
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The composed methods above are built on these and cannot write ill-formed
|
|
142
|
+
output; with the primitives, well-formedness is yours. Values keep their one
|
|
143
|
+
spelling either way (there is no STX/ETX primitive), and names are still
|
|
144
|
+
checked. The two layers share the record state and mix freely:
|
|
145
|
+
`b.rs("a").field("b")` writes `␞a␟b`, `b.record().us("c")` writes `␞␟c`.
|
|
146
|
+
|
|
147
|
+
## Install / build
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
pip install c0data-py # the package is c0data-py (PyPI treats c0data as codata); in code it is `import c0`
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
From a checkout, the C core and the shared conformance vectors are git submodules:
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
git submodule update --init # pulls in c0-c (the header) and c0-spec
|
|
157
|
+
pip install . # builds the extension
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Requires a C compiler and the Python development headers.
|
|
161
|
+
|
|
162
|
+
### C0DIFF
|
|
163
|
+
|
|
164
|
+
A diff (`DIFF.md`) is an ordinary document of `shape` `diff`: files of
|
|
165
|
+
records, each record one anchored pattern whose substitutions are written
|
|
166
|
+
as a sub-unit, `old␚␟new`: `new` is the sub-unit of `old`.
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
␖␁shape␟diff
|
|
170
|
+
␜greeting.txt
|
|
171
|
+
␞␂Hello ␃␟world␚␟universe␟!
|
|
172
|
+
␄
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
diff = c0.diff.build(lambda d: (
|
|
177
|
+
d.file("greeting.txt"),
|
|
178
|
+
d.record(lambda r: (r.anchor("Hello "), r.sub("world", "universe"), r.anchor("!"))),
|
|
179
|
+
d.insert_after("universe!", "\n"),
|
|
180
|
+
d.assert_("Hello"), # must be present once; nothing changes
|
|
181
|
+
d.eot(),
|
|
182
|
+
))
|
|
183
|
+
c0.diff.apply(diff, {"greeting.txt": source}) # validates everything first; a dict of the results
|
|
184
|
+
c0.diff.apply_files(diff, "src") # on disk, atomically
|
|
185
|
+
c0.parse_diff(diff) # [FileEdit(path, [Pattern([Unit(old, new), …])])]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Each record's search text must match once from the previous match to the
|
|
189
|
+
end of the file. There is no replace-all: a tool expands one into anchored
|
|
190
|
+
records against the target (DIFF.md). Anything the text diff does not use
|
|
191
|
+
(keyed units, levels, references, header rows, GS, dead records and dead
|
|
192
|
+
file lines, two sub-units on one unit, a sub-unit of a sub-unit, an empty
|
|
193
|
+
search text, a bare insertion, `␟␚␟`, a record before the first FS, an
|
|
194
|
+
empty path, the same path twice) is refused with `c0.DiffError`; a hunk is
|
|
195
|
+
kept without applying by writing it in a note, `␇␂␞…␃`.
|
|
196
|
+
|
|
197
|
+
### Stream logs
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
r = c0.StreamReader(open("events.c0", "rb").read())
|
|
201
|
+
r.torn # uncommitted tail?
|
|
202
|
+
r.damaged # indexes of committed blocks that are not well-formed
|
|
203
|
+
for rec in r: # every committed record, header rows and heading excluded
|
|
204
|
+
...
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Any error inside a committed block damages that block alone: it delivers
|
|
208
|
+
nothing and adds nothing to the parse state. By default the reader yields
|
|
209
|
+
the records before it and raises `c0.DamagedBlockError` with the index and
|
|
210
|
+
the offset to resume at (`r.resume_offset`); with
|
|
211
|
+
`c0.StreamReader(buf, continue_past_damage=True)` it reads on past it.
|
|
212
|
+
|
|
213
|
+
## Status
|
|
214
|
+
|
|
215
|
+
Binds the c0-c 1.0 core at c0-spec f677533: one parser (parse is validate),
|
|
216
|
+
zero-copy body/document/container readers reporting the result model
|
|
217
|
+
(files, groups with SUB GS depth, header rows, positional and keyed records
|
|
218
|
+
with their units and sub-units, nested levels, reference patterns, headings
|
|
219
|
+
with `schema`, EM blobs), canonical check, the builder, ETB stream mode
|
|
220
|
+
with damaged blocks, pretty form (`pretty_format` / `pretty_parse`) and
|
|
221
|
+
C0DIFF (`c0.diff`). Passes all 295 shared conformance vectors from
|
|
222
|
+
[c0-spec](https://github.com/c0data/c0-spec).
|
|
223
|
+
|
|
224
|
+
Converters (CSV / JSON) are not yet wrapped.
|
|
225
|
+
|
|
226
|
+
## Test
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
python -m unittest discover -s tests
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# c0
|
|
2
|
+
|
|
3
|
+
A Python implementation of [C0DATA](https://github.com/c0data) 1.0 — structured
|
|
4
|
+
data built on ASCII C0 control codes.
|
|
5
|
+
|
|
6
|
+
This is **not** a reimplementation: it's a thin, fast binding over the
|
|
7
|
+
[c0 C core](https://github.com/c0data/c0-c) (a CPython extension, no third-party
|
|
8
|
+
dependencies), so the scan-heavy work runs as native C while the Python layer
|
|
9
|
+
provides an idiomatic API. The read path is **zero-copy** — raw accessors return
|
|
10
|
+
`memoryview` slices into the input buffer; logical values come back as `bytes`,
|
|
11
|
+
with `str` conveniences beside them.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import c0
|
|
17
|
+
|
|
18
|
+
# Write: SYN heading, a group with a header row (␁␞), records, EOT
|
|
19
|
+
buf = c0.build(lambda b: (
|
|
20
|
+
b.syn(schema="ledger"),
|
|
21
|
+
b.group("users", ["name", "amount"]),
|
|
22
|
+
b.record("Alice", "100"),
|
|
23
|
+
b.record("Bob", "200"),
|
|
24
|
+
b.eot(),
|
|
25
|
+
))
|
|
26
|
+
# ␖␁schema␟ledger␝users␁␞name␟amount␞Alice␟100␞Bob␟200␄
|
|
27
|
+
|
|
28
|
+
# Read (parse is validate: malformed input raises c0.C0SyntaxError)
|
|
29
|
+
doc = c0.Document(buf) # or c0.Container(buf) for several documents / an EM tail
|
|
30
|
+
doc.heading.schema # b'ledger'
|
|
31
|
+
users = doc.group("users") # by name or index; doc["users"] too
|
|
32
|
+
users.headers # [b'name', b'amount']
|
|
33
|
+
for rec in users:
|
|
34
|
+
rec.values # [b'Alice', b'100'] — plain trimmed, text decoded
|
|
35
|
+
rec.string("amount") # '100', by header name or index
|
|
36
|
+
rec.field(0) # memoryview of the raw unit (zero-copy)
|
|
37
|
+
|
|
38
|
+
# The body is a canonical span — hashable for content addressing
|
|
39
|
+
assert c0.canonical(c0.build(lambda b: (b.group("users", ["name", "amount"]),
|
|
40
|
+
b.record("Alice", "100"), b.record("Bob", "200"))))
|
|
41
|
+
c0.validate(buf) # the same grammar check on a buffer, without building a model
|
|
42
|
+
|
|
43
|
+
# Streams and pretty form
|
|
44
|
+
log = c0.StreamReader(open("claims.c0", "rb").read()) # .torn, .blocks, .damaged, .records
|
|
45
|
+
print(c0.pretty_format(buf)) # Unicode Control Pictures
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The model is the **result**: a dead element (`␇␞…`) is parsed and dropped
|
|
49
|
+
with everything it holds (a dead group takes its header row, records and
|
|
50
|
+
subsections, a dead file its groups). SUB makes the element after it a
|
|
51
|
+
child of the element before, at that code's grain: `␚␝s` is a subsection
|
|
52
|
+
at depth two (`g.depth`; a run of SUB is the depth, never skipped), and
|
|
53
|
+
`␞a␟b␚␟c` is two units with `c` a sub-unit of `b`, not a unit of the
|
|
54
|
+
record (`field_count`, `values`, `pairs` see units only). `rec.unit(n)` and
|
|
55
|
+
`rec.unit(key)` give a `Unit` with its `subs`, each a `Unit` in turn;
|
|
56
|
+
`rec.units` lists them all. `␚␞`, `␚␜` and `␚␁` are reserved and refused.
|
|
57
|
+
A SUB prefix is structure: canonical form keeps it. Canonical checking is
|
|
58
|
+
opt-in (`c0.canonical`) and never fails parsing.
|
|
59
|
+
|
|
60
|
+
Values are written in their one spelling: plain when they can be, text
|
|
61
|
+
(`␂…␃`) when they hold a control byte or edge whitespace. Keyed records
|
|
62
|
+
(`␞␁host␟localhost␁port␟5432`) are maps that keep their order:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
buf = c0.build(lambda b: (b.group("database"), b.map(host="localhost", port="5432")))
|
|
66
|
+
rec = c0.Body(buf).group(0).record(0)
|
|
67
|
+
rec.keyed, rec.keys, rec["port"] # True, [b'host', b'port'], b'5432'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Nested levels, lists and references
|
|
71
|
+
|
|
72
|
+
A unit may be a nested level, `␐…␘`, whose contents are a whole body. A
|
|
73
|
+
level holding one record is a list; `Builder.list` writes one and
|
|
74
|
+
`Record.list` reads it back.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
buf = c0.build(lambda b: (
|
|
78
|
+
b.group("users", ["name", "roles", "address"]),
|
|
79
|
+
b.record("Alice"),
|
|
80
|
+
b.list(["Admin", "Editor"]), # ␐␞Admin␟Editor␘
|
|
81
|
+
b.level(lambda l: (l.header("street", "city"), # ␐␁␞street␟city␞1 Main␟Springfield␘
|
|
82
|
+
l.record("1 Main", "Springfield"))),
|
|
83
|
+
))
|
|
84
|
+
rec = c0.Body(buf).group(0).record(0)
|
|
85
|
+
rec.list(1) # [b'Admin', b'Editor']
|
|
86
|
+
rec.level(2).record(0).string("city") # 'Springfield'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`level()` and `key_level(name)` also work as context managers (`with b.level(): …`).
|
|
90
|
+
|
|
91
|
+
A reference is a group name written plain, `b.ref("tags")` (`␅tags`), or a
|
|
92
|
+
**pattern**: a level that is one branch of the target with its values left
|
|
93
|
+
out, `b.ref("tags", "001", "label")` (`␅␐␝tags␞001␟label␘`) or any pattern
|
|
94
|
+
built in a callable, `b.ref(lambda: b.gs("s").rs().soh("host").us())`
|
|
95
|
+
(`␅␐␝s␞␁host␟␘`). `Record.ref` reads one back as a `Reference` with a
|
|
96
|
+
`name` or a `pattern` (the level as a `Body`); `ref.path` linearises it and
|
|
97
|
+
`ref.is_group` says whether it names a group as a whole. `b.sub_unit("new")`
|
|
98
|
+
writes `␚␟new`, a sub-unit of the unit just written (`depth=2` one deeper);
|
|
99
|
+
`b.group("intro", depth=2)` writes `␚␝intro`, a subsection.
|
|
100
|
+
|
|
101
|
+
The builder also has `file`, `section`, `header`, `field`, `key`, `note`,
|
|
102
|
+
`etb(payload)`, `em(tail)`, `block` and `item`, matching the Crystal reference.
|
|
103
|
+
Every error derives from `c0.C0Error`.
|
|
104
|
+
|
|
105
|
+
### Primitives
|
|
106
|
+
|
|
107
|
+
Underneath, the builder has one method per code: `fs`, `gs`, `rs`, `us`,
|
|
108
|
+
`sub`, `soh`, `dle`, `can`, `bel`, `enq`, `syn`, `eot`, `etb`, `em`. Each
|
|
109
|
+
writes its code and, when given, what follows it, so a chain reads like the
|
|
110
|
+
bytes, and anything the grammar allows can be written. The argument is
|
|
111
|
+
optional where the grammar allows the code alone (`gs()`, `rs()`, `us()`,
|
|
112
|
+
`soh()`, `bel()`, `etb()`); `sub()` is bare; `enq` takes a group name or a
|
|
113
|
+
callable that writes a pattern:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
b.rs("Alice").us("1502.30") # ␞Alice␟1502.30
|
|
117
|
+
b.rs().soh("host").us("localhost").soh("port").us("5432") # ␞␁host␟localhost␁port␟5432
|
|
118
|
+
b.gs("users").soh().rs("name").us("amount") # ␝users␁␞name␟amount: a header row
|
|
119
|
+
b.rs("cfg").us().dle().rs().soh("b").us("2").can() # ␞cfg␟␐␞␁b␟2␘
|
|
120
|
+
b.bel("checked").rs("x") # ␇checked␞x: a note
|
|
121
|
+
b.bel().gs("old") # ␇␝old: a dead group
|
|
122
|
+
b.rs("old").sub().us("new") # ␞old␚␟new: new is a sub-unit of old
|
|
123
|
+
b.gs("top").gs("sub", depth=2) # ␝top␚␝sub: a subsection
|
|
124
|
+
b.gs("sub").rs().enq("users").us().enq(lambda: b.gs("users").rs("1")) # ␝sub␞␅users␟␅␐␝users␞1␘
|
|
125
|
+
b.syn().soh("version").us("1").rs("a").eot() # ␖␁version␟1␞a␄
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The composed methods above are built on these and cannot write ill-formed
|
|
129
|
+
output; with the primitives, well-formedness is yours. Values keep their one
|
|
130
|
+
spelling either way (there is no STX/ETX primitive), and names are still
|
|
131
|
+
checked. The two layers share the record state and mix freely:
|
|
132
|
+
`b.rs("a").field("b")` writes `␞a␟b`, `b.record().us("c")` writes `␞␟c`.
|
|
133
|
+
|
|
134
|
+
## Install / build
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
pip install c0data-py # the package is c0data-py (PyPI treats c0data as codata); in code it is `import c0`
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
From a checkout, the C core and the shared conformance vectors are git submodules:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
git submodule update --init # pulls in c0-c (the header) and c0-spec
|
|
144
|
+
pip install . # builds the extension
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Requires a C compiler and the Python development headers.
|
|
148
|
+
|
|
149
|
+
### C0DIFF
|
|
150
|
+
|
|
151
|
+
A diff (`DIFF.md`) is an ordinary document of `shape` `diff`: files of
|
|
152
|
+
records, each record one anchored pattern whose substitutions are written
|
|
153
|
+
as a sub-unit, `old␚␟new`: `new` is the sub-unit of `old`.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
␖␁shape␟diff
|
|
157
|
+
␜greeting.txt
|
|
158
|
+
␞␂Hello ␃␟world␚␟universe␟!
|
|
159
|
+
␄
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
diff = c0.diff.build(lambda d: (
|
|
164
|
+
d.file("greeting.txt"),
|
|
165
|
+
d.record(lambda r: (r.anchor("Hello "), r.sub("world", "universe"), r.anchor("!"))),
|
|
166
|
+
d.insert_after("universe!", "\n"),
|
|
167
|
+
d.assert_("Hello"), # must be present once; nothing changes
|
|
168
|
+
d.eot(),
|
|
169
|
+
))
|
|
170
|
+
c0.diff.apply(diff, {"greeting.txt": source}) # validates everything first; a dict of the results
|
|
171
|
+
c0.diff.apply_files(diff, "src") # on disk, atomically
|
|
172
|
+
c0.parse_diff(diff) # [FileEdit(path, [Pattern([Unit(old, new), …])])]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Each record's search text must match once from the previous match to the
|
|
176
|
+
end of the file. There is no replace-all: a tool expands one into anchored
|
|
177
|
+
records against the target (DIFF.md). Anything the text diff does not use
|
|
178
|
+
(keyed units, levels, references, header rows, GS, dead records and dead
|
|
179
|
+
file lines, two sub-units on one unit, a sub-unit of a sub-unit, an empty
|
|
180
|
+
search text, a bare insertion, `␟␚␟`, a record before the first FS, an
|
|
181
|
+
empty path, the same path twice) is refused with `c0.DiffError`; a hunk is
|
|
182
|
+
kept without applying by writing it in a note, `␇␂␞…␃`.
|
|
183
|
+
|
|
184
|
+
### Stream logs
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
r = c0.StreamReader(open("events.c0", "rb").read())
|
|
188
|
+
r.torn # uncommitted tail?
|
|
189
|
+
r.damaged # indexes of committed blocks that are not well-formed
|
|
190
|
+
for rec in r: # every committed record, header rows and heading excluded
|
|
191
|
+
...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Any error inside a committed block damages that block alone: it delivers
|
|
195
|
+
nothing and adds nothing to the parse state. By default the reader yields
|
|
196
|
+
the records before it and raises `c0.DamagedBlockError` with the index and
|
|
197
|
+
the offset to resume at (`r.resume_offset`); with
|
|
198
|
+
`c0.StreamReader(buf, continue_past_damage=True)` it reads on past it.
|
|
199
|
+
|
|
200
|
+
## Status
|
|
201
|
+
|
|
202
|
+
Binds the c0-c 1.0 core at c0-spec f677533: one parser (parse is validate),
|
|
203
|
+
zero-copy body/document/container readers reporting the result model
|
|
204
|
+
(files, groups with SUB GS depth, header rows, positional and keyed records
|
|
205
|
+
with their units and sub-units, nested levels, reference patterns, headings
|
|
206
|
+
with `schema`, EM blobs), canonical check, the builder, ETB stream mode
|
|
207
|
+
with damaged blocks, pretty form (`pretty_format` / `pretty_parse`) and
|
|
208
|
+
C0DIFF (`c0.diff`). Passes all 295 shared conformance vectors from
|
|
209
|
+
[c0-spec](https://github.com/c0data/c0-spec).
|
|
210
|
+
|
|
211
|
+
Converters (CSV / JSON) are not yet wrapped.
|
|
212
|
+
|
|
213
|
+
## Test
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
python -m unittest discover -s tests
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|