gedcom-x 0.5.2__py3-none-any.whl → 0.5.6__py3-none-any.whl

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.
@@ -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
+
gedcomx/Fact.py CHANGED
@@ -22,7 +22,7 @@ from enum import Enum
22
22
 
23
23
  from collections.abc import Sized
24
24
 
25
- from ._Links import _Link, _LinkList
25
+ from .Extensions.rs10.rsLink import rsLink, _rsLinkList
26
26
 
27
27
 
28
28
  class FactType(Enum):
@@ -401,14 +401,14 @@ class Fact(Conclusion):
401
401
  sources: Optional[List[SourceReference]] = [],
402
402
  analysis: Optional[Resource | Document] = None,
403
403
  notes: Optional[List[Note]] = [],
404
- confidence: ConfidenceLevel = None,
405
- attribution: Attribution = None,
406
- type: FactType = None,
404
+ confidence: Optional[ConfidenceLevel] = None,
405
+ attribution: Optional[Attribution] = None,
406
+ type: Optional[FactType] = None,
407
407
  date: Optional[Date] = None,
408
408
  place: Optional[PlaceReference] = None,
409
409
  value: Optional[str] = None,
410
410
  qualifiers: Optional[List[FactQualifier]] = None,
411
- links: Optional[_LinkList] = None):
411
+ links: Optional[_rsLinkList] = None):
412
412
  super().__init__(id, lang, sources, analysis, notes, confidence, attribution, links=links)
413
413
  self.type = type
414
414
  self.date = date
@@ -433,12 +433,11 @@ class Fact(Conclusion):
433
433
  # Only add Relationship-specific fields
434
434
  fact_dict.update( {
435
435
  'type': self.type.value if self.type else None,
436
- 'date': self.date._prop_dict() if self.date else None,
436
+ 'date': self.date._as_dict_ if self.date else None,
437
437
  'place': self.place._as_dict_ if self.place else None,
438
438
  'value': self.value,
439
439
  'qualifiers': [q.value for q in self.qualifiers] if self.qualifiers else []
440
440
  })
441
-
442
441
 
443
442
  return Serialization.serialize_dict(fact_dict)
444
443
 
@@ -463,7 +462,7 @@ class Fact(Conclusion):
463
462
  if data.get('place') else None)
464
463
  value = data.get('value')
465
464
  qualifiers = [Qualifier._from_json_(q) for q in data.get('qualifiers', [])]
466
- links = _LinkList._from_json_(data.get('links')) if data.get('links') else None
465
+ links = _rsLinkList._from_json_(data.get('links')) if data.get('links') else None
467
466
 
468
467
  return cls(
469
468
  id=id_,
gedcomx/Gedcom.py CHANGED
@@ -1,419 +1,53 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
-
4
- import html
5
- import os
6
- from typing import List, Optional, Tuple
7
1
  import re
8
2
 
9
- BOM = '\ufeff'
10
-
11
- GEDCOM7_LINE_RE = re.compile(
12
- r"""^
13
- (?P<level>\d+) # Level
14
- (?:\s+@(?P<xref>[^@]+)@)? # Optional record identifier
15
- \s+(?P<tag>[A-Z0-9_-]+) # Tag
16
- (?:\s+(?P<value>.+))? # Optional value (may be XREF)
17
- $""",
18
- re.VERBOSE
19
- )
20
-
21
- XREF_RE = re.compile(r'^@[^@]+@$')
22
-
23
- # Add hash table for XREF of Zero Recrods?
24
-
25
- nonzero = '[1-9]'
26
- level = f'(?P<level>0|{nonzero}[0-9]*)'
27
- atsign = '@'
28
- underscore = '_'
29
- ucletter = '[A-Z]'
30
- tagchar = f'({ucletter}|[0-9]|{underscore})'
31
- xref = f'{atsign}({tagchar})+{atsign}'
32
- d = '\\ '
33
- stdtag = f'{ucletter}({tagchar})*'
34
- exttag = f'{underscore}({tagchar})+'
35
- tag = f'({stdtag}|{exttag})'
36
- voidptr = '@VOID@'
37
- pointer = f'(?P<pointer>{voidptr}|{xref})'
38
- nonat = '[\t -?A-\\U0010ffff]'
39
- noneol = '[\t -\\U0010ffff]'
40
- linestr = f'(?P<linestr>({nonat}|{atsign}{atsign})({noneol})*)'
41
- lineval = f'({pointer}|{linestr})'
42
- eol = '(\\\r(\\\n)?|\\\n)'
43
- line = f'{level}{d}((?P<xref>{xref}){d})?(?P<tag>{tag})({d}{lineval})?{eol}'
44
-
45
- class GedcomRecord():
46
-
47
- def __init__(self,line_num: Optional[int] =None,level: int =-1, tag='NONR', xref: Optional[str] = None, value: Optional[str] = None) -> None:
48
- self.line = line_num
49
- self._subRecords = []
50
- self.level = int(level)
51
- self.xref = xref
52
- self.pointer: bool = False
53
- self.tag = str(tag).strip()
54
- self.value = value
55
-
56
- self.parent = None
57
- self.root = None
58
-
59
- #if self.value and (self.value.endswith('@') and self.value.startswith('@')):
60
- # self.xref = self.value.replace('@','')
61
- # if level > 0:
62
- # self.pointer = True
63
-
64
- @property
65
- def _as_dict_(self):
66
- record_dict = {
67
- 'level':self.level,
68
- 'xref':self.xref,
69
- 'tag': self.tag,
70
- 'pointer': self.pointer,
71
- 'value': self.value,
72
- 'subrecords': [subrecord._as_dict_ for subrecord in self._subRecords]
73
- }
74
- return record_dict
75
-
76
- def addSubRecord(self, record):
77
- if record and record.level == self.level+1:
78
- record.parent = self
79
- self._subRecords.append(record)
80
- else:
81
- raise ValueError(f"SubRecord must be next level from this record (level:{self.level}, subRecord has level {record.level})")
82
-
83
- def recordOnly(self):
84
- return GedcomRecord(line_num=self.line,level=self.level,tag=self.tag,value=self.value)
85
-
86
- def dump(self):
87
- record_dump = f"Level: {self.level}, tag: {self.tag}, value: {self.value}, subRecords: {len(self._subRecords)}\n"
88
- for record in self._subRecords:
89
- record_dump += "\t" + record.dump() # Recursively call dump on sub_records and concatenate
90
- return record_dump
91
-
92
- def describe(self,subRecords: bool = False):
93
- level_str = '\t'* self.level
94
- description = f"Line {self.line}: {level_str} Level: {self.level}, tag: '{self.tag}', xref={self.xref} value: '{self.value}', subRecords: {len(self._subRecords)}"
95
- if subRecords:
96
- for subRecord in self.subRecords():
97
- description = description + '\n' + subRecord.describe(subRecords=True)
98
- return description
99
-
100
-
101
- def subRecord(self, tag):
102
- result = [record for record in self._subRecords if record.tag == tag]
103
- if len(result) == 0: return None
104
- return result
105
-
106
- def subRecords(self, tag: str = None):
107
- if not tag:
108
- return self._subRecords
109
- else:
110
- tags = tag.split("/", 1) # Split into first tag and the rest
111
-
112
- # Collect all records matching the first tag
113
- matching_records = [record for record in self._subRecords if record.tag == tags[0]]
114
-
115
- if not matching_records:
116
- return None # No matching records found for the first tag
117
-
118
- if len(tags) == 1:
119
- return matching_records # Return all matching records for the final tag
120
-
121
- # Recurse into each matching record's subRecords and collect results
122
- results = []
123
- for record in matching_records:
124
- sub_result = record.subRecords(tags[1])
125
- if sub_result:
126
- if isinstance(sub_result, list):
127
- results.extend(sub_result)
128
- else:
129
- results.append(sub_result)
130
-
131
- return results if results else None
132
-
133
- def __call__(self) -> None:
134
- return self.describe()
135
-
136
- def __iter__(self):
137
- return self._flatten_subrecords(self)
138
-
139
- def _flatten_subrecords(self, record):
140
- yield record
141
- for subrecord in record._subRecords:
142
- yield from self._flatten_subrecords(subrecord)
143
-
144
3
  class Gedcom():
145
- """
146
- Object representing a Genealogy in legacy GEDCOM 5.x / 7 format.
4
+ def __init__(self) -> None:
5
+ pass
147
6
 
148
- Parameters
149
- ----------
150
- records : List[GedcomReord]
151
- List of GedcomRecords to initialize the genealogy with
152
- filepath : str
153
- path to a GEDCOM (``*``.ged), if provided object will read, parse and initialize with records in the file.
154
-
155
- Note
156
- ----
157
- **file_path** takes precidence over **records**.
158
- If no arguments are provided, Gedcom Object will initialize with no records.
159
-
160
-
161
- """
162
- _top_level_tags = ['INDI', 'FAM', 'OBJE', 'SOUR', 'REPO', 'NOTE', 'HEAD','SNOTE']
163
-
164
- def __init__(self, records: Optional[List[GedcomRecord]] = None,filepath: str = None) -> None:
165
- if filepath:
166
- self.records = self._records_from_file(filepath)
167
- elif records:
168
- self.records: List[GedcomRecord] = records if records else []
169
-
170
-
171
-
172
- self._sources = []
173
- self._repositories = []
174
- self._individuals = []
175
- self._families = []
176
- self._objects = []
177
- self._snotes = []
178
-
179
- if self.records:
180
- for record in self.records:
181
- if record.tag == 'INDI':
182
-
183
- self._individuals.append(record)
184
- if record.tag == 'SOUR' and record.level == 0:
185
-
186
- self._sources.append(record)
187
- if record.tag == 'REPO' and record.level == 0:
188
- print(record.describe())
189
-
190
- self._repositories.append(record)
191
- if record.tag == 'FAM' and record.level == 0:
192
-
193
- self._families.append(record)
194
- if record.tag == 'OBJE' and record.level == 0:
195
-
196
- self._objects.append(record)
197
- if record.tag == 'SNOTE' and record.level == 0:
198
-
199
- record.xref = record.value
200
- self._snotes.append(record)
201
-
202
-
203
- # =========================================================
204
- # 2. PROPERTY ACCESSORS (GETTERS & SETTERS)
205
- # =========================================================
206
-
207
- @property
208
- def json(self):
209
- import json
210
- return json.dumps({'Individuals': [indi._as_dict_ for indi in self._individuals]},indent=4)
211
-
212
- def stats(self):
213
- def print_table(pairs):
214
-
215
- # Calculate the width of the columns
216
- name_width = max(len(name) for name, _ in pairs)
217
- value_width = max(len(str(value)) for _, value in pairs)
218
-
219
- # Print the header
220
- print('GEDCOM Import Results')
221
- header = f"{'Type'.ljust(name_width)} | {'Count'.ljust(value_width)}"
222
- print('-' * len(header))
223
- print(header)
224
- print('-' * len(header))
225
-
226
- # Print each pair in the table
227
- for name, value in pairs:
228
- print(f"{name.ljust(name_width)} | {str(value).ljust(value_width)}")
229
-
230
- imports_stats = [
231
- ('Top Level Records', len(self.records)),
232
- ('Individuals', len(self.individuals)),
233
- ('Family Group Records', len(self.families)),
234
- ('Repositories', len(self.repositories)),
235
- ('Sources', len(self.sources)),
236
- ('Objects', len(self.objects))
237
- ]
238
-
239
- print_table(imports_stats)
240
-
241
- @property
242
- def sources(self) -> List[GedcomRecord]:
243
- return self._sources
244
-
245
- @sources.setter
246
- def sources(self, value: List[GedcomRecord]):
247
- if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
248
- raise ValueError("sources must be a list of GedcomRecord objects.")
249
- self._sources = value
250
-
251
- @property
252
- def repositories(self) -> List[GedcomRecord]:
253
- """
254
- List of **REPO** records found in the Genealogy
255
- """
256
- return self._repositories
257
-
258
- @repositories.setter
259
- def repositories(self, value: List[GedcomRecord]):
260
- if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
261
- raise ValueError("repositories must be a list of GedcomRecord objects.")
262
- self._repositories = value
263
-
264
- @property
265
- def individuals(self) -> List[GedcomRecord]:
266
- return self._individuals
267
-
268
- @individuals.setter
269
- def individuals(self, value: List[GedcomRecord]):
270
- if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
271
- raise ValueError("individuals must be a list of GedcomRecord objects.")
272
- self._individuals = value
273
-
274
- @property
275
- def families(self) -> List[GedcomRecord]:
276
- return self._families
277
-
278
- @families.setter
279
- def families(self, value: List[GedcomRecord]):
280
- if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
281
- raise ValueError("families must be a list of GedcomRecord objects.")
282
- self._families = value
283
-
284
- @property
285
- def objects(self) -> List[GedcomRecord]:
286
- return self._objects
287
-
288
- @objects.setter
289
- def objects(self, value: List[GedcomRecord]):
290
- if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
291
- raise ValueError("objects must be a list of GedcomRecord objects.")
292
- self._objects = value
293
-
294
-
295
-
296
- def write(self) -> bool:
7
+ @staticmethod
8
+ def read_gedcom_version(filepath: str) -> str | None:
297
9
  """
298
- Method placeholder for writing GEDCOM files.
10
+ Reads only the HEAD section of a GEDCOM file and returns the GEDCOM standard version.
11
+ Looks specifically for HEAD → GEDC → VERS.
299
12
 
300
- Raises
301
- ------
302
- NotImplementedError
303
- writing to legacy GEDCOM file is not currently implimented.
13
+ Returns:
14
+ str: GEDCOM version (e.g., "5.5.1" or "7.0.0"), or None if not found.
304
15
  """
305
- raise NotImplementedError("Writing of GEDCOM files is not implemented.")
306
-
307
- @staticmethod
308
- def _records_from_file(filepath: str) -> List[GedcomRecord]:
309
- def parse_gedcom7_line(line: str) -> Optional[Tuple[int, Optional[str], str, Optional[str], Optional[str]]]:
310
- """
311
- Parse a GEDCOM 7 line into: level, xref_id (record), tag, value, xref_value (if value is an @X@)
312
-
313
- Returns:
314
- (level, xref_id, tag, value, xref_value)
315
- """
316
- match = GEDCOM7_LINE_RE.match(line.strip())
317
- if not match:
318
- return None
319
-
320
- level = int(match.group("level"))
321
- xref_id = match.group("xref")
322
- tag = match.group("tag")
323
- value = match.group("value")
324
- if value == 'None': value = None
325
- xref_value = value.strip("@") if value and XREF_RE.match(value.strip()) else None
326
-
327
- return level, xref_id, tag, value, xref_value
328
- extension = '.ged'
329
-
330
- if not os.path.exists(filepath):
331
- print(f"File does not exist: {filepath}")
332
- raise FileNotFoundError
333
- elif not filepath.lower().endswith(extension.lower()):
334
- print(f"File does not have the correct extension: {filepath}")
335
- raise Exception("File does not appear to be a GEDCOM")
336
-
337
- print("Reading from GEDCOM file")
338
- with open(filepath, 'r', encoding='utf-8') as file:
339
- lines = [line.strip() for line in file]
340
-
341
- records = []
342
- record_map = {0: None, 1: None, 2: None, 3: None, 4: None, 5: None}
343
-
344
- for l, line in enumerate(lines):
345
- if line.startswith(BOM):
346
- line = line.lstrip(BOM)
347
- line = html.unescape(line).replace('&quot;', '')
348
-
349
- if line.strip() == '':
16
+ version = None
17
+ inside_head = False
18
+ inside_gedc = False
19
+
20
+ with open(filepath, "r", encoding="utf-8") as f:
21
+ for line in f:
22
+ parts = line.strip().split(maxsplit=2)
23
+ if not parts:
350
24
  continue
351
25
 
352
- level, tag, value = '', '', ''
353
-
354
- # Split the line into the first two columns and the rest
355
- parts = line.split(maxsplit=2)
356
- if len(parts) == 3:
357
- level, col2, col3 = parts
358
-
359
- if col3 in Gedcom._top_level_tags:
360
- tag = col3
361
- value = col2
362
- else:
363
- tag = col2
364
- value = col3
365
-
366
- else:
367
- level, tag = parts
368
-
369
- level, xref, tag, value, xref_value = parse_gedcom7_line(line)
370
-
371
- if xref is None and xref_value is not None:
372
- xref = xref_value
373
- # print(l, level, xref, tag, value, xref_value)
374
-
375
- level = int(level)
376
-
377
- new_record = GedcomRecord(line_num=l + 1, level=level, tag=tag, xref=xref,value=value)
378
-
379
-
380
- if level == 0:
381
- records.append(new_record)
382
- else:
383
- new_record.root = record_map[0]
384
- new_record.parent = record_map[int(level) - 1]
385
- record_map[int(level) - 1].addSubRecord(new_record)
386
- record_map[int(level)] = new_record
387
-
388
-
389
- return records if records else None
390
-
391
- @staticmethod
392
- def fromFile(filepath: str) -> 'Gedcom':
393
- """
394
- Static method to create a Gedcom object from a GEDCOM file.
26
+ level = int(parts[0])
27
+ tag = parts[1] if len(parts) > 1 else ""
28
+ value = parts[2] if len(parts) > 2 else None
395
29
 
396
- Args:
397
- filepath (str): The path to the GEDCOM file.
30
+ # Enter HEAD
31
+ if level == 0 and tag == "HEAD":
32
+ inside_head = True
33
+ continue
398
34
 
399
- Returns:
400
- Gedcom: An instance of the Gedcom class.
401
- """
402
- records = Gedcom._records_from_file(filepath)
403
-
404
- gedcom = Gedcom(records=records)
35
+ # Leave HEAD block
36
+ if inside_head and level == 0:
37
+ break
405
38
 
406
- return gedcom
407
-
408
- def merge_with_file(self, file_path: str) -> bool:
409
- """
410
- Adds records from a valid (``*``.ged) file to the current Genealogy
39
+ # Inside HEAD, look for GEDC
40
+ if inside_head and level == 1 and tag == "GEDC":
41
+ inside_gedc = True
42
+ continue
411
43
 
412
- Args:
413
- filepath (str): The path to the GEDCOM file.
44
+ # If we drop back to level 1 (but not GEDC), stop looking inside GEDC
45
+ if inside_gedc and level == 1:
46
+ inside_gedc = False
414
47
 
415
- Returns:
416
- bool: Indicates if merge was successful.
417
- """
418
- return True
48
+ # Inside GEDC, look for VERS
49
+ if inside_gedc and tag == "VERS":
50
+ version = value
51
+ break
419
52
 
53
+ return version