PortWatch_ports_database
FeatureServer/0/queryPort catalog: provider portid, name, country, coordinates, LOCODE,
vessel-count profile, industry profile, and national maritime import and export shares.
Ekho-Labs / airflow-dags · open draft
Three public IMF PortWatch ArcGIS layers become five audited Mongo collections behind exact LOCODE identity, resumable monthly backfill, and two Airflow DAGs that ship paused. Every value is an IMF PortWatch AIS-derived estimate - not a raw AIS message, a customs declaration, or a port-authority measurement.
Three public, unauthenticated ArcGIS FeatureServer layers. The collector requests all fields,
no geometry, in ascending ObjectId order with pages of at most 1,000 records. Field names define the
stored groups; this integration assigns and converts no units beyond the provider's published schema.
Port catalog: provider portid, name, country, coordinates, LOCODE,
vessel-count profile, industry profile, and national maritime import and export shares.
Daily port calls, imports and exports, split into container, dry bulk, general cargo, RoRo, tanker, cargo aggregate, and total fields.
Daily chokepoint vessel counts and capacity, split into the same vessel classes plus cargo aggregate and total fields. Also serves the distinct identity query used to validate canal overrides.
data.global-port-traffic-signals or data.global-port-traffic-latest.A run enters at the left, is typed and gated in the middle, and lands in five named Mongo collections at the right. Hover or focus a block to trace its whole upstream and downstream; click to pin, Escape to release.
A run enters at stage 01 and lands in five named Mongo collections at stage 05. Edge overlays are drawn at 1240px and wider; here the stages read in numbered order. Hover or focus a block to trace it; click to pin, Escape to release.
01 Entry
02 Public source
03 Boundary
04 Collection
05 Store · database data
The two streams deliberately compute their checkpoint identity differently, because a change to the monitored-port roster means something for one stream and nothing for the other.
Daily ports roster-sensitive
portid set, so an unmatched or ambiguous LOCODE contributes nothing.Daily chokepoints roster-independent
Both streams share the same natural key, (portid, date), and the same resume rule: a
checkpoint carries the schema fingerprint it was collected under, and a resumed window whose required provider
schema has drifted raises rather than silently mixing two shapes.
The runtime writes only these five collections, all in database data. These are also the
downstream and operator read paths.
| Collection | Natural key and indexes | Purpose |
|---|---|---|
| portwatch-ports | unique portid | Current provider catalog with provenance and schema fingerprint. |
| portwatch-daily-ports | unique compound (portid, date) | AIS-derived daily port estimates for exactly matched monitored ports. |
| portwatch-daily-chokepoints | unique compound (portid, date) | AIS-derived daily chokepoint estimates, independent of the monitored-port roster. |
| portwatch-port-mappings | unique mapping_key; compound (status, normalized_locode) | Exact LOCODE mapping outcomes and roster snapshot identity. |
| portwatch-backfill-checkpoints | unique checkpoint_id; compound (stream, window_start, window_end); compound (status, updated_at desc) | Cursor, counts, mapping snapshot, schema fingerprint and completion state. |
This table is wider than the screen. Scroll it sideways, or focus it and use the arrow keys.
There are no TTL indexes. Catalog, daily rows, mappings and checkpoints remain until a separately reviewed retention change. Documents carry provider, methodology, source dataset, source URL, provider object id, schema fingerprint, provenance, a content hash, and first- and last-seen timestamps.
The collector reads distinct monitored LOCODE values, trims outer whitespace, uppercases, removes all remaining whitespace, and accepts only a five-character alphanumeric result. It then performs an exact normalized match against the catalog. Runtime never fuzzy-matches.
Every roster value gets exactly one outcome
portid becomes eligible for daily port collection.The complete outcome roster, including ambiguous candidates and override provenance, is written to
portwatch-port-mappings on a live run.
Normalization, in order
The same normalization is applied to catalog LOCODE values before comparison, so both
sides of the match are normalized identically. There is no similarity threshold and no fallback.
Audited provider identities 11 catalog identity mismatches or duplicates, plus 2 monitored canals
| Monitored LOCODE | Provider identity | Expected name | Validated against |
|---|---|---|---|
| CNNGB | port824 | Ningbo | catalog |
| CNNSA | port425 | Guangzhou (Nansha) | catalog |
| CNSHA | port1188 | Shanghai | catalog |
| CNTAO | port1069 | Qingdao Port | catalog |
| CNXMN | port1404 | Xiamen | catalog |
| EGPSE | port192 | Port Said | catalog |
| MAPTM | port1265 | Tangier-Mediterranean | catalog |
| MYPKG | port960 | Port Klang | catalog |
| SGSIN | port1201 | Singapore | catalog |
| USNWK | port815 | New York-New Jersey | catalog |
| VNHPH | port434 | Hai Phong | catalog |
| EGSUZ | chokepoint1 | Suez Canal | live distinct identities |
| PAPAN | chokepoint2 | Panama Canal | live distinct identities |
This table is wider than the screen. Scroll it sideways, or focus it and use the arrow keys.
An override resolves only when both the provider id and the expected name are present. If either drifts, the outcome falls through and no provider entity is selected - collection fails closed rather than binding a monitored location to the wrong entity. The two canal mappings are validated against chokepoint history, not daily-port history.
Both DAGs are created paused, allow one active run, and push the same scraper metric grouping on success and failure.
imf-portwatch-backfill manual only
collect-imf-portwatch-history2019-01-01; end_date defaults to the run's data interval end. The range is divided into calendar-month windows.imf-portwatch-daily 0 3 * * *
collect-imf-portwatch-dailyBackfillRequired with missing port and chokepoint window counts instead of silently creating partial history.Keep imf-portwatch-backfill paused after promotion: it is a manual recovery and history
tool, not a schedule.
Collection and backfill were completed by direct runtime execution, not by deployed Airflow DAG runs. Readings below are the final Mongo statistics after that rollout.
portwatch-daily-ports rows over 48 entities, 2019-01-01 through 2026-08-07, exactly 2,776 rows per entity.portwatch-daily-chokepoints rows over 28 entities, 2019-01-01 through 2026-08-09, exactly 2,778 rows per entity.(portid, date) natural keys in the daily-port and daily-chokepoint streams.Rollout gates, in the order they were exercised
Verification recorded 2026-08-17
Four findings raised in review and resolved on this head, and three risks that remain open at merge time. Severity is what the issue could have cost, not how hard it was to fix. Filter by status and by severity; a finding must satisfy both filters to stay visible.
Showing all 7 findings.
dry_run of false would have reached the callable as a non-empty string, which is truthy, so a run requested as a dry run would have created indexes and written catalog, mappings, daily rows and checkpoints.chokepoint1 and chokepoint2. A constant alone would keep resolving even if the provider reassigned or renamed those identities, silently attributing one canal's traffic to another.Daily_Chokepoints_Data identity request returns that exact provider id and the expected name. On any drift the outcome falls through and no provider entity is selected.runtime.py, runtime_types.py and window_collection.py; storage into storage.py, storage_contract.py, storage_checkpoints.py and storage_writes.py. Every module in the package now sits under the 250 pure-LOC ceiling, the largest being the mapping module.data.global-port-traffic-signals or data.global-port-traffic-latest. This is a deliberate exclusion, not an oversight.portwatch-* collections directly and interpret imports, exports, capacity and share values against the provider's own published methodology.No finding matches both filters. Re-enable a status or a severity to see the rest.
29 files, all additions. Grouped by the job each group does, not by directory.
Provider boundary
Make untyped ArcGIS JSON safe to store
Frozen models keep every provider field and alias verbatim while requiring the handful the
collector depends on. The client owns pagination, the half-open date predicate, the strictly-increasing
ObjectId assertion, and the required-field fingerprint that refuses a drifted or error-enveloped
payload.
Identity
Decide what a monitored LOCODE means, exactly
Normalization, exact matching, the four outcome statuses, the audited port and canal overrides, the roster snapshot hash used by checkpoint identity, and the matched-portid projection that bounds the daily-port query.
Windowed collection
Make a multi-year backfill resumable and bounded
Per-stream window collection with keyset pagination, running and completed checkpoints, request and record budgets, the schema-drift resume guard, and the readiness rule that stops the daily DAG from manufacturing partial history.
Persistence
One write surface, unique natural keys
Collection names, index creation, the checkpoint dataclass and its identity rule, checkpoint load and save, and idempotent upserts that attach provenance, source dataset, source URL, schema fingerprint and a content hash to every document.
Read-only reporting
Prove coverage without a write path
A single read-only callable that reports the latest roster's four status counts, mapped ports and canals with and without history, row and entity coverage, minimum and maximum dates, and duplicate natural keys per stream. It is scoped to the latest roster snapshot so an older roster cannot inflate the reading.
Airflow entry points
Schedule the runtime, paused, with metrics
Two DAG modules that construct the internal Mongo URI, push the same scraper metric grouping on success and failure, ship paused, and - on the backfill side - render parameters as native objects and reject a non-boolean dry-run flag.
Live-payload tests
Lock the contract against real payloads
Contract, regression, storage, statistics, override and DAG suites covering half-open windows, keyset ordering, the HTTP-200 error envelope, additive-versus-required schema drift, roster-sensitive versus roster-independent checkpoint identity, natural-key uniqueness without TTL indexes, and the latest-roster statistics scope.
Live fixtures
Real provider responses, with their queries
Five captured payloads plus a README that records the exact public query behind each one, so any fixture can be re-captured and diffed against the provider.
Operations
One runbook for the whole integration
Source, mapping, Mongo, DAG and statistics contracts, the recorded rollout evidence, the exact verification commands, the remaining promotion steps, and the exclusions - in one document.
Merge candidate
pending CI and deployment
The data contract is proven; the operational one is not
The provider boundary, identity rules, checkpoint semantics and persistence are exercised against real payloads and against a completed 211,032-record backfill with zero duplicate natural keys. What has not happened is an Airflow deployment: both DAGs are checked in paused and have never been seen by a scheduler, and no required check has reported on this head while the pull request is still a draft.
Remaining promotion
imf-portwatch-daily only. Leave imf-portwatch-backfill paused.