### Install followthemoney and verify CLI
Source: https://followthemoney.tech/docs/cli
Install the package via pip and check the help menu to confirm installation.
```bash
pip install followthemoney
ftm --help
```
--------------------------------
### Install followthemoney-store with SQLite
Source: https://followthemoney.tech/docs/cli
Install the store package with default SQLite support.
```bash
pip install followthemoney-store
```
--------------------------------
### Install followthemoney-store with PostgreSQL
Source: https://followthemoney.tech/docs/cli
Install the store package with PostgreSQL support and configure the database URI.
```bash
pip install followthemoney-store[postgresql]
export FTM_STORE_URI=postgresql://localhost/followthemoney
```
--------------------------------
### Query Examples
Source: https://followthemoney.tech/docs/metadata
Various examples of dataset query structures.
```json
"us_ofac_sdn"
```
```json
["us_ofac_sdn", "eu_fsf", "gb_hmt_sanctions"]
```
```json
"#list.sanction"
```
```json
{"and": ["#issuer.eu", "#list.sanction"]}
```
```json
{"and": [
{"or": ["#issuer.west", "#list.sanction", "#list.debarment"]},
{"not": "lt_fiu_sanctions"},
{"not": "#issuer.ru"}
]}
```
```json
{"and": [
"#list.sanction",
{"not": {"or": ["lt_fiu_sanctions", "ru_fedsfm"]}}
]}
```
--------------------------------
### Install TypeScript/JavaScript library
Source: https://followthemoney.tech/libraries
Use npm to install the FollowTheMoney package for TypeScript and JavaScript environments.
```bash
npm install @opensanctions/followthemoney
```
--------------------------------
### Advanced Property Mapping Hacks
Source: https://followthemoney.tech/docs/mappings
Examples for handling multiple columns, joining strings, setting constants, and formatting dates.
```yaml
# Setting multiple values for a property:
properties:
name:
columns:
- person_name
- maiden_name
# Merging values ad-hoc:
properties:
name:
columns:
- first_name
- last_name
join: " "
# Setting a constant value:
properties:
country:
literal: "SS"
# Defining a date format:
properties:
birthDate:
column: dob
format: "%d.%m.%Y"
```
--------------------------------
### Install ICU dependencies
Source: https://followthemoney.tech/docs/cli
Required for transliteration and fuzzy name comparison features.
```bash
apt install libicu-dev
pip install pyicu
```
--------------------------------
### Entity Key Collision Example
Source: https://followthemoney.tech/docs/mappings
An example of an insufficient key configuration that leads to entity collisions when names are not unique.
```yaml
entities:
person:
schema: Person
keys:
- FirstName
- LastName
properties:
firstName:
column: FirstName
lastName:
column: LastName
birthDate:
column: DoB
```
--------------------------------
### Example entity format
Source: https://followthemoney.tech/docs/cli
The standard newline-delimited JSON format for entities.
```json
{"id": "person-1", "schema": "Person", "properties": {"name": ["Jane Doe"]}}
```
--------------------------------
### Complex Mapping Configuration
Source: https://followthemoney.tech/docs/mappings
Example of mapping multiple CSV sources to Company entities with specific property assignments.
```yaml
- csv_url: http://assets.data.occrp.org/tools/aleph/fixtures/md_companies/licensed.csv
entities:
company:
schema: Company
keys:
- Company_IDNO
- Company_Name
properties:
sector:
column: Denumire
- csv_url: http://assets.data.occrp.org/tools/aleph/fixtures/md_companies/unlicensed.csv
entities:
company:
schema: Company
keys:
- Company_IDNO
- Company_Name
properties:
sector:
column: Denumire
caemCode:
column: Cod_CAEM
```
--------------------------------
### Define SQL database mapping
Source: https://followthemoney.tech/docs/mappings
Configures a PostgreSQL database connection with table aliases and join conditions. Ensure required Python drivers for the specific database backend are installed.
```yaml
za_cipc:
queries:
- database: postgresql://localhost/cipc
tables:
- table: za_cipc_companies
alias: companies
- table: za_cipc_directors
alias: directors
joins:
- left: companies.regno
right: directors.company_regno
```
--------------------------------
### Get entity caption
Source: https://followthemoney.tech/python/statements
Calculates the user-facing label for the entity based on schema caption properties.
```python
@property
def caption(self) -> str:
"""The user-facing label to be used for this entity. This checks a list
of properties defined by the schema (caption) and returns the first
available value. If no caption is available, return the schema label.
This implementation prefers statements where the language property is that
of the preferred system language."""
if self._caption is None:
for prop_ in self.schema.caption:
stmts = self._statements.get(prop_)
if stmts is None:
continue
prop = self.schema.properties[prop_]
if prop.type == registry.name and len(stmts) > 1:
values = [LangStr(s.value, lang=s.lang) for s in stmts]
name = pick_lang_name(values)
if name is not None:
self._caption = name
return self._caption
for stmt in sorted(stmts):
self._caption = stmt.value
return self._caption
if self._caption is None:
self._caption = self.schema.label
return self._caption
```
--------------------------------
### Get Entity Length
Source: https://followthemoney.tech/python/statements
Returns the count of statements plus one.
```python
def __len__(self) -> int:
return len(list(self._iter_stmt())) + 1
```
--------------------------------
### Interact with Schemas and Entities
Source: https://followthemoney.tech/python
Demonstrates loading the model, creating entity proxies, assigning properties, and serializing data.
```python
# Load the standard instance of the model
from followthemoney import model
## Schema metadata
# Access a schema metadata object
schema = model.get('Person')
# Access a property metadata object
prop = schema.get('birthDate')
## Working with entities and entity proxies
# Next, let's instantiate a proxy object for a new Person entity:
entity = model.make_entity(schema)
# First, you'll want to assign an ID to the entity. You can do this directly:
entity.id = 'john-smith'
# Or you can use a hashing function to make a safe ID:
entity.make_id('John Smith', '1979')
# Now, let's assign this entity a birthDate property (see above):
entity.add(prop, '1979-08-23')
# You can also assign properties by name:
entity.add('firstName', 'John')
entity.add('lastName', 'Smith')
entity.add('name', 'John Smith')
# Adding a property value will perform some data normalisation and validation:
entity.add('nationality', 'Atlantis')
assert not entity.has('nationality')
entity.add('nationality', 'Germani', fuzzy=True)
assert 'de' == entity.first('nationality')
enttiy.add('nationality', '阿拉伯聯合大公國')
assert 'ae' in entity.get('nationality')
# Lets make a second entity, this time for a passport:
passport_entity = model.make_entity('Passport')
passport_entity.make_id(entity.id, 'C716818')
passport_entity.add('number', 'C716818')
# Entities can link to other entities like this:
passport_entity.add('holder', entity)
# Which is the same as:
passport_entity.add('holder', entity.id)
# Finally, you can turn the contents of the entity proxy into a plain dictionary
# that is suitable for JSON serialization or storage in a database:
data = entity.to_dict()
assert data.get('id') == entity.id
# If you want to turn this back into an entity proxy:
entity2 = model.get_proxy(data)
assert entity2 == entity
```
--------------------------------
### Execute mapping via command-line
Source: https://followthemoney.tech/docs/mappings
Run the mapping file using the ftm command-line tool to output JSON entities.
```bash
ftm map brexitonians.yml
```
--------------------------------
### Aggregate datasets using the store CLI
Source: https://followthemoney.tech/docs/cli
Perform a write-then-iterate workflow to aggregate entities using the store command.
```bash
cat us_ofac.ijson | ftm store write -d us_ofac
ftm store iterate -d us_ofac > aggregated.ijson
ftm store delete -d us_ofac
```
--------------------------------
### Accessing and using property types via registry
Source: https://followthemoney.tech/python/registry
Demonstrates cleaning values, comparing entities, and iterating over matchable types using the registry singleton.
```python
from followthemoney.types import registry
# Clean a value
phone = registry.phone.clean("+1 (415) 555-0100")
# Compare two values
score = registry.name.compare("Jane Doe", "J. Doe")
# Iterate all types that support comparison
for t in registry.matchable:
print(t.name, t.label)
```
--------------------------------
### Configure Java Maven dependency
Source: https://followthemoney.tech/libraries
Add the followthemoney dependency to your project's pom.xml file.
```xml
tech.followthemoney
followthemoney
4.10.1
```
--------------------------------
### Aggregate entities using Python
Source: https://followthemoney.tech/docs/fragments
Using the ftmstore library to bulk load fragments and iterate over combined entities or partials.
```python
import os
from ftmstore import get_dataset
# Assume a function that will emit fragments:
from myapp.data import generate_fragments
# If no `database_uri` is given, ftmstore will read connection from
# $FTM_STORE_URI, or create a file called `followthemoney.sqlite` in
# the current directory.
database_uri = os.environ.get('DATABASE_URI')
dataset = get_dataset('myapp_dataset', database_uri=database_uri)
bulk = dataset.bulk()
for idx, proxy in enumerate(generate_fragments()):
bulk.put(proxy, fragment=idx)
bulk.flush()
# This will print the number of combined entities (ie. DISTINCT id):
print(len(dataset))
# This will return combined entities:
for entity in dataset.iterate():
print(entity.caption)
# You could also iterate the underlying fragments:
for proxy in dataset.partials():
print(proxy)
# Note: `dataset.partials()` returns `EntityProxy` objects. The method
# `dataset.fragments()` would return raw Python dictionaries instead.
# All three methods also support the `entity_id` filter, which can also be
# shortened to `get`:
entity = dataset.get(entity_id)
```
--------------------------------
### Execute a mapping from a YAML file
Source: https://followthemoney.tech/docs/cli
Downloads a mapping file and executes it to generate an ijson stream of entities.
```bash
curl -o md_companies.yml https://raw.githubusercontent.com/alephdata/aleph/main/mappings/md_companies.yml
ftm map md_companies.yml > moldova.ijson
```
--------------------------------
### Perform Neo4J bulk import
Source: https://followthemoney.tech/docs/cli
Generate CSV files and a shell script for high-performance Neo4J bulk loading.
```bash
cat us_ofac.ijson | ftm export-neo4j-bulk -o neo4j_import/ -e iban -e address
```
--------------------------------
### Sign an Ownership entity with a namespace
Source: https://followthemoney.tech/docs/namespace
Demonstrates the transformation of an Ownership entity's bare IDs into signed IDs using a specific namespace.
```json
{
"id": "owner-ship-1",
"schema": "Ownership",
"properties": {
"owner": ["person-1"],
"asset": ["company-1"]
}
}
```
```json
{
"id": "owner-ship-1.a40a29300ac6bb79dd2f911e77bbda7a3b502126",
"schema": "Ownership",
"properties": {
"owner": ["person-1.c2c1…"],
"asset": ["company-1.8b3f…"]
}
}
```
--------------------------------
### Aggregate entities using CLI
Source: https://followthemoney.tech/docs/fragments
Commands to map CSV data to fragments, store them in a database, and iterate over merged entities.
```bash
# Generate entities from a CSV file and a mapping:
cat company-registry.csv | ftm map-csv mapping_file.yml > fragments.ijson
# Write the fragments to a table `company_registry`:
cat fragments.ijson | ftm store write -d company_registry
# List the tables in the store:
ftm store list
# Output merged entities:
ftm store iterate -d company_registry
```
--------------------------------
### Pick a name from a set
Source: https://followthemoney.tech/explorer/types/name
Selects the most plausible user-facing name from a sequence of name strings.
```python
def pick(self, values: Sequence[str]) -> str | None:
"""From a set of names, pick the most plausible user-facing one."""
return pick_name(list(values))
```
--------------------------------
### Export graph data to GEXF
Source: https://followthemoney.tech/docs/cli
Create GEXF files for quantitative analysis in tools like Gephi.
```bash
cat us_ofac.ijson | ftm validate | ftm export-gexf -e iban -o ofac.gexf
```
--------------------------------
### Pull and run Docker container
Source: https://followthemoney.tech/libraries
Commands to pull the latest FollowTheMoney Docker image and run it interactively.
```bash
docker pull ghcr.io/opensanctions/followthemoney:latest
docker run --rm -t -i ghcr.io/opensanctions/followthemoney:latest
```
--------------------------------
### Normalize phone numbers with country hints
Source: https://followthemoney.tech/explorer/types/phone
Demonstrates cleaning phone numbers and adding them to entities using the registry and entity proxy.
```python
from followthemoney import model
from followthemoney.types import registry
registry.phone.clean("020 8366 1177") # None
registry.phone.clean("020 8366 1177", format="gb") # '+442083661177'
entity = model.make_entity("Company")
entity.add("phone", "020 8366 1177", format="gb")
```
--------------------------------
### Explore schema model with ftm ref
Source: https://followthemoney.tech/docs/cli
Browse schema definitions, properties, and types. Outputs tables for terminals or JSON when piped.
```bash
ftm ref # model overview + the subcommand index
ftm ref schemata # every schema, with matchable/abstract flags
ftm ref schemata --matchable # only schemata that can be matched
ftm ref schema Person # one schema, with all inherited properties
ftm ref types # every property type
ftm ref type country # one type, including its enum values
ftm ref prop Person:nationality # one property's full definition
```
--------------------------------
### Read and process FtM entities in Java
Source: https://followthemoney.tech/libraries
Demonstrates loading the default model and iterating through entity data from a JSON file.
```java
package tech.followthemoney;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import tech.followthemoney.entity.ValueEntity;
import tech.followthemoney.exc.SchemaException;
import tech.followthemoney.model.Model;
public class FtMLibraryDemo {
public static void main(String[] args) {
String path = "entities.ftm.json";
try {
BufferedReader reader = new BufferedReader(new FileReader(path));
Model model = Model.loadDefault();
ObjectMapper mapper = new ObjectMapper();
String line;
while ((line = reader.readLine()) != null) {
JsonNode node = mapper.readTree(line);
ValueEntity entity = ValueEntity.fromJson(model, node);
System.out.println("Processing entity: " + entity.getId());
System.out.println(" -> Entity name: " + entity.getCaption());
System.out.println(" -> Entity type: " + entity.getSchema().getLabel());
}
} catch (IOException | SchemaException e) {
e.printStackTrace();
}
}
}
```
--------------------------------
### followthemoney.compare.compare(left, right, weights=COMPARE_WEIGHTS)
Source: https://followthemoney.tech/python/lib
Compares two entity proxies and returns a float representing the match score based on provided weights.
```APIDOC
## followthemoney.compare.compare
### Description
Compares two entities and returns a match score as a float. If the entities have identical checksums and contain properties, it returns a perfect match score of 1.0.
### Signature
`compare(left: EntityProxy, right: EntityProxy, weights: Weights = COMPARE_WEIGHTS) -> float`
### Parameters
- **left** (EntityProxy) - Required - The first entity to compare.
- **right** (EntityProxy) - Required - The second entity to compare.
- **weights** (Weights) - Optional - The weighting configuration used for scoring, defaults to COMPARE_WEIGHTS.
### Returns
- **float** - The calculated match score.
```
--------------------------------
### Compare two entities using followthemoney.compare
Source: https://followthemoney.tech/python/lib
Calculates a match score between two EntityProxy objects based on provided weights. Returns a float representing the similarity score.
```python
def compare(
left: EntityProxy,
right: EntityProxy,
weights: Weights = COMPARE_WEIGHTS,
) -> float:
"""Compare two entities and return a match score."""
if left.checksum == right.checksum:
# Check if there is any data at all (ie any basis for making a decision),
# if so, return a perfect match. This avoids marking two empty entities
# as matching. Bit ambiguous, but practical.
if len(left.properties) > 0 and len(right.properties) > 0:
return 1.0
scores = compare_scores(left, right)
return _compare(scores, weights)
```
--------------------------------
### Load and iterate a dataset catalog
Source: https://followthemoney.tech/python/dataset
Load a catalog from a YAML file and iterate through the resources of a specific dataset.
```python
from followthemoney.dataset import DataCatalog, Dataset
catalog = DataCatalog.from_path(Dataset, "catalog.yml")
ofac = catalog.require("us_ofac_sdn")
for resource in ofac.resources:
print(resource.name, resource.url)
```
--------------------------------
### Apply filters to source data
Source: https://followthemoney.tech/docs/mappings
Demonstrates filtering imported data using equality conditions. Complex filtering logic should be handled during data cleaning before mapping execution.
```yaml
gb_parliament_57:
queries:
- csv_url: http://bit.ly/uk-mps-csv
filters:
group: 'Conservative'
filters_not:
gender: 'male'
entities:
member:
schema: Person
keys:
- id
properties:
name:
column: name
```
--------------------------------
### Create Statement instance from dictionary
Source: https://followthemoney.tech/python/statements
Instantiates a Statement object using a dictionary of statement attributes.
```python
@classmethod
def from_dict(cls, data: StatementDict) -> "Statement":
return cls(
entity_id=data["entity_id"],
prop=data["prop"],
schema=data["schema"],
value=data["value"],
dataset=data["dataset"],
lang=data.get("lang", None),
original_value=data.get("original_value", None),
first_seen=data.get("first_seen", None),
external=data.get("external", False),
id=data.get("id", None),
canonical_id=data.get("canonical_id", None),
last_seen=data.get("last_seen", None),
origin=data.get("origin", None),
)
```
--------------------------------
### Process statement-based workflows
Source: https://followthemoney.tech/docs/cli
Convert between entity streams and statement tables for provenance-aware data handling.
```bash
# Entities → statements (CSV, attributed to dataset 'md_companies')
cat moldova.ijson | ftm statements -d md_companies > statements.csv
# Statements → entities, reconstructing full records
ftm aggregate-statements -i statements.csv > rebuilt.ijson
```
--------------------------------
### Model and Schema Access
Source: https://followthemoney.tech/python
Accessing schema and property metadata from the model singleton.
```APIDOC
## model.get(name)
### Description
Retrieves a schema metadata object by its name.
### Parameters
- **name** (string) - Required - The name of the schema (e.g., 'Person').
## schema.get(name)
### Description
Retrieves a property metadata object from a schema.
### Parameters
- **name** (string) - Required - The name of the property (e.g., 'birthDate').
```
--------------------------------
### Create Statement instance from database row
Source: https://followthemoney.tech/python/statements
Instantiates a Statement object from a database row object.
```python
@classmethod
def from_db_row(cls, row: Row[Any]) -> "Statement":
return cls(
id=row.id,
canonical_id=row.canonical_id,
entity_id=row.entity_id,
prop=row.prop,
schema=row.schema,
value=row.value,
dataset=row.dataset,
lang=row.lang,
original_value=row.original_value,
first_seen=datetime_iso(row.first_seen),
external=row.external,
last_seen=datetime_iso(row.last_seen),
origin=row.origin,
)
```
--------------------------------
### Initialize StatementEntity
Source: https://followthemoney.tech/python/statements
Defines the StatementEntity class which links entities to their source datasets.
```python
class StatementEntity(EntityProxy):
"""An entity object that can link to a set of datasets that it is sourced from."""
__slots__ = (
"_caption",
"_statements",
"dataset",
"extra_referents",
"id",
"last_change",
"schema",
)
def __init__(
self,
dataset: Dataset,
data: dict[str, Any],
cleaned: bool = True,
) -> None:
data = dict(data or {})
schema = Model.instance().get(data.pop("schema", None))
if schema is None:
raise InvalidData(gettext("No schema for entity."))
self.schema = schema
```
--------------------------------
### Standard Column Mapping
Source: https://followthemoney.tech/docs/mappings
Basic configuration for mapping a single column to a property.
```yaml
properties:
name:
column: person_name
```
--------------------------------
### followthemoney.types.Registry.get(name)
Source: https://followthemoney.tech/python/registry
Retrieves a specific property type object by its name.
```APIDOC
## get(name)
### Description
Retrieves the property type object associated with the provided name. This can also be accessed via attribute access on the registry instance (e.g., registry.phone).
### Parameters
- **name** (string) - Required - The name of the property type to retrieve.
```
--------------------------------
### Aggregate Entities from Multiple Files
Source: https://followthemoney.tech/docs/statements
Map multiple CSV files to statements, sort them, and aggregate them into a single entity file.
```bash
# Map several source files to FtM. Some of the source files may emit copies of
# the same entity.
ftm map-csv mapping.yml -i source1.csv | ftm statements -o source1.json
ftm map-csv mapping.yml -i source2.csv | ftm statements -o source2.json
ftm map-csv mapping.yml -i source3.csv | ftm statements -o source3.json
# Invoke a normal UNIX sort:
sort -o combined.json source1.json source2.json source3.json
# Now, all statements representing one entity are grouped and can be turned
# into FtM entities:
ftm aggregate-statements -i combined.json -o entities.ftm.json
```
--------------------------------
### Statement Cloning and Key Generation
Source: https://followthemoney.tech/python/statements
Methods for creating deep copies of statements and generating unique keys for data identification.
```python
def clone(
self: Self,
*,
entity_id: str | None = None,
prop: str | None = None,
schema: str | None = None,
value: str | None = None,
dataset: str | None = None,
lang: str | None | object = UNSET,
original_value: str | None | object = UNSET,
first_seen: str | None | object = UNSET,
external: bool | None = None,
canonical_id: str | None = None,
last_seen: str | None | object = UNSET,
origin: str | None | object = UNSET,
) -> "Statement":
"""Make a deep copy of the given statement."""
lang = lang if is_not_unset(lang) else self._lang
ov = original_value if is_not_unset(original_value) else self.original_value
first_seen = first_seen if is_not_unset(first_seen) else self.first_seen
last_seen = last_seen if is_not_unset(last_seen) else self.last_seen
origin = origin if is_not_unset(origin) else self.origin
if external is None:
external = self._external
if canonical_id is None and self._entity_id != self.canonical_id:
canonical_id = self.canonical_id
# Decide if the statement ID can be kept the same:
stmt_id = self.id
if entity_id is not None and entity_id != self.entity_id:
stmt_id = None
if prop is not None and prop != self._prop:
stmt_id = None
if schema is not None and schema != self._schema:
stmt_id = None
if value is not None and value != self._value:
stmt_id = None
if dataset is not None and dataset != self._dataset:
stmt_id = None
if external != self._external:
stmt_id = None
if lang != self._lang:
stmt_id = None
return Statement(
id=stmt_id,
entity_id=entity_id or self._entity_id,
prop=prop or self._prop,
schema=schema or self._schema,
value=value or self._value,
dataset=dataset or self._dataset,
lang=lang,
original_value=ov,
first_seen=first_seen,
external=external,
canonical_id=canonical_id,
last_seen=last_seen,
origin=origin,
)
```
```python
def generate_key(self) -> str | None:
return self.make_key(
self._dataset,
self._entity_id,
self._prop,
self._value,
self._external,
lang=self._lang,
)
```
--------------------------------
### pick(values)
Source: https://followthemoney.tech/explorer/types/name
Selects the most plausible user-facing name from a set of names.
```APIDOC
## pick(values)
### Description
From a set of names, pick the most plausible user-facing one.
### Parameters
- **values** (Sequence[str]) - Required - A list of name strings.
### Returns
- **str | None** - The most plausible name or None.
```
--------------------------------
### Caption Method
Source: https://followthemoney.tech/explorer/types/country
Converts a country code into its corresponding short name.
```python
def caption(self, value: str, format: str | None = None) -> str:
"""Turn country codes into short names."""
territory = get_territory(value)
if territory is not None:
return territory.name
return value
```
--------------------------------
### Convert and Aggregate Statements
Source: https://followthemoney.tech/docs/statements
Convert entities to statement streams and perform the inverse aggregation operation.
```bash
# Convert "traditional" FtM entities to a statement stream
cat entities.ftm.json | ftm statements --format json -o statements.json
#. The inverse operation:
ftm aggregate-statements -i statements.json -o entities.ftm.json
```
--------------------------------
### Sign entity IDs with a namespace
Source: https://followthemoney.tech/docs/cli
Applies an HMAC signature to entity IDs in a stream using a specified namespace key.
```bash
cat entities.ijson | ftm sign -s md_companies > signed.ijson
```
--------------------------------
### apply(proxy, shallow=False)
Source: https://followthemoney.tech/docs/namespace
Rewrites an entity proxy so that all IDs mentioned are limited to the current namespace.
```APIDOC
## apply(proxy, shallow=False)
### Description
Rewrite an entity proxy so all IDs mentioned are limited to the namespace.
### Parameters
- **proxy** (object) - Required - The entity proxy to rewrite.
- **shallow** (bool) - Optional - Whether to perform a shallow rewrite.
```
--------------------------------
### compare(left, right)
Source: https://followthemoney.tech/explorer/types/url
Compares two URL strings and returns a similarity score, ignoring fragments and performing hard normalization.
```APIDOC
## compare(left, right)
### Description
Compare two URLs and return a float indicating how similar they are. This ignores fragments and performs hard URL normalisation.
### Parameters
- **left** (str) - Required - The first URL to compare.
- **right** (str) - Required - The second URL to compare.
### Returns
- **float** - A similarity score between the two URLs.
```
--------------------------------
### Parse Compact Query String
Source: https://followthemoney.tech/docs/metadata
Convert a compact string query into a format suitable for evaluation.
```python
from followthemoney.dataset import parse_query, evaluate_query
query = parse_query("(#issuer.west|#list.sanction)-lt_fiu-#issuer.ru")
results = evaluate_query(catalog, query)
```
--------------------------------
### Entity Proxy Operations
Source: https://followthemoney.tech/python
Methods for creating, identifying, and populating entity proxies.
```APIDOC
## model.make_entity(schema)
### Description
Instantiates a new entity proxy object based on the provided schema.
## entity.make_id(*args)
### Description
Generates a safe ID for the entity based on provided arguments.
## entity.add(prop, value, fuzzy=False)
### Description
Assigns a property value to the entity, performing normalization and validation.
## entity.to_dict()
### Description
Converts the entity proxy into a plain dictionary suitable for JSON serialization.
## model.get_proxy(data)
### Description
Reconstructs an entity proxy from a dictionary representation.
```
--------------------------------
### Export entities to RDF
Source: https://followthemoney.tech/docs/cli
Convert entity streams into NTriples format.
```bash
cat us_ofac.ijson | ftm validate | ftm export-rdf > ofac.nt
```
--------------------------------
### Statement Magic Methods
Source: https://followthemoney.tech/python/statements
Standard Python magic methods for hashing, representation, equality, and comparison of Statement objects.
```python
def __hash__(self) -> int:
if self.id is None:
warnings.warn(
"Hashing a statement without an ID results in undefined behaviour",
RuntimeWarning,
)
return hash(self.id)
```
```python
def __repr__(self) -> str:
return f""
```
```python
def __eq__(self, other: Any) -> bool:
return not self.id != other.id
```
```python
def __lt__(self, other: Any) -> bool:
self_key = (self._prop != BASE_ID, self.id or "")
other_key = (other._prop != BASE_ID, other.id or "")
return self_key < other_key
```
--------------------------------
### clone(*, entity_id=None, prop=None, schema=None, value=None, dataset=None, lang=UNSET, original_value=UNSET, first_seen=UNSET, external=None, canonical_id=None, last_seen=UNSET, origin=UNSET)
Source: https://followthemoney.tech/python/statements
Creates a deep copy of the current statement, allowing for optional overrides of specific attributes.
```APIDOC
## clone
### Description
Creates a deep copy of the given statement. If any parameters are provided, they override the values from the original statement.
### Parameters
- **entity_id** (str, optional) - The ID of the entity.
- **prop** (str, optional) - The property name.
- **schema** (str, optional) - The schema name.
- **value** (str, optional) - The property value.
- **dataset** (str, optional) - The dataset name.
- **lang** (str, optional) - The language of the value.
- **original_value** (str, optional) - The original value.
- **first_seen** (str, optional) - Timestamp of first observation.
- **external** (bool, optional) - Whether the statement is external.
- **canonical_id** (str, optional) - The canonical entity ID.
- **last_seen** (str, optional) - Timestamp of last observation.
- **origin** (str, optional) - The origin of the statement.
```
--------------------------------
### Dataset Query Grammar
Source: https://followthemoney.tech/docs/metadata
The recursive structure for defining dataset queries.
```text
DatasetQuery = str | list[DatasetQuery]
| {"or": list[DatasetQuery]}
| {"and": list[DatasetQuery]}
| {"not": DatasetQuery}
```
--------------------------------
### Filter a catalog using a query
Source: https://followthemoney.tech/python/dataset
Parse a query string and evaluate it against a catalog to retrieve matching datasets.
```python
from followthemoney.dataset import evaluate_query, parse_query
query = parse_query("#list.sanction & #issuer.eu")
matched = evaluate_query(catalog, query)
```
--------------------------------
### Manage fragment origins via CLI
Source: https://followthemoney.tech/docs/fragments
Commands to write fragments with specific origins and selectively delete them from a dataset.
```bash
cat us_ofac.ijson | ftm store write -d sanctions -o us_ofac
cat eu_eeas.ijson | ftm store write -d sanctions -o eu_eeas
# Will now have entities from both source files:
ftm store iterate -d sanctions | wc -l
# Delete all fragments from the second file:
ftm store delete -d sanctions -o eu_eeas
# Only one source file is left:
ftm store iterate -d sanctions | wc -l
```
--------------------------------
### StatementEntity Class Implementation
Source: https://followthemoney.tech/python/statements
Core methods and properties for managing entity statements, including iteration, dataset tracking, and statement addition.
```python
self._caption: str | None = None
"""A pre-computed label for this entity."""
self.extra_referents: set[str] = set(data.pop("referents", []))
"""The IDs of all entities which are included in this canonical entity."""
self.last_change: str | None = data.get("last_change", None)
"""The last time this entity was changed."""
self.dataset = dataset
"""The default dataset for new statements."""
self.id: str | None = data.pop("id", None)
self._statements: dict[str, set[Statement]] = {}
properties = data.pop("properties", None)
if isinstance(properties, Mapping):
for key, value in properties.items():
self.add(key, value, cleaned=cleaned, quiet=True)
for stmt_data in data.pop("statements", []):
stmt = Statement.from_dict(stmt_data)
if self.id is not None:
stmt.canonical_id = self.id
self.add_statement(stmt)
@property
def _properties(self) -> dict[str, list[str]]: # type: ignore
return {p: [s.value for s in v] for p, v in self._statements.items()}
def _iter_stmt(self) -> Generator[Statement, None, None]:
for stmts in self._statements.values():
for stmt in stmts:
if stmt.entity_id is None and self.id is not None:
stmt = stmt.clone(entity_id=self.id)
if stmt.id is None:
stmt.id = stmt.generate_key()
yield stmt
@property
def statements(self) -> Generator[Statement, None, None]:
"""Return all statements for this entity, with extra ID statement."""
ids: list[str] = []
last_seen: set[str] = set()
first_seen: set[str] = set()
for stmt in self._iter_stmt():
yield stmt
if stmt.id is not None:
ids.append(stmt.id)
if stmt.last_seen is not None:
last_seen.add(stmt.last_seen)
if stmt.first_seen is not None:
first_seen.add(stmt.first_seen)
if self.id is not None:
digest = sha1(self.schema.name.encode(HASH_ENCODING))
for id in sorted(ids):
digest.update(id.encode(HASH_ENCODING))
checksum = digest.hexdigest()
# This is to make the last_change value stable across
# serialisation:
first = self.last_change or min(first_seen, default=None)
yield Statement(
canonical_id=self.id,
entity_id=self.id,
prop=BASE_ID,
schema=self.schema.name,
value=checksum,
dataset=self.dataset.name,
first_seen=first,
last_seen=max(last_seen, default=None),
)
@property
def first_seen(self) -> str | None:
seen = (s.first_seen for s in self._iter_stmt() if s.first_seen is not None)
return min(seen, default=None)
@property
def last_seen(self) -> str | None:
seen = (s.last_seen for s in self._iter_stmt() if s.last_seen is not None)
return max(seen, default=None)
@property
def datasets(self) -> set[str]:
datasets: set[str] = set()
for stmt in self._iter_stmt():
datasets.add(stmt.dataset)
return datasets
@property
def referents(self) -> set[str]:
referents: set[str] = set(self.extra_referents)
for stmt in self._iter_stmt():
if stmt.entity_id is not None and stmt.entity_id != self.id:
referents.add(stmt.entity_id)
return referents
@property
def key_prefix(self) -> str | None:
return self.dataset.name
@key_prefix.setter
def key_prefix(self, dataset: str | None) -> None:
raise NotImplementedError()
def add_statement(self, stmt: Statement) -> None:
schema = self.schema
if schema.name != stmt.schema and not schema.is_a(stmt.schema):
try:
self.schema = schema.model.common_schema(schema, stmt.schema)
except InvalidData as exc:
raise InvalidData(f"{self.id}: {exc}") from exc
if stmt.prop == BASE_ID:
if stmt.first_seen is not None:
# The last_change attribute describes the latest checksum change
# of any emitted component of the entity, which is stored in the BASE
# field.
if self.last_change is None:
self.last_change = stmt.first_seen
else:
self.last_change = max(self.last_change, stmt.first_seen)
else:
self._caption = None
if stmt.prop not in self._statements:
self._statements[stmt.prop] = set()
self._statements[stmt.prop].add(stmt)
```
--------------------------------
### Retrieve values by type
Source: https://followthemoney.tech/python/statements
Returns a list of values associated with a specific property type.
```python
def get_type_values(
self, type_: PropertyType, matchable: bool = False
) -> list[str]:
combined: set[str] = set()
for stmt in self.get_type_statements(type_, matchable=matchable):
combined.add(stmt.value)
return list(combined)
```
--------------------------------
### Export entities to tabular formats
Source: https://followthemoney.tech/docs/cli
Convert entity streams into Excel workbooks or directories of CSV files.
```bash
cat us_ofac.ijson | ftm validate | ftm export-excel -o OFAC.xlsx
```
```bash
cat us_ofac.ijson | ftm validate | ftm export-csv -o OFAC/
```
--------------------------------
### Retrieve statements by type
Source: https://followthemoney.tech/python/statements
Returns a list of Statement objects filtered by property type and matchability.
```python
def get_type_statements(
self, type_: PropertyType, matchable: bool = False
) -> list[Statement]:
combined: list[Statement] = []
for prop_name, statements in self._statements.items():
prop = self.schema.properties[prop_name]
# Used in performance-critical code paths:
if prop.type is not type_:
continue
if matchable and not prop.matchable:
continue
combined.extend(statements)
return combined
```
--------------------------------
### followthemoney.types.Registry.get_types(names)
Source: https://followthemoney.tech/python/registry
Retrieves a list of property type objects based on a collection of names.
```APIDOC
## get_types(names)
### Description
Returns a list of property type objects corresponding to the provided set of names.
### Parameters
- **names** (list) - Required - A list of property type names to retrieve.
```
--------------------------------
### Serialize Entity to Context Dictionary
Source: https://followthemoney.tech/python/statements
Converts the entity into a dictionary representation suitable for context, including metadata like referents, datasets, and temporal information.
```python
def to_context_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of the entity for context."""
data: dict[str, Any] = {
"id": self.id,
"caption": self.caption,
"schema": self.schema.name,
}
referents: set[str | None] = set(self.extra_referents)
datasets = set(self.datasets)
origins: set[str] = set()
first_seen = None
last_seen = None
for stmts in self._statements.values():
for stmt in stmts:
if stmt.first_seen is not None:
if first_seen is None or stmt.first_seen < first_seen:
first_seen = stmt.first_seen
if stmt.last_seen is not None:
if last_seen is None or stmt.last_seen > last_seen:
last_seen = stmt.last_seen
if stmt.entity_id is not None and stmt.entity_id != self.id:
referents.add(stmt.entity_id)
datasets.add(stmt.dataset)
if stmt.origin is not None:
origins.add(stmt.origin)
data["referents"] = list(referents)
data["datasets"] = [d for d in datasets if d != Dataset.UNDEFINED]
if origins:
data["origin"] = list(origins)
if first_seen is not None:
data["first_seen"] = first_seen
if last_seen is not None:
data["last_seen"] = last_seen
if self.last_change is not None:
data["last_change"] = self.last_change
return data
```
--------------------------------
### Serialize Entity to Dictionary
Source: https://followthemoney.tech/python/statements
Returns a full dictionary representation of the entity, including its properties.
```python
def to_dict(self) -> dict[str, Any]:
data = self.to_context_dict()
data["properties"] = self.properties
return data
```
--------------------------------
### Implement caption method for NumberType
Source: https://followthemoney.tech/explorer/types/number
This method formats a numeric string for display purposes, optionally applying a format string and appending a unit if present.
```python
def caption(self, value: str, format: str | None = None) -> str:
"""Return a caption for the number. This is used for display purposes.
Args:
value (str): The string to format.
format (Optional[str]): An optional format string to use for formatting the number.
Returns:
str: The formatted number string, possibly with a unit.
"""
number, unit = self.parse(value)
if number is None:
return value
try:
fnumber = float(number)
except ValueError:
return value
if format is not None:
number = format.format(fnumber)
elif fnumber.is_integer():
number = self._INT_FMT.format(int(fnumber))
else:
number = self._FLOAT_FMT.format(fnumber)
if unit is not None:
return f"{number} {unit}"
return number
```
--------------------------------
### Compare Sets Method
Source: https://followthemoney.tech/explorer/types/country
Compares two sets of country values and returns a score based on overlap.
```python
def compare_sets(
self,
left: Sequence[str],
right: Sequence[str],
func: Callable[[Sequence[float]], float] = max,
) -> float:
"""Compare two sets of values and select the highest-scored result."""
overlap = territories_intersect(left, right)
return 1.0 if len(overlap) else 0.0
```
--------------------------------
### Format entity streams for humans
Source: https://followthemoney.tech/docs/cli
Indents entity JSON for terminal inspection. Do not use this output as input for other ftm commands.
```bash
head -n 5 moldova.ijson | ftm pretty
```
--------------------------------
### Export entities to Cypher for Neo4J
Source: https://followthemoney.tech/docs/cli
Generate Cypher scripts for loading data into Neo4J, including options for reifying property types into nodes.
```bash
cat us_ofac.ijson | ftm export-cypher | cypher-shell -u user -p password
```
```bash
cat us_ofac.ijson | ftm export-cypher -e name -e iban -e address
```
```cypher
MATCH ()-[r:ANCESTORS]-() DELETE r;
MATCH ()-[r:PARENT]-() DELETE r;
MATCH (n:Page) DETACH DELETE n;
// Delete reified value nodes that connect to only one entity:
MATCH (n:name) WHERE size((n)--()) <= 1 DETACH DELETE (n);
MATCH (n:email) WHERE size((n)--()) <= 1 DETACH DELETE (n);
MATCH (n:address) WHERE size((n)--()) <= 1 DETACH DELETE (n);
```
```cypher
MATCH (n) DETACH DELETE n;
```
--------------------------------
### Create Entity from Dictionary
Source: https://followthemoney.tech/python/statements
Factory methods for creating a StatementEntity instance from a dictionary or raw data.
```python
@classmethod
def from_dict(
cls,
data: dict[str, Any],
cleaned: bool = True,
default_dataset: Dataset | None = None,
) -> Self:
# Exists only for backwards compatibility.
dataset = default_dataset or UndefinedDataset
return cls(dataset, data, cleaned=cleaned)
```
```python
@classmethod
def from_data(
cls,
dataset: Dataset,
data: dict[str, Any],
cleaned: bool = True,
) -> Self:
return cls(dataset, data, cleaned=cleaned)
```
--------------------------------
### Event Entity Key Configuration
Source: https://followthemoney.tech/docs/mappings
Configuration for linking entities via an event schema using a combination of participant keys.
```yaml
entities:
company:
schema: Company
keys:
- company_name
owner:
schema: Person
keys:
- owner_name
ownership:
schema: Ownership
keys:
- company_name
- owner_name
```
--------------------------------
### Compare two URLs
Source: https://followthemoney.tech/explorer/types/url
The compare method returns a similarity score between two URLs, ignoring fragments and performing normalization.
```python
def compare(self, left: str, right: str) -> float:
"""Compare two URLs and return a float indicating how similar they are. This ignores
fragments and peforms hard URL normalisation."""
return compare_urls(left, right)
```
--------------------------------
### parse(value, decimal=DECIMAL, separator=SEPARATOR)
Source: https://followthemoney.tech/explorer/types/number
Parses a string into a numeric value and a unit. The numeric value is aligned with the provided decimal and separator settings, and the unit is stripped of whitespace.
```APIDOC
## parse(value, decimal=DECIMAL, separator=SEPARATOR)
### Description
Parses a number into a numeric value and a unit. The numeric value is aligned with the decimal and separator settings. The unit is stripped of whitespace and returned as a string. If no unit or number is found, returns (None, None).
### Parameters
- **value** (str) - Required - The string to parse.
- **decimal** (str) - Optional - The character used as the decimal separator. Defaults to DECIMAL.
- **separator** (str) - Optional - The character used to separate thousands, lakhs, or crores. Defaults to SEPARATOR.
### Returns
- **tuple[str | None, str | None]** - A tuple of (number, unit), where number is a string and unit is a string or None.
```
--------------------------------
### Iterate over properties
Source: https://followthemoney.tech/python/statements
Returns a list of Property objects present in the entity.
```python
def iterprops(self) -> list[Property]:
return [self.schema.properties[p] for p in self._statements]
```
--------------------------------
### caption(value, format=None)
Source: https://followthemoney.tech/explorer/types/number
Formats a numerical string for display purposes, optionally applying a specific format string and preserving units.
```APIDOC
## Method: caption(value, format=None)
### Description
Returns a caption for the number. This is used for display purposes.
### Parameters
- **value** (str) - Required - The string to format.
- **format** (Optional[str]) - Optional - An optional format string to use for formatting the number.
### Returns
- **str** - The formatted number string, possibly with a unit.
```
--------------------------------
### Dump schema model
Source: https://followthemoney.tech/docs/cli
Exports the full FtM schema definition to a JSON file.
```bash
ftm dump-model -o model.json
```