SOIL - Structured Observation Ingestion Library
Version: 0.2.8 · Python: ≥ 3.10 · PyPI:
soil-etl
YAML-driven ETL package for importing Canadian forestry soil datasets into the ForSITE PostGIS database model, built on the OGC Observations, Measurements & Samples specification.
Acknowledgements
| Project Partners |
|---|
This project was developed through joint funding provided by Agriculture and Agri-Food Canada (AAFC) under
the CSBO program and Natural Resources Canada (NRCan) for
the ForSITE-Soil degradation project.
The code developed as part of this project includes contributions from:
Installation
pip install soil-etl
Or with Poetry:
poetry add soil-etl
Quick Start
YAML-only pipeline (recommended)
from pathlib import Path
from soil_etl.yaml_mapper import YamlDatasetMapper
mapper = YamlDatasetMapper(
config_path=Path("my_config.yaml"),
data_file_path=Path("data.xlsx"),
)
metrics = mapper.import_data(
database_connection_string="postgresql+psycopg2://user:password@host:5432/database",
)
print(metrics)
All new ETL pipelines must use YamlDatasetMapper. See AGENTS.md for full architecture guidance.
Canonical imports
from soil_etl.yaml_mapper import YamlDatasetMapper
from soil_etl.bindings import Binding, FromColumn
from soil_etl.db import ImportationInterface
Boilerplate - Scaffolding for the new project
The fastest way to start a new import project is to create a ready-to-use project from the packaged template. It creates a complete, self-contained project:
- a runner (
.pyfile used to import the data), - an importation template (
YAML configfile), - Hydra DB (
YAML configfile), config, an.env.example, and a preconfigured Copilot agent under.github/(chatmodes, prompts, skills and embedded knowledge about the package, the YAML format and the database model).
From the command line
# Scaffold into a new subfolder
soil-etl start-project my-soil-imports
# ...or into the current directory
soil-etl start-project
# Options
soil-etl start-project my-soil-imports --dest ./projects --overwrite
Equivalent module invocation:
python -m soil_etl start-project my-soil-imports
From Python
import soil_etl
soil_etl.start_project("my-soil-imports") # into a new subfolder
soil_etl.start_project() # into the current directory
soil_etl.start_project(name="my-soil-imports", overwrite=True)
Generated project layout
my-soil-imports/
+-- pyproject.toml # depends on soil-etl
+-- .env.example # env vars (DB_CONN, etc.)
+-- .gitignore
+-- configs/
| +-- configs.yaml # Hydra root (selects db: local)
| +-- db/
| | +-- base_con.yaml
| | +-- local.yaml
+-- data/ # source files (Excel/CSV) - not versioned
+-- pipelines/
| +-- import_example_dataset.py # 3-line runner
| +-- example_dataset.yaml # pipeline YAML config (to adapt)
+-- .github/ # Copilot agent + embedded knowledge
+-- copilot-instructions.md
+-- chatmodes/ # soil-etl-pipeline agent (VS Code)
+-- agents/ # same agent (PyCharm / JetBrains)
+-- prompts/ # /new-pipeline, /review-yaml-config, /debug-failed-import
+-- instructions/ # auto-applied skills (YAML + Python runner)
+-- knowledge/ # YAML format, DB model, enums, template
Next steps after boilerplate exec.:
cd my-soil-imports
poetry install # installs soil-etl
Copy-Item|cp .env.example .env # then fill in DB_CONN
poetry run python pipelines/import_example_dataset.py # dry-run
Maintainers: the editable source of the template lives in
project-template/at the repo root and is synced into the packaged copy (src/soil_etl/_project_template/) withpython scripts/sync_template.py(use--checkin CI to detect drift).
Database Connection
Connection resolution priority (first non-null wins):
- Explicit
database_connection_stringargument toimport_data() DB_CONNenvironment variable- Hydra config directory (
database_config_path/CONFIG_PATHenv var)
# Option 1 - direct connection string
mapper.import_data(
database_connection_string="postgresql+psycopg2://user:password@host:5432/database",
database_engine_kwargs={"pool_size": 10, "pool_pre_ping": True},
)
# Option 2 - Hydra config files
mapper.import_data(
database_config_path="configs",
database_config_name="configs.yaml",
)
# Option 3 - environment variables
import os
os.environ["DB_CONN"] = "postgresql+psycopg2://user:password@host:5432/database"
mapper.import_data()
Environment variables recognized:
| Variable | Description |
|---|---|
DB_CONN |
Full SQLAlchemy connection string |
DB_USER |
Database username (used alongside Hydra config) |
DB_PASSWORD |
Database password (used alongside Hydra config) |
CONFIG_PATH |
Hydra config directory path |
IMPORT_BATCH_SIZE |
Override batch size for this run (positive integer) |
EXPORT_RETREATED_DATAFRAME |
Export transformed DataFrame to *_retreated.xlsx (1/true/yes) |
import_data() Signature
mapper.import_data(
error_file_path=Path("errors.xlsx"), # defaults to <dataset_title>_ERROR.xlsx
dry_run=False, # True: extract + validate only, no DB writes
database_connection_string="postgresql+...",
database_config_path="configs",
database_config_name="configs.yaml",
database_engine_kwargs={"pool_size": 10},
) -> ImportRunMetrics
Returns an ImportRunMetrics dataclass:
@dataclass(frozen=True)
class ImportRunMetrics:
rows_processed: int
error_rows: int
generic_results: int
horizon_results: int
feature_relationships: int
sample_information: int
batches: int
batch_size: int
dry_run: bool
extract_seconds: float
persist_seconds: float
total_seconds: float
YAML Configuration
The YAML config drives the entire mapping: Excel reading parameters, project metadata, feature of interest hierarchy, observations, and results.
Reference files (packaged):
| File | Purpose |
|---|---|
src/soil_etl/yaml_mapper/dummy_config_file.yaml |
Fully-documented boilerplate with all options |
src/soil_etl/yaml_mapper/template.yaml |
Real-world example with YAML anchors |
src/soil_etl/yaml_mapper/examples/on_master.yaml |
Province-specific example |
examples/configs/example_config.yaml |
Additional annotated example |
For full documentation of the YAML format, see docs/README_yaml_format_en.md.
Top-level sections
source: # Excel reading params (sheet_name, header, skiprows, null_values...)
export_retreated_dataframe: false
import_batch_size: 1000 # Rows per DB batch (default: 5000)
project: # Project metadata (strategy, name, abbrev, province, type...)
partners: # Partner organizations
stewards: # Data steward contacts
feature_relationships: # Optional parent-child FOI hierarchies (e.g. ProfileElement -> Sample)
sample_information: # Optional sample metadata (type, horizon, dimensions)
profile_elements: # Soil horizons/layers: depth bounds + designation on a PROFILE feature
observations: # v2: each observation carries one nested result (1:1)
v2 data model: the pipeline targets
om_observation_v2/om_result_v2/profile_element. Declare anobservations:list where each observation carries exactly one nestedresult:(1:1). The observed property (element_name) lives on the observation. The observationtypefield is deprecated/ignored. Depth bounds live onprofile_elements(aPROFILEfeature of interest), not on the sample; a profile element can own an observation or be the parent of aSAMPLE. Soil horizons/layers are modeled withprofile_elements(aPROFILEFOI carrying depth bounds + designation) with observations attached, following the chainFOI > Profile > Observation(v2) > Result(v2)(orFOI > Profile > Sample > [FOI/Profile/Sample relations] > Observation(v2) > Result(v2)). The former root keysresults:/horizon_results:have been removed (rejected at load time). See docs/README_yaml_format_en.md.
Python Subclass (advanced)
When YAML alone cannot express the required logic, subclass YamlDatasetMapper and override properties:
from pathlib import Path
from soil_etl.yaml_mapper import YamlDatasetMapper
from soil_etl.model_bindings import FeatureOfInterestBinding
class MyMapper(YamlDatasetMapper):
@property
def _feature_of_interest(self) -> FeatureOfInterestBinding:
# custom logic here
...
mapper = MyMapper(
config_path=Path("my_config.yaml"),
data_file_path=Path("data.xlsx"),
)
mapper.import_data(database_connection_string="postgresql+psycopg2://...")
See examples/subclass_runner.py for a complete example.
Development
pip install poetry
poetry install --with dev,test
poetry run pytest tests
Tests use testcontainers with postgis/postgis:17-3.4 - Docker must be running.
Linting & formatting
poetry run black src tests
poetry run ruff check src tests
poetry run isort src tests
Build & Publish
python -m build
twine check dist/*
twine upload dist/*
Links
- Repository: gitlab.com/nrcan-rncan-cfs-scf/lfc-cfl/soil-etl
- Issues: soil-etl/issues
- YAML format reference: docs/README_yaml_format_en.md
- Testing guide: docs/testing.md
- Project template: project-template/README.md
Metadata
Release files for soil-etl 0.2.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| soil_etl-0.2.9.tar.gz | 285.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| soil_etl-0.2.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 678.9 kB
Release files / soil_etl-0.2.9.tar.gz
| Download URL | soil_etl-0.2.9.tar.gz |
|---|---|
| Size | 285.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
819c753b89ff4339efa04f6e27bae9b35b4de8dc0f15ea2252b2bff0c8b0c636
|
|
BLAKE2b-256 checksum How to use checksums |
4b5fbe1654c7b7fb24f0c4501b5532baec90f4e279cb66b0d25476bf5525eba7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / soil_etl-0.2.9-py3-none-any.whl
| Download URL | soil_etl-0.2.9-py3-none-any.whl |
|---|---|
| Size | 393.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a7a389edceecae3a11eb9e039377ae61bb50092cd92a7a8af20971745840d6f2
|
|
BLAKE2b-256 checksum How to use checksums |
a60c63ad681dc722e880caeaf1b160017bba9a27c5295f299f2acd8238708001
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|