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