followthemoney.types.common

  1from inspect import cleandoc
  2from itertools import product
  3from babel.core import Locale
  4from banal import ensure_list
  5from normality import stringify
  6from typing import Any, Dict, Optional, Sequence, Callable, TYPE_CHECKING, TypedDict
  7
  8from followthemoney.value import Value
  9from followthemoney.util import get_locale
 10from followthemoney.util import gettext, sanitize_text
 11
 12if TYPE_CHECKING:
 13    from followthemoney.proxy import EntityProxy
 14
 15EnumValues = Dict[str, str]
 16
 17
 18class PropertyTypeToDict(TypedDict, total=False):
 19    label: str
 20    plural: str
 21    description: Optional[str]
 22    maxLength: int
 23    group: Optional[str]
 24    matchable: Optional[bool]
 25    pivot: Optional[bool]
 26    values: Optional[EnumValues]
 27
 28
 29class PropertyType(object):
 30    """Base class for all property types."""
 31
 32    name: str = "any"
 33    """A machine-facing, variable safe name for the given type."""
 34
 35    group: Optional[str] = None
 36    """Groups are used to invert all the properties of an entity that have a
 37    given  type into a single list before indexing them. This way, in Aleph,
 38    you can query for ``countries:gb`` instead of having to make a set of filters
 39    like ``properties.jurisdiction:gb OR properties.country:gb OR ...``."""
 40
 41    label: str = "Any"
 42    """A name for this type to be shown to users."""
 43
 44    plural: str = "Any"
 45    """A plural name for this type which can be used in appropriate places in
 46    a user interface."""
 47
 48    matchable: bool = True
 49    """Matchable types allow properties to be compared with each other in order to
 50    assess entity similarity. While it makes sense to compare names, countries or
 51    phone numbers, the same isn't true for raw JSON blobs or descriptive text
 52    snippets."""
 53
 54    pivot: bool = False
 55    """Pivot property types are like a stronger form of :attr:`~matchable` types:
 56    they will be used when value-based lookups are used to find commonalities
 57    between entities. For example, pivot typed-properties are used to show all the
 58    other entities that mention the same phone number, email address or name as the
 59    one currently seen by the user."""
 60
 61    max_length: int = 250
 62    """The maximum length of a single value of this type. This is used to warn when
 63    adding individual values that may be malformed or too long to be stored in
 64    downstream databases with fixed column lengths. The unit is unicode codepoints
 65    (not bytes), the output of Python len()."""
 66
 67    total_size: Optional[int] = None
 68    """Some types have overall size limitations in place in order to avoid generating
 69    entities that are very large (upstream ElasticSearch has a 100MB document limit).
 70    Once the total size of all properties of this type has exceed the given limit,
 71    an entity will refuse to add further values."""
 72
 73    @property
 74    def docs(self) -> Optional[str]:
 75        if not self.__doc__:
 76            return None
 77
 78        return cleandoc(self.__doc__)
 79
 80    def validate(
 81        self, value: str, fuzzy: bool = False, format: Optional[str] = None
 82    ) -> bool:
 83        """Returns a boolean to indicate if the given value is a valid instance of
 84        the type."""
 85        cleaned = self.clean(value, fuzzy=fuzzy, format=format)
 86        return cleaned is not None
 87
 88    def clean(
 89        self,
 90        raw: Value,
 91        fuzzy: bool = False,
 92        format: Optional[str] = None,
 93        proxy: Optional["EntityProxy"] = None,
 94    ) -> Optional[str]:
 95        """Create a clean version of a value of the type, suitable for storage
 96        in an entity proxy."""
 97        text = sanitize_text(raw)
 98        if text is None:
 99            return None
100        return self.clean_text(text, fuzzy=fuzzy, format=format, proxy=proxy)
101
102    def clean_text(
103        self,
104        text: str,
105        fuzzy: bool = False,
106        format: Optional[str] = None,
107        proxy: Optional["EntityProxy"] = None,
108    ) -> Optional[str]:
109        """Specific types can apply their own cleaning routines here (this is called
110        by ``clean`` after the value has been converted to a string and null values
111        have been filtered)."""
112        return text
113
114    def join(self, values: Sequence[str]) -> str:
115        """Helper function for converting multi-valued FtM data into formats that
116        allow only a single value per field (e.g. CSV). This is not fully reversible
117        and should be used as a last option."""
118        values = ensure_list(values)
119        return "; ".join(values)
120
121    def _specificity(self, value: str) -> float:
122        return 1.0
123
124    def specificity(self, value: Optional[str]) -> float:
125        """Return a score for how specific the given value is. This can be used as a
126        weighting factor in entity comparisons in order to rate matching property
127        values by how specific they are. For example: a longer address is considered
128        to be more specific than a short one, a full date more specific than just a
129        year number, etc."""
130        if not self.matchable or value is None:
131            return 0.0
132        return self._specificity(value)
133
134    def compare_safe(self, left: Optional[str], right: Optional[str]) -> float:
135        """Compare, but support None values on either side of the comparison."""
136        left = stringify(left)
137        right = stringify(right)
138        if left is None or right is None:
139            return 0.0
140        return self.compare(left, right)
141
142    def compare(self, left: str, right: str) -> float:
143        """Comparisons are a float between 0 and 1. They can assume
144        that the given data is cleaned, but not normalised."""
145        if left.lower() == right.lower():
146            return 1.0 * self.specificity(left)
147        return 0.0
148
149    def compare_sets(
150        self,
151        left: Sequence[str],
152        right: Sequence[str],
153        func: Callable[[Sequence[float]], float] = max,
154    ) -> float:
155        """Compare two sets of values and select the highest-scored result."""
156        results = []
157        for le, ri in product(ensure_list(left), ensure_list(right)):
158            results.append(self.compare(le, ri))
159        if not len(results):
160            return 0.0
161        return func(results)
162
163    def country_hint(self, value: str) -> Optional[str]:
164        """Determine if the given value allows us to infer a country that it may
165        be related to (e.g. using a country prefix on a phone number or IBAN)."""
166        return None
167
168    def pick(self, values: Sequence[str]) -> Optional[str]:
169        """Pick the best value to show to the user."""
170        raise NotImplementedError
171
172    def node_id(self, value: str) -> Optional[str]:
173        """Return an ID suitable to identify this entity as a typed node in a
174        graph representation of some FtM data. It's usually the same as the the
175        RDF form."""
176        return f"{self.name}:{value}"
177
178    def node_id_safe(self, value: Optional[str]) -> Optional[str]:
179        """Wrapper for node_id to handle None values."""
180        if value is None:
181            return None
182        return self.node_id(value)
183
184    def caption(self, value: str, format: Optional[str] = None) -> str:
185        """Return a label for the given property value. This is often the same as the
186        value, but for types like countries or languages, it would return the label,
187        while other values like phone numbers can be formatted to be nicer to read."""
188        return value
189
190    def to_dict(self) -> PropertyTypeToDict:
191        """Return a serialisable description of this data type."""
192        data: PropertyTypeToDict = {
193            "label": gettext(self.label),
194            "plural": gettext(self.plural),
195            "description": gettext(self.docs),
196            "maxLength": self.max_length,
197        }
198        if self.group:
199            data["group"] = self.group
200        if self.matchable:
201            data["matchable"] = True
202        if self.pivot:
203            data["pivot"] = True
204        return data
205
206    def __eq__(self, other: Any) -> bool:
207        if not isinstance(other, PropertyType):
208            return False
209        return self.name == other.name
210
211    def __hash__(self) -> int:
212        return hash(self.name)
213
214    def __str__(self) -> str:
215        return self.name
216
217    def __repr__(self) -> str:
218        return f"<{self.name}>"
219
220
221class EnumType(PropertyType):
222    """Enumerated type properties are used for types which have a defined set
223    of possible values, like languages and countries."""
224
225    def __init__(self) -> None:
226        self._names: Dict[Locale, EnumValues] = {}
227        self.codes = set(self.names.keys())
228
229    def _locale_names(self, locale: Locale) -> EnumValues:
230        return {}
231
232    @property
233    def names(self) -> EnumValues:
234        """Return a mapping from property values to their labels in the current
235        locale."""
236        locale = get_locale()
237        if locale not in self._names:
238            self._names[locale] = self._locale_names(locale)
239        return self._names[locale]
240
241    def validate(
242        self, value: str, fuzzy: bool = False, format: Optional[str] = None
243    ) -> bool:
244        """Make sure that the given code value is one of the supported set."""
245        if value is None:
246            return False
247        return str(value).lower().strip() in self.codes
248
249    def clean_text(
250        self,
251        code: str,
252        fuzzy: bool = False,
253        format: Optional[str] = None,
254        proxy: Optional["EntityProxy"] = None,
255    ) -> Optional[str]:
256        """All code values are cleaned to be lowercase and trailing whitespace is
257        removed."""
258        code = code.lower().strip()
259        if code not in self.codes:
260            return None
261        return code
262
263    def caption(self, value: str, format: Optional[str] = None) -> str:
264        """Given a code value, return the label that should be shown to a user."""
265        return self.names.get(value, value)
266
267    def to_dict(self) -> PropertyTypeToDict:
268        """When serialising the model to JSON, include all values."""
269        data = super(EnumType, self).to_dict()
270        data["values"] = self.names
271        return data
EnumValues = typing.Dict[str, str]
class PropertyTypeToDict(typing.TypedDict):
19class PropertyTypeToDict(TypedDict, total=False):
20    label: str
21    plural: str
22    description: Optional[str]
23    maxLength: int
24    group: Optional[str]
25    matchable: Optional[bool]
26    pivot: Optional[bool]
27    values: Optional[EnumValues]
label: str
plural: str
description: Optional[str]
maxLength: int
group: Optional[str]
matchable: Optional[bool]
pivot: Optional[bool]
def values(unknown):

D.values() -> an object providing a view on D's values

class PropertyType:
 30class PropertyType(object):
 31    """Base class for all property types."""
 32
 33    name: str = "any"
 34    """A machine-facing, variable safe name for the given type."""
 35
 36    group: Optional[str] = None
 37    """Groups are used to invert all the properties of an entity that have a
 38    given  type into a single list before indexing them. This way, in Aleph,
 39    you can query for ``countries:gb`` instead of having to make a set of filters
 40    like ``properties.jurisdiction:gb OR properties.country:gb OR ...``."""
 41
 42    label: str = "Any"
 43    """A name for this type to be shown to users."""
 44
 45    plural: str = "Any"
 46    """A plural name for this type which can be used in appropriate places in
 47    a user interface."""
 48
 49    matchable: bool = True
 50    """Matchable types allow properties to be compared with each other in order to
 51    assess entity similarity. While it makes sense to compare names, countries or
 52    phone numbers, the same isn't true for raw JSON blobs or descriptive text
 53    snippets."""
 54
 55    pivot: bool = False
 56    """Pivot property types are like a stronger form of :attr:`~matchable` types:
 57    they will be used when value-based lookups are used to find commonalities
 58    between entities. For example, pivot typed-properties are used to show all the
 59    other entities that mention the same phone number, email address or name as the
 60    one currently seen by the user."""
 61
 62    max_length: int = 250
 63    """The maximum length of a single value of this type. This is used to warn when
 64    adding individual values that may be malformed or too long to be stored in
 65    downstream databases with fixed column lengths. The unit is unicode codepoints
 66    (not bytes), the output of Python len()."""
 67
 68    total_size: Optional[int] = None
 69    """Some types have overall size limitations in place in order to avoid generating
 70    entities that are very large (upstream ElasticSearch has a 100MB document limit).
 71    Once the total size of all properties of this type has exceed the given limit,
 72    an entity will refuse to add further values."""
 73
 74    @property
 75    def docs(self) -> Optional[str]:
 76        if not self.__doc__:
 77            return None
 78
 79        return cleandoc(self.__doc__)
 80
 81    def validate(
 82        self, value: str, fuzzy: bool = False, format: Optional[str] = None
 83    ) -> bool:
 84        """Returns a boolean to indicate if the given value is a valid instance of
 85        the type."""
 86        cleaned = self.clean(value, fuzzy=fuzzy, format=format)
 87        return cleaned is not None
 88
 89    def clean(
 90        self,
 91        raw: Value,
 92        fuzzy: bool = False,
 93        format: Optional[str] = None,
 94        proxy: Optional["EntityProxy"] = None,
 95    ) -> Optional[str]:
 96        """Create a clean version of a value of the type, suitable for storage
 97        in an entity proxy."""
 98        text = sanitize_text(raw)
 99        if text is None:
100            return None
101        return self.clean_text(text, fuzzy=fuzzy, format=format, proxy=proxy)
102
103    def clean_text(
104        self,
105        text: str,
106        fuzzy: bool = False,
107        format: Optional[str] = None,
108        proxy: Optional["EntityProxy"] = None,
109    ) -> Optional[str]:
110        """Specific types can apply their own cleaning routines here (this is called
111        by ``clean`` after the value has been converted to a string and null values
112        have been filtered)."""
113        return text
114
115    def join(self, values: Sequence[str]) -> str:
116        """Helper function for converting multi-valued FtM data into formats that
117        allow only a single value per field (e.g. CSV). This is not fully reversible
118        and should be used as a last option."""
119        values = ensure_list(values)
120        return "; ".join(values)
121
122    def _specificity(self, value: str) -> float:
123        return 1.0
124
125    def specificity(self, value: Optional[str]) -> float:
126        """Return a score for how specific the given value is. This can be used as a
127        weighting factor in entity comparisons in order to rate matching property
128        values by how specific they are. For example: a longer address is considered
129        to be more specific than a short one, a full date more specific than just a
130        year number, etc."""
131        if not self.matchable or value is None:
132            return 0.0
133        return self._specificity(value)
134
135    def compare_safe(self, left: Optional[str], right: Optional[str]) -> float:
136        """Compare, but support None values on either side of the comparison."""
137        left = stringify(left)
138        right = stringify(right)
139        if left is None or right is None:
140            return 0.0
141        return self.compare(left, right)
142
143    def compare(self, left: str, right: str) -> float:
144        """Comparisons are a float between 0 and 1. They can assume
145        that the given data is cleaned, but not normalised."""
146        if left.lower() == right.lower():
147            return 1.0 * self.specificity(left)
148        return 0.0
149
150    def compare_sets(
151        self,
152        left: Sequence[str],
153        right: Sequence[str],
154        func: Callable[[Sequence[float]], float] = max,
155    ) -> float:
156        """Compare two sets of values and select the highest-scored result."""
157        results = []
158        for le, ri in product(ensure_list(left), ensure_list(right)):
159            results.append(self.compare(le, ri))
160        if not len(results):
161            return 0.0
162        return func(results)
163
164    def country_hint(self, value: str) -> Optional[str]:
165        """Determine if the given value allows us to infer a country that it may
166        be related to (e.g. using a country prefix on a phone number or IBAN)."""
167        return None
168
169    def pick(self, values: Sequence[str]) -> Optional[str]:
170        """Pick the best value to show to the user."""
171        raise NotImplementedError
172
173    def node_id(self, value: str) -> Optional[str]:
174        """Return an ID suitable to identify this entity as a typed node in a
175        graph representation of some FtM data. It's usually the same as the the
176        RDF form."""
177        return f"{self.name}:{value}"
178
179    def node_id_safe(self, value: Optional[str]) -> Optional[str]:
180        """Wrapper for node_id to handle None values."""
181        if value is None:
182            return None
183        return self.node_id(value)
184
185    def caption(self, value: str, format: Optional[str] = None) -> str:
186        """Return a label for the given property value. This is often the same as the
187        value, but for types like countries or languages, it would return the label,
188        while other values like phone numbers can be formatted to be nicer to read."""
189        return value
190
191    def to_dict(self) -> PropertyTypeToDict:
192        """Return a serialisable description of this data type."""
193        data: PropertyTypeToDict = {
194            "label": gettext(self.label),
195            "plural": gettext(self.plural),
196            "description": gettext(self.docs),
197            "maxLength": self.max_length,
198        }
199        if self.group:
200            data["group"] = self.group
201        if self.matchable:
202            data["matchable"] = True
203        if self.pivot:
204            data["pivot"] = True
205        return data
206
207    def __eq__(self, other: Any) -> bool:
208        if not isinstance(other, PropertyType):
209            return False
210        return self.name == other.name
211
212    def __hash__(self) -> int:
213        return hash(self.name)
214
215    def __str__(self) -> str:
216        return self.name
217
218    def __repr__(self) -> str:
219        return f"<{self.name}>"

Base class for all property types.

name: str = 'any'

A machine-facing, variable safe name for the given type.

group: Optional[str] = None

Groups are used to invert all the properties of an entity that have a given type into a single list before indexing them. This way, in Aleph, you can query for countries:gb instead of having to make a set of filters like properties.jurisdiction:gb OR properties.country:gb OR ....

label: str = 'Any'

A name for this type to be shown to users.

plural: str = 'Any'

A plural name for this type which can be used in appropriate places in a user interface.

matchable: bool = True

Matchable types allow properties to be compared with each other in order to assess entity similarity. While it makes sense to compare names, countries or phone numbers, the same isn't true for raw JSON blobs or descriptive text snippets.

pivot: bool = False

Pivot property types are like a stronger form of ~matchable types: they will be used when value-based lookups are used to find commonalities between entities. For example, pivot typed-properties are used to show all the other entities that mention the same phone number, email address or name as the one currently seen by the user.

max_length: int = 250

The maximum length of a single value of this type. This is used to warn when adding individual values that may be malformed or too long to be stored in downstream databases with fixed column lengths. The unit is unicode codepoints (not bytes), the output of Python len().

total_size: Optional[int] = None

Some types have overall size limitations in place in order to avoid generating entities that are very large (upstream ElasticSearch has a 100MB document limit). Once the total size of all properties of this type has exceed the given limit, an entity will refuse to add further values.

docs: Optional[str]
74    @property
75    def docs(self) -> Optional[str]:
76        if not self.__doc__:
77            return None
78
79        return cleandoc(self.__doc__)
def validate( self, value: str, fuzzy: bool = False, format: Optional[str] = None) -> bool:
81    def validate(
82        self, value: str, fuzzy: bool = False, format: Optional[str] = None
83    ) -> bool:
84        """Returns a boolean to indicate if the given value is a valid instance of
85        the type."""
86        cleaned = self.clean(value, fuzzy=fuzzy, format=format)
87        return cleaned is not None

Returns a boolean to indicate if the given value is a valid instance of the type.

def clean( self, raw: Union[str, int, float, bool, datetime.date, datetime.datetime, prefixdate.parse.DatePrefix, NoneType], fuzzy: bool = False, format: Optional[str] = None, proxy: Optional[followthemoney.proxy.EntityProxy] = None) -> Optional[str]:
 89    def clean(
 90        self,
 91        raw: Value,
 92        fuzzy: bool = False,
 93        format: Optional[str] = None,
 94        proxy: Optional["EntityProxy"] = None,
 95    ) -> Optional[str]:
 96        """Create a clean version of a value of the type, suitable for storage
 97        in an entity proxy."""
 98        text = sanitize_text(raw)
 99        if text is None:
100            return None
101        return self.clean_text(text, fuzzy=fuzzy, format=format, proxy=proxy)

Create a clean version of a value of the type, suitable for storage in an entity proxy.

def clean_text( self, text: str, fuzzy: bool = False, format: Optional[str] = None, proxy: Optional[followthemoney.proxy.EntityProxy] = None) -> Optional[str]:
103    def clean_text(
104        self,
105        text: str,
106        fuzzy: bool = False,
107        format: Optional[str] = None,
108        proxy: Optional["EntityProxy"] = None,
109    ) -> Optional[str]:
110        """Specific types can apply their own cleaning routines here (this is called
111        by ``clean`` after the value has been converted to a string and null values
112        have been filtered)."""
113        return text

Specific types can apply their own cleaning routines here (this is called by clean after the value has been converted to a string and null values have been filtered).

def join(self, values: Sequence[str]) -> str:
115    def join(self, values: Sequence[str]) -> str:
116        """Helper function for converting multi-valued FtM data into formats that
117        allow only a single value per field (e.g. CSV). This is not fully reversible
118        and should be used as a last option."""
119        values = ensure_list(values)
120        return "; ".join(values)

Helper function for converting multi-valued FtM data into formats that allow only a single value per field (e.g. CSV). This is not fully reversible and should be used as a last option.

def specificity(self, value: Optional[str]) -> float:
125    def specificity(self, value: Optional[str]) -> float:
126        """Return a score for how specific the given value is. This can be used as a
127        weighting factor in entity comparisons in order to rate matching property
128        values by how specific they are. For example: a longer address is considered
129        to be more specific than a short one, a full date more specific than just a
130        year number, etc."""
131        if not self.matchable or value is None:
132            return 0.0
133        return self._specificity(value)

Return a score for how specific the given value is. This can be used as a weighting factor in entity comparisons in order to rate matching property values by how specific they are. For example: a longer address is considered to be more specific than a short one, a full date more specific than just a year number, etc.

def compare_safe(self, left: Optional[str], right: Optional[str]) -> float:
135    def compare_safe(self, left: Optional[str], right: Optional[str]) -> float:
136        """Compare, but support None values on either side of the comparison."""
137        left = stringify(left)
138        right = stringify(right)
139        if left is None or right is None:
140            return 0.0
141        return self.compare(left, right)

Compare, but support None values on either side of the comparison.

def compare(self, left: str, right: str) -> float:
143    def compare(self, left: str, right: str) -> float:
144        """Comparisons are a float between 0 and 1. They can assume
145        that the given data is cleaned, but not normalised."""
146        if left.lower() == right.lower():
147            return 1.0 * self.specificity(left)
148        return 0.0

Comparisons are a float between 0 and 1. They can assume that the given data is cleaned, but not normalised.

def compare_sets( self, left: Sequence[str], right: Sequence[str], func: Callable[[Sequence[float]], float] = <built-in function max>) -> float:
150    def compare_sets(
151        self,
152        left: Sequence[str],
153        right: Sequence[str],
154        func: Callable[[Sequence[float]], float] = max,
155    ) -> float:
156        """Compare two sets of values and select the highest-scored result."""
157        results = []
158        for le, ri in product(ensure_list(left), ensure_list(right)):
159            results.append(self.compare(le, ri))
160        if not len(results):
161            return 0.0
162        return func(results)

Compare two sets of values and select the highest-scored result.

def country_hint(self, value: str) -> Optional[str]:
164    def country_hint(self, value: str) -> Optional[str]:
165        """Determine if the given value allows us to infer a country that it may
166        be related to (e.g. using a country prefix on a phone number or IBAN)."""
167        return None

Determine if the given value allows us to infer a country that it may be related to (e.g. using a country prefix on a phone number or IBAN).

def pick(self, values: Sequence[str]) -> Optional[str]:
169    def pick(self, values: Sequence[str]) -> Optional[str]:
170        """Pick the best value to show to the user."""
171        raise NotImplementedError

Pick the best value to show to the user.

def node_id(self, value: str) -> Optional[str]:
173    def node_id(self, value: str) -> Optional[str]:
174        """Return an ID suitable to identify this entity as a typed node in a
175        graph representation of some FtM data. It's usually the same as the the
176        RDF form."""
177        return f"{self.name}:{value}"

Return an ID suitable to identify this entity as a typed node in a graph representation of some FtM data. It's usually the same as the the RDF form.

def node_id_safe(self, value: Optional[str]) -> Optional[str]:
179    def node_id_safe(self, value: Optional[str]) -> Optional[str]:
180        """Wrapper for node_id to handle None values."""
181        if value is None:
182            return None
183        return self.node_id(value)

Wrapper for node_id to handle None values.

def caption(self, value: str, format: Optional[str] = None) -> str:
185    def caption(self, value: str, format: Optional[str] = None) -> str:
186        """Return a label for the given property value. This is often the same as the
187        value, but for types like countries or languages, it would return the label,
188        while other values like phone numbers can be formatted to be nicer to read."""
189        return value

Return a label for the given property value. This is often the same as the value, but for types like countries or languages, it would return the label, while other values like phone numbers can be formatted to be nicer to read.

def to_dict(self) -> PropertyTypeToDict:
191    def to_dict(self) -> PropertyTypeToDict:
192        """Return a serialisable description of this data type."""
193        data: PropertyTypeToDict = {
194            "label": gettext(self.label),
195            "plural": gettext(self.plural),
196            "description": gettext(self.docs),
197            "maxLength": self.max_length,
198        }
199        if self.group:
200            data["group"] = self.group
201        if self.matchable:
202            data["matchable"] = True
203        if self.pivot:
204            data["pivot"] = True
205        return data

Return a serialisable description of this data type.

class EnumType(PropertyType):
222class EnumType(PropertyType):
223    """Enumerated type properties are used for types which have a defined set
224    of possible values, like languages and countries."""
225
226    def __init__(self) -> None:
227        self._names: Dict[Locale, EnumValues] = {}
228        self.codes = set(self.names.keys())
229
230    def _locale_names(self, locale: Locale) -> EnumValues:
231        return {}
232
233    @property
234    def names(self) -> EnumValues:
235        """Return a mapping from property values to their labels in the current
236        locale."""
237        locale = get_locale()
238        if locale not in self._names:
239            self._names[locale] = self._locale_names(locale)
240        return self._names[locale]
241
242    def validate(
243        self, value: str, fuzzy: bool = False, format: Optional[str] = None
244    ) -> bool:
245        """Make sure that the given code value is one of the supported set."""
246        if value is None:
247            return False
248        return str(value).lower().strip() in self.codes
249
250    def clean_text(
251        self,
252        code: str,
253        fuzzy: bool = False,
254        format: Optional[str] = None,
255        proxy: Optional["EntityProxy"] = None,
256    ) -> Optional[str]:
257        """All code values are cleaned to be lowercase and trailing whitespace is
258        removed."""
259        code = code.lower().strip()
260        if code not in self.codes:
261            return None
262        return code
263
264    def caption(self, value: str, format: Optional[str] = None) -> str:
265        """Given a code value, return the label that should be shown to a user."""
266        return self.names.get(value, value)
267
268    def to_dict(self) -> PropertyTypeToDict:
269        """When serialising the model to JSON, include all values."""
270        data = super(EnumType, self).to_dict()
271        data["values"] = self.names
272        return data

Enumerated type properties are used for types which have a defined set of possible values, like languages and countries.

codes
names: Dict[str, str]
233    @property
234    def names(self) -> EnumValues:
235        """Return a mapping from property values to their labels in the current
236        locale."""
237        locale = get_locale()
238        if locale not in self._names:
239            self._names[locale] = self._locale_names(locale)
240        return self._names[locale]

Return a mapping from property values to their labels in the current locale.

def validate( self, value: str, fuzzy: bool = False, format: Optional[str] = None) -> bool:
242    def validate(
243        self, value: str, fuzzy: bool = False, format: Optional[str] = None
244    ) -> bool:
245        """Make sure that the given code value is one of the supported set."""
246        if value is None:
247            return False
248        return str(value).lower().strip() in self.codes

Make sure that the given code value is one of the supported set.

def clean_text( self, code: str, fuzzy: bool = False, format: Optional[str] = None, proxy: Optional[followthemoney.proxy.EntityProxy] = None) -> Optional[str]:
250    def clean_text(
251        self,
252        code: str,
253        fuzzy: bool = False,
254        format: Optional[str] = None,
255        proxy: Optional["EntityProxy"] = None,
256    ) -> Optional[str]:
257        """All code values are cleaned to be lowercase and trailing whitespace is
258        removed."""
259        code = code.lower().strip()
260        if code not in self.codes:
261            return None
262        return code

All code values are cleaned to be lowercase and trailing whitespace is removed.

def caption(self, value: str, format: Optional[str] = None) -> str:
264    def caption(self, value: str, format: Optional[str] = None) -> str:
265        """Given a code value, return the label that should be shown to a user."""
266        return self.names.get(value, value)

Given a code value, return the label that should be shown to a user.

def to_dict(self) -> PropertyTypeToDict:
268    def to_dict(self) -> PropertyTypeToDict:
269        """When serialising the model to JSON, include all values."""
270        data = super(EnumType, self).to_dict()
271        data["values"] = self.names
272        return data

When serialising the model to JSON, include all values.