Skip to content

Sanctions crawler examples

Short synthetic crawlers that show the rules in developing sanctions crawlers working together.

Consolidated list with programs

A JSON API listing each designation with labelled names and one or more regimes.

names:
  schema_rules:
    LegalEntity:
      # Set from studying the source's names.
      reject_chars: "()"

lookups:
  type:
    options:
      - match: Individual
        value: Person
      - match: Entity
        value: LegalEntity
      - match: Ship
        value: Vessel
  name_type:
    options:
      - match: Primary name
        value: name
      - match: AKA
        value: alias
      - match: Low quality AKA
        value: weakAlias
      - match: FKA
        value: previousName
  sanction.program:
    options:
      - match: The Russia (Sanctions) (EU Exit) Regulations 2019
        value: GB-RUS
      - match: The Cyber (Sanctions) (EU Exit) Regulations 2020
        value: GB-CYB
  sanction.measure:
    options:
      - match: Asset freeze
        value: Asset freeze
      - match: Travel ban
        value: Travel ban
      - match: Trust services sanctions
        value: Services ban
from typing import Any

from zavod import Context
from zavod import helpers as h
from zavod.stateful.review import assert_all_accepted


def crawl_designation(context: Context, item: dict[str, Any]) -> None:
    schema = context.lookup_value("type", item.pop("type"), warn_unmatched=True)
    if schema is None:
        return
    entity = context.make(schema)
    entity.id = context.make_slug(item.pop("id"))

    names = h.Names()
    for name in item.pop("names"):
        prop = context.lookup_value("name_type", name["type"], warn_unmatched=True)
        if prop is None:
            return
        names.add(prop, name["value"])
    h.apply_reviewed_names(context, entity, original=names)

    dob = item.pop("date_of_birth")
    imo_number = item.pop("imo_number")
    flag = item.pop("flag")
    if entity.schema.is_a("Person"):
        h.apply_date(entity, "birthDate", dob)
    if entity.schema.is_a("Vessel"):
        entity.add("imoNumber", imo_number)
        entity.add("flag", flag)

    for regime in item.pop("regimes"):
        program = regime.pop("name")
        sanction = h.make_sanction(
            context,
            entity,
            key=program,
            program_name=program,
            source_program_key=program,
            program_key=h.lookup_sanction_program_key(context, program),
        )
        h.apply_date(sanction, "listingDate", regime.pop("date_published"))
        h.apply_date(sanction, "startDate", regime.pop("date_in_force"))
        for measure in regime.pop("measures"):
            sanction.add(
                "provisions",
                context.lookup_value("sanction.measure", measure, warn_unmatched=True),
            )
        context.audit_data(regime)
        context.emit(sanction)

    entity.add("topics", "sanction")
    context.emit(entity)
    context.audit_data(item)


def crawl(context: Context) -> None:
    data = context.fetch_json(context.data_url)
    for item in data["designations"]:
        crawl_designation(context, item)
    assert_all_accepted(context, raise_on_unaccepted=False)

Single national list

A spreadsheet with no type column, no reliable ID, and de-listed rows still present. The list is one piece, so it sets no program.

from python_calamine import CalamineWorkbook
from rigour.mime.types import XLSX

from zavod import Context
from zavod import helpers as h
from zavod.stateful.review import assert_all_accepted


def crawl_row(context: Context, row: dict[str, str | None]) -> None:
    if row.pop("statut") == "RadiƩ":
        context.log.info("Skipping de-listed entry", row=row)
        return
    name = row.pop("nom")
    dob = row.pop("date_de_naissance")
    nationality = row.pop("nationalite")

    entity = context.make("Person" if dob is not None else "LegalEntity")
    entity.id = context.make_id(name, dob, nationality)
    h.apply_reviewed_names(context, entity, original=h.Names(name=name), lang="fra")
    if entity.schema.is_a("Person"):
        h.apply_date(entity, "birthDate", dob)
        entity.add("nationality", nationality)
    else:
        entity.add("country", nationality)

    sanction = h.make_sanction(context, entity)
    sanction.add("reason", row.pop("motif"), lang="fra")
    h.apply_date(sanction, "listingDate", row.pop("date_inscription"))

    entity.add("topics", "sanction")
    context.emit(sanction)
    context.emit(entity)
    context.audit_data(row)


def crawl(context: Context) -> None:
    path = context.fetch_resource("source.xlsx", context.data_url)
    context.export_resource(path, XLSX, title=context.SOURCE_TITLE)
    workbook = CalamineWorkbook.from_path(path)
    for row in h.parse_xlsx_sheet(context, workbook.get_sheet_by_index(0)):
        crawl_row(context, row)
    assert_all_accepted(context, raise_on_unaccepted=False)

Linked parties

Owners, operators and wallets named alongside a designated entity.

from zavod import Context, Entity
from zavod import helpers as h


def crawl_linked(context: Context, entity: Entity, row: dict[str, str | None]) -> None:
    owner_name = row.pop("parent_company")
    if owner_name is not None:
        owner = context.make("Organization")
        owner.id = context.make_id("owner", owner_name)
        owner.add("name", owner_name)
        context.emit(owner)

        entity.add_schema("Company")
        ownership = context.make("Ownership")
        ownership.id = context.make_id(owner.id, "owns", entity.id)
        ownership.add("owner", owner)
        ownership.add("asset", entity)
        context.emit(ownership)

    operator_name = row.pop("previous_operator")
    if operator_name is not None:
        operator = context.make("Organization")
        operator.id = context.make_id("operator", operator_name)
        operator.add("name", operator_name)
        context.emit(operator)

        link = context.make("UnknownLink")
        link.id = context.make_id(operator.id, "operated", entity.id)
        link.add("subject", operator)
        link.add("object", entity)
        link.add("role", "Previous operator")
        context.emit(link)

    for address, currency in h.extract_cryptos(row.pop("other_information")).items():
        wallet = context.make("CryptoWallet")
        wallet.id = context.make_id(address)
        wallet.add("publicKey", address)
        wallet.add("currency", currency)
        wallet.add("holder", entity)
        context.emit(wallet)