followthemoney.property
1from banal import is_mapping, as_bool 2from typing import TYPE_CHECKING, cast, Any, List, Optional, TypedDict 3 4from followthemoney.exc import InvalidModel 5from followthemoney.types import registry 6from followthemoney.rdf import NS, URIRef 7from followthemoney.util import gettext, get_entity_id 8 9if TYPE_CHECKING: 10 from followthemoney.schema import Schema 11 from followthemoney.model import Model 12 13 14class ReverseSpec(TypedDict, total=False): 15 name: str 16 label: Optional[str] 17 hidden: Optional[bool] 18 19 20class PropertyDict(TypedDict, total=False): 21 label: Optional[str] 22 description: Optional[str] 23 type: Optional[str] 24 hidden: Optional[bool] 25 matchable: Optional[bool] 26 deprecated: Optional[bool] 27 # stub: Optional[bool] 28 rdf: Optional[str] 29 range: Optional[str] 30 format: Optional[str] 31 32 33class PropertySpec(PropertyDict): 34 reverse: ReverseSpec 35 36 37class PropertyToDict(PropertyDict, total=False): 38 name: str 39 qname: str 40 reverse: Optional[str] 41 stub: Optional[bool] 42 43 44class Property: 45 """A definition of a value-holding field on a schema. Properties define 46 the field type and other possible constraints. They also serve as entity 47 to entity references.""" 48 49 __slots__ = ( 50 "model", 51 "schema", 52 "name", 53 "qname", 54 "_label", 55 "_hash", 56 "_description", 57 "hidden", 58 "type", 59 "matchable", 60 "deprecated", 61 "_range", 62 "format", 63 "range", 64 "stub", 65 "_reverse", 66 "reverse", 67 "uri", 68 ) 69 70 #: Invalid property names. 71 RESERVED = ["id", "caption", "schema", "schemata"] 72 73 def __init__(self, schema: "Schema", name: str, data: PropertySpec) -> None: 74 #: The schema which the property is defined for. This is always the 75 #: most abstract schema that has this property, not the possible 76 #: child schemata that inherit it. 77 self.schema = schema 78 79 #: Machine-readable name for this property. 80 self.name = name 81 82 #: Qualified property name, which also includes the schema name. 83 self.qname = "%s:%s" % (schema.name, self.name) 84 if self.name in self.RESERVED: 85 raise InvalidModel("Reserved name: %s" % self.name) 86 87 self._hash = hash("<Property(%r)>" % self.qname) 88 89 self._label = data.get("label", name) 90 self._description = data.get("description") 91 92 #: This property is deprecated and should not be used. 93 self.deprecated = as_bool(data.get("deprecated", False)) 94 95 #: This property should not be shown or mentioned in the user interface. 96 self.hidden = as_bool(data.get("hidden")) 97 98 type_ = data.get("type", "string") 99 if type_ is None or type_ not in registry.named: 100 raise InvalidModel("Invalid type: %s" % type_) 101 102 #: The data type for this property. 103 self.type = registry[type_] 104 105 #: Whether this property should be used for matching and cross-referencing. 106 _matchable = data.get("matchable") 107 if _matchable is not None: 108 self.matchable = as_bool(data.get("matchable")) 109 else: 110 self.matchable = self.type.matchable 111 112 #: If the property is of type ``entity``, the set of valid schema to be added 113 #: in this property can be constrained. For example, an asset can be owned, 114 #: but a person cannot be owned. 115 self._range = data.get("range") 116 self.range: Optional["Schema"] = None 117 118 #: If the property is of type ``identifier``, a more narrow definition of the 119 #: identifier format can be provided. For example, LEI, INN or IBAN codes 120 #: can be automatically validated. 121 self.format: Optional[str] = data.get("format") 122 123 #: When a property points to another schema, a reverse property is added for 124 #: various administrative reasons. These properties are, however, not real 125 #: and cannot be written to. That's why they are marked as stubs and adding 126 #: values to them will raise an exception. 127 self.stub: Optional[bool] = False 128 129 #: When a property points to another schema, a stub reverse property is 130 #: added as a place to store metadata to help display the link in inverted 131 #: views. 132 self._reverse = data.get("reverse") 133 self.reverse: Optional["Property"] = None 134 135 #: RDF term for this property (i.e. the predicate URI). 136 self.uri = URIRef(cast(str, data.get("rdf", NS[self.qname]))) 137 138 def generate(self, model: "Model") -> None: 139 """Setup method used when loading the model in order to build out the reverse 140 links of the property.""" 141 model.properties.add(self) 142 143 if self.type == registry.entity: 144 if self.range is None and self._range is not None: 145 self.range = model.get(self._range) 146 147 if self.reverse is None and self.range and self._reverse: 148 if not is_mapping(self._reverse): 149 raise InvalidModel("Invalid reverse: %s" % self) 150 self.reverse = self.range._add_reverse(model, self._reverse, self) 151 152 @property 153 def label(self) -> str: 154 """User-facing title for this property.""" 155 return gettext(self._label) 156 157 @property 158 def description(self) -> str: 159 """A longer description of the semantics of this property.""" 160 return gettext(self._description) 161 162 def specificity(self, value: str) -> float: 163 """Return a measure of how precise the given value is.""" 164 if not self.matchable: 165 return 0.0 166 return self.type.specificity(value) 167 168 def validate(self, data: List[Any]) -> Optional[str]: 169 """Validate that the data should be stored. 170 171 Since the types system doesn't really have validation, this currently 172 tries to normalize the value to see if it passes strict parsing. 173 """ 174 values = [] 175 for val in data: 176 if self.stub: 177 return gettext("Property cannot be written") 178 val = get_entity_id(val) 179 if val is None: 180 continue 181 if not self.type.validate(val): 182 return gettext("Invalid value") 183 if val is not None: 184 values.append(val) 185 return None 186 187 def __eq__(self, other: Any) -> bool: 188 return self._hash == hash(other) 189 190 def __hash__(self) -> int: 191 return self._hash 192 193 def to_dict(self) -> PropertyToDict: 194 """Return property metadata in a serializable form.""" 195 data: PropertyToDict = { 196 "name": self.name, 197 "qname": self.qname, 198 "label": self.label, 199 "type": self.type.name, 200 } 201 if self.description: 202 data["description"] = self.description 203 if self.stub: 204 data["stub"] = True 205 if self.matchable: 206 data["matchable"] = True 207 if self.hidden: 208 data["hidden"] = True 209 if self.deprecated: 210 data["deprecated"] = True 211 if self.range is not None: 212 data["range"] = self.range.name 213 if self.reverse is not None: 214 data["reverse"] = self.reverse.name 215 if self.format is not None: 216 data["format"] = self.format 217 return data 218 219 def __repr__(self) -> str: 220 return "<Property(%r)>" % self.qname 221 222 def __str__(self) -> str: 223 return self.qname
class
ReverseSpec(typing.TypedDict):
class
PropertyDict(typing.TypedDict):
21class PropertyDict(TypedDict, total=False): 22 label: Optional[str] 23 description: Optional[str] 24 type: Optional[str] 25 hidden: Optional[bool] 26 matchable: Optional[bool] 27 deprecated: Optional[bool] 28 # stub: Optional[bool] 29 rdf: Optional[str] 30 range: Optional[str] 31 format: Optional[str]
class
PropertySpec(builtins.dict):
reverse: ReverseSpec
Inherited Members
- builtins.dict
- get
- setdefault
- pop
- popitem
- keys
- items
- values
- update
- fromkeys
- clear
- copy
class
PropertyToDict(builtins.dict):
38class PropertyToDict(PropertyDict, total=False): 39 name: str 40 qname: str 41 reverse: Optional[str] 42 stub: Optional[bool]
Inherited Members
- builtins.dict
- get
- setdefault
- pop
- popitem
- keys
- items
- values
- update
- fromkeys
- clear
- copy
class
Property:
45class Property: 46 """A definition of a value-holding field on a schema. Properties define 47 the field type and other possible constraints. They also serve as entity 48 to entity references.""" 49 50 __slots__ = ( 51 "model", 52 "schema", 53 "name", 54 "qname", 55 "_label", 56 "_hash", 57 "_description", 58 "hidden", 59 "type", 60 "matchable", 61 "deprecated", 62 "_range", 63 "format", 64 "range", 65 "stub", 66 "_reverse", 67 "reverse", 68 "uri", 69 ) 70 71 #: Invalid property names. 72 RESERVED = ["id", "caption", "schema", "schemata"] 73 74 def __init__(self, schema: "Schema", name: str, data: PropertySpec) -> None: 75 #: The schema which the property is defined for. This is always the 76 #: most abstract schema that has this property, not the possible 77 #: child schemata that inherit it. 78 self.schema = schema 79 80 #: Machine-readable name for this property. 81 self.name = name 82 83 #: Qualified property name, which also includes the schema name. 84 self.qname = "%s:%s" % (schema.name, self.name) 85 if self.name in self.RESERVED: 86 raise InvalidModel("Reserved name: %s" % self.name) 87 88 self._hash = hash("<Property(%r)>" % self.qname) 89 90 self._label = data.get("label", name) 91 self._description = data.get("description") 92 93 #: This property is deprecated and should not be used. 94 self.deprecated = as_bool(data.get("deprecated", False)) 95 96 #: This property should not be shown or mentioned in the user interface. 97 self.hidden = as_bool(data.get("hidden")) 98 99 type_ = data.get("type", "string") 100 if type_ is None or type_ not in registry.named: 101 raise InvalidModel("Invalid type: %s" % type_) 102 103 #: The data type for this property. 104 self.type = registry[type_] 105 106 #: Whether this property should be used for matching and cross-referencing. 107 _matchable = data.get("matchable") 108 if _matchable is not None: 109 self.matchable = as_bool(data.get("matchable")) 110 else: 111 self.matchable = self.type.matchable 112 113 #: If the property is of type ``entity``, the set of valid schema to be added 114 #: in this property can be constrained. For example, an asset can be owned, 115 #: but a person cannot be owned. 116 self._range = data.get("range") 117 self.range: Optional["Schema"] = None 118 119 #: If the property is of type ``identifier``, a more narrow definition of the 120 #: identifier format can be provided. For example, LEI, INN or IBAN codes 121 #: can be automatically validated. 122 self.format: Optional[str] = data.get("format") 123 124 #: When a property points to another schema, a reverse property is added for 125 #: various administrative reasons. These properties are, however, not real 126 #: and cannot be written to. That's why they are marked as stubs and adding 127 #: values to them will raise an exception. 128 self.stub: Optional[bool] = False 129 130 #: When a property points to another schema, a stub reverse property is 131 #: added as a place to store metadata to help display the link in inverted 132 #: views. 133 self._reverse = data.get("reverse") 134 self.reverse: Optional["Property"] = None 135 136 #: RDF term for this property (i.e. the predicate URI). 137 self.uri = URIRef(cast(str, data.get("rdf", NS[self.qname]))) 138 139 def generate(self, model: "Model") -> None: 140 """Setup method used when loading the model in order to build out the reverse 141 links of the property.""" 142 model.properties.add(self) 143 144 if self.type == registry.entity: 145 if self.range is None and self._range is not None: 146 self.range = model.get(self._range) 147 148 if self.reverse is None and self.range and self._reverse: 149 if not is_mapping(self._reverse): 150 raise InvalidModel("Invalid reverse: %s" % self) 151 self.reverse = self.range._add_reverse(model, self._reverse, self) 152 153 @property 154 def label(self) -> str: 155 """User-facing title for this property.""" 156 return gettext(self._label) 157 158 @property 159 def description(self) -> str: 160 """A longer description of the semantics of this property.""" 161 return gettext(self._description) 162 163 def specificity(self, value: str) -> float: 164 """Return a measure of how precise the given value is.""" 165 if not self.matchable: 166 return 0.0 167 return self.type.specificity(value) 168 169 def validate(self, data: List[Any]) -> Optional[str]: 170 """Validate that the data should be stored. 171 172 Since the types system doesn't really have validation, this currently 173 tries to normalize the value to see if it passes strict parsing. 174 """ 175 values = [] 176 for val in data: 177 if self.stub: 178 return gettext("Property cannot be written") 179 val = get_entity_id(val) 180 if val is None: 181 continue 182 if not self.type.validate(val): 183 return gettext("Invalid value") 184 if val is not None: 185 values.append(val) 186 return None 187 188 def __eq__(self, other: Any) -> bool: 189 return self._hash == hash(other) 190 191 def __hash__(self) -> int: 192 return self._hash 193 194 def to_dict(self) -> PropertyToDict: 195 """Return property metadata in a serializable form.""" 196 data: PropertyToDict = { 197 "name": self.name, 198 "qname": self.qname, 199 "label": self.label, 200 "type": self.type.name, 201 } 202 if self.description: 203 data["description"] = self.description 204 if self.stub: 205 data["stub"] = True 206 if self.matchable: 207 data["matchable"] = True 208 if self.hidden: 209 data["hidden"] = True 210 if self.deprecated: 211 data["deprecated"] = True 212 if self.range is not None: 213 data["range"] = self.range.name 214 if self.reverse is not None: 215 data["reverse"] = self.reverse.name 216 if self.format is not None: 217 data["format"] = self.format 218 return data 219 220 def __repr__(self) -> str: 221 return "<Property(%r)>" % self.qname 222 223 def __str__(self) -> str: 224 return self.qname
A definition of a value-holding field on a schema. Properties define the field type and other possible constraints. They also serve as entity to entity references.
Property( schema: followthemoney.schema.Schema, name: str, data: PropertySpec)
74 def __init__(self, schema: "Schema", name: str, data: PropertySpec) -> None: 75 #: The schema which the property is defined for. This is always the 76 #: most abstract schema that has this property, not the possible 77 #: child schemata that inherit it. 78 self.schema = schema 79 80 #: Machine-readable name for this property. 81 self.name = name 82 83 #: Qualified property name, which also includes the schema name. 84 self.qname = "%s:%s" % (schema.name, self.name) 85 if self.name in self.RESERVED: 86 raise InvalidModel("Reserved name: %s" % self.name) 87 88 self._hash = hash("<Property(%r)>" % self.qname) 89 90 self._label = data.get("label", name) 91 self._description = data.get("description") 92 93 #: This property is deprecated and should not be used. 94 self.deprecated = as_bool(data.get("deprecated", False)) 95 96 #: This property should not be shown or mentioned in the user interface. 97 self.hidden = as_bool(data.get("hidden")) 98 99 type_ = data.get("type", "string") 100 if type_ is None or type_ not in registry.named: 101 raise InvalidModel("Invalid type: %s" % type_) 102 103 #: The data type for this property. 104 self.type = registry[type_] 105 106 #: Whether this property should be used for matching and cross-referencing. 107 _matchable = data.get("matchable") 108 if _matchable is not None: 109 self.matchable = as_bool(data.get("matchable")) 110 else: 111 self.matchable = self.type.matchable 112 113 #: If the property is of type ``entity``, the set of valid schema to be added 114 #: in this property can be constrained. For example, an asset can be owned, 115 #: but a person cannot be owned. 116 self._range = data.get("range") 117 self.range: Optional["Schema"] = None 118 119 #: If the property is of type ``identifier``, a more narrow definition of the 120 #: identifier format can be provided. For example, LEI, INN or IBAN codes 121 #: can be automatically validated. 122 self.format: Optional[str] = data.get("format") 123 124 #: When a property points to another schema, a reverse property is added for 125 #: various administrative reasons. These properties are, however, not real 126 #: and cannot be written to. That's why they are marked as stubs and adding 127 #: values to them will raise an exception. 128 self.stub: Optional[bool] = False 129 130 #: When a property points to another schema, a stub reverse property is 131 #: added as a place to store metadata to help display the link in inverted 132 #: views. 133 self._reverse = data.get("reverse") 134 self.reverse: Optional["Property"] = None 135 136 #: RDF term for this property (i.e. the predicate URI). 137 self.uri = URIRef(cast(str, data.get("rdf", NS[self.qname])))
range: Optional[followthemoney.schema.Schema]
reverse: Optional[Property]
139 def generate(self, model: "Model") -> None: 140 """Setup method used when loading the model in order to build out the reverse 141 links of the property.""" 142 model.properties.add(self) 143 144 if self.type == registry.entity: 145 if self.range is None and self._range is not None: 146 self.range = model.get(self._range) 147 148 if self.reverse is None and self.range and self._reverse: 149 if not is_mapping(self._reverse): 150 raise InvalidModel("Invalid reverse: %s" % self) 151 self.reverse = self.range._add_reverse(model, self._reverse, self)
Setup method used when loading the model in order to build out the reverse links of the property.
label: str
153 @property 154 def label(self) -> str: 155 """User-facing title for this property.""" 156 return gettext(self._label)
User-facing title for this property.
description: str
158 @property 159 def description(self) -> str: 160 """A longer description of the semantics of this property.""" 161 return gettext(self._description)
A longer description of the semantics of this property.
def
specificity(self, value: str) -> float:
163 def specificity(self, value: str) -> float: 164 """Return a measure of how precise the given value is.""" 165 if not self.matchable: 166 return 0.0 167 return self.type.specificity(value)
Return a measure of how precise the given value is.
def
validate(self, data: List[Any]) -> Optional[str]:
169 def validate(self, data: List[Any]) -> Optional[str]: 170 """Validate that the data should be stored. 171 172 Since the types system doesn't really have validation, this currently 173 tries to normalize the value to see if it passes strict parsing. 174 """ 175 values = [] 176 for val in data: 177 if self.stub: 178 return gettext("Property cannot be written") 179 val = get_entity_id(val) 180 if val is None: 181 continue 182 if not self.type.validate(val): 183 return gettext("Invalid value") 184 if val is not None: 185 values.append(val) 186 return None
Validate that the data should be stored.
Since the types system doesn't really have validation, this currently tries to normalize the value to see if it passes strict parsing.
194 def to_dict(self) -> PropertyToDict: 195 """Return property metadata in a serializable form.""" 196 data: PropertyToDict = { 197 "name": self.name, 198 "qname": self.qname, 199 "label": self.label, 200 "type": self.type.name, 201 } 202 if self.description: 203 data["description"] = self.description 204 if self.stub: 205 data["stub"] = True 206 if self.matchable: 207 data["matchable"] = True 208 if self.hidden: 209 data["hidden"] = True 210 if self.deprecated: 211 data["deprecated"] = True 212 if self.range is not None: 213 data["range"] = self.range.name 214 if self.reverse is not None: 215 data["reverse"] = self.reverse.name 216 if self.format is not None: 217 data["format"] = self.format 218 return data
Return property metadata in a serializable form.