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.
- gedcom_x-0.5.6/PKG-INFO +144 -0
- gedcom_x-0.5.6/README.md +126 -0
- gedcom_x-0.5.6/gedcom_x.egg-info/PKG-INFO +144 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcom_x.egg-info/SOURCES.txt +3 -0
- gedcom_x-0.5.6/gedcomx/ExtensibleEnum.py +183 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Gedcom5x.py +43 -22
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/GedcomX.py +12 -5
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Identifier.py +1 -1
- gedcom_x-0.5.6/gedcomx/LoggingHub.py +186 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/__init__.py +1 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/pyproject.toml +1 -1
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/setup.py +10 -4
- gedcom_x-0.5.5/PKG-INFO +0 -17
- gedcom_x-0.5.5/gedcom_x.egg-info/PKG-INFO +0 -17
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcom_x.egg-info/dependency_links.txt +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcom_x.egg-info/top_level.txt +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Address.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Agent.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Attribution.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Conclusion.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Coverage.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Date.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Document.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Event.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/EvidenceReference.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Exceptions.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Fact.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Gedcom.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Gender.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Group.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Logging.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Mutations.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Name.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Note.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/OnlineAccount.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Person.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/PlaceDescription.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/PlaceReference.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Qualifier.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Relationship.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Resource.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Serialization.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/SourceCitation.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/SourceDescription.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/SourceReference.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Subject.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/TextValue.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/TopLevelTypeCollection.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Translation.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/URI.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/gedcomx/Zip.py +0 -0
- {gedcom_x-0.5.5 → gedcom_x-0.5.6}/setup.cfg +0 -0
gedcom_x-0.5.6/PKG-INFO
ADDED
@@ -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>
|
gedcom_x-0.5.6/README.md
ADDED
@@ -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(
|
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(
|
470
|
-
print(f"File does not exist: {
|
474
|
+
if not os.path.exists(file_path):
|
475
|
+
print(f"File does not exist: {file_path}")
|
471
476
|
raise FileNotFoundError
|
472
|
-
elif not
|
473
|
-
print(f"File does not have the correct extension: {
|
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(
|
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(
|
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 =
|
546
|
+
records = Gedcom5x._records_from_file(file_path)
|
542
547
|
|
543
|
-
gedcom =
|
548
|
+
gedcom = Gedcom5x(records=records)
|
544
549
|
|
545
550
|
return gedcom
|
546
551
|
|
547
|
-
def
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
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
|
-
|
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:
|
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
|
@@ -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,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.
|
9
|
+
version='0.5.6',
|
7
10
|
packages=find_packages(),
|
8
|
-
|
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
|
-
|
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
|
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
|