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.
@@ -0,0 +1,6 @@
1
+ include c0-c/c0.h
2
+ include c0-c/c0_diff.h
3
+ include c0-c/c0_diff.c
4
+ include c0-c/c0_strbuf.h
5
+ include README.md
6
+ include LICENSE
@@ -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