gedcomkit 0.1.5__tar.gz → 0.1.7__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.
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/CHANGELOG.md +104 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/Cargo.lock +1 -1
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/Cargo.toml +1 -1
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/PKG-INFO +1 -1
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/Cargo.lock +2 -2
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/Cargo.toml +1 -1
- gedcomkit-0.1.7/bindings/gedcomkit-py/gedcomkit.pyi +90 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/src/lib.rs +236 -5
- gedcomkit-0.1.7/src/build.rs +711 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/convert.rs +479 -68
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/dates.rs +546 -28
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/lib.rs +3 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/LICENSE +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/README.md +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/LICENSE +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/README.md +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/README.md +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/LICENSE +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/zip-interop/README.md +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/pyproject.toml +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/decode.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/family.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/gedzip.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/names.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/schema.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/synthetic.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/tags.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/validate.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/view.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/endurance.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/external_charset.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/properties.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/requirement_corpus.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/spec_conformance.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/vendored_corpus.rs +0 -0
- {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/version_conversion.rs +0 -0
|
@@ -5,6 +5,110 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
5
5
|
the project uses semantic versioning within the usual pre-1.0 compatibility
|
|
6
6
|
rules.
|
|
7
7
|
|
|
8
|
+
## [0.1.7] - 2026-09-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `dates::GedcomDate::to_gedcom_7`, the reading written in GEDCOM 7.0's own
|
|
13
|
+
date grammar (`about 5 January 1882` → `ABT 5 JAN 1882`), or `None` when
|
|
14
|
+
that grammar cannot hold it without loss — no `INT`, no dual year, and the
|
|
15
|
+
calendar as a bare word (`JULIAN 10 MAR 1700`).
|
|
16
|
+
- Python and JavaScript/WASM: `parse_date` / `parseDate`'s reading gained a
|
|
17
|
+
`gedcom7` key alongside `gedcom551`.
|
|
18
|
+
- `convert::to_version_7_with`'s owned-extension-tags parameter, now exposed
|
|
19
|
+
by both bindings: Python's `Gedcom.to_version7(owned=[(tag, uri), ...])`
|
|
20
|
+
and WASM's `toVersion7([[tag, uri], ...])`, either omittable.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `convert::to_version_7` now rewrites every `DATE` payload the lenient
|
|
25
|
+
reader understands but that is not version 7's own grammar — `29 May
|
|
26
|
+
1823`, `about 1575`, `13. september 1900`, `<1873>`, `ABT Oct 1803`,
|
|
27
|
+
`1740/41`, `@#DJULIAN@ 10 MAR 1700`, `44 B.C.`, and the like — instead of
|
|
28
|
+
passing most of them through unreported. Each becomes the version 7
|
|
29
|
+
reading (or, when none exists — an unparsed expression, a dual year, a
|
|
30
|
+
calendar version 7 cannot spell — an empty `DATE`), with the original kept
|
|
31
|
+
verbatim in a `PHRASE` substructure and the change named in the report. A
|
|
32
|
+
payload already in version 7's grammar is left untouched, byte for byte.
|
|
33
|
+
- `convert::to_version_7` now converts `OBJE.FILE.FORM`'s file extension
|
|
34
|
+
(`jpg`, `png`, `pdf`, `html`, `txt`, …) to a version 7 media type
|
|
35
|
+
(`image/jpeg`, `image/png`, `application/pdf`, `text/html`,
|
|
36
|
+
`text/plain`, …) and `FORM.MEDI`'s source medium to version 7's
|
|
37
|
+
enumeration, upper case (`PHOTO`, `NEWSPAPER`, …), folding anything else
|
|
38
|
+
into `OTHER` with the original kept as a `PHRASE`. An extension with no
|
|
39
|
+
known media type is kept as written and reported as kept rather than
|
|
40
|
+
guessed. Both are named in the report under new `FILE.FORM` and
|
|
41
|
+
`FORM.MEDI` notes, alongside the existing `OBJE.FORM` note for the move
|
|
42
|
+
from object to file. This covers 5.5.1's own layout, where `FORM` already
|
|
43
|
+
sits under `FILE` and the source medium is `TYPE` (renamed `MEDI`), as
|
|
44
|
+
well as 5.5's `FORM` on the object. Known extensions now include `heic`,
|
|
45
|
+
`webp`, `svg`, `json`, `xml`, `eml` and `mp4`.
|
|
46
|
+
- `convert::to_version_7` percent-encodes a `FILE` path that is not a URI
|
|
47
|
+
reference, as version 7 requires — a space, a backslash, anything outside
|
|
48
|
+
ASCII — and reports it under a `FILE` note. A path that is already a URI
|
|
49
|
+
reference, escapes included, is untouched.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- A `DATE` rewritten by the version 7 conversion no longer keeps the
|
|
54
|
+
`CONT`/`CONC` lines its old payload was split across, which a later read
|
|
55
|
+
folded back into the new payload. A `DATE` that already carries a
|
|
56
|
+
`PHRASE` is left as it is and reported as kept, rather than given a
|
|
57
|
+
second `PHRASE`.
|
|
58
|
+
- `GedcomDate::to_gedcom_7` returns `None` for a Hebrew or French
|
|
59
|
+
Republican date with a month, rather than spelling that month with a
|
|
60
|
+
Gregorian abbreviation (`FRENCH_R 22 MAR 5`). A bare year is still
|
|
61
|
+
written.
|
|
62
|
+
|
|
63
|
+
## [0.1.6] - 2026-09-25
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- `build`, for a producer writing its own data as GEDCOM rather than
|
|
68
|
+
reading a file. `build::NodeSpec` describes a structure (tag, optional
|
|
69
|
+
identifier and payload, substructures) and `NodeSpec::into_node` checks
|
|
70
|
+
it under `Limits`, refusing with an error what `Node::new` would panic on;
|
|
71
|
+
with the `serde` feature it deserializes, refusing unknown keys.
|
|
72
|
+
`Document::append_records` adds records before the trailer, all or none,
|
|
73
|
+
refusing a repeated identifier or a second header or trailer, and
|
|
74
|
+
`Document::append_structures` adds substructures to any record or the
|
|
75
|
+
header (which 5.5.1 wants a `SUBM` pointer in, and `new_v551` cannot
|
|
76
|
+
supply), leaving every existing line as it was.
|
|
77
|
+
- `Document::wrap_long_lines`, which fits a 5.5.1 document to a line limit
|
|
78
|
+
by splitting payloads across `CONC` lines — at character boundaries, and
|
|
79
|
+
not beside a space where the text allows — and names every structure it
|
|
80
|
+
rewrote. Lines that fit are untouched; a version 7 document is refused.
|
|
81
|
+
- `dates::GedcomDate::to_gedcom_5_5_1`, the reading written in 5.5.1's own
|
|
82
|
+
date grammar (`about 5 January 1882` → `ABT 5 JAN 1882`), or `None` when
|
|
83
|
+
that grammar cannot hold it without loss. A dual year it can spell
|
|
84
|
+
(`1740/41`) is kept; a slashed pair it cannot makes an exact date an `INT`
|
|
85
|
+
date carrying the original as its phrase.
|
|
86
|
+
- Python and JavaScript/WASM bindings: `Gedcom.new_v551` / `newV551`,
|
|
87
|
+
`new_v7` / `newV7`, `append_records` / `appendRecords` (records as plain
|
|
88
|
+
dicts/objects), `append_structures` / `appendStructures`, `wrap_long_lines` / `wrapLongLines`, and
|
|
89
|
+
`parse_date` / `parseDate`, a date reading with its `display` and
|
|
90
|
+
`gedcom551` renderings.
|
|
91
|
+
- Python only: `ArchiveFileWriter`, a GEDZIP writer whose sink is a file,
|
|
92
|
+
with `add_file` streaming an entry from disk without the bytes passing
|
|
93
|
+
through Python — so a multi-gigabyte archive no longer has to fit in
|
|
94
|
+
memory twice. And type stubs (`gedcomkit.pyi`), so the package is typed
|
|
95
|
+
for mypy and pyright.
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
- The date reader accepts 5.5.1's `B.C.` epoch as well as 7.0's `BCE`.
|
|
100
|
+
- The date reader matched its keywords by ASCII case only, so `après 12
|
|
101
|
+
juillet 1670` and `før 1679` went unread although both words were in its
|
|
102
|
+
tables. Keywords now compare case in every script, and may run straight
|
|
103
|
+
into the date (`abt1810`, `c1700`).
|
|
104
|
+
- `April 9, 1950` read its `9` as September and failed; a month written as a
|
|
105
|
+
word now decides the order. `10 January 1566/67`, a dual year ending a full
|
|
106
|
+
date, now reads as 1566 as the bare `1566/67` already did.
|
|
107
|
+
- More spellings: `vers`, `cir`, `c.`, `around`, `probably`, `omtrent`,
|
|
108
|
+
`cirka` (about); `før` (before); `etter`, `efter` (after); `entre … et …`,
|
|
109
|
+
`mellom … og …` (between); `du … au …`, `fra … til …` (from … to);
|
|
110
|
+
and the months `déc`, `févr`, `janv`, `avr`, `juil`, `desember`.
|
|
111
|
+
|
|
8
112
|
## [0.1.5] - 2026-09-18
|
|
9
113
|
|
|
10
114
|
### Added
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# was checked on crates.io, npm, and PyPI before the initial release.
|
|
4
4
|
[package]
|
|
5
5
|
name = "gedcomkit"
|
|
6
|
-
version = "0.1.
|
|
6
|
+
version = "0.1.7"
|
|
7
7
|
description = "A byte-preserving GEDCOM document model: decoding, parsing, readings, version conversion, plausibility checks, and the GEDZIP container, for GEDCOM 5.5 through 7.x."
|
|
8
8
|
edition = "2024"
|
|
9
9
|
# The true minimum, not the workspace's toolchain: a parser library is a leaf
|
|
@@ -35,7 +35,7 @@ dependencies = [
|
|
|
35
35
|
|
|
36
36
|
[[package]]
|
|
37
37
|
name = "gedcomkit"
|
|
38
|
-
version = "0.1.
|
|
38
|
+
version = "0.1.7"
|
|
39
39
|
dependencies = [
|
|
40
40
|
"flate2",
|
|
41
41
|
"serde",
|
|
@@ -43,7 +43,7 @@ dependencies = [
|
|
|
43
43
|
|
|
44
44
|
[[package]]
|
|
45
45
|
name = "gedcomkit-py"
|
|
46
|
-
version = "0.1.
|
|
46
|
+
version = "0.1.7"
|
|
47
47
|
dependencies = [
|
|
48
48
|
"gedcomkit",
|
|
49
49
|
"pyo3",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# abi3 keeps it to one wheel per platform across Python versions.
|
|
3
3
|
[package]
|
|
4
4
|
name = "gedcomkit-py"
|
|
5
|
-
version = "0.1.
|
|
5
|
+
version = "0.1.7"
|
|
6
6
|
description = "Python bindings for gedcomkit: an archival-grade GEDCOM library — byte-preserving round-trip, encoding detection, GEDCOM 5.5 through 7.x, GEDZIP."
|
|
7
7
|
edition = "2024"
|
|
8
8
|
rust-version = "1.88"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Type stubs for gedcomkit, an archival-grade GEDCOM library.
|
|
2
|
+
|
|
3
|
+
Views, findings and reports are plain dicts built from the core's JSON
|
|
4
|
+
serialization; their keys are camelCase, as the Rust types name them.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from os import PathLike
|
|
8
|
+
from typing import Any, TypedDict
|
|
9
|
+
|
|
10
|
+
from typing_extensions import NotRequired
|
|
11
|
+
|
|
12
|
+
__version__: str
|
|
13
|
+
|
|
14
|
+
class RecordSpec(TypedDict):
|
|
15
|
+
"""A structure to add: only ``tag`` is required, and any other key is refused."""
|
|
16
|
+
|
|
17
|
+
tag: str
|
|
18
|
+
xref: NotRequired[str | None]
|
|
19
|
+
value: NotRequired[str | None]
|
|
20
|
+
children: NotRequired[list[RecordSpec]]
|
|
21
|
+
|
|
22
|
+
class WrappedLine(TypedDict):
|
|
23
|
+
record: str
|
|
24
|
+
path: list[int]
|
|
25
|
+
tag: str
|
|
26
|
+
|
|
27
|
+
class Gedcom:
|
|
28
|
+
@staticmethod
|
|
29
|
+
def parse(data: bytes) -> Gedcom:
|
|
30
|
+
"""Read a document from a file's bytes; raises ValueError past the bounds."""
|
|
31
|
+
@staticmethod
|
|
32
|
+
def new_v551(source_system: str) -> Gedcom:
|
|
33
|
+
"""An empty GEDCOM 5.5.1 document naming its producing system, declaring UTF-8."""
|
|
34
|
+
@staticmethod
|
|
35
|
+
def new_v7() -> Gedcom:
|
|
36
|
+
"""An empty GEDCOM 7.0 document."""
|
|
37
|
+
@property
|
|
38
|
+
def encoding(self) -> str: ...
|
|
39
|
+
@property
|
|
40
|
+
def version(self) -> str | None: ...
|
|
41
|
+
@property
|
|
42
|
+
def content_records(self) -> int: ...
|
|
43
|
+
def records(self) -> list[dict[str, Any]]: ...
|
|
44
|
+
def record(self, xref: str) -> dict[str, Any]: ...
|
|
45
|
+
def individuals(self) -> list[dict[str, Any]]: ...
|
|
46
|
+
def families(self) -> list[dict[str, Any]]: ...
|
|
47
|
+
def validate(self) -> list[dict[str, Any]]: ...
|
|
48
|
+
def unresolved_pointers(self) -> list[dict[str, Any]]: ...
|
|
49
|
+
def parents_of(self, xref: str) -> list[str]: ...
|
|
50
|
+
def children_of(self, xref: str) -> list[str]: ...
|
|
51
|
+
def partners_of(self, xref: str) -> list[str]: ...
|
|
52
|
+
def append_records(self, records: list[RecordSpec]) -> None:
|
|
53
|
+
"""Add records before the trailer, all or none; raises ValueError."""
|
|
54
|
+
def append_structures(
|
|
55
|
+
self, record: str, path: list[int], structures: list[RecordSpec]
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Add substructures under ``path`` in ``record`` (an xref, or "HEAD")."""
|
|
58
|
+
def wrap_long_lines(self, max_line_bytes: int = 255) -> list[WrappedLine]:
|
|
59
|
+
"""Split lines over the limit across CONC lines and name each one rewritten."""
|
|
60
|
+
def set_logical_value(self, xref: str, path: list[int], value: str) -> None: ...
|
|
61
|
+
def to_text(self) -> str: ...
|
|
62
|
+
def to_bytes(self) -> bytes: ...
|
|
63
|
+
def to_version7(
|
|
64
|
+
self, owned: list[tuple[str, str]] | None = None
|
|
65
|
+
) -> tuple[Gedcom, list[dict[str, Any]]]:
|
|
66
|
+
"""Convert to GEDCOM 7.0. ``owned`` declares the caller's own
|
|
67
|
+
extension tags as ``(tag, uri)`` pairs; any other extension is
|
|
68
|
+
declared as undocumented rather than dropped."""
|
|
69
|
+
def to_version551(self) -> tuple[Gedcom, list[dict[str, Any]]]: ...
|
|
70
|
+
def __len__(self) -> int: ...
|
|
71
|
+
|
|
72
|
+
class ArchiveWriter:
|
|
73
|
+
@staticmethod
|
|
74
|
+
def new() -> ArchiveWriter: ...
|
|
75
|
+
def add(self, name: str, data: bytes) -> None: ...
|
|
76
|
+
def finish(self) -> bytes: ...
|
|
77
|
+
|
|
78
|
+
class ArchiveFileWriter:
|
|
79
|
+
"""A GEDZIP archive written straight to a file, one entry at a time."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, path: str | PathLike[str]) -> None: ...
|
|
82
|
+
def add(self, name: str, data: bytes) -> None: ...
|
|
83
|
+
def add_file(self, name: str, source: str | PathLike[str]) -> None: ...
|
|
84
|
+
def finish(self) -> int:
|
|
85
|
+
"""Close the archive and return its size in bytes."""
|
|
86
|
+
|
|
87
|
+
def archive_total_size(data: bytes) -> int: ...
|
|
88
|
+
def parse_date(text: str) -> dict[str, Any]:
|
|
89
|
+
"""A date reading; its ``gedcom551`` and ``gedcom7`` keys are the 5.5.1
|
|
90
|
+
and 7.0 spellings, each ``None`` when that grammar cannot hold it."""
|
|
@@ -4,17 +4,24 @@
|
|
|
4
4
|
//! wrapping the document and its encoding report, read-only views handed
|
|
5
5
|
//! over as plain Python objects (via JSON), and the one edit primitive the
|
|
6
6
|
//! byte-preservation design blesses — `set_logical_value` addressed by
|
|
7
|
-
//! record and structure path.
|
|
7
|
+
//! record and structure path. A producer writing its own data starts from
|
|
8
|
+
//! `new_v551`/`new_v7` and adds records described as plain dicts. Everything
|
|
9
|
+
//! else stays in Rust.
|
|
8
10
|
|
|
11
|
+
use gedcomkit::build::NodeSpec;
|
|
12
|
+
use gedcomkit::dates::GedcomDate;
|
|
9
13
|
use gedcomkit::family::FamilyGraph;
|
|
10
14
|
use gedcomkit::gedzip::{self, Archive};
|
|
11
15
|
use gedcomkit::view::{FamilyView, IndividualView, RecordView};
|
|
12
|
-
use gedcomkit::{Document, EncodingReport};
|
|
13
|
-
use pyo3::exceptions::{PyKeyError, PyValueError};
|
|
16
|
+
use gedcomkit::{Document, EncodingReport, GedcomEncoding, Limits};
|
|
17
|
+
use pyo3::exceptions::{PyKeyError, PyOSError, PyValueError};
|
|
14
18
|
use pyo3::prelude::*;
|
|
15
19
|
use pyo3::types::{PyBytes, PyModule};
|
|
16
20
|
use pyo3::wrap_pyfunction;
|
|
17
21
|
use serde::Serialize;
|
|
22
|
+
use std::fs::File;
|
|
23
|
+
use std::io::{BufWriter, Write as _};
|
|
24
|
+
use std::path::PathBuf;
|
|
18
25
|
|
|
19
26
|
/// A parsed GEDCOM document and the report of how its bytes were read.
|
|
20
27
|
#[pyclass]
|
|
@@ -34,11 +41,36 @@ fn to_py<T: Serialize>(py: Python<'_>, value: &T) -> PyResult<Py<PyAny>> {
|
|
|
34
41
|
Ok(loads.call1((json,))?.unbind())
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
/// The Python object passed in, read as the JSON it would serialize to —
|
|
45
|
+
/// the inverse of `to_py`, so both directions share one conversion.
|
|
46
|
+
fn from_py<T: serde::de::DeserializeOwned>(
|
|
47
|
+
py: Python<'_>,
|
|
48
|
+
value: &Bound<'_, PyAny>,
|
|
49
|
+
) -> PyResult<T> {
|
|
50
|
+
let dumps = py.import("json")?.getattr("dumps")?;
|
|
51
|
+
let json: String = dumps.call1((value,))?.extract()?;
|
|
52
|
+
serde_json::from_str(&json).map_err(value_error)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn value_error(error: impl std::fmt::Display) -> PyErr {
|
|
56
|
+
PyValueError::new_err(error.to_string())
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
impl Gedcom {
|
|
38
60
|
fn graph(&mut self) -> &FamilyGraph {
|
|
39
61
|
self.graph
|
|
40
62
|
.get_or_insert_with(|| FamilyGraph::new(&self.document))
|
|
41
63
|
}
|
|
64
|
+
|
|
65
|
+
/// A handle on a document this process wrote rather than read: its text
|
|
66
|
+
/// is UTF-8 by construction.
|
|
67
|
+
fn written(document: Document) -> Self {
|
|
68
|
+
Self {
|
|
69
|
+
document,
|
|
70
|
+
report: EncodingReport::for_encoding(GedcomEncoding::Utf8),
|
|
71
|
+
graph: None,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
42
74
|
}
|
|
43
75
|
|
|
44
76
|
#[pymethods]
|
|
@@ -56,6 +88,85 @@ impl Gedcom {
|
|
|
56
88
|
})
|
|
57
89
|
}
|
|
58
90
|
|
|
91
|
+
/// An empty GEDCOM 5.5.1 document — header and trailer — naming the
|
|
92
|
+
/// producing system, which 5.5.1 requires, and declaring UTF-8.
|
|
93
|
+
#[staticmethod]
|
|
94
|
+
fn new_v551(source_system: &str) -> Self {
|
|
95
|
+
Self::written(Document::new_v551(source_system))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// An empty GEDCOM 7.0 document: header and trailer.
|
|
99
|
+
#[staticmethod]
|
|
100
|
+
fn new_v7() -> Self {
|
|
101
|
+
Self::written(Document::new_v7())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Adds records before the trailer, all or none. Each record is a dict
|
|
105
|
+
/// `{"tag", "xref", "value", "children"}` — only `tag` required, any
|
|
106
|
+
/// other key refused — and each child the same shape without `xref`. A
|
|
107
|
+
/// newline in a value becomes a `CONT` line. Raises `ValueError`, adding
|
|
108
|
+
/// nothing, for a tag or identifier the parser would not read back, a
|
|
109
|
+
/// repeated identifier, a header or trailer, or input past the default
|
|
110
|
+
/// bounds.
|
|
111
|
+
fn append_records(&mut self, py: Python<'_>, records: &Bound<'_, PyAny>) -> PyResult<()> {
|
|
112
|
+
let specs: Vec<NodeSpec> = from_py(py, records)?;
|
|
113
|
+
let nodes = specs
|
|
114
|
+
.into_iter()
|
|
115
|
+
.map(|spec| spec.into_node(Limits::DEFAULT))
|
|
116
|
+
.collect::<Result<Vec<_>, _>>()
|
|
117
|
+
.map_err(value_error)?;
|
|
118
|
+
self.document.append_records(nodes).map_err(value_error)?;
|
|
119
|
+
self.graph = None;
|
|
120
|
+
Ok(())
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Adds substructures, described as `append_records` describes records
|
|
124
|
+
/// but without `xref`, after the existing ones of the structure at
|
|
125
|
+
/// `path` in `record` — a cross-reference identifier, or `"HEAD"` for
|
|
126
|
+
/// the header. Every existing line stays as it was. Raises `ValueError`,
|
|
127
|
+
/// adding nothing, when nothing answers to the address or a description
|
|
128
|
+
/// is refused.
|
|
129
|
+
fn append_structures(
|
|
130
|
+
&mut self,
|
|
131
|
+
py: Python<'_>,
|
|
132
|
+
record: &str,
|
|
133
|
+
path: Vec<usize>,
|
|
134
|
+
structures: &Bound<'_, PyAny>,
|
|
135
|
+
) -> PyResult<()> {
|
|
136
|
+
let specs: Vec<NodeSpec> = from_py(py, structures)?;
|
|
137
|
+
let nodes = specs
|
|
138
|
+
.into_iter()
|
|
139
|
+
.map(|spec| spec.into_substructure(Limits::DEFAULT))
|
|
140
|
+
.collect::<Result<Vec<_>, _>>()
|
|
141
|
+
.map_err(value_error)?;
|
|
142
|
+
self.document
|
|
143
|
+
.append_structures(record, &path, nodes)
|
|
144
|
+
.map_err(value_error)?;
|
|
145
|
+
self.graph = None;
|
|
146
|
+
Ok(())
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Splits every line longer than `max_line_bytes` (terminator included;
|
|
150
|
+
/// 255 is GEDCOM 5.5.1's limit) across `CONC` lines, and returns each
|
|
151
|
+
/// structure it rewrote as `{"record", "path", "tag"}`. Lines that fit
|
|
152
|
+
/// are left exactly as they are. Raises `ValueError` for a version 7
|
|
153
|
+
/// document, which has no `CONC`, or a limit under 32.
|
|
154
|
+
#[pyo3(signature = (max_line_bytes = 255))]
|
|
155
|
+
fn wrap_long_lines(
|
|
156
|
+
&mut self,
|
|
157
|
+
py: Python<'_>,
|
|
158
|
+
max_line_bytes: usize,
|
|
159
|
+
) -> PyResult<Vec<Py<PyAny>>> {
|
|
160
|
+
let wrapped = self
|
|
161
|
+
.document
|
|
162
|
+
.wrap_long_lines(max_line_bytes)
|
|
163
|
+
.map_err(value_error)?;
|
|
164
|
+
if !wrapped.is_empty() {
|
|
165
|
+
self.graph = None;
|
|
166
|
+
}
|
|
167
|
+
wrapped.iter().map(|line| to_py(py, line)).collect()
|
|
168
|
+
}
|
|
169
|
+
|
|
59
170
|
/// What decoding decided, one line — show it before accepting an import.
|
|
60
171
|
#[getter]
|
|
61
172
|
fn encoding(&self) -> String {
|
|
@@ -172,8 +283,24 @@ impl Gedcom {
|
|
|
172
283
|
|
|
173
284
|
/// An explicit conversion to GEDCOM 7.0: the converted document and the
|
|
174
285
|
/// notes naming every construct touched, dropped, or kept.
|
|
175
|
-
|
|
176
|
-
|
|
286
|
+
///
|
|
287
|
+
/// `owned` names the extension tags the caller writes itself, each paired
|
|
288
|
+
/// with the URI it is documented at — `[("_PRIM", "https://example.com/PRIM")]`.
|
|
289
|
+
/// Those are declared by name; any other extension in the file is
|
|
290
|
+
/// declared as undocumented rather than dropped, because it is somebody's
|
|
291
|
+
/// data either way.
|
|
292
|
+
#[pyo3(signature = (owned = None))]
|
|
293
|
+
fn to_version7(
|
|
294
|
+
&self,
|
|
295
|
+
py: Python<'_>,
|
|
296
|
+
owned: Option<Vec<(String, String)>>,
|
|
297
|
+
) -> PyResult<(Self, Vec<Py<PyAny>>)> {
|
|
298
|
+
let owned = owned.unwrap_or_default();
|
|
299
|
+
let owned: Vec<(&str, &str)> = owned
|
|
300
|
+
.iter()
|
|
301
|
+
.map(|(tag, uri)| (tag.as_str(), uri.as_str()))
|
|
302
|
+
.collect();
|
|
303
|
+
let outcome = gedcomkit::convert::to_version_7_with(&self.document, &owned);
|
|
177
304
|
let notes = outcome
|
|
178
305
|
.notes
|
|
179
306
|
.iter()
|
|
@@ -284,6 +411,108 @@ impl Default for ArchiveWriter {
|
|
|
284
411
|
}
|
|
285
412
|
}
|
|
286
413
|
|
|
414
|
+
/// Writes a GEDZIP archive straight to a file, one entry at a time, so
|
|
415
|
+
/// memory stays a small multiple of the largest single entry however large
|
|
416
|
+
/// the archive grows. Python only: the WASM binding has no filesystem.
|
|
417
|
+
///
|
|
418
|
+
/// The file is created at once and is complete only after `finish`; an
|
|
419
|
+
/// archive abandoned part-way (an error, or no `finish`) leaves a partial
|
|
420
|
+
/// file behind. Write to a temporary path and rename it after `finish`.
|
|
421
|
+
#[pyclass]
|
|
422
|
+
struct ArchiveFileWriter {
|
|
423
|
+
/// `None` once `finish` has been called.
|
|
424
|
+
inner: Option<gedzip::ArchiveWriter<BufWriter<File>>>,
|
|
425
|
+
path: PathBuf,
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#[pymethods]
|
|
429
|
+
impl ArchiveFileWriter {
|
|
430
|
+
/// Creates (or truncates) the file at `path` and starts an archive in it
|
|
431
|
+
/// under the default bounds. Raises `OSError` when the file cannot be
|
|
432
|
+
/// created.
|
|
433
|
+
#[new]
|
|
434
|
+
fn new(path: PathBuf) -> PyResult<Self> {
|
|
435
|
+
let file = File::create(&path).map_err(|error| os_error(&path, &error))?;
|
|
436
|
+
Ok(Self {
|
|
437
|
+
inner: Some(gedzip::ArchiveWriter::new(BufWriter::new(file))),
|
|
438
|
+
path,
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/// Adds one entry from bytes. Raises `ValueError` for an unsafe or
|
|
443
|
+
/// repeated path, one past the per-entry limit, a failed write, or a call
|
|
444
|
+
/// after `finish`.
|
|
445
|
+
fn add(&mut self, name: &str, data: &[u8]) -> PyResult<()> {
|
|
446
|
+
self.writer()?.add(name, data).map_err(value_error)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/// Adds one entry read from the file at `source`, without the bytes ever
|
|
450
|
+
/// passing through Python. Raises `OSError` when it cannot be opened and
|
|
451
|
+
/// `ValueError` as `add` does.
|
|
452
|
+
fn add_file(&mut self, name: &str, source: PathBuf) -> PyResult<()> {
|
|
453
|
+
let file = File::open(&source).map_err(|error| os_error(&source, &error))?;
|
|
454
|
+
self.writer()?.add(name, file).map_err(value_error)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/// Writes the central directory, flushes, and closes the file, returning
|
|
458
|
+
/// its size in bytes. Valid to call once.
|
|
459
|
+
fn finish(&mut self) -> PyResult<u64> {
|
|
460
|
+
let writer = self
|
|
461
|
+
.inner
|
|
462
|
+
.take()
|
|
463
|
+
.ok_or_else(|| PyValueError::new_err("finish() was already called"))?;
|
|
464
|
+
let mut sink = writer.finish().map_err(value_error)?;
|
|
465
|
+
sink.flush().map_err(|error| os_error(&self.path, &error))?;
|
|
466
|
+
drop(sink);
|
|
467
|
+
std::fs::metadata(&self.path)
|
|
468
|
+
.map(|metadata| metadata.len())
|
|
469
|
+
.map_err(|error| os_error(&self.path, &error))
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
impl ArchiveFileWriter {
|
|
474
|
+
fn writer(&mut self) -> PyResult<&mut gedzip::ArchiveWriter<BufWriter<File>>> {
|
|
475
|
+
self.inner
|
|
476
|
+
.as_mut()
|
|
477
|
+
.ok_or_else(|| PyValueError::new_err("finish() was already called"))
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fn os_error(path: &std::path::Path, error: &std::io::Error) -> PyErr {
|
|
482
|
+
PyOSError::new_err(format!("{}: {error}", path.display()))
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/// A date reading, with the renderings a caller most often wants.
|
|
486
|
+
#[derive(Serialize)]
|
|
487
|
+
struct DateReading<'a> {
|
|
488
|
+
#[serde(flatten)]
|
|
489
|
+
date: &'a GedcomDate,
|
|
490
|
+
display: String,
|
|
491
|
+
gedcom551: Option<String>,
|
|
492
|
+
gedcom7: Option<String>,
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/// Reads a date as the library reads any `DATE` payload — every language
|
|
496
|
+
/// and spelling it knows — and returns the reading as a dict: `original`,
|
|
497
|
+
/// `calendar`, `kind`, `earliest`, `latest`, `phrase`, the `display` form,
|
|
498
|
+
/// `gedcom551`, the reading in GEDCOM 5.5.1's own grammar (`ABT 5 JAN
|
|
499
|
+
/// 1882`), and `gedcom7`, the reading in GEDCOM 7.0's own grammar (`ABT 5
|
|
500
|
+
/// JAN 1882`, with no `INT` and no dual year) — either `None` when that
|
|
501
|
+
/// grammar cannot hold it without loss.
|
|
502
|
+
#[pyfunction]
|
|
503
|
+
fn parse_date(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
|
|
504
|
+
let date = GedcomDate::parse(text);
|
|
505
|
+
to_py(
|
|
506
|
+
py,
|
|
507
|
+
&DateReading {
|
|
508
|
+
display: date.display(),
|
|
509
|
+
gedcom551: date.to_gedcom_5_5_1(),
|
|
510
|
+
gedcom7: date.to_gedcom_7(),
|
|
511
|
+
date: &date,
|
|
512
|
+
},
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
287
516
|
/// The sum of a GEDZIP archive's entries' claimed decompressed sizes, read
|
|
288
517
|
/// from its central directory alone — nothing is decompressed to answer
|
|
289
518
|
/// this. Raises `ValueError` for bytes that do not open as an archive.
|
|
@@ -304,7 +533,9 @@ fn archive_total_size(data: &[u8]) -> PyResult<u64> {
|
|
|
304
533
|
fn gedcomkit_py(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
305
534
|
module.add_class::<Gedcom>()?;
|
|
306
535
|
module.add_class::<ArchiveWriter>()?;
|
|
536
|
+
module.add_class::<ArchiveFileWriter>()?;
|
|
307
537
|
module.add_function(wrap_pyfunction!(archive_total_size, module)?)?;
|
|
538
|
+
module.add_function(wrap_pyfunction!(parse_date, module)?)?;
|
|
308
539
|
module.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
|
309
540
|
Ok(())
|
|
310
541
|
}
|