gedcom-x 0.5.5__tar.gz → 0.5.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 (52) hide show
  1. gedcom_x-0.5.6/PKG-INFO +144 -0
  2. gedcom_x-0.5.6/README.md +126 -0
  3. gedcom_x-0.5.6/gedcom_x.egg-info/PKG-INFO +144 -0
  4. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcom_x.egg-info/SOURCES.txt +3 -0
  5. gedcom_x-0.5.6/gedcomx/ExtensibleEnum.py +183 -0
  6. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Gedcom5x.py +43 -22
  7. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/GedcomX.py +12 -5
  8. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Identifier.py +1 -1
  9. gedcom_x-0.5.6/gedcomx/LoggingHub.py +186 -0
  10. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/__init__.py +1 -0
  11. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/pyproject.toml +1 -1
  12. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/setup.py +10 -4
  13. gedcom_x-0.5.5/PKG-INFO +0 -17
  14. gedcom_x-0.5.5/gedcom_x.egg-info/PKG-INFO +0 -17
  15. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcom_x.egg-info/dependency_links.txt +0 -0
  16. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcom_x.egg-info/top_level.txt +0 -0
  17. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Address.py +0 -0
  18. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Agent.py +0 -0
  19. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Attribution.py +0 -0
  20. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Conclusion.py +0 -0
  21. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Coverage.py +0 -0
  22. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Date.py +0 -0
  23. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Document.py +0 -0
  24. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Event.py +0 -0
  25. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/EvidenceReference.py +0 -0
  26. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Exceptions.py +0 -0
  27. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Fact.py +0 -0
  28. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Gedcom.py +0 -0
  29. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Gender.py +0 -0
  30. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Group.py +0 -0
  31. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Logging.py +0 -0
  32. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Mutations.py +0 -0
  33. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Name.py +0 -0
  34. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Note.py +0 -0
  35. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/OnlineAccount.py +0 -0
  36. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Person.py +0 -0
  37. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/PlaceDescription.py +0 -0
  38. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/PlaceReference.py +0 -0
  39. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Qualifier.py +0 -0
  40. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Relationship.py +0 -0
  41. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Resource.py +0 -0
  42. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Serialization.py +0 -0
  43. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/SourceCitation.py +0 -0
  44. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/SourceDescription.py +0 -0
  45. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/SourceReference.py +0 -0
  46. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Subject.py +0 -0
  47. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/TextValue.py +0 -0
  48. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/TopLevelTypeCollection.py +0 -0
  49. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Translation.py +0 -0
  50. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/URI.py +0 -0
  51. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Zip.py +0 -0
  52. {gedcom_x-0.5.5 → gedcom_x-0.5.6}/setup.cfg +0 -0
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: gedcom-x
3
+ Version: 0.5.6
4
+ Summary: Python implimentation of gedcom-x standard
5
+ Author-email: "David J. Cartwright" <davidcartwright@hotmail.com>
6
+ License: MIT
7
+ Keywords: gedcom,gedcomx,ged
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Environment :: Console
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Utilities
14
+ Classifier: Topic :: Software Development :: User Interfaces
15
+ Requires-Python: >=3.6
16
+ Description-Content-Type: text/markdown
17
+ Dynamic: requires-python
18
+
19
+ # GEDCOM-X Python Toolkit (gedcom-x beta 0.5.5)
20
+
21
+ A lightweight, class-based Python implementation of the [GEDCOM-X data model](https://github.com/FamilySearch/gedcomx).
22
+
23
+ ## ⚠️ Project Status
24
+
25
+ This project is currently in **beta**.
26
+ While the core GEDCOM-X classes and serialization are functional, some features may not be fully implemented or may not behave exactly as expected.
27
+
28
+ - Certain GEDCOM 7 tags are not yet mapped
29
+ - Some classes may be missing methods or fields
30
+ - Error handling and validation are still evolving
31
+ - Backward compatibility is **not guaranteed** until the first stable release
32
+
33
+ ### ✅ What You Can Do
34
+ - Create and manipulate GEDCOM-X objects in Python
35
+ - Serialize and deserialize data to/from JSON
36
+ - Experimentally convert GEDCOM 5x & 7 files into GEDCOM-X JSON
37
+ - Extend the classes to handle new GEDCOM tags or custom attributes
38
+ - Use the library as a foundation for genealogy-related tooling or RAG pipelines
39
+
40
+ ### ❌ What You Can’t Do (Yet)
41
+ - Rely on complete coverage of all GEDCOM 7 tags
42
+ - Expect perfect compliance with the GEDCOM-X specification
43
+ - Assume strong validation or error recovery on malformed input
44
+ - Use it as a drop-in replacement for production genealogy software
45
+ - Write GEDCOM-X to GEDCOM 5x / 7
46
+ - Create Graphs from Genealogies
47
+
48
+ Contributors and testers are welcome — feedback will help stabilize the library!
49
+
50
+ ---
51
+
52
+ This library aims to provide:
53
+
54
+ - Python classes for every GEDCOM-X type (Person, Fact, Source, etc.)
55
+ - Extensibility, with current GEDCOM RS etc, extension built in
56
+ - Serialization and Deserialization to/from GEDCOM-X JSON
57
+ - Utilities to convert GEDCOM 5x & 7 GEDCOM Files into GEDCOM-X and back
58
+ - Type-safe field definitions and extensibility hooks for future tags
59
+
60
+ ---
61
+
62
+ ## Features
63
+
64
+ - **Complete GEDCOM-X Class Coverage**
65
+ Each GEDCOM-X type is represented as a Python class with fields and types.
66
+
67
+ - **Serialization / Deserialization**
68
+ Every class can serialize to JSON and reconstruct from JSON via `_as_dict_()` and `_from_json()` methods.
69
+
70
+ - **Type Checking & Enum Validation**
71
+ Uses Python type hints and enums to ensure correct values (e.g. FactType, EventType, ConfidenceLevel).
72
+
73
+ - **Composable / Nestable Classes**
74
+ Nested objects (e.g. Person → Name → NameForm → TextValue) are constructed and validated recursively.
75
+
76
+ - **GEDCOM 7 → GEDCOM-X Conversion**
77
+ Experimental parser to read GEDCOM 7 files and convert them into structured GEDCOM-X JSON.
78
+
79
+ ---
80
+
81
+ ## Installation
82
+
83
+ Clone the repository and install dependencies:
84
+
85
+ ```bash
86
+ git clone https://github.com/yourusername/gedcom-x.git
87
+ cd gedcom-x
88
+ pip install -r requirements.txt
89
+ ```
90
+ or
91
+ ```
92
+ pip install gedcom-x
93
+ ```
94
+ ---
95
+
96
+ ## Examples
97
+
98
+ <details>
99
+
100
+ <summary>Create a Person Gedcom-X Type</summary>
101
+
102
+ ```python
103
+ import json
104
+ from gedcomx import Person, Name, NameForm, TextValue
105
+
106
+ person = Person(
107
+ id="P-123",
108
+ names=[Name(
109
+ nameForms=[NameForm(
110
+ fullText=TextValue(value="John Doe")
111
+ )]
112
+ )]
113
+ )
114
+
115
+ print(json.dumps(person._as_dict_,indent=4))
116
+ ```
117
+ result
118
+ ```text
119
+ {
120
+ "id": "P-123",
121
+ "lang": "en",
122
+ "private": false,
123
+ "living": false,
124
+ "gender": {
125
+ "lang": "en",
126
+ "type": "http://gedcomx.org/Unknown"
127
+ },
128
+ "names": [
129
+ {
130
+ "lang": "en",
131
+ "nameForms": [
132
+ {
133
+ "lang": "en",
134
+ "fullText": {
135
+ "lang": "en",
136
+ "value": "John Doe"
137
+ }
138
+ }
139
+ ]
140
+ }
141
+ ]
142
+ }
143
+
144
+ </details>
@@ -0,0 +1,126 @@
1
+ # GEDCOM-X Python Toolkit (gedcom-x beta 0.5.5)
2
+
3
+ A lightweight, class-based Python implementation of the [GEDCOM-X data model](https://github.com/FamilySearch/gedcomx).
4
+
5
+ ## ⚠️ Project Status
6
+
7
+ This project is currently in **beta**.
8
+ While the core GEDCOM-X classes and serialization are functional, some features may not be fully implemented or may not behave exactly as expected.
9
+
10
+ - Certain GEDCOM 7 tags are not yet mapped
11
+ - Some classes may be missing methods or fields
12
+ - Error handling and validation are still evolving
13
+ - Backward compatibility is **not guaranteed** until the first stable release
14
+
15
+ ### ✅ What You Can Do
16
+ - Create and manipulate GEDCOM-X objects in Python
17
+ - Serialize and deserialize data to/from JSON
18
+ - Experimentally convert GEDCOM 5x & 7 files into GEDCOM-X JSON
19
+ - Extend the classes to handle new GEDCOM tags or custom attributes
20
+ - Use the library as a foundation for genealogy-related tooling or RAG pipelines
21
+
22
+ ### ❌ What You Can’t Do (Yet)
23
+ - Rely on complete coverage of all GEDCOM 7 tags
24
+ - Expect perfect compliance with the GEDCOM-X specification
25
+ - Assume strong validation or error recovery on malformed input
26
+ - Use it as a drop-in replacement for production genealogy software
27
+ - Write GEDCOM-X to GEDCOM 5x / 7
28
+ - Create Graphs from Genealogies
29
+
30
+ Contributors and testers are welcome — feedback will help stabilize the library!
31
+
32
+ ---
33
+
34
+ This library aims to provide:
35
+
36
+ - Python classes for every GEDCOM-X type (Person, Fact, Source, etc.)
37
+ - Extensibility, with current GEDCOM RS etc, extension built in
38
+ - Serialization and Deserialization to/from GEDCOM-X JSON
39
+ - Utilities to convert GEDCOM 5x & 7 GEDCOM Files into GEDCOM-X and back
40
+ - Type-safe field definitions and extensibility hooks for future tags
41
+
42
+ ---
43
+
44
+ ## Features
45
+
46
+ - **Complete GEDCOM-X Class Coverage**
47
+ Each GEDCOM-X type is represented as a Python class with fields and types.
48
+
49
+ - **Serialization / Deserialization**
50
+ Every class can serialize to JSON and reconstruct from JSON via `_as_dict_()` and `_from_json()` methods.
51
+
52
+ - **Type Checking & Enum Validation**
53
+ Uses Python type hints and enums to ensure correct values (e.g. FactType, EventType, ConfidenceLevel).
54
+
55
+ - **Composable / Nestable Classes**
56
+ Nested objects (e.g. Person → Name → NameForm → TextValue) are constructed and validated recursively.
57
+
58
+ - **GEDCOM 7 → GEDCOM-X Conversion**
59
+ Experimental parser to read GEDCOM 7 files and convert them into structured GEDCOM-X JSON.
60
+
61
+ ---
62
+
63
+ ## Installation
64
+
65
+ Clone the repository and install dependencies:
66
+
67
+ ```bash
68
+ git clone https://github.com/yourusername/gedcom-x.git
69
+ cd gedcom-x
70
+ pip install -r requirements.txt
71
+ ```
72
+ or
73
+ ```
74
+ pip install gedcom-x
75
+ ```
76
+ ---
77
+
78
+ ## Examples
79
+
80
+ <details>
81
+
82
+ <summary>Create a Person Gedcom-X Type</summary>
83
+
84
+ ```python
85
+ import json
86
+ from gedcomx import Person, Name, NameForm, TextValue
87
+
88
+ person = Person(
89
+ id="P-123",
90
+ names=[Name(
91
+ nameForms=[NameForm(
92
+ fullText=TextValue(value="John Doe")
93
+ )]
94
+ )]
95
+ )
96
+
97
+ print(json.dumps(person._as_dict_,indent=4))
98
+ ```
99
+ result
100
+ ```text
101
+ {
102
+ "id": "P-123",
103
+ "lang": "en",
104
+ "private": false,
105
+ "living": false,
106
+ "gender": {
107
+ "lang": "en",
108
+ "type": "http://gedcomx.org/Unknown"
109
+ },
110
+ "names": [
111
+ {
112
+ "lang": "en",
113
+ "nameForms": [
114
+ {
115
+ "lang": "en",
116
+ "fullText": {
117
+ "lang": "en",
118
+ "value": "John Doe"
119
+ }
120
+ }
121
+ ]
122
+ }
123
+ ]
124
+ }
125
+
126
+ </details>
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: gedcom-x
3
+ Version: 0.5.6
4
+ Summary: Python implimentation of gedcom-x standard
5
+ Author-email: "David J. Cartwright" <davidcartwright@hotmail.com>
6
+ License: MIT
7
+ Keywords: gedcom,gedcomx,ged
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Environment :: Console
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Utilities
14
+ Classifier: Topic :: Software Development :: User Interfaces
15
+ Requires-Python: >=3.6
16
+ Description-Content-Type: text/markdown
17
+ Dynamic: requires-python
18
+
19
+ # GEDCOM-X Python Toolkit (gedcom-x beta 0.5.5)
20
+
21
+ A lightweight, class-based Python implementation of the [GEDCOM-X data model](https://github.com/FamilySearch/gedcomx).
22
+
23
+ ## ⚠️ Project Status
24
+
25
+ This project is currently in **beta**.
26
+ While the core GEDCOM-X classes and serialization are functional, some features may not be fully implemented or may not behave exactly as expected.
27
+
28
+ - Certain GEDCOM 7 tags are not yet mapped
29
+ - Some classes may be missing methods or fields
30
+ - Error handling and validation are still evolving
31
+ - Backward compatibility is **not guaranteed** until the first stable release
32
+
33
+ ### ✅ What You Can Do
34
+ - Create and manipulate GEDCOM-X objects in Python
35
+ - Serialize and deserialize data to/from JSON
36
+ - Experimentally convert GEDCOM 5x & 7 files into GEDCOM-X JSON
37
+ - Extend the classes to handle new GEDCOM tags or custom attributes
38
+ - Use the library as a foundation for genealogy-related tooling or RAG pipelines
39
+
40
+ ### ❌ What You Can’t Do (Yet)
41
+ - Rely on complete coverage of all GEDCOM 7 tags
42
+ - Expect perfect compliance with the GEDCOM-X specification
43
+ - Assume strong validation or error recovery on malformed input
44
+ - Use it as a drop-in replacement for production genealogy software
45
+ - Write GEDCOM-X to GEDCOM 5x / 7
46
+ - Create Graphs from Genealogies
47
+
48
+ Contributors and testers are welcome — feedback will help stabilize the library!
49
+
50
+ ---
51
+
52
+ This library aims to provide:
53
+
54
+ - Python classes for every GEDCOM-X type (Person, Fact, Source, etc.)
55
+ - Extensibility, with current GEDCOM RS etc, extension built in
56
+ - Serialization and Deserialization to/from GEDCOM-X JSON
57
+ - Utilities to convert GEDCOM 5x & 7 GEDCOM Files into GEDCOM-X and back
58
+ - Type-safe field definitions and extensibility hooks for future tags
59
+
60
+ ---
61
+
62
+ ## Features
63
+
64
+ - **Complete GEDCOM-X Class Coverage**
65
+ Each GEDCOM-X type is represented as a Python class with fields and types.
66
+
67
+ - **Serialization / Deserialization**
68
+ Every class can serialize to JSON and reconstruct from JSON via `_as_dict_()` and `_from_json()` methods.
69
+
70
+ - **Type Checking & Enum Validation**
71
+ Uses Python type hints and enums to ensure correct values (e.g. FactType, EventType, ConfidenceLevel).
72
+
73
+ - **Composable / Nestable Classes**
74
+ Nested objects (e.g. Person → Name → NameForm → TextValue) are constructed and validated recursively.
75
+
76
+ - **GEDCOM 7 → GEDCOM-X Conversion**
77
+ Experimental parser to read GEDCOM 7 files and convert them into structured GEDCOM-X JSON.
78
+
79
+ ---
80
+
81
+ ## Installation
82
+
83
+ Clone the repository and install dependencies:
84
+
85
+ ```bash
86
+ git clone https://github.com/yourusername/gedcom-x.git
87
+ cd gedcom-x
88
+ pip install -r requirements.txt
89
+ ```
90
+ or
91
+ ```
92
+ pip install gedcom-x
93
+ ```
94
+ ---
95
+
96
+ ## Examples
97
+
98
+ <details>
99
+
100
+ <summary>Create a Person Gedcom-X Type</summary>
101
+
102
+ ```python
103
+ import json
104
+ from gedcomx import Person, Name, NameForm, TextValue
105
+
106
+ person = Person(
107
+ id="P-123",
108
+ names=[Name(
109
+ nameForms=[NameForm(
110
+ fullText=TextValue(value="John Doe")
111
+ )]
112
+ )]
113
+ )
114
+
115
+ print(json.dumps(person._as_dict_,indent=4))
116
+ ```
117
+ result
118
+ ```text
119
+ {
120
+ "id": "P-123",
121
+ "lang": "en",
122
+ "private": false,
123
+ "living": false,
124
+ "gender": {
125
+ "lang": "en",
126
+ "type": "http://gedcomx.org/Unknown"
127
+ },
128
+ "names": [
129
+ {
130
+ "lang": "en",
131
+ "nameForms": [
132
+ {
133
+ "lang": "en",
134
+ "fullText": {
135
+ "lang": "en",
136
+ "value": "John Doe"
137
+ }
138
+ }
139
+ ]
140
+ }
141
+ ]
142
+ }
143
+
144
+ </details>
@@ -1,3 +1,4 @@
1
+ README.md
1
2
  pyproject.toml
2
3
  setup.py
3
4
  gedcom_x.egg-info/PKG-INFO
@@ -14,6 +15,7 @@ gedcomx/Document.py
14
15
  gedcomx/Event.py
15
16
  gedcomx/EvidenceReference.py
16
17
  gedcomx/Exceptions.py
18
+ gedcomx/ExtensibleEnum.py
17
19
  gedcomx/Fact.py
18
20
  gedcomx/Gedcom.py
19
21
  gedcomx/Gedcom5x.py
@@ -22,6 +24,7 @@ gedcomx/Gender.py
22
24
  gedcomx/Group.py
23
25
  gedcomx/Identifier.py
24
26
  gedcomx/Logging.py
27
+ gedcomx/LoggingHub.py
25
28
  gedcomx/Mutations.py
26
29
  gedcomx/Name.py
27
30
  gedcomx/Note.py
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Any, Dict, Iterator, Literal
4
+
5
+ """
6
+ ======================================================================
7
+ Project: Gedcom-X
8
+ File: ExtensibleEnum.py
9
+ Author: David J. Cartwright
10
+ Purpose: Create a class that can act like an enum but be extended by the user at runtime.
11
+
12
+ Created: 2025-08-25
13
+ Updated:
14
+ - YYYY-MM-DD: <change description>
15
+
16
+ ======================================================================
17
+ """
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class _EnumItem:
21
+ """
22
+ A single registered member of an :class:`ExtensibleEnum`.
23
+
24
+ Each `_EnumItem` represents one (name, value) pair that belongs
25
+ to a particular `ExtensibleEnum` subclass. Items are immutable
26
+ once created.
27
+
28
+ Attributes
29
+ ----------
30
+ owner : type
31
+ The subclass of :class:`ExtensibleEnum` that owns this member
32
+ (e.g., `Color`).
33
+ name : str
34
+ The symbolic name of the member (e.g., `"RED"`).
35
+ value : Any
36
+ The underlying value associated with the member (e.g., `"r"`).
37
+
38
+ Notes
39
+ -----
40
+ - Equality is determined by object identity (not overridden).
41
+ - Instances are hashable by default since the dataclass is frozen.
42
+ - The `__repr__` and `__str__` provide user-friendly string forms.
43
+
44
+ Examples
45
+ --------
46
+ >>> class Color(ExtensibleEnum): ...
47
+ >>> red = Color.register("RED", "r")
48
+ >>> repr(red)
49
+ 'Color.RED'
50
+ >>> str(red)
51
+ 'RED'
52
+ >>> red.value
53
+ 'r'
54
+ """
55
+ owner: type
56
+ name: str
57
+ value: Any
58
+ def __repr__(self) -> str: # print(...) shows "Color.RED"
59
+ return f"{self.owner.__name__}.{self.name}"
60
+ def __str__(self) -> str:
61
+ return self.name
62
+
63
+ class _ExtEnumMeta(type):
64
+ def __iter__(cls) -> Iterator[_EnumItem]:
65
+ return iter(cls._members.values())
66
+ def __contains__(cls, item: object) -> bool:
67
+ return item in cls._members.values()
68
+ # Support Color('RED') / Color(2)
69
+ def __call__(cls, arg: Any, /, *, by: Literal["auto","name","value"]="auto") -> _EnumItem:
70
+ if isinstance(arg, _EnumItem):
71
+ if arg.owner is cls:
72
+ return arg
73
+ raise TypeError(f"{arg!r} is not a member of {cls.__name__}")
74
+ if by == "name":
75
+ return cls.get(str(arg))
76
+ if by == "value":
77
+ return cls.from_value(arg)
78
+ if isinstance(arg, str) and arg in cls._members:
79
+ return cls.get(arg)
80
+ return cls.from_value(arg)
81
+
82
+ class ExtensibleEnum(metaclass=_ExtEnumMeta):
83
+ """
84
+ A lightweight, **runtime-extensible**, enum-like base class.
85
+
86
+ Subclass this to create an enum whose members can be registered at runtime.
87
+ Registered members are exposed as class attributes (e.g., `Color.RED`) and
88
+ can be retrieved by name (`Color.get("RED")`) or by value
89
+ (`Color.from_value("r")`). Square-bracket lookup (`Color["RED"]`) is also
90
+ supported via ``__class_getitem__``.
91
+
92
+ This is useful when:
93
+ - The full set of enum values is not known until runtime (plugins, config).
94
+ - You need attribute-style access (`Color.RED`) but want to add members
95
+ dynamically and/or validate uniqueness of names/values.
96
+
97
+ Notes
98
+ -----
99
+ - **Uniqueness:** Names and values are unique within a subclass.
100
+ - **Per-subclass registry:** Each subclass has its own member registry.
101
+ - **Thread safety:** Registration is **not** thread-safe. If multiple threads
102
+ may register members, wrap `register()` calls in your own lock.
103
+ - **Immutability:** Once registered, a member’s `name` and `value` are fixed.
104
+ Re-registering the same `name` with the *same* `value` returns the existing
105
+ item; a different value raises an error.
106
+
107
+ Examples
108
+ --------
109
+ Define an extensible enum and register members:
110
+
111
+ >>> class Color(ExtensibleEnum):
112
+ ... pass
113
+ ...
114
+ >>> Color.register("RED", "r")
115
+ _EnumItem(owner=Color, name='RED', value='r')
116
+ >>> Color.register("GREEN", "g")
117
+ _EnumItem(owner=Color, name='GREEN', value='g')
118
+
119
+ Access members:
120
+
121
+ >>> Color.RED is Color.get("RED")
122
+ True
123
+ >>> Color["GREEN"] is Color.get("GREEN")
124
+ True
125
+ >>> Color.from_value("g") is Color.GREEN
126
+ True
127
+ >>> Color.names()
128
+ ['RED', 'GREEN']
129
+
130
+ Error cases:
131
+
132
+ >>> Color.register("RED", "different") # doctest: +IGNORE_EXCEPTION_DETAIL
133
+ ValueError: name 'RED' already used with different value 'r'
134
+ >>> Color.get("BLUE") # doctest: +IGNORE_EXCEPTION_DETAIL
135
+ KeyError: Color has no member named 'BLUE'
136
+ >>> Color.from_value("b") # doctest: +IGNORE_EXCEPTION_DETAIL
137
+ KeyError: Color has no member with value 'b'
138
+ """
139
+ """Runtime-extensible enum-like base."""
140
+ _members: Dict[str, _EnumItem] = {}
141
+
142
+ def __init_subclass__(cls, **kw):
143
+ super().__init_subclass__(**kw)
144
+ cls._members = {} # fresh registry per subclass
145
+
146
+ @classmethod
147
+ def __class_getitem__(cls, key: str) -> _EnumItem: # Color['RED']
148
+ return cls.get(key)
149
+
150
+ @classmethod
151
+ def register(cls, name: str, value: Any) -> _EnumItem:
152
+ if not isinstance(name, str) or not name.isidentifier():
153
+ raise ValueError("name must be a valid identifier")
154
+ if name in cls._members:
155
+ item = cls._members[name]
156
+ if item.value != value:
157
+ raise ValueError(f"name {name!r} already used with different value {item.value!r}")
158
+ return item
159
+ if any(m.value == value for m in cls._members.values()):
160
+ raise ValueError(f"value {value!r} already used")
161
+ item = _EnumItem(owner=cls, name=name, value=value)
162
+ cls._members[name] = item
163
+ setattr(cls, name, item) # enables Color.RED attribute
164
+ return item
165
+
166
+ @classmethod
167
+ def names(cls) -> list[str]:
168
+ return list(cls._members.keys())
169
+
170
+ @classmethod
171
+ def get(cls, name: str) -> _EnumItem:
172
+ try:
173
+ return cls._members[name]
174
+ except KeyError as e:
175
+ raise KeyError(f"{cls.__name__} has no member named {name!r}") from e
176
+
177
+ @classmethod
178
+ def from_value(cls, value: Any) -> _EnumItem:
179
+ for m in cls._members.values():
180
+ if m.value == value:
181
+ return m
182
+ raise KeyError(f"{cls.__name__} has no member with value {value!r}")
183
+
@@ -8,6 +8,10 @@ import re
8
8
  from collections import defaultdict
9
9
  from typing import Iterable, Iterator, List, Optional, Tuple, Union
10
10
 
11
+ from .LoggingHub import LoggingHub, ChannelConfig
12
+ hub = LoggingHub("GEDCOM5x")
13
+ hub.init_root()
14
+
11
15
  BOM = '\ufeff'
12
16
 
13
17
  GEDCOM7_LINE_RE = re.compile(
@@ -219,11 +223,14 @@ class Gedcom5x():
219
223
  _top_level_tags = ['INDI', 'FAM', 'OBJE', 'SOUR', 'REPO', 'NOTE', 'HEAD','SNOTE']
220
224
 
221
225
  def __init__(self, records: Optional[List[GedcomRecord]] = None,filepath: str = None) -> None:
226
+
227
+ self.records: List[GedcomRecord] = records or []
222
228
  if filepath:
223
229
  self.records = self._records_from_file(filepath)
224
230
  elif records:
225
231
  self.records: List[GedcomRecord] = records if records else []
226
232
 
233
+
227
234
  # Fast tag index: {'HEAD': [rec], 'INDI': [rec1, rec2, ...], ...}
228
235
  self._tag_index: dict[str, List[GedcomRecord]] = defaultdict(list)
229
236
  self._reindex()
@@ -356,7 +363,7 @@ class Gedcom5x():
356
363
  value_width = max(len(str(value)) for _, value in pairs)
357
364
 
358
365
  # Print the header
359
- print('GEDCOM Import Results')
366
+ print(f'GEDCOM {self.version} Import Results')
360
367
  header = f"{'Type'.ljust(name_width)} | {'Count'.ljust(value_width)}"
361
368
  print('-' * len(header))
362
369
  print(header)
@@ -430,8 +437,6 @@ class Gedcom5x():
430
437
  raise ValueError("objects must be a list of GedcomRecord objects.")
431
438
  self._objects = value
432
439
 
433
-
434
-
435
440
  def write(self) -> bool:
436
441
  """
437
442
  Method placeholder for writing GEDCOM files.
@@ -444,7 +449,7 @@ class Gedcom5x():
444
449
  raise NotImplementedError("Writing of GEDCOM files is not implemented.")
445
450
 
446
451
  @staticmethod
447
- def _records_from_file(filepath: str) -> List[GedcomRecord]:
452
+ def _records_from_file(file_path: str) -> List[GedcomRecord]:
448
453
  def parse_gedcom7_line(line: str) -> Optional[Tuple[int, Optional[str], str, Optional[str], Optional[str]]]:
449
454
  """
450
455
  Parse a GEDCOM 7 line into: level, xref_id (record), tag, value, xref_value (if value is an @X@)
@@ -466,15 +471,15 @@ class Gedcom5x():
466
471
  return level, xref_id, tag, value, xref_value
467
472
  extension = '.ged'
468
473
 
469
- if not os.path.exists(filepath):
470
- print(f"File does not exist: {filepath}")
474
+ if not os.path.exists(file_path):
475
+ print(f"File does not exist: {file_path}")
471
476
  raise FileNotFoundError
472
- elif not filepath.lower().endswith(extension.lower()):
473
- print(f"File does not have the correct extension: {filepath}")
477
+ elif not file_path.lower().endswith(extension.lower()):
478
+ print(f"File does not have the correct extension: {file_path}")
474
479
  raise Exception("File does not appear to be a GEDCOM")
475
480
 
476
481
  print("Reading from GEDCOM file")
477
- with open(filepath, 'r', encoding='utf-8') as file:
482
+ with open(file_path, 'r', encoding='utf-8') as file:
478
483
  lines = [line.strip() for line in file]
479
484
 
480
485
  records = []
@@ -528,7 +533,7 @@ class Gedcom5x():
528
533
  return records if records else None
529
534
 
530
535
  @staticmethod
531
- def fromFile(filepath: str) -> 'Gedcom':
536
+ def fromFile(file_path: str) -> 'Gedcom':
532
537
  """
533
538
  Static method to create a Gedcom object from a GEDCOM file.
534
539
 
@@ -538,21 +543,37 @@ class Gedcom5x():
538
543
  Returns:
539
544
  Gedcom: An instance of the Gedcom class.
540
545
  """
541
- records = Gedcom._records_from_file(filepath)
546
+ records = Gedcom5x._records_from_file(file_path)
542
547
 
543
- gedcom = Gedcom(records=records)
548
+ gedcom = Gedcom5x(records=records)
544
549
 
545
550
  return gedcom
546
551
 
547
- def merge_with_file(self, file_path: str) -> bool:
548
- """
549
- Adds records from a valid (``*``.ged) file to the current Genealogy
550
-
551
- Args:
552
- filepath (str): The path to the GEDCOM file.
552
+ def load_file(self,file_path: str) -> None:
553
+ records = Gedcom5x._records_from_file(file_path)
554
+ if records:
555
+ self.records.extend(records)
556
+ for record in self.records:
557
+ if record.tag == 'HEAD':
558
+ pass
559
+ #self.header = record
560
+ #version = record['GEDC']['VERS'].value
561
+ #if not str(version)[0:2] == str(self.version)[0:2]: #TODO Deal with no VERS
562
+ # raise ValueError(f'Wrong Version Current: {str(version)[0:2]}, new file: {str(self.version)[0:2]}')
563
+ if record.tag == 'INDI':
564
+ self._individuals.append(record)
565
+ if record.tag == 'SOUR' and record.level == 0:
566
+ self._sources.append(record)
567
+ if record.tag == 'REPO' and record.level == 0:
568
+ self._repositories.append(record)
569
+ if record.tag == 'FAM' and record.level == 0:
570
+ self._families.append(record)
571
+ if record.tag == 'OBJE' and record.level == 0:
572
+ self._objects.append(record)
573
+ if record.tag == 'SNOTE' and record.level == 0:
574
+ record.xref = record.value
575
+ self._snotes.append(record)
576
+ else:
577
+ raise ValueError()
553
578
 
554
- Returns:
555
- bool: Indicates if merge was successful.
556
- """
557
- return True
558
579
 
@@ -20,7 +20,7 @@ from .Exceptions import TagConversionError
20
20
  from .Event import Event,EventType,EventRole,EventRoleType
21
21
  from .Fact import Fact, FactType, FactQualifier
22
22
  from .Gedcom import Gedcom
23
- from .Gedcom5x import GedcomRecord
23
+ from .Gedcom5x import Gedcom5x, GedcomRecord
24
24
  from .Gender import Gender, GenderType
25
25
  from .Group import Group
26
26
  from .Identifier import Identifier, IdentifierType, make_uid, IdentifierList
@@ -340,7 +340,8 @@ class GedcomX:
340
340
  if agent.id is None:
341
341
  agent.id = Agent.default_id_generator()
342
342
  if self.agents.byId(agent.id):
343
- raise ValueError
343
+ pass #TODO Deal with duplicates
344
+ #raise ValueError
344
345
  print(f'Added Agent with id: {agent.id}')
345
346
  self.agents.append(agent)
346
347
 
@@ -380,7 +381,7 @@ class GedcomX:
380
381
  return json.dumps(gedcomx_json, indent=4)
381
382
 
382
383
  class Translater():
383
- def __init__(self,gedcom: Gedcom) -> None:
384
+ def __init__(self,gedcom: Gedcom5x) -> None:
384
385
  self.handlers = {}
385
386
  self.gedcom: Gedcom = gedcom
386
387
  self.gedcomx = GedcomX()
@@ -972,7 +973,7 @@ class Translater():
972
973
  elif record.parent.tag == 'TRAN':
973
974
  pass #TODO
974
975
  else:
975
- raise TagConversionError(record=record,levelstack=self.object_map)
976
+ convert_log.error(f"raise TagConversionError(record=record,levelstack=self.object_map")
976
977
 
977
978
  def handle_givn(self, record: GedcomRecord):
978
979
  if isinstance(self.object_map[record.level-1], Name):
@@ -1051,6 +1052,12 @@ class Translater():
1051
1052
  self.object_map[record.level-1].changeMessage = record.value
1052
1053
  else:
1053
1054
  self.object_map[record.level-1].changeMessage = self.object_map[record.level-1].changeMessage + '' + record.value
1055
+ elif isinstance(self.object_map[record.level-1], Note):
1056
+ gxobject = Note(text=Translater.clean_str(record.value))
1057
+ self.object_map[record.level-2].add_note(gxobject)
1058
+
1059
+ self.object_stack.append(gxobject)
1060
+ self.object_map[record.level] = gxobject
1054
1061
 
1055
1062
  else:
1056
1063
  raise ValueError(f"Could not handle 'NOTE' tag in record {record.describe()}, last stack object {type(self.object_map[record.level-1])}")
@@ -1306,7 +1313,7 @@ class Translater():
1306
1313
 
1307
1314
  self.object_map[record.level]._add_name_part(gxobject)
1308
1315
  else:
1309
- raise TagConversionError(record=record,levelstack=self.object_map)
1316
+ convert_log.error(f"raise TagConversionError(record=record,levelstack=self.object_map)")
1310
1317
 
1311
1318
  def handle_tran(self, record: GedcomRecord):
1312
1319
  pass
@@ -7,7 +7,7 @@ from collections.abc import Iterator
7
7
  import json
8
8
  from .Resource import Resource
9
9
  from .URI import URI
10
- from .Extensible.extensibles import ExtensibleEnum
10
+ from .ExtensibleEnum import ExtensibleEnum
11
11
 
12
12
  import secrets
13
13
  import string
@@ -0,0 +1,186 @@
1
+ # logging_hub.py
2
+ from __future__ import annotations
3
+ import logging
4
+ import contextvars
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass
7
+ from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
8
+ from typing import Dict, Optional
9
+
10
+ # Context key: which "channel" (log) is current?
11
+ _current_channel: contextvars.ContextVar[str] = contextvars.ContextVar("current_log_channel", default="default")
12
+
13
+ def get_current_channel() -> str:
14
+ return _current_channel.get()
15
+
16
+ def set_current_channel(name: str) -> None:
17
+ _current_channel.set(name)
18
+
19
+ class ChannelFilter(logging.Filter):
20
+ """Injects the current channel into every LogRecord."""
21
+ def filter(self, record: logging.LogRecord) -> bool:
22
+ record.log_channel = get_current_channel()
23
+ return True
24
+
25
+ class DispatchingHandler(logging.Handler):
26
+ """
27
+ Routes records to a per-channel handler (file/stream), based on LogRecord.log_channel
28
+ which is set by ChannelFilter.
29
+ """
30
+ def __init__(self):
31
+ super().__init__()
32
+ self._channel_handlers: Dict[str, logging.Handler] = {}
33
+ self._enabled: Dict[str, bool] = {}
34
+ self._default_channel = "default"
35
+
36
+ def set_default_channel(self, name: str) -> None:
37
+ self._default_channel = name
38
+
39
+ def add_channel(self, name: str, handler: logging.Handler, enabled: bool = True) -> None:
40
+ self._channel_handlers[name] = handler
41
+ self._enabled[name] = enabled
42
+
43
+ def enable(self, name: str) -> None:
44
+ self._enabled[name] = True
45
+
46
+ def disable(self, name: str) -> None:
47
+ self._enabled[name] = False
48
+
49
+ def remove_channel(self, name: str) -> None:
50
+ h = self._channel_handlers.pop(name, None)
51
+ self._enabled.pop(name, None)
52
+ if h:
53
+ try:
54
+ h.flush()
55
+ h.close()
56
+ except Exception:
57
+ pass
58
+
59
+ def has_channel(self, name: str) -> bool:
60
+ return name in self._channel_handlers
61
+
62
+ def emit(self, record: logging.LogRecord) -> None:
63
+ channel = getattr(record, "log_channel", None) or self._default_channel
64
+ handler = self._channel_handlers.get(channel) or self._channel_handlers.get(self._default_channel)
65
+ if not handler:
66
+ return # nothing to write to
67
+ if not self._enabled.get(channel, True):
68
+ return # channel muted
69
+ handler.emit(record)
70
+
71
+ @dataclass
72
+ class ChannelConfig:
73
+ name: str
74
+ path: Optional[str] = None
75
+ level: int = logging.INFO
76
+ fmt: str = "[%(asctime)s] %(levelname)s %(log_channel)s %(name)s: %(message)s"
77
+ datefmt: str = "%Y-%m-%d %H:%M:%S"
78
+ rotation: Optional[str] = None
79
+ # rotation options:
80
+ # None -> plain FileHandler
81
+ # "size:10MB:3" -> RotatingFileHandler(maxBytes=10MB, backupCount=3)
82
+ # "time:midnight:7" -> TimedRotatingFileHandler(when="midnight", backupCount=7)
83
+
84
+ class LoggingHub:
85
+ """
86
+ A centralized, context-aware logging hub.
87
+ Usage:
88
+ hub = LoggingHub()
89
+ hub.init_root() # do once at startup
90
+ hub.start_channel(ChannelConfig(name="default", path="logs/default.log"))
91
+ hub.set_current("default")
92
+
93
+ # In any module:
94
+ log = logging.getLogger("gedcomx")
95
+ log.info("hello") # goes to current channel
96
+
97
+ with hub.use("import-job-42"):
98
+ log.info("within job 42")
99
+ """
100
+ def __init__(self, root_logger_name: str = "gedcomx"):
101
+ self.root_name = root_logger_name
102
+ self._root = logging.getLogger(self.root_name)
103
+ self._dispatch = DispatchingHandler()
104
+ self._root.setLevel(logging.DEBUG) # Let handlers control final level
105
+
106
+ self._filter = ChannelFilter()
107
+ self._initialized = False
108
+
109
+ # -------- Initialization --------
110
+ def init_root(self) -> None:
111
+ if self._initialized:
112
+ return
113
+ # Clean existing handlers on the root logger (optional safety)
114
+ for h in list(self._root.handlers):
115
+ self._root.removeHandler(h)
116
+ self._root.addFilter(self._filter)
117
+ self._root.addHandler(self._dispatch)
118
+ self._initialized = True
119
+
120
+ # -------- Channel Management --------
121
+ def start_channel(self, cfg: ChannelConfig, make_current: bool = False, enabled: bool = True) -> None:
122
+ """Create/replace a channel with a file/rotating handler."""
123
+ handler: logging.Handler
124
+ formatter = logging.Formatter(cfg.fmt, datefmt=cfg.datefmt)
125
+
126
+ if cfg.path is None:
127
+ # StreamHandler to stdout if no path provided
128
+ handler = logging.StreamHandler()
129
+ else:
130
+ # Rotation options
131
+ if cfg.rotation and cfg.rotation.startswith("size:"):
132
+ # "size:10MB:3"
133
+ _, size_str, backups_str = cfg.rotation.split(":")
134
+ size_str = size_str.upper().replace("MB", "*1024*1024").replace("KB", "*1024")
135
+ max_bytes = int(eval(size_str)) # safe for the limited substitutions we made
136
+ backup_count = int(backups_str)
137
+ handler = RotatingFileHandler(cfg.path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8")
138
+ elif cfg.rotation and cfg.rotation.startswith("time:"):
139
+ # "time:midnight:7" or "time:H:24"
140
+ parts = cfg.rotation.split(":")
141
+ when = parts[1]
142
+ backup_count = int(parts[2]) if len(parts) > 2 else 7
143
+ handler = TimedRotatingFileHandler(cfg.path, when=when, backupCount=backup_count, encoding="utf-8", utc=False)
144
+ else:
145
+ handler = logging.FileHandler(cfg.path, encoding="utf-8")
146
+
147
+ handler.setLevel(cfg.level)
148
+ handler.setFormatter(formatter)
149
+
150
+ # Replace if exists
151
+ if self._dispatch.has_channel(cfg.name):
152
+ self._dispatch.remove_channel(cfg.name)
153
+ self._dispatch.add_channel(cfg.name, handler, enabled=enabled)
154
+
155
+ if make_current:
156
+ self.set_current(cfg.name)
157
+
158
+ def stop_channel(self, name: str) -> None:
159
+ self._dispatch.remove_channel(name)
160
+
161
+ def enable(self, name: str) -> None:
162
+ self._dispatch.enable(name)
163
+
164
+ def disable(self, name: str) -> None:
165
+ self._dispatch.disable(name)
166
+
167
+ def list_channels(self) -> Dict[str, bool]:
168
+ """Return dict of channel -> enabled?"""
169
+ return {name: enabled for name, enabled in self._dispatch._enabled.items()}
170
+
171
+ # -------- Current Channel --------
172
+ def set_current(self, name: str) -> None:
173
+ set_current_channel(name)
174
+
175
+ @contextmanager
176
+ def use(self, name: str):
177
+ """Temporarily switch to a channel within a with-block."""
178
+ token = _current_channel.set(name)
179
+ try:
180
+ yield
181
+ finally:
182
+ _current_channel.reset(token)
183
+
184
+ # -------- Utilities --------
185
+ def set_default_channel(self, name: str) -> None:
186
+ self._dispatch.set_default_channel(name)
@@ -7,6 +7,7 @@ from .Date import Date
7
7
  from .Document import Document
8
8
  from .Document import DocumentType
9
9
  from .EvidenceReference import EvidenceReference
10
+ from .ExtensibleEnum import ExtensibleEnum
10
11
  from .Event import Event
11
12
  from .Event import EventType
12
13
  from .Event import EventRole
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gedcom-x"
3
- version = "0.5.5"
3
+ version = "0.5.6"
4
4
  description = "Python implimentation of gedcom-x standard"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -1,14 +1,20 @@
1
1
  # setup.py
2
2
  from setuptools import setup, find_packages
3
3
 
4
+ with open("README.md", "r", encoding="utf-8") as f:
5
+ long_description = f.read()
6
+
4
7
  setup(
5
8
  name='gedcom-x',
6
- version='0.5.5',
9
+ version='0.5.6',
7
10
  packages=find_packages(),
8
- install_requires=[
11
+ description="A Python toolkit for working with GEDCOM-X",
12
+ long_description=long_description,
13
+ long_description_content_type="text/markdown",
14
+ #install_requires=[
9
15
  # List your project dependencies here, e.g.,
10
- 'ged4py',
11
- ],
16
+ # None,
17
+ #],
12
18
  entry_points={
13
19
  'console_scripts': [
14
20
  # Define command-line scripts if needed
gedcom_x-0.5.5/PKG-INFO DELETED
@@ -1,17 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: gedcom-x
3
- Version: 0.5.5
4
- Summary: Python implimentation of gedcom-x standard
5
- Author-email: "David J. Cartwright" <davidcartwright@hotmail.com>
6
- License: MIT
7
- Keywords: gedcom,gedcomx,ged
8
- Classifier: Development Status :: 4 - Beta
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Environment :: Console
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Operating System :: OS Independent
13
- Classifier: Topic :: Utilities
14
- Classifier: Topic :: Software Development :: User Interfaces
15
- Requires-Python: >=3.6
16
- Description-Content-Type: text/markdown
17
- Dynamic: requires-python
@@ -1,17 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: gedcom-x
3
- Version: 0.5.5
4
- Summary: Python implimentation of gedcom-x standard
5
- Author-email: "David J. Cartwright" <davidcartwright@hotmail.com>
6
- License: MIT
7
- Keywords: gedcom,gedcomx,ged
8
- Classifier: Development Status :: 4 - Beta
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Environment :: Console
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Operating System :: OS Independent
13
- Classifier: Topic :: Utilities
14
- Classifier: Topic :: Software Development :: User Interfaces
15
- Requires-Python: >=3.6
16
- Description-Content-Type: text/markdown
17
- Dynamic: requires-python
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes