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):
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    # stub: Optional[bool]
29    rdf: Optional[str]
30    range: Optional[str]
label: Optional[str]
description: Optional[str]
type: Optional[str]
hidden: Optional[bool]
matchable: Optional[bool]
deprecated: Optional[bool]
rdf: Optional[str]
range: Optional[str]
class PropertySpec(builtins.dict):
33class PropertySpec(PropertyDict):
34    reverse: ReverseSpec
reverse: ReverseSpec
label: Optional[str]
description: Optional[str]
type: Optional[str]
hidden: Optional[bool]
matchable: Optional[bool]
deprecated: Optional[bool]
rdf: Optional[str]
range: Optional[str]
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]
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]
rdf: Optional[str]
range: Optional[str]
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])))
RESERVED = ['id', 'caption', 'schema', 'schemata']
schema
name
qname
deprecated
hidden
type
range: Optional[followthemoney.schema.Schema]
stub: Optional[bool]
reverse: Optional[Property]
uri
def generate(self, model: followthemoney.model.Model) -> None:
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.

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

matchable
model