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