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 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 "_range", 61 "range", 62 "stub", 63 "_reverse", 64 "reverse", 65 "uri", 66 ) 67 68 #: Invalid property names. 69 RESERVED = ["id", "caption", "schema", "schemata"] 70 71 def __init__(self, schema: "Schema", name: str, data: PropertySpec) -> None: 72 #: The schema which the property is defined for. This is always the 73 #: most abstract schema that has this property, not the possible 74 #: child schemata that inherit it. 75 self.schema = schema 76 77 #: Machine-readable name for this property. 78 self.name = name 79 80 #: Qualified property name, which also includes the schema name. 81 self.qname = "%s:%s" % (schema.name, self.name) 82 if self.name in self.RESERVED: 83 raise InvalidModel("Reserved name: %s" % self.name) 84 85 self._hash = hash("<Property(%r)>" % self.qname) 86 87 self._label = data.get("label", name) 88 self._description = data.get("description") 89 90 #: This property is deprecated and should not be used. 91 self.deprecated = as_bool(data.get("deprecated", False)) 92 93 #: This property should not be shown or mentioned in the user interface. 94 self.hidden = as_bool(data.get("hidden")) 95 96 type_ = data.get("type", "string") 97 if type_ is None or type_ not in registry.named: 98 raise InvalidModel("Invalid type: %s" % type_) 99 100 #: The data type for this property. 101 self.type = registry[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 #: If the property is of type ``entity``, the set of valid schema to be added 111 #: in this property can be constrained. For example, an asset can be owned, 112 #: but a person cannot be owned. 113 self._range = data.get("range") 114 self.range: Optional["Schema"] = None 115 116 #: When a property points to another schema, a reverse property is added for 117 #: various administrative reasons. These properties are, however, not real 118 #: and cannot be written to. That's why they are marked as stubs and adding 119 #: values to them will raise an exception. 120 self.stub: Optional[bool] = False 121 122 #: When a property points to another schema, a stub reverse property is 123 #: added as a place to store metadata to help display the link in inverted 124 #: views. 125 self._reverse = data.get("reverse") 126 self.reverse: Optional["Property"] = None 127 128 #: RDF term for this property (i.e. the predicate URI). 129 self.uri = URIRef(cast(str, data.get("rdf", NS[self.qname]))) 130 131 def generate(self, model: "Model") -> None: 132 """Setup method used when loading the model in order to build out the reverse 133 links of the property.""" 134 model.properties.add(self) 135 136 if self.type == registry.entity: 137 if self.range is None and self._range is not None: 138 self.range = model.get(self._range) 139 140 if self.reverse is None and self.range and self._reverse: 141 if not is_mapping(self._reverse): 142 raise InvalidModel("Invalid reverse: %s" % self) 143 self.reverse = self.range._add_reverse(model, self._reverse, self) 144 145 @property 146 def label(self) -> str: 147 """User-facing title for this property.""" 148 return gettext(self._label) 149 150 @property 151 def description(self) -> str: 152 """A longer description of the semantics of this property.""" 153 return gettext(self._description) 154 155 def specificity(self, value: str) -> float: 156 """Return a measure of how precise the given value is.""" 157 if not self.matchable: 158 return 0.0 159 return self.type.specificity(value) 160 161 def validate(self, data: List[Any]) -> Optional[str]: 162 """Validate that the data should be stored. 163 164 Since the types system doesn't really have validation, this currently 165 tries to normalize the value to see if it passes strict parsing. 166 """ 167 values = [] 168 for val in data: 169 if self.stub: 170 return gettext("Property cannot be written") 171 val = get_entity_id(val) 172 if not self.type.validate(val): 173 return gettext("Invalid value") 174 if val is not None: 175 values.append(val) 176 return None 177 178 def __eq__(self, other: Any) -> bool: 179 return self._hash == hash(other) 180 181 def __hash__(self) -> int: 182 return self._hash 183 184 def to_dict(self) -> PropertyToDict: 185 """Return property metadata in a serializable form.""" 186 data: PropertyToDict = { 187 "name": self.name, 188 "qname": self.qname, 189 "label": self.label, 190 "type": self.type.name, 191 } 192 if self.description: 193 data["description"] = self.description 194 if self.stub: 195 data["stub"] = True 196 if self.matchable: 197 data["matchable"] = True 198 if self.hidden: 199 data["hidden"] = True 200 if self.deprecated: 201 data["deprecated"] = True 202 if self.range is not None: 203 data["range"] = self.range.name 204 if self.reverse is not None: 205 data["reverse"] = self.reverse.name 206 return data 207 208 def __repr__(self) -> str: 209 return "<Property(%r)>" % self.qname 210 211 def __str__(self) -> str: 212 return self.qname
class
ReverseSpec(typing.TypedDict):
class
PropertyDict(typing.TypedDict):
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):
37class PropertyToDict(PropertyDict, total=False): 38 name: str 39 qname: str 40 reverse: Optional[str] 41 stub: Optional[bool]
Inherited Members
- builtins.dict
- get
- setdefault
- pop
- popitem
- keys
- items
- values
- update
- fromkeys
- clear
- copy
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 "_range", 62 "range", 63 "stub", 64 "_reverse", 65 "reverse", 66 "uri", 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", "string") 98 if type_ is None or type_ not in registry.named: 99 raise InvalidModel("Invalid type: %s" % type_) 100 101 #: The data type for this property. 102 self.type = registry[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 #: If the property is of type ``entity``, the set of valid schema to be added 112 #: in this property can be constrained. For example, an asset can be owned, 113 #: but a person cannot be owned. 114 self._range = data.get("range") 115 self.range: Optional["Schema"] = None 116 117 #: When a property points to another schema, a reverse property is added for 118 #: various administrative reasons. These properties are, however, not real 119 #: and cannot be written to. That's why they are marked as stubs and adding 120 #: values to them will raise an exception. 121 self.stub: Optional[bool] = False 122 123 #: When a property points to another schema, a stub reverse property is 124 #: added as a place to store metadata to help display the link in inverted 125 #: views. 126 self._reverse = data.get("reverse") 127 self.reverse: Optional["Property"] = None 128 129 #: RDF term for this property (i.e. the predicate URI). 130 self.uri = URIRef(cast(str, data.get("rdf", NS[self.qname]))) 131 132 def generate(self, model: "Model") -> None: 133 """Setup method used when loading the model in order to build out the reverse 134 links of the property.""" 135 model.properties.add(self) 136 137 if self.type == registry.entity: 138 if self.range is None and self._range is not None: 139 self.range = model.get(self._range) 140 141 if self.reverse is None and self.range and self._reverse: 142 if not is_mapping(self._reverse): 143 raise InvalidModel("Invalid reverse: %s" % self) 144 self.reverse = self.range._add_reverse(model, self._reverse, self) 145 146 @property 147 def label(self) -> str: 148 """User-facing title for this property.""" 149 return gettext(self._label) 150 151 @property 152 def description(self) -> str: 153 """A longer description of the semantics of this property.""" 154 return gettext(self._description) 155 156 def specificity(self, value: str) -> float: 157 """Return a measure of how precise the given value is.""" 158 if not self.matchable: 159 return 0.0 160 return self.type.specificity(value) 161 162 def validate(self, data: List[Any]) -> Optional[str]: 163 """Validate that the data should be stored. 164 165 Since the types system doesn't really have validation, this currently 166 tries to normalize the value to see if it passes strict parsing. 167 """ 168 values = [] 169 for val in data: 170 if self.stub: 171 return gettext("Property cannot be written") 172 val = get_entity_id(val) 173 if not self.type.validate(val): 174 return gettext("Invalid value") 175 if val is not None: 176 values.append(val) 177 return None 178 179 def __eq__(self, other: Any) -> bool: 180 return self._hash == hash(other) 181 182 def __hash__(self) -> int: 183 return self._hash 184 185 def to_dict(self) -> PropertyToDict: 186 """Return property metadata in a serializable form.""" 187 data: PropertyToDict = { 188 "name": self.name, 189 "qname": self.qname, 190 "label": self.label, 191 "type": self.type.name, 192 } 193 if self.description: 194 data["description"] = self.description 195 if self.stub: 196 data["stub"] = True 197 if self.matchable: 198 data["matchable"] = True 199 if self.hidden: 200 data["hidden"] = True 201 if self.deprecated: 202 data["deprecated"] = True 203 if self.range is not None: 204 data["range"] = self.range.name 205 if self.reverse is not None: 206 data["reverse"] = self.reverse.name 207 return data 208 209 def __repr__(self) -> str: 210 return "<Property(%r)>" % self.qname 211 212 def __str__(self) -> str: 213 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)
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", "string") 98 if type_ is None or type_ not in registry.named: 99 raise InvalidModel("Invalid type: %s" % type_) 100 101 #: The data type for this property. 102 self.type = registry[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 #: If the property is of type ``entity``, the set of valid schema to be added 112 #: in this property can be constrained. For example, an asset can be owned, 113 #: but a person cannot be owned. 114 self._range = data.get("range") 115 self.range: Optional["Schema"] = None 116 117 #: When a property points to another schema, a reverse property is added for 118 #: various administrative reasons. These properties are, however, not real 119 #: and cannot be written to. That's why they are marked as stubs and adding 120 #: values to them will raise an exception. 121 self.stub: Optional[bool] = False 122 123 #: When a property points to another schema, a stub reverse property is 124 #: added as a place to store metadata to help display the link in inverted 125 #: views. 126 self._reverse = data.get("reverse") 127 self.reverse: Optional["Property"] = None 128 129 #: RDF term for this property (i.e. the predicate URI). 130 self.uri = URIRef(cast(str, data.get("rdf", NS[self.qname])))
range: Optional[followthemoney.schema.Schema]
reverse: Optional[Property]
132 def generate(self, model: "Model") -> None: 133 """Setup method used when loading the model in order to build out the reverse 134 links of the property.""" 135 model.properties.add(self) 136 137 if self.type == registry.entity: 138 if self.range is None and self._range is not None: 139 self.range = model.get(self._range) 140 141 if self.reverse is None and self.range and self._reverse: 142 if not is_mapping(self._reverse): 143 raise InvalidModel("Invalid reverse: %s" % self) 144 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
146 @property 147 def label(self) -> str: 148 """User-facing title for this property.""" 149 return gettext(self._label)
User-facing title for this property.
description: str
151 @property 152 def description(self) -> str: 153 """A longer description of the semantics of this property.""" 154 return gettext(self._description)
A longer description of the semantics of this property.
def
specificity(self, value: str) -> float:
156 def specificity(self, value: str) -> float: 157 """Return a measure of how precise the given value is.""" 158 if not self.matchable: 159 return 0.0 160 return self.type.specificity(value)
Return a measure of how precise the given value is.
def
validate(self, data: List[Any]) -> Optional[str]:
162 def validate(self, data: List[Any]) -> Optional[str]: 163 """Validate that the data should be stored. 164 165 Since the types system doesn't really have validation, this currently 166 tries to normalize the value to see if it passes strict parsing. 167 """ 168 values = [] 169 for val in data: 170 if self.stub: 171 return gettext("Property cannot be written") 172 val = get_entity_id(val) 173 if not self.type.validate(val): 174 return gettext("Invalid value") 175 if val is not None: 176 values.append(val) 177 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.
185 def to_dict(self) -> PropertyToDict: 186 """Return property metadata in a serializable form.""" 187 data: PropertyToDict = { 188 "name": self.name, 189 "qname": self.qname, 190 "label": self.label, 191 "type": self.type.name, 192 } 193 if self.description: 194 data["description"] = self.description 195 if self.stub: 196 data["stub"] = True 197 if self.matchable: 198 data["matchable"] = True 199 if self.hidden: 200 data["hidden"] = True 201 if self.deprecated: 202 data["deprecated"] = True 203 if self.range is not None: 204 data["range"] = self.range.name 205 if self.reverse is not None: 206 data["reverse"] = self.reverse.name 207 return data
Return property metadata in a serializable form.