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.
gedcomx/LoggingHub.py ADDED
@@ -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)
gedcomx/Mutations.py ADDED
@@ -0,0 +1,228 @@
1
+ from .Gedcom5x import GedcomRecord
2
+ from .Fact import Fact, FactType
3
+ from .Event import Event, EventType
4
+
5
+ fact_event_table = {
6
+ # Person Fact / Event Types
7
+ "ADOP": {
8
+ "Fact": FactType.AdoptiveParent,
9
+ "Event": EventType.Adoption,
10
+ },
11
+ "CHR": {
12
+ "Fact": FactType.AdultChristening,
13
+ "Event": EventType.AdultChristening,
14
+ },
15
+ "EVEN": {
16
+ "Fact": FactType.Amnesty,
17
+ # no Event
18
+ },
19
+ "BAPM": {
20
+ "Fact": FactType.Baptism,
21
+ "Event": EventType.Baptism,
22
+ },
23
+ "BARM": {
24
+ "Fact": FactType.BarMitzvah,
25
+ "Event": EventType.BarMitzvah,
26
+ },
27
+ "BASM": {
28
+ "Fact": FactType.BatMitzvah,
29
+ "Event": EventType.BatMitzvah,
30
+ },
31
+ "BIRT": {
32
+ "Fact": FactType.Birth,
33
+ "Event": EventType.Birth,
34
+ },
35
+ "BIRT, CHR": {
36
+ "Fact": FactType.Birth,
37
+ "Event": EventType.Birth,
38
+ },
39
+ "BLES": {
40
+ "Fact": FactType.Blessing,
41
+ "Event": EventType.Blessing,
42
+ },
43
+ "BURI": {
44
+ "Fact": FactType.Burial,
45
+ "Event": EventType.Burial,
46
+ },
47
+ "CAST": {
48
+ "Fact": FactType.Caste,
49
+ # no Event
50
+ },
51
+ "CENS": {
52
+ "Fact": FactType.Census,
53
+ "Event": EventType.Census,
54
+ },
55
+ "CIRC": {
56
+ "Fact": FactType.Circumcision,
57
+ "Event": EventType.Circumcision,
58
+ },
59
+ "CONF": {
60
+ "Fact": FactType.Confirmation,
61
+ "Event": EventType.Confirmation,
62
+ },
63
+ "CREM": {
64
+ "Fact": FactType.Cremation,
65
+ "Event": EventType.Cremation,
66
+ },
67
+ "DEAT": {
68
+ "Fact": FactType.Death,
69
+ "Event": EventType.Death,
70
+ },
71
+ "EDUC": {
72
+ "Fact": FactType.Education,
73
+ "Event": EventType.Education,
74
+ },
75
+ "EMIG": {
76
+ "Fact": FactType.Emigration,
77
+ "Event": EventType.Emigration,
78
+ },
79
+ "FCOM": {
80
+ "Fact": FactType.FirstCommunion,
81
+ "Event": EventType.FirstCommunion,
82
+ },
83
+ "GRAD": {
84
+ "Fact": FactType.Graduation,
85
+ # no Event
86
+ },
87
+ "IMMI": {
88
+ "Fact": FactType.Immigration,
89
+ "Event": EventType.Immigration,
90
+ },
91
+ "MIL": {
92
+ "Fact": FactType.MilitaryService,
93
+ # no Event
94
+ },
95
+ "NATI": {
96
+ "Fact": FactType.Nationality,
97
+ # no Event
98
+ },
99
+ "NATU": {
100
+ "Fact": FactType.Naturalization,
101
+ "Event": EventType.Naturalization,
102
+ },
103
+ "OCCU": {
104
+ "Fact": FactType.Occupation,
105
+ # no Event
106
+ },
107
+ "ORDN": {
108
+ "Fact": FactType.Ordination,
109
+ "Event": EventType.Ordination,
110
+ },
111
+ "DSCR": {
112
+ "Fact": FactType.PhysicalDescription,
113
+ # no Event
114
+ },
115
+ "PROB": {
116
+ "Fact": FactType.Probate,
117
+ # no Event
118
+ },
119
+ "PROP": {
120
+ "Fact": FactType.Property,
121
+ # no Event
122
+ },
123
+ "RELI": {
124
+ "Fact": FactType.Religion,
125
+ # no Event
126
+ },
127
+ "RESI": {
128
+ "Fact": FactType.Residence,
129
+ # no Event
130
+ },
131
+ "WILL": {
132
+ "Fact": FactType.Will,
133
+ # no Event
134
+ },
135
+
136
+ # Couple Relationship Fact / Event Types
137
+ "ANUL": {
138
+ "Fact": FactType.Annulment,
139
+ "Event": EventType.Annulment,
140
+ },
141
+ "DIV": {
142
+ "Fact": FactType.Divorce,
143
+ "Event": EventType.Divorce,
144
+ },
145
+ "DIVF": {
146
+ "Fact": FactType.DivorceFiling,
147
+ "Event": EventType.DivorceFiling,
148
+ },
149
+ "ENGA": {
150
+ "Fact": FactType.Engagement,
151
+ "Event": EventType.Engagement,
152
+ },
153
+ "MARR": {
154
+ "Fact": FactType.Marriage,
155
+ "Event": EventType.Marriage,
156
+ },
157
+ "MARB": {
158
+ "Fact": FactType.MarriageBanns,
159
+ # no Event
160
+ },
161
+ "MARC": {
162
+ "Fact": FactType.MarriageContract,
163
+ # no Event
164
+ },
165
+ "MARL": {
166
+ "Fact": FactType.MarriageLicense,
167
+ # no Event
168
+ },
169
+ "MARS":{
170
+ "Fact":EventType.MarriageSettlment
171
+
172
+ },
173
+ "SEPA": {
174
+ "Fact": FactType.Separation,
175
+ # no Event
176
+ },
177
+
178
+ }
179
+
180
+ class GedcomXObject:
181
+ def __init__(self,record: GedcomRecord | None = None) -> None:
182
+ self.created_with_tag: str = record.tag if record and isinstance(record, GedcomRecord) else None
183
+ self.created_at_level: int = record.level if record and isinstance(record, GedcomRecord) else None
184
+ self.created_at_line_number: int = record.line_number if record and isinstance(record, GedcomRecord) else None
185
+
186
+ class GedcomXSourceOrDocument(GedcomXObject):
187
+ def __init__(self,record: GedcomRecord | None = None) -> None:
188
+ super().__init__(record)
189
+ self.title: str = None
190
+ self.citation: str = None
191
+ self.page: str = None
192
+ self.contributor: str = None
193
+ self.publisher: str = None
194
+ self.rights: str = None
195
+ self.url: str = None
196
+ self.medium: str = None
197
+ self.type: str = None
198
+ self.format: str = None
199
+ self.created: str = None
200
+ self.modified: str = None
201
+ self.language: str = None
202
+ self.relation: str = None
203
+ self.identifier: str = None
204
+ self.description: str = None
205
+
206
+ class GedcomXEventOrFact(GedcomXObject):
207
+ def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
208
+ super().__init__(record)
209
+ if record.tag in fact_event_table.keys():
210
+
211
+ if 'Fact' in fact_event_table[record.tag].keys():
212
+ obj = Fact(type=fact_event_table[record.tag]['Fact'])
213
+ return obj
214
+ elif 'Event' in fact_event_table[record.tag].keys():
215
+ obj = Event(type=fact_event_table[record.tag]['Fact'])
216
+ else:
217
+ raise ValueError
218
+ else:
219
+ raise ValueError(f"{record.tag} not found in map")
220
+
221
+ class GedcomXRelationshipBuilder(GedcomXObject):
222
+ def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
223
+ last_relationship = object_stack.get('lastrelationship',None)
224
+ last_relationship_data = object_stack.get('lastrelationshipdata',None)
225
+ if not isinstance(last_relationship_data,dict):
226
+ last_relationship_data = None
227
+
228
+
gedcomx/Name.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from enum import Enum
2
2
  from typing import List,Optional
3
+ from typing_extensions import Self
3
4
 
4
5
  from .Attribution import Attribution
5
6
  from .Conclusion import Conclusion, ConfidenceLevel
@@ -220,6 +221,11 @@ class Name(Conclusion):
220
221
 
221
222
  return name_as_dict
222
223
 
224
+ class QuickName():
225
+ def __new__(cls,name: str) -> Name:
226
+ obj = Name(nameForms=[NameForm(fullText=name)])
227
+ return obj
228
+
223
229
  def ensure_list(val):
224
230
  if val is None:
225
231
  return []
gedcomx/Person.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from enum import Enum
2
2
  from typing import List, Optional
3
+ from urllib.parse import urljoin
3
4
 
4
5
  from .Attribution import Attribution
5
6
  from .Conclusion import ConfidenceLevel
@@ -8,13 +9,14 @@ from .EvidenceReference import EvidenceReference
8
9
  from .Fact import Fact, FactType
9
10
  from .Gender import Gender, GenderType
10
11
  from .Identifier import IdentifierList
11
- from .Name import Name
12
+ from .Name import Name, QuickName
12
13
  from .Note import Note
13
14
  from .SourceReference import SourceReference
15
+ from .Serialization import Serialization
14
16
  from .Subject import Subject
15
17
  from .Resource import Resource
16
18
  from collections.abc import Sized
17
- from ._Links import _LinkList
19
+ from .Extensions.rs10.rsLink import _rsLinkList
18
20
 
19
21
  class Person(Subject):
20
22
  """A person in the system.
@@ -47,9 +49,10 @@ class Person(Subject):
47
49
  names: Optional[List[Name]] = None,
48
50
  facts: Optional[List[Fact]] = None,
49
51
  living: Optional[bool] = False,
50
- links: Optional[_LinkList] = None) -> None:
52
+ links: Optional[_rsLinkList] = None,
53
+ uri: Optional[Resource] = None) -> None:
51
54
  # Call superclass initializer if needed
52
- super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers,links=links)
55
+ super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers,links=links,uri=uri)
53
56
 
54
57
  # Initialize mutable attributes to empty lists if None
55
58
  self.sources = sources if sources is not None else []
@@ -108,103 +111,59 @@ class Person(Subject):
108
111
 
109
112
  @property
110
113
  def _as_dict_(self):
111
- def _serialize(value):
112
- if isinstance(value, (str, int, float, bool, type(None))):
113
- return value
114
- elif isinstance(value, dict):
115
- return {k: _serialize(v) for k, v in value.items()}
116
- elif isinstance(value, (list, tuple, set)):
117
- return [_serialize(v) for v in value]
118
- elif hasattr(value, "_as_dict_"):
119
- return value._as_dict_
120
- else:
121
- return str(value) # fallback for unknown objects
122
-
123
- subject_fields = super()._as_dict_ # Start with base class fields
114
+ type_as_dict = super()._as_dict_ # Start with base class fields
124
115
  # Only add Relationship-specific fields
125
- subject_fields.update({
116
+ type_as_dict.update({
126
117
  'private': self.private,
127
118
  'living': self.living,
128
119
  'gender': self.gender._as_dict_ if self.gender else None,
129
120
  'names': [name._as_dict_ for name in self.names],
130
- 'facts': [fact for fact in self.facts],
131
- 'uri': 'uri: ' + self.uri.value
121
+ 'facts': [fact._as_dict_ for fact in self.facts],
122
+ 'uri': self.uri._as_dict_ if self.uri else None
132
123
 
133
124
  })
134
125
 
135
- # Serialize and exclude None values
136
- for key, value in subject_fields.items():
137
- if value is not None:
138
- subject_fields[key] = _serialize(value)
139
-
140
- # 3) merge and filter out None *at the top level*
141
- return {
142
- k: v
143
- for k, v in subject_fields.items()
144
- if v is not None and not (isinstance(v, Sized) and len(v) == 0)
145
- }
146
-
147
-
126
+ return Serialization.serialize_dict(type_as_dict)
127
+
148
128
  @classmethod
149
129
  def _from_json_(cls, data: dict):
150
130
  """
151
131
  Create a Person instance from a JSON-dict (already parsed).
152
132
  """
153
-
154
-
155
- # Basic scalar fields
156
- id_ = data.get('id')
157
- lang = data.get('lang', 'en')
158
- private = data.get('private', False)
159
- extracted = data.get('extracted', False)
160
-
161
- living = data.get('extracted', False)
162
-
163
- # Complex singletons
164
- analysis = Resource._from_json_(data['analysis']) if data.get('analysis') else None
165
- attribution = Attribution._from_json_(data['attribution']) if data.get('attribution') else None
166
- confidence = ConfidenceLevel_from_json_(data['confidence']) if data.get('confidence') else None
167
-
168
- # Gender (string or dict depending on your JSON)
169
- gender_json = data.get('gender')
170
- if isinstance(gender_json, dict):
171
- gender = Gender._from_json_(gender_json)
172
- else:
173
- # if it's just the enum value
174
- gender = Gender(type=GenderType(gender_json)) if gender_json else Gender(type=GenderType.Unknown)
175
-
176
-
177
- sources = [SourceReference._from_json_(o) for o in data.get('sources')] if data.get('sources') else None
178
- notes = [Note._from_json_(o) for o in data.get('notes')] if data.get('notes') else None
179
- evidence = [EvidenceReference._from_json_(o) for o in data.get('evidence')] if data.get('evidence') else None
180
- media = [SourceReference._from_json_(o) for o in data.get('media')] if data.get('media') else None
181
- identifiers = IdentifierList._from_json_(data.get('identifiers')) if data.get('identifiers') else None
182
- names = [Name._from_json_(o) for o in data.get('names')] if data.get('names') else None
183
- facts = [Fact._from_json_(o) for o in data.get('facts')] if data.get('facts') else None
184
- links = _LinkList._from_json_(data.get('links')) if data.get('links') else None
185
-
186
- # Build the instance
187
- inst = cls(
188
- id = id_,
189
- lang = lang,
190
- sources = sources,
191
- analysis = analysis,
192
- notes = notes,
193
- confidence = confidence,
194
- attribution = attribution,
195
- extracted = extracted,
196
- evidence = evidence,
197
- media = media,
198
- identifiers = identifiers,
199
- private = private,
200
- gender = gender,
201
- names = names,
202
- facts = facts,
203
- living = living,
204
- links = links
205
- )
206
-
207
- return inst
133
+ return Serialization.deserialize(data, Person)
134
+
135
+ @classmethod
136
+ def from_familysearch(cls, pid: str, token: str, *, base_url: Optional[str] = None):
137
+ """
138
+ Fetch a single person by PID from FamilySearch and return a Person.
139
+ - pid: e.g. "KPHP-4B4"
140
+ - token: OAuth2 access token (Bearer)
141
+ - base_url: override API base (defaults to settings.FS_API_BASE or prod)
142
+ """
143
+ import requests
144
+ default_base = "https://apibeta.familysearch.org/platform/"
145
+
146
+ base = (base_url or default_base).rstrip("/") + "/"
147
+ url = urljoin(base, f"tree/persons/{pid}")
148
+
149
+ headers = {
150
+ "Accept": "application/json",
151
+ "Authorization": f"Bearer {token}",
152
+ }
153
+
154
+ resp = requests.get(url, headers=headers, timeout=(5, 30))
155
+ resp.raise_for_status()
156
+
157
+ payload = resp.json()
158
+ persons = payload.get("persons") or []
159
+
160
+ # Prefer exact match on PID, else first item if present
161
+ person_json = next((p for p in persons if (p.get("id") == pid)), None) or (persons[0] if persons else None)
162
+ if not person_json:
163
+ raise ValueError(f"FamilySearch returned no person for PID {pid}")
164
+
165
+ # Keep your existing deserialization helper
166
+ return Serialization.deserialize(person_json, Person)
208
167
 
209
168
  class QuickPerson:
210
169
  """A GedcomX Person Data Type created with basic information.
@@ -221,7 +180,7 @@ class QuickPerson:
221
180
  Raises:
222
181
  ValueError: If `id` is not a valid UUID.
223
182
  """
224
- def __new__(cls, name: Optional[str], dob: Optional[str], dod: Optional[str]):
183
+ def __new__(cls, name: str, dob: Optional[str] = None, dod: Optional[str] = None):
225
184
  # Build facts from args
226
185
  facts = []
227
186
  if dob:
@@ -230,4 +189,4 @@ class QuickPerson:
230
189
  facts.append(Fact(type=FactType.Death, date=Date(original=dod)))
231
190
 
232
191
  # Return the different class instance
233
- return Person(facts=facts, names=[name] if name else None)
192
+ return Person(facts=facts, names=[QuickName(name=name)] if name else None)