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.
Files changed (98) hide show
  1. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/CHANGELOG.md +104 -0
  2. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/Cargo.lock +1 -1
  3. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/Cargo.toml +1 -1
  4. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/PKG-INFO +1 -1
  5. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/Cargo.lock +2 -2
  6. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/Cargo.toml +1 -1
  7. gedcomkit-0.1.7/bindings/gedcomkit-py/gedcomkit.pyi +90 -0
  8. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/src/lib.rs +236 -5
  9. gedcomkit-0.1.7/src/build.rs +711 -0
  10. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/convert.rs +479 -68
  11. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/dates.rs +546 -28
  12. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/lib.rs +3 -0
  13. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/LICENSE +0 -0
  14. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/README.md +0 -0
  15. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/LICENSE +0 -0
  16. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/bindings/gedcomkit-py/README.md +0 -0
  17. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
  18. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/README.md +0 -0
  19. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
  20. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
  21. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
  22. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
  23. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
  24. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
  25. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
  26. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
  27. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
  28. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
  29. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
  30. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
  31. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
  32. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
  33. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
  34. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
  35. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
  36. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
  37. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
  38. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
  39. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
  40. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
  41. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
  42. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
  43. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
  44. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
  45. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
  46. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
  47. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
  48. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
  49. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
  50. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
  51. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
  52. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
  53. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
  54. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
  55. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
  56. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
  57. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
  58. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
  59. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
  60. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
  61. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
  62. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
  63. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
  64. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
  65. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
  66. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
  67. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
  68. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
  69. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
  70. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
  71. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
  72. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
  73. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
  74. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
  75. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
  76. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
  77. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/LICENSE +0 -0
  78. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
  79. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/zip-interop/README.md +0 -0
  80. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
  81. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
  82. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/pyproject.toml +0 -0
  83. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/decode.rs +0 -0
  84. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/family.rs +0 -0
  85. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/gedzip.rs +0 -0
  86. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/names.rs +0 -0
  87. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/schema.rs +0 -0
  88. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/synthetic.rs +0 -0
  89. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/tags.rs +0 -0
  90. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/validate.rs +0 -0
  91. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/src/view.rs +0 -0
  92. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/endurance.rs +0 -0
  93. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/external_charset.rs +0 -0
  94. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/properties.rs +0 -0
  95. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/requirement_corpus.rs +0 -0
  96. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/spec_conformance.rs +0 -0
  97. {gedcomkit-0.1.5 → gedcomkit-0.1.7}/tests/vendored_corpus.rs +0 -0
  98. {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
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.5"
38
+ version = "0.1.7"
39
39
  dependencies = [
40
40
  "flate2",
41
41
  "serde",
@@ -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.5"
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gedcomkit
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.5"
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.5"
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"
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. Everything else stays in Rust.
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
- fn to_version7(&self, py: Python<'_>) -> PyResult<(Self, Vec<Py<PyAny>>)> {
176
- let outcome = gedcomkit::convert::to_version_7(&self.document);
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
  }