gedcomkit 0.1.5__tar.gz → 0.1.6__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.6}/CHANGELOG.md +49 -0
  2. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/Cargo.lock +1 -1
  3. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/Cargo.toml +1 -1
  4. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/PKG-INFO +1 -1
  5. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/bindings/gedcomkit-py/Cargo.lock +2 -2
  6. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/bindings/gedcomkit-py/Cargo.toml +1 -1
  7. gedcomkit-0.1.6/bindings/gedcomkit-py/gedcomkit.pyi +84 -0
  8. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/bindings/gedcomkit-py/src/lib.rs +214 -3
  9. gedcomkit-0.1.6/src/build.rs +711 -0
  10. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/dates.rs +368 -28
  11. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/lib.rs +3 -0
  12. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/LICENSE +0 -0
  13. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/README.md +0 -0
  14. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/bindings/gedcomkit-py/LICENSE +0 -0
  15. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/bindings/gedcomkit-py/README.md +0 -0
  16. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
  17. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom-spec/README.md +0 -0
  18. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
  19. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
  20. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
  21. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
  22. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
  23. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
  24. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
  25. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
  26. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
  27. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
  28. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
  29. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
  30. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
  31. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
  32. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
  33. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
  34. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
  35. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
  36. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
  37. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
  38. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
  39. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
  40. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
  41. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
  42. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
  43. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
  44. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
  45. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
  46. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
  47. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
  48. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
  49. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
  50. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
  51. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
  52. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
  53. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
  54. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
  55. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
  56. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
  57. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
  58. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
  59. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
  60. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
  61. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
  62. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
  63. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
  64. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
  65. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
  66. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
  67. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
  68. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
  69. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
  70. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
  71. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
  72. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
  73. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
  74. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
  75. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
  76. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/LICENSE +0 -0
  77. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
  78. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/zip-interop/README.md +0 -0
  79. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
  80. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
  81. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/pyproject.toml +0 -0
  82. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/convert.rs +0 -0
  83. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/decode.rs +0 -0
  84. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/family.rs +0 -0
  85. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/gedzip.rs +0 -0
  86. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/names.rs +0 -0
  87. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/schema.rs +0 -0
  88. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/synthetic.rs +0 -0
  89. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/tags.rs +0 -0
  90. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/validate.rs +0 -0
  91. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/src/view.rs +0 -0
  92. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/tests/endurance.rs +0 -0
  93. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/tests/external_charset.rs +0 -0
  94. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/tests/properties.rs +0 -0
  95. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/tests/requirement_corpus.rs +0 -0
  96. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/tests/spec_conformance.rs +0 -0
  97. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/tests/vendored_corpus.rs +0 -0
  98. {gedcomkit-0.1.5 → gedcomkit-0.1.6}/tests/version_conversion.rs +0 -0
@@ -5,6 +5,55 @@ 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.6] - 2026-09-25
9
+
10
+ ### Added
11
+
12
+ - `build`, for a producer writing its own data as GEDCOM rather than
13
+ reading a file. `build::NodeSpec` describes a structure (tag, optional
14
+ identifier and payload, substructures) and `NodeSpec::into_node` checks
15
+ it under `Limits`, refusing with an error what `Node::new` would panic on;
16
+ with the `serde` feature it deserializes, refusing unknown keys.
17
+ `Document::append_records` adds records before the trailer, all or none,
18
+ refusing a repeated identifier or a second header or trailer, and
19
+ `Document::append_structures` adds substructures to any record or the
20
+ header (which 5.5.1 wants a `SUBM` pointer in, and `new_v551` cannot
21
+ supply), leaving every existing line as it was.
22
+ - `Document::wrap_long_lines`, which fits a 5.5.1 document to a line limit
23
+ by splitting payloads across `CONC` lines — at character boundaries, and
24
+ not beside a space where the text allows — and names every structure it
25
+ rewrote. Lines that fit are untouched; a version 7 document is refused.
26
+ - `dates::GedcomDate::to_gedcom_5_5_1`, the reading written in 5.5.1's own
27
+ date grammar (`about 5 January 1882` → `ABT 5 JAN 1882`), or `None` when
28
+ that grammar cannot hold it without loss. A dual year it can spell
29
+ (`1740/41`) is kept; a slashed pair it cannot makes an exact date an `INT`
30
+ date carrying the original as its phrase.
31
+ - Python and JavaScript/WASM bindings: `Gedcom.new_v551` / `newV551`,
32
+ `new_v7` / `newV7`, `append_records` / `appendRecords` (records as plain
33
+ dicts/objects), `append_structures` / `appendStructures`, `wrap_long_lines` / `wrapLongLines`, and
34
+ `parse_date` / `parseDate`, a date reading with its `display` and
35
+ `gedcom551` renderings.
36
+ - Python only: `ArchiveFileWriter`, a GEDZIP writer whose sink is a file,
37
+ with `add_file` streaming an entry from disk without the bytes passing
38
+ through Python — so a multi-gigabyte archive no longer has to fit in
39
+ memory twice. And type stubs (`gedcomkit.pyi`), so the package is typed
40
+ for mypy and pyright.
41
+
42
+ ### Fixed
43
+
44
+ - The date reader accepts 5.5.1's `B.C.` epoch as well as 7.0's `BCE`.
45
+ - The date reader matched its keywords by ASCII case only, so `après 12
46
+ juillet 1670` and `før 1679` went unread although both words were in its
47
+ tables. Keywords now compare case in every script, and may run straight
48
+ into the date (`abt1810`, `c1700`).
49
+ - `April 9, 1950` read its `9` as September and failed; a month written as a
50
+ word now decides the order. `10 January 1566/67`, a dual year ending a full
51
+ date, now reads as 1566 as the bare `1566/67` already did.
52
+ - More spellings: `vers`, `cir`, `c.`, `around`, `probably`, `omtrent`,
53
+ `cirka` (about); `før` (before); `etter`, `efter` (after); `entre … et …`,
54
+ `mellom … og …` (between); `du … au …`, `fra … til …` (from … to);
55
+ and the months `déc`, `févr`, `janv`, `avr`, `juil`, `desember`.
56
+
8
57
  ## [0.1.5] - 2026-09-18
9
58
 
10
59
  ### Added
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.5"
38
+ version = "0.1.6"
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.6"
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.6
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.6"
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.6"
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.6"
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,84 @@
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(self) -> tuple[Gedcom, list[dict[str, Any]]]: ...
64
+ def to_version551(self) -> tuple[Gedcom, list[dict[str, Any]]]: ...
65
+ def __len__(self) -> int: ...
66
+
67
+ class ArchiveWriter:
68
+ @staticmethod
69
+ def new() -> ArchiveWriter: ...
70
+ def add(self, name: str, data: bytes) -> None: ...
71
+ def finish(self) -> bytes: ...
72
+
73
+ class ArchiveFileWriter:
74
+ """A GEDZIP archive written straight to a file, one entry at a time."""
75
+
76
+ def __init__(self, path: str | PathLike[str]) -> None: ...
77
+ def add(self, name: str, data: bytes) -> None: ...
78
+ def add_file(self, name: str, source: str | PathLike[str]) -> None: ...
79
+ def finish(self) -> int:
80
+ """Close the archive and return its size in bytes."""
81
+
82
+ def archive_total_size(data: bytes) -> int: ...
83
+ def parse_date(text: str) -> dict[str, Any]:
84
+ """A date reading; its ``gedcom551`` key is the 5.5.1 spelling, or None."""
@@ -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 {
@@ -284,6 +395,104 @@ impl Default for ArchiveWriter {
284
395
  }
285
396
  }
286
397
 
398
+ /// Writes a GEDZIP archive straight to a file, one entry at a time, so
399
+ /// memory stays a small multiple of the largest single entry however large
400
+ /// the archive grows. Python only: the WASM binding has no filesystem.
401
+ ///
402
+ /// The file is created at once and is complete only after `finish`; an
403
+ /// archive abandoned part-way (an error, or no `finish`) leaves a partial
404
+ /// file behind. Write to a temporary path and rename it after `finish`.
405
+ #[pyclass]
406
+ struct ArchiveFileWriter {
407
+ /// `None` once `finish` has been called.
408
+ inner: Option<gedzip::ArchiveWriter<BufWriter<File>>>,
409
+ path: PathBuf,
410
+ }
411
+
412
+ #[pymethods]
413
+ impl ArchiveFileWriter {
414
+ /// Creates (or truncates) the file at `path` and starts an archive in it
415
+ /// under the default bounds. Raises `OSError` when the file cannot be
416
+ /// created.
417
+ #[new]
418
+ fn new(path: PathBuf) -> PyResult<Self> {
419
+ let file = File::create(&path).map_err(|error| os_error(&path, &error))?;
420
+ Ok(Self {
421
+ inner: Some(gedzip::ArchiveWriter::new(BufWriter::new(file))),
422
+ path,
423
+ })
424
+ }
425
+
426
+ /// Adds one entry from bytes. Raises `ValueError` for an unsafe or
427
+ /// repeated path, one past the per-entry limit, a failed write, or a call
428
+ /// after `finish`.
429
+ fn add(&mut self, name: &str, data: &[u8]) -> PyResult<()> {
430
+ self.writer()?.add(name, data).map_err(value_error)
431
+ }
432
+
433
+ /// Adds one entry read from the file at `source`, without the bytes ever
434
+ /// passing through Python. Raises `OSError` when it cannot be opened and
435
+ /// `ValueError` as `add` does.
436
+ fn add_file(&mut self, name: &str, source: PathBuf) -> PyResult<()> {
437
+ let file = File::open(&source).map_err(|error| os_error(&source, &error))?;
438
+ self.writer()?.add(name, file).map_err(value_error)
439
+ }
440
+
441
+ /// Writes the central directory, flushes, and closes the file, returning
442
+ /// its size in bytes. Valid to call once.
443
+ fn finish(&mut self) -> PyResult<u64> {
444
+ let writer = self
445
+ .inner
446
+ .take()
447
+ .ok_or_else(|| PyValueError::new_err("finish() was already called"))?;
448
+ let mut sink = writer.finish().map_err(value_error)?;
449
+ sink.flush().map_err(|error| os_error(&self.path, &error))?;
450
+ drop(sink);
451
+ std::fs::metadata(&self.path)
452
+ .map(|metadata| metadata.len())
453
+ .map_err(|error| os_error(&self.path, &error))
454
+ }
455
+ }
456
+
457
+ impl ArchiveFileWriter {
458
+ fn writer(&mut self) -> PyResult<&mut gedzip::ArchiveWriter<BufWriter<File>>> {
459
+ self.inner
460
+ .as_mut()
461
+ .ok_or_else(|| PyValueError::new_err("finish() was already called"))
462
+ }
463
+ }
464
+
465
+ fn os_error(path: &std::path::Path, error: &std::io::Error) -> PyErr {
466
+ PyOSError::new_err(format!("{}: {error}", path.display()))
467
+ }
468
+
469
+ /// A date reading, with the two renderings a caller most often wants.
470
+ #[derive(Serialize)]
471
+ struct DateReading<'a> {
472
+ #[serde(flatten)]
473
+ date: &'a GedcomDate,
474
+ display: String,
475
+ gedcom551: Option<String>,
476
+ }
477
+
478
+ /// Reads a date as the library reads any `DATE` payload — every language
479
+ /// and spelling it knows — and returns the reading as a dict: `original`,
480
+ /// `calendar`, `kind`, `earliest`, `latest`, `phrase`, the `display` form,
481
+ /// and `gedcom551`, the reading in GEDCOM 5.5.1's own grammar (`ABT 5 JAN
482
+ /// 1882`), or `None` when that grammar cannot hold it without loss.
483
+ #[pyfunction]
484
+ fn parse_date(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
485
+ let date = GedcomDate::parse(text);
486
+ to_py(
487
+ py,
488
+ &DateReading {
489
+ display: date.display(),
490
+ gedcom551: date.to_gedcom_5_5_1(),
491
+ date: &date,
492
+ },
493
+ )
494
+ }
495
+
287
496
  /// The sum of a GEDZIP archive's entries' claimed decompressed sizes, read
288
497
  /// from its central directory alone — nothing is decompressed to answer
289
498
  /// this. Raises `ValueError` for bytes that do not open as an archive.
@@ -304,7 +513,9 @@ fn archive_total_size(data: &[u8]) -> PyResult<u64> {
304
513
  fn gedcomkit_py(module: &Bound<'_, PyModule>) -> PyResult<()> {
305
514
  module.add_class::<Gedcom>()?;
306
515
  module.add_class::<ArchiveWriter>()?;
516
+ module.add_class::<ArchiveFileWriter>()?;
307
517
  module.add_function(wrap_pyfunction!(archive_total_size, module)?)?;
518
+ module.add_function(wrap_pyfunction!(parse_date, module)?)?;
308
519
  module.add("__version__", env!("CARGO_PKG_VERSION"))?;
309
520
  Ok(())
310
521
  }