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):
15class ReverseSpec(TypedDict, total=False):
16    name: str
17    label: Optional[str]
18    hidden: Optional[bool]
name: str
label: Optional[str]
hidden: Optional[bool]
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]
label: Optional[str]
description: Optional[str]
type: Optional[str]
hidden: Optional[bool]
matchable: Optional[bool]
deprecated: Optional[bool]
maxLength: Optional[int]
rdf: Optional[str]
range: Optional[str]
format: Optional[str]
class PropertySpec(builtins.dict):
35class PropertySpec(PropertyDict):
36    reverse: ReverseSpec
reverse: ReverseSpec
label: Optional[str]
description: Optional[str]
type: Optional[str]
hidden: Optional[bool]
matchable: Optional[bool]
deprecated: Optional[bool]
maxLength: Optional[int]
rdf: Optional[str]
range: Optional[str]
format: Optional[str]
class PropertyToDict(builtins.dict):
39class PropertyToDict(PropertyDict, total=False):
40    name: str
41    qname: str
42    reverse: Optional[str]
43    stub: Optional[bool]
name: str
qname: str
reverse: Optional[str]
stub: Optional[bool]
label: Optional[str]
description: Optional[str]
type: Optional[str]
hidden: Optional[bool]
matchable: Optional[bool]
deprecated: Optional[bool]
maxLength: Optional[int]
rdf: Optional[str]
range: Optional[str]
format: Optional[str]
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])))
RESERVED = ['id', 'caption', 'schema', 'schemata']
schema
name
qname
deprecated
hidden
type
max_length
range: Optional[followthemoney.schema.Schema]
format: Optional[str]
stub: Optional[bool]
reverse: Optional[Property]
uri
def generate(self, model: followthemoney.model.Model) -> None:
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.

def to_dict(self) -> PropertyToDict:
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.

matchable
model