This document explains how datasource mappers are discovered, how DataSourceABCImpl runs them, which functions you can implement in a mapper class, and in which order those functions are called.
For a complete per-method reference organized by ETL phase (signatures, defaults, use cases), see lifecycle-methods-reference.md.
Main files:
The pipeline creates a DataSourceMapper instance and passes in the datasource list from config.yaml.
For each enabled datasource:
class_name is read from config.data_mappers/{class_name}Mapper.py.{ClassName}Mapper.DataSourceABCImpl.Examples:
class_name in config |
Expected file | Expected class |
|---|---|---|
weather |
data_mappers/weatherMapper.py |
WeatherMapper |
pleasantBicycling |
data_mappers/pleasantBicyclingMapper.py |
PleasantBicyclingMapper |
tree |
data_mappers/treeMapper.py |
TreeMapper |
DataSourceABCImpl doesDataSourceABCImpl is the base ETL implementation for almost every datasource in this project. It already provides:
Most mapper classes only implement the parts specific to their dataset.
DataSourceABCImpl ships a format-aware _auto_read() method that handles common file types automatically. You do not need to override read_file_content() for these formats — just set response_type in the source config and the base class reads the file for you.
response_type |
Library used | Returns | Notes |
|---|---|---|---|
gpkg |
geopandas | list[dict] |
Geometry column dropped; use override if you need WKB/WKT |
shp |
geopandas | list[dict] |
Same as above |
geojson |
geopandas | list[dict] |
Same as above |
parquet |
pandas | list[dict] |
Columnar; columns become dict keys |
csv |
pandas | list[dict] |
Comma-separated |
tsv |
pandas | list[dict] |
Tab-separated |
xlsx / xls |
pandas | list[dict] |
First sheet |
json |
orjson (fallback: stdlib json) | dict or list |
Raw parse; source_filter() can reshape |
gz, zip, xml, pbf |
— | falls through to FileHandler | Use a mapper override for these |
Extension detection priority: source.response_type config value → actual file suffix.
For compound types like json.gz, the last segment wins (gz) — the outer container is handled by FileHandler or a mapper override.
reader: config blockFor spatial formats you can pass engine and CRS hints without touching Python:
source:
response_type: gpkg
reader:
engine: pyogrio # "pyogrio" (default) or "fiona"
target_crs: 25833 # EPSG code; auto-reprojects if source CRS differs
If reader: is omitted, engine defaults to "pyogrio" and no reprojection is applied.
A CSV datasource with no mapper override:
datasources:
- name: my_stops
enable: true
class_name: myStops # data_mappers/myStopsMapper.py with no read_file_content override
source:
fetch: http
url: "https://example.com/stops.csv"
response_type: csv
destination: "tmp/stops/stops.csv"
storage:
staging: {table_name: stops_staging, table_schema: test_osm_base_graph}
enrichment: {table_name: stops_enrichment, table_schema: test_osm_base_graph}
mapping:
enable: true
strategy: {type: knn}
table_name: stops_mapping
table_schema: test_osm_base_graph
The mapper file only needs to exist and inherit from DataSourceABCImpl:
from main_core.data_source_abc_impl import DataSourceABCImpl
class MyStopsMapper(DataSourceABCImpl):
pass # _auto_read handles csv; no read_file_content needed
source:
response_type: gpkg
reader:
engine: pyogrio
target_crs: 25833
destination: "tmp/my_layer/data.gpkg"
class MyLayerMapper(DataSourceABCImpl):
pass # gpd.read_file() + reproject to EPSG:25833 happens automatically
read_file_content()Override when:
gz, zip, xml, pbf)class MyComplexMapper(DataSourceABCImpl):
def read_file_content(self, path: str) -> list[dict]:
# custom logic here
...
For formats not handled by the built-in reader, the only method you need is read_file_content().
from main_core.data_source_abc_impl import DataSourceABCImpl
class ExampleMapper(DataSourceABCImpl):
def read_file_content(self, path: str):
# Read one file and return either:
# - list[dict] ← preferred
# - dict
# - str
return [{"id": 1, "value": "example"}]
If the datasource also needs filtering, staging SQL, enrichment SQL, or mapping SQL, you override the corresponding hook.
This is the actual execution order used by run().
When the mapper object is instantiated:
__init__()run() is called immediatelyHigh-level order:
start_execution()_mark_metadata_run_started()execute_run_pipeline()extract()source()prepare_run_resources()process_extracted_paths()finalize_after_file_processing()run_job_response()_mark_metadata_run_finished()run_end_cleanup()For each extracted path:
before_process_file(path)transform(path)read_files(path)read_file_content(path)before_filter_pipeline(data, path)pre_filter_processing(data)source_filter(data)post_filter_processing(data)after_filter_pipeline(data, path)should_load_transformed_data(transformed_data, path)load(data)before_load(data)pre_database_processing()after_load(data)after_process_file(path, transformed_data)After all files are processed:
post_database_processing()sync_raw_to_staging()create_indexes_for_table("staging")execute_on_staging()staging_db_query()sync_staging_to_enrichment()create_indexes_for_table("enrichment")execute_on_enrichment()enrichment_db_query()map_to_base()execute_mapping_strategy()create_indexes_for_table("mapping")after_datasource_success()trigger_materialized_views()cleanup_after_finalize(sync_result)clean_raw_staging_table(...)These attributes are always available on self inside any hook:
# Logger (prefixed with datasource name)
self.logger.info(msg)
self.logger.warning(msg)
self.logger.error(msg, exc_info=True) # includes traceback
self.logger.debug(msg)
# Database
self.db.bulk_insert(table_name, schema, records_list, upsert=True)
self.db.call_sql(sql_string)
self.db.call_sql_batched(sql_string, batch_size=10_000)
self.db.get_table_count(table_name, schema) # → int
self.db.table_exists(table_name, schema) # → bool
# Config — table names and schema for each stage
self.data_source_config.storage.staging.table_name
self.data_source_config.storage.staging.table_schema
self.data_source_config.storage.enrichment.table_name
self.data_source_config.storage.enrichment.table_schema
self.data_source_config.mapping.table_name
self.data_source_config.mapping.table_schema
# Datasource identity
self.data_source_name # "weather_station_bright_sky"
self.job_configuration # JobConfigurationDTO
# Metadata service
self.metadata_service.has_completed_successfully(datasource_name) # → bool
self.metadata_service.is_dataset_expired(datasource_name, expires_after) # → bool
# All other datasource configs (for cross-datasource dependencies)
self.peer_configs # dict[name → DataSourceDTO]
These are the main extension points in DataSourceABCImpl.
| Method | Signature | When to override |
|---|---|---|
read_file_content(path) |
(str) → list[dict] |
Custom/binary format (gz, zip, xml, pbf); WKB geometry needed |
source_filter(data) |
(list\|dict) → list[dict] |
Flatten nested JSON, filter rows, add derived fields |
before_filter_pipeline(data, path) |
(list, str) → None |
Per-file setup before any filtering |
pre_filter_processing(data) |
(list) → None |
Build in-memory index (KDTree) before source_filter |
post_filter_processing(data) |
(list) → None |
Post-filter validation or file export |
after_filter_pipeline(data, path) |
(list, str) → None |
Per-file metrics, progress counter |
should_load_transformed_data(data, path) |
(list, str) → bool |
Return False to skip DB insert for this file |
| Method | Signature | When to override |
|---|---|---|
before_process_file(path) |
(str) → None |
Per-file setup before transform() |
after_process_file(path, data) |
(str, list) → None |
Per-file cleanup after load() |
on_process_file_error(path, error) |
(str, Exception) → None |
Custom error handling or file quarantine |
| Method | Signature | When to override |
|---|---|---|
before_load(data) |
(list) → None |
Final row mutation or validation before bulk insert |
pre_database_processing() |
() → None |
DB-side prep before bulk insert |
after_load(data) |
(list) → None |
Stats, reset per-file state |
post_database_processing() |
() → None |
Flush in-memory results accumulated across all files |
load(data) |
(list) → None |
Override entirely to skip DB (keep data in memory) |
| Method | Signature | When to override |
|---|---|---|
staging_db_query() |
() → str \| None |
Return SQL to run after raw→staging sync; None skips stage |
sync_raw_to_staging() |
() → dict |
Override for custom deduplication or aggregation on sync |
def staging_db_query(self) -> str | None:
stg = self.data_source_config.storage.staging
return f"""
UPDATE {stg.table_schema}.{stg.table_name}
SET geom_4326 = ST_SetSRID(ST_MakePoint(lon, lat), 4326)
WHERE lon IS NOT NULL AND geom_4326 IS NULL
"""
| Method | Signature | When to override |
|---|---|---|
enrichment_db_query() |
() → str \| None |
Return SQL to run after staging→enrichment sync; None skips stage |
sync_staging_to_enrichment() |
override | Custom aggregation on sync (hourly rollup, spatial grid) |
# Real example from weatherStationMapper.py
def enrichment_db_query(self) -> str | None:
staging = self.data_source_config.storage.staging
enrichment = self.data_source_config.storage.enrichment
return f"""
UPDATE {enrichment.table_schema}.{enrichment.table_name} e
SET point = ST_SetSRID(ST_MakePoint(s.lon, s.lat), 4326)
FROM {staging.table_schema}.{staging.table_name} s
WHERE e.dwd_station_id = s.dwd_station_id
AND e.point IS NULL
"""
# Real example from airQualityDataMapper.py — CRS transform
def enrichment_db_query(self) -> str | None:
enrichment = self.data_source_config.storage.enrichment
return f"""
UPDATE {enrichment.table_schema}.{enrichment.table_name}
SET geom_4326 = ST_Transform(geom_25833, 4326)
WHERE geom_25833 IS NOT NULL AND geom_4326 IS NULL
"""
| Method | Signature | When to override |
|---|---|---|
mapping_db_query() |
() → str \| None |
Full INSERT…SELECT for strategy.type: custom; None skips |
def mapping_db_query(self) -> str | None:
enr = self.data_source_config.storage.enrichment
m = self.data_source_config.mapping
return f"""
INSERT INTO {m.table_schema}.{m.table_name}
(way_id, dwd_station_id, distance)
SELECT DISTINCT ON (e.dwd_station_id)
w.id,
e.dwd_station_id,
ST_Distance(w.geometry_25833, e.point::geometry) AS distance
FROM {enr.table_schema}.{enr.table_name} e
CROSS JOIN LATERAL (
SELECT id, geometry_25833
FROM {m.table_schema}.ways_base
ORDER BY geometry_25833 <-> e.point::geometry
LIMIT 1
) w
ON CONFLICT (way_id) DO UPDATE SET
dwd_station_id = EXCLUDED.dwd_station_id,
distance = EXCLUDED.distance
"""
| Method | Signature | When to override |
|---|---|---|
run_end_cleanup(succeeded, error) |
(bool, Exception\|None) → None |
Always fires — temp file cleanup, memory release |
check_before_update() |
() → bool |
Return False to abort run before extraction starts |
after_datasource_success() |
() → None |
Extra success-side effects (notify external system) |
on_run_error(error) |
(Exception) → None |
Custom run-level error reporting |
prepare_run_resources(paths) |
(list) → None |
Extra setup before file processing starts |
get_process_file_worker_count() |
() → int |
Control thread pool parallelism |
Built-in mapping modes in data_source_abc_impl.py:
custom — calls mapper’s mapping_db_query() (full SQL control)sql_template — uses mapping.config.sql template with {mapping_table}, {enrichment_table} placeholdersnone — skips mapping entirelyknn / nearest_neighbour — nearest road segment per featurewithin_distance — all features within a buffer radiusintersection — spatially intersecting featuresnearest_k / knn_multiple — K nearest road segments per featureaggregate_within_distance / buffer_aggregate — aggregate features within buffer per roadattribute_join / id_join — standard SQL JOIN on a shared column (no geometry)# nearest_k — 5 nearest stations per road
mapping:
strategy:
type: nearest_k
k: 5
base_geometry_column: geometry
enrichment_geometry_column: point
order_by_sql: "ST_Distance({base_geometry}::geography, {enrichment_geometry}::geography)"
# aggregate_within_distance — all trees within 50 m per road
mapping:
strategy:
type: aggregate_within_distance
max_distance: 50
aggregation_type: jsonb_agg
aggregation_column: tree_id
aggregation_alias: nearby_trees
base_geometry_column: geometry_25833
enrichment_geometry_column: geometry_25833
# attribute_join — join on shared OSM id column
mapping:
strategy:
type: attribute_join
link_on:
base_column: osm_id
mapping_column: external_osm_id
join_type: INNER
Use this order when adding a new datasource mapper:
data_mappers/.DataSourceABCImpl.read_file_content().source_filter() if the source payload is not already row-shaped.staging_db_query() if staging needs normalization SQL.enrichment_db_query() if enrichment needs derived columns or geometry logic.mapping_db_query() with mapping.strategy.type: custom, or configure mapping.strategy.type: sql_template if mapping to ways_base is required.run_end_cleanup() if files or temporary artifacts must be removed.config.yaml.sequenceDiagram
participant A as Application
participant DSM as DataSourceMapper
participant M as Custom Mapper
participant B as DataSourceABCImpl
participant FH as File/HTTP Handlers
participant DB as Database
participant MAP as Mapping Strategy
participant MV as Materialized Views
A->>DSM: start_execution()
DSM->>DSM: import mapper module from class_name
DSM->>M: instantiate mapper
M->>B: __init__()
alt scheduler missing
B->>B: run()
else scheduler enabled
B->>B: create_job()
end
B->>B: start_execution()
B->>B: extract()
B->>B: source()
B->>FH: fetch / multi_fetch / local path resolve
B->>DB: create_data_tables()
loop each extracted file
B->>B: before_process_file()
B->>B: transform()
B->>FH: read_files()
FH->>M: read_file_content()
B->>M: before_filter_pipeline()
B->>M: pre_filter_processing()
B->>M: source_filter()
B->>M: post_filter_processing()
B->>M: after_filter_pipeline()
B->>M: should_load_transformed_data()
alt load allowed
B->>M: before_load()
B->>M: pre_database_processing()
B->>DB: bulk_insert(raw_staging)
B->>M: after_load()
end
B->>M: after_process_file()
end
B->>M: post_database_processing()
B->>DB: sync raw -> staging
B->>M: staging_db_query()
B->>DB: execute staging SQL
B->>DB: sync staging -> enrichment
B->>M: enrichment_db_query()
B->>DB: execute enrichment SQL
B->>B: map_to_base()
B->>MAP: execute mapping strategy
B->>MV: trigger materialized views
B->>M: run_end_cleanup()
source_filter() to flatten per-station weather arrayssource_filter() and enrichment_db_query() to build point geometryread_file_content() (gzip JSON) and enrichment_db_query() (CRS transform)pre_filter_processing() (raster sampling) and post_database_processing() (flush metrics)read_file_content(); uses enrichment_operators in YAML instead of enrichment_db_query()DataSourceMapper filters out disabled datasources before import.process_file() uses a thread pool by default.mapping.enable is true.Shows the exact call order from run() to materialized view refresh, including multi-file branching and thread pool processing:
sequenceDiagram
participant S as Scheduler/Caller
participant D as DataSourceABCImpl.run()
participant M as metadata_service
participant H as HttpHandler/FileHandler
participant T as ThreadPool (process_file)
participant DB as Database
participant BG as BaseGraph/Mapping
participant MV as MaterializedViewManager
S->>D: run()
D->>D: start_execution()
D->>M: mark_run_started()
D->>D: execute_run_pipeline()
D->>D: extract()
D->>D: source()
alt SINGLE mode
D->>H: metadata check (remote vs saved)
alt changed
D->>H: download file
else unchanged
D->>H: resolve latest saved local file
end
else MULTI mode
loop each param/url variant
D->>H: metadata check
alt changed
D->>H: download variant file
else unchanged
D->>H: use latest saved variant file
end
end
end
D->>M: update_runtime_file_paths(paths)
alt no paths
D->>D: run_job_response("No files available")
else has paths
D->>DB: create_data_tables() + raw_staging clone
D->>T: run_threadpool_file_processing(paths)
par each file path
T->>D: process_file(path)
D->>D: transform(path)
D->>H: read_files()
D->>D: before/pre/source_filter/post/after hooks
alt transformed_data exists
D->>DB: bulk_insert(raw_staging, data)
else empty
D->>D: skip load
end
end
D->>DB: post_database_processing()
D->>DB: sync raw_staging → staging
D->>DB: create staging indexes (if deferred)
D->>DB: execute_on_staging() SQL hook
D->>DB: sync staging → enrichment
D->>DB: create enrichment indexes (if deferred)
D->>DB: execute_on_enrichment() SQL hook
D->>BG: map_to_base() / execute mapping strategy
D->>DB: create mapping indexes (if deferred)
D->>MV: trigger materialized views
D->>DB: clean_raw_staging_table(backup_if_sync_failed)
D->>D: run_job_response("Job finished Successfully !!!")
end
alt exception anywhere in run pipeline
D->>D: on_run_error()
D->>D: run_job_response("Job failed")
end
D->>M: mark_run_finished(success, message)
D->>D: run_end_cleanup(success/error)