Class: ActiveSanction::Sources::EuFsf
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/sources/eu_fsf.rb,
lib/active_sanction/sources/eu_fsf/record.rb
Overview
The EU Consolidated Financial Sanctions List: every person and entity subject to an EU financial sanction, in one document, as the Financial Sanctions Files (FSF) export.
snapshot = ActiveSanction::Sources[:eu_fsf].new.sync
One file, one record shape, ten times the size
https://webgate.ec.europa.eu/fsd/fsf/public/files/xmlFullSanctionsList_1_1/content
<export><sanctionEntity> 6,234 records, 25.7 MB
An order of magnitude larger than the UN's 2.2 MB or Canada's 2.9 MB, and the first list to actually exercise the streaming interface the XML toolkit was built around. It reads in one pass at roughly 90 MB of resident memory, most of which is the payload itself; nothing here ever holds two records at once.
The token in the URL
The public endpoint takes a token query parameter. Omitting it answers
403 and a wrong one answers 500, so it is not optional -- but it is also
not a credential: dG9rZW4tMjAxNw is base64 for token-2017, it is the
value the Commission's own public download page has carried since that
year, and it is the same string for every caller. It is declared inline
rather than configured, the way any other part of a published URL is.
If the Commission ever rotates it, no release of this gem is needed:
ActiveSanction::Sources::EuFsf.token = "..." # or
ActiveSanction::Sources::EuFsf.url :main, "https://..."
Conditional GET does not work here, and that is the publisher's doing
The endpoint serves Last-Modified but answers If-Modified-Since with
200 and the whole file, and sends no ETag and Cache-Control: no-store. So every sync of this list downloads 25.7 MB where the other
four usually download nothing. The conditional request is still sent --
it costs nothing and the day the Commission honours it, it works -- and
the snapshot checksum is unchanged when the content is, so a re-download
of identical bytes still diffs (#35) to nothing.
The two traps in this list
No name is the name. The EU marks no <nameAlias> as the official
one: all 31,053 carry strong="true", and every other candidate signal
is wrong somewhere in the file. Document order files Qusay Hussein's
French transliteration ahead of his English name; nameLanguage files a
Cyrillic spelling of Anatoliy Sidorov's name under EN; ordering by
logicalId picks a non-Latin name for 3,203 of the 5,502 multi-name
records. So Record picks one by a stated rule -- see #primary_name -- and
what "primary" means for this source is the first name the EU published
that the EU did not itself annotate as an alias, which is weaker than
what it means for OFAC. It costs nothing in score: the scorer takes the
best of an entity's names and the kind only reaches the reason line.
Four birth dates are not in the Gregorian calendar. calendarType
is ISLAMIC on four records, where year, monthOfYear and
dayOfMonth hold a Hijri date -- year="1343" for a man born in 1964.
Read as published those become dates in the fourteenth century, and a
fourteenth-century date does not merely fail to match a real one, it
conflicts with it, and the scorer penalizes the record. Three of the
four carry no Gregorian equivalent at all, so they produce no date of
birth and the published Hijri date goes to remarks instead.
What a clean EU result is worth
More than a Canadian one and less than an OFAC one. 3,013 identification
documents across 6,234 records is real corroborating signal, and 2,764
citizenships arrive as ISO codes rather than as prose. What is thin is
the alias grading: the EU publishes no quality column, and the 499
gradings this adapter does read are prose the Commission happened to put
in a <remark> -- so most EU aliases arrive ungraded, which the scorer
correctly treats as unstated rather than as good.
What this adapter does not do
It does not infer a vessel. subjectType publishes only person and
enterprise, and the 41 records carrying an imo document are shipping
companies holding IMO company numbers -- Chongchongang Shipping, Korea
Ansan Shipping -- not the ships themselves. Typing those as :vessel on
the strength of the document kind would be a guess that hides them from
every organization search.
Instance Attribute Summary collapse
-
#warnings ⇒ Array<Parsers::Warning>
readonly
Records that could not be used, and fields that could not be read.
Attributes inherited from Base
#cache, #fetcher, #instrumenter, #logger
Class Method Summary collapse
-
.token=(value) ⇒ String
Re-points the list at the same endpoint with a different token, for the day the Commission rotates the one baked in above.
Instance Method Summary collapse
- #initialize(*args, **options) ⇒ void constructor
- #parse(raw) ⇒ Array<Entity>
- #source_version ⇒ String?
Methods inherited from Base
#authority, #column_shapes, #column_tallies, #file_key, #floors, #format, #fresh?, #inspect, #jurisdiction, #key, #parse_argument, published_remarks, #retrieve, #snapshot, #stale?, #sync, #url, #urls
Methods included from Definition
#authority, #declarations, #declared?, #declared_floors, #declared_urls, #file_key, #floor, #floors, #format, #jurisdiction, #key, key!, #licence_notice, #licence_url, #multi_url?, #to_h, #url, #urls
Constructor Details
Instance Attribute Details
#warnings ⇒ Array<Parsers::Warning> (readonly)
Records that could not be used, and fields that could not be read. Read after #parse; sync orchestration (#34) reports them.
151 152 153 |
# File 'lib/active_sanction/sources/eu_fsf.rb', line 151 def warnings @warnings end |
Class Method Details
.token=(value) ⇒ String
Re-points the list at the same endpoint with a different token, for the day the Commission rotates the one baked in above.
ActiveSanction::Sources::EuFsf.token = "..."
144 145 146 |
# File 'lib/active_sanction/sources/eu_fsf.rb', line 144 def self.token=(value) url :main, "#{ENDPOINT}?token=#{value}" end |
Instance Method Details
#parse(raw) ⇒ Array<Entity>
162 163 164 165 166 167 168 |
# File 'lib/active_sanction/sources/eu_fsf.rb', line 162 def parse(raw) reader = LIST.read(raw) entities = build(reader) @generated_at = reader.root[GENERATED_AT] @warnings = reader.warnings + @unmapped entities end |
#source_version ⇒ String?
171 |
# File 'lib/active_sanction/sources/eu_fsf.rb', line 171 def source_version = @generated_at || super |