Un pipeline ETL robusto y escalable para la ingesta de datos de la Colección 2 Nivel 2 de Landsat en una base de datos PostGIS.
Tabla de Contenidos
- Visión General
- Arquitectura del Proyecto
- Modelo de Datos
- Flujo de Trabajo del ETL
- Instalación y Configuración
- Guía de Uso del CLI
- Documentación Adicional
Este proyecto implementa un pipeline ETL (Extracción, Transformación y Carga) diseñado para automatizar la descarga y el almacenamiento de datos de la Colección 2, Nivel 2 de Landsat desde la API M2M del USGS. Los datos de las bandas espectrales se ingieren como rasters en una base de datos PostgreSQL con la extensión PostGIS, utilizando un diseño de base de datos particionado para un rendimiento óptimo en consultas temporales.
El sistema es modular, configurable y está preparado para ser orquestado por herramientas como Apache Airflow, gracias a su punto de entrada CLI.
La estructura del proyecto está diseñada para separar responsabilidades (SoC), garantizando un código limpio, mantenible y escalable.
.
├── config/ # Configuración (YAML, GeoJSON)
├── data/ # Datos temporales y logs (ignorados por git)
├── docs/ # Documentación adicional
├── etl/ # Código fuente del pipeline ETL
│ ├── bronze_ingestion.py
│ ├── m2m_client.py
│ ├── mtl_parser.py
│ └── utils.py
├── scripts/ # Scripts de utilidad
├── sql/ # Scripts SQL (schemas)
├── .env # Variables de entorno (secretos)
├── .gitignore
├── main.py # Punto de entrada CLI
├── README.md
└── requirements.txt # Dependencias Python
-
/main.py: Punto de Entrada Principal (Entry Point). Es la única interfaz de línea de comandos (CLI) que orquesta todo el proceso. Centraliza la ejecución y es ideal para ser llamado desde flujos de trabajo automatizados. -
/etl/: Paquete Core del ETL. Contiene la lógica de negocio reutilizable del pipeline, funcionando como una librería interna.bronze_ingestion.py: El orquestador principal que gestiona el flujo de búsqueda, descarga e inserción.m2m_client.py: Un cliente dedicado para interactuar con la API M2M del USGS, manejando la autenticación, reintentos y peticiones.mtl_parser.py: Utilidad para parsear los archivos de metadatos (_MTL.txt) de las escenas Landsat.utils.py: Funciones de utilidad para cargar configuración, gestionar secretos, configurar logging y otras operaciones comunes.
-
/config/: Configuración del Proyecto.landsat_config.yaml: Archivo de configuración principal y única fuente de verdad para todos los parámetros no sensibles (bandas a descargar, URLs, umbrales, etc.).roi_valencialake.geojson: Un archivo GeoJSON que define el Área de Interés (AOI) para la búsqueda de escenas.
-
/.env: Secretos y Variables de Entorno. Este archivo, ignorado por Git, contiene exclusivamente información sensible: credenciales de la base de datos y tokens de API. -
/sql/: Infraestructura de Base de Datos.schemas/01_bronze_raster.sql: Script DDL (Data Definition Language) que define y crea todo el esquema (bronze), las tablas, particiones por año, índices y vistas. Es la base para la reproducibilidad de la base de datos.
-
/scripts/: Contiene scripts de utilidad para diagnóstico o pruebas (debug_m2m_download.py). -
/data/,/logs/: Carpetas de trabajo (rastreadas por Git a través de.gitkeeppero con su contenido ignorado) donde se guardan los archivos temporales descargados y los logs de ejecución, respectivamente.
La base de datos está diseñada para el almacenamiento eficiente de grandes volúmenes de datos geoespaciales-temporales.
- Esquema
bronze: Alberga los datos crudos o mínimamente procesados, siguiendo la primera etapa de una arquitectura Medallion.
Almacena los metadatos de cada escena única.
| Columna | Tipo | Descripción |
|---|---|---|
scene_id |
SERIAL (PK) |
Identificador único interno para cada escena. |
entity_id |
TEXT (UNIQUE) |
ID de la entidad de USGS (ej. LC09_L2SP_004053_20240125_20240126_02_T1). |
acquisition_date |
DATE |
Fecha de captura de la escena. |
sensor |
TEXT |
Sensor que capturó la escena (ej. 'OLI', 'ETM+'). |
cloud_cover |
REAL |
Porcentaje de cobertura de nubes (0-100). |
footprint |
GEOMETRY(POLYGON) |
La huella geoespacial de la escena en coordenadas geográficas. |
Almacena los datos raster de cada banda, teselados (en tiles) para optimizar el acceso.
| Columna | Tipo | Descripción |
|---|---|---|
rid |
SERIAL (PK part) |
Identificador único para cada tile de raster. |
scene_id |
INTEGER (FK) |
Referencia a la escena a la que pertenece esta banda (landsat_scenes). |
band_name |
TEXT |
Nombre de la banda (ej. 'SR_B3', 'QA_PIXEL'). |
year |
INTEGER (PK part) |
Año de adquisición. Clave de particionamiento. |
rast |
RASTER |
Los datos del píxel en formato PostGIS Raster. |
filename |
TEXT |
Nombre del archivo original del cual se ingirió el raster. |
Registro de auditoría para cada descarga realizada de archivos Landsat.
| Columna | Tipo | Descripción |
|---|---|---|
log_id |
SERIAL (PK) |
Identificador único del registro de descarga. |
entity_id |
TEXT |
ID de la entidad de USGS de la escena descargada. |
band_name |
TEXT |
Nombre de la banda o producto descargado. |
download_url |
TEXT |
URL de descarga del archivo. |
download_status |
TEXT |
Estado de la descarga ('pending', 'success', 'failed', 'skipped'). |
attempt_count |
INTEGER |
Número de intentos de descarga. |
error_message |
TEXT |
Mensaje de error si la descarga falló. |
file_size_mb |
REAL |
Tamaño del archivo descargado en MB. |
download_duration_seconds |
REAL |
Duración de la descarga en segundos. |
created_at |
TIMESTAMP |
Marca de tiempo de creación del registro. |
updated_at |
TIMESTAMP |
Marca de tiempo de la última actualización del registro. |
Configuración de las bandas requeridas para cada tipo de sensor, utilizada en la lógica del ETL y para funciones.
| Columna | Tipo | Descripción |
|---|---|---|
sensor |
TEXT (PK) |
Nombre del sensor (ej. 'OLI', 'ETM+', 'TM'). |
green_band |
TEXT |
Nombre de la banda verde para este sensor. |
swir_band |
TEXT |
Nombre de la banda SWIR para este sensor. |
qa_bands |
TEXT[] |
Array de nombres de bandas QA requeridas para este sensor. |
date_range_start |
DATE |
Fecha de inicio de validez de esta configuración (opcional). |
date_range_end |
DATE |
Fecha de fin de validez de esta configuración (opcional). |
La tabla landsat_bands está particionada por año. Esta decisión de diseño es fundamental para la escalabilidad del sistema por varias razones:
-
Rendimiento de Consultas (Query Performance): Cuando se realiza una consulta que filtra por un rango de fechas (ej. "analizar todas las bandas de 2023"), el planificador de consultas de PostgreSQL (Query Planner) es lo suficientemente inteligente como para escanear únicamente la partición
landsat_bands_2023, ignorando por completo los datos de otros años. Esto se conoce como Partition Pruning y reduce drásticamente los tiempos de lectura. -
Mantenimiento Eficiente: La gestión del ciclo de vida de los datos se vuelve trivial. Si en el futuro se necesita borrar datos de hace 10 años, en lugar de ejecutar un costoso
DELETE FROM ... WHERE year = ..., simplemente se puede ejecutarDROP TABLE landsat_bands_2014;. Esta operación es casi instantánea, no genera sobrecarga transaccional y no fragmenta los índices. -
Carga de Datos Optimizada: Permite estrategias de carga de datos más eficientes y la creación de índices por partición, lo que acelera tanto la escritura como la lectura.
El proceso se ejecuta de la siguiente manera al invocar python main.py ingest:
- Inicio y Configuración:
main.pyrecibe los parámetros y llama a la lógica de ingesta. Se carga la configuración desdeconfig/landsat_config.yamly los secretos desde.env. - Búsqueda de Escenas:
M2MClientse autentica en la API del USGS y busca escenas que se intersecten con el AOI y cumplan con los filtros de fecha y nubes. - Descarga: Las bandas de las escenas nuevas se descargan como archivos
.TIFen la carpetadata/temp/. - Ingesta en BD (por cada banda):
a. Tabla Temporal: Se utiliza
raster2pgsqlpara crear una tabla temporal con los datos del raster del archivo.TIF. b. Inserción: Se ejecuta unINSERT INTO ... SELECT FROM ...para copiar los datos a la partición anual correcta (ej.landsat_bands_2024), inyectando metadatos. c. Limpieza: La tabla temporal se elimina. - Finalización: El proceso registra un resumen de la operación y limpia listas temporales.
- Python 3.12+
- PostgreSQL 16.4+ con PostGIS 3.4+ (incluyendo
postgis_raster). - Credenciales para la API M2M de USGS.
- Opcional: Docker para una configuración de base de datos más sencilla.
-
Clonar el Repositorio:
git clone https://github.com/chachr81/landsat_data.git cd landsat_data -
Instalar Dependencias:
pip install -r requirements.txt
-
Configurar la Base de Datos:
- En tu base de datos, habilita las extensiones:
CREATE EXTENSION postgis; CREATE EXTENSION postgis_raster;
- Ejecuta el script DDL para crear la estructura:
psql -U tu_usuario -d tu_base_de_datos -h tu_host -f sql/schemas/01_bronze_raster.sql
- En tu base de datos, habilita las extensiones:
-
Configurar Secretos (
.env):- Crea un archivo
.enven la raíz del proyecto y añade tus credenciales:POSTGRES_HOST=localhost POSTGRES_USER=postgres POSTGRES_PASSWORD=secret POSTGRES_DB=gis_engine POSTGRES_PORT=5432 M2M_USERNAME=tu_usuario_m2m M2M_PASSWORD=tu_contraseña_m2m # M2M_API_KEY=your_api_key_if_used_instead_of_password
- Crea un archivo
El único punto de entrada es main.py.
| Argumento | Requerido | Descripción |
|---|---|---|
--start |
Sí | Fecha de inicio de la búsqueda (YYYY-MM-DD). |
--end |
Sí | Fecha de fin de la búsqueda (YYYY-MM-DD). |
--datasets |
No | Colecciones a procesar. Opciones leídas de config.yaml. |
--clouds |
No | Porcentaje máximo de nubes (0-100). |
--dry-run |
No | Simula la ejecución sin descargar ni escribir en la BD. |
--log-level |
No | Nivel de logging (DEBUG, INFO, WARNING). |
Ejemplos de Ejecución:
# Ingestar datos para el primer trimestre de 2024
python main.py ingest --start 2024-01-01 --end 2024-04-01
# Ingestar solo datos de Landsat 7 con un máximo de 10% de nubes
python main.py ingest --start 2022-01-01 --end 2023-01-01 --datasets landsat_7 --clouds 10Mantiene la API M2M limpiando listas de escenas temporales.
| Argumento | Requerido | Descripción |
|---|---|---|
--list-id |
Sí | ID(s) de las listas a borrar. |
--dry-run |
No | Simula el borrado. |
--force |
No | Omite la confirmación. |
Ejemplo:
python main.py cleanup-lists --list-id temp_list_12345 temp_list_67890- Para una descripción detallada de las bandas de calidad (QA) y cómo interpretarlas, consulta el siguiente documento:
Table of Contents
- Overview
- Project Architecture
- Data Model
- ETL Workflow
- Installation and Setup
- CLI Usage Guide
- Additional Documentation
This project implements an ETL (Extract, Transform, Load) pipeline designed to automate the download and storage of Landsat Collection 2, Level 2 data from the USGS M2M API. The spectral band data is ingested as rasters into a PostgreSQL database with the PostGIS extension, using a partitioned database design for optimal performance on temporal queries.
The system is modular, configurable, and ready to be orchestrated by tools like Apache Airflow, thanks to its CLI entry point.
The project structure is designed to separate concerns (SoC), ensuring clean, maintainable, and scalable code.
.
├── config/ # Configuration (YAML, GeoJSON)
├── data/ # Temporary data and logs (ignored by git)
├── docs/ # Additional documentation
├── etl/ # ETL pipeline source code
│ ├── bronze_ingestion.py
│ ├── m2m_client.py
│ ├── mtl_parser.py
│ └── utils.py
├── scripts/ # Utility scripts
├── sql/ # SQL scripts (schemas)
├── .env # Environment variables (secrets)
├── .gitignore
├── main.py # CLI Entry Point
├── README.md
└── requirements.txt # Python dependencies
-
/main.py: Main Entry Point. A CLI that orchestrates the entire process. All operations, such as data ingestion, are initiated from here. It is the only script that needs to be run. -
/etl/: Core ETL Package. Contains the reusable business logic of the pipeline, functioning as an internal library.bronze_ingestion.py: The main orchestrator that manages the search, download, and insertion flow.m2m_client.py: A dedicated client for interacting with the USGS M2M API, handling authentication, retries, and requests.mtl_parser.py: A utility for parsing metadata files (_MTL.txt) from Landsat scenes.utils.py: Utility functions for loading configuration, managing secrets, setting up logging, and other common operations.
-
/config/: Project Configuration.landsat_config.yaml: The main configuration file and single source of truth for all non-sensitive parameters (bands to download, URLs, thresholds, etc.).roi_valencialake.geojson: A GeoJSON file defining the Area of Interest (AOI) for scene searches.
-
/.env: Secrets and Environment Variables. This file, ignored by Git, exclusively contains sensitive information: database credentials and API tokens. -
/sql/: Database Infrastructure.schemas/01_bronze_raster.sql: DDL (Data Definition Language) script that defines and creates the entire schema (bronze), tables, year-based partitions, indexes, and views. It is the foundation for database reproducibility.
-
/scripts/: Contains utility scripts for diagnostics or testing (debug_m2m_download.py). -
/data/,/logs/: Working directories (tracked by Git via.gitkeepbut their content is ignored) where temporary downloaded files and execution logs are stored, respectively.
The database is designed for the efficient storage of large volumes of geospatial-temporal data.
bronzeSchema: Houses the "raw" or minimally processed data, following the first stage of a Medallion architecture.
Stores metadata for each unique scene.
| Column | Type | Description |
|---|---|---|
scene_id |
SERIAL (PK) |
Internal unique identifier for each scene. |
entity_id |
TEXT (UNIQUE) |
USGS entity ID (e.g., LC09_L2SP_004053_20240125_20240126_02_T1). |
acquisition_date |
DATE |
Date the scene was captured. |
sensor |
TEXT |
Sensor that captured the scene (e.g., 'OLI', 'ETM+'). |
cloud_cover |
REAL |
Cloud cover percentage (0-100). |
footprint |
GEOMETRY(POLYGON) |
The geospatial footprint of the scene in geographic coordinates. |
Stores raster data for each band, tiled for optimized access.
| Column | Type | Description |
|---|---|---|
rid |
SERIAL (PK part) |
Unique identifier for each raster tile. |
scene_id |
INTEGER (FK) |
Reference to the scene this band belongs to (landsat_scenes). |
band_name |
TEXT |
Name of the band (e.g., 'SR_B3', 'QA_PIXEL'). |
year |
INTEGER (PK part) |
Acquisition year. Partitioning Key. |
rast |
RASTER |
The pixel data in PostGIS Raster format. |
filename |
TEXT |
Original filename from which the raster was ingested. |
Audit log for each Landsat file download performed.
| Column | Type | Description |
|---|---|---|
log_id |
SERIAL (PK) |
Unique identifier for the download record. |
entity_id |
TEXT |
USGS entity ID of the downloaded scene. |
band_name |
TEXT |
Name of the band or product downloaded. |
download_url |
TEXT |
URL of the file download. |
download_status |
TEXT |
Status of the download ('pending', 'success', 'failed', 'skipped'). |
attempt_count |
INTEGER |
Number of download attempts. |
error_message |
TEXT |
Error message if the download failed. |
file_size_mb |
REAL |
Size of the downloaded file in MB. |
download_duration_seconds |
REAL |
Duration of the download in seconds. |
created_at |
TIMESTAMP |
Timestamp of record creation. |
updated_at |
TIMESTAMP |
Timestamp of last record update. |
Configuration of required bands for each sensor type, used in ETL logic and functions.
| Column | Type | Description |
|---|---|---|
sensor |
TEXT (PK) |
Sensor name (e.g., 'OLI', 'ETM+', 'TM'). |
green_band |
TEXT |
Name of the green band for this sensor. |
swir_band |
TEXT |
Name of the SWIR band for this sensor. |
qa_bands |
TEXT[] |
Array of required QA band names for this sensor. |
date_range_start |
DATE |
Start date of validity for this configuration (optional). |
date_range_end |
DATE |
End date of validity for this configuration (optional). |
The landsat_bands table is partitioned by year. This design decision is fundamental for the system's scalability for several reasons:
-
Query Performance: When a query filters by a date range (e.g., "analyze all bands from 2023"), the PostgreSQL Query Planner is smart enough to scan only the
landsat_bands_2023partition, completely ignoring data from other years. This is known as Partition Pruning and dramatically reduces read times. -
Efficient Maintenance: Data lifecycle management becomes trivial. If, in the future, you need to delete data from 10 years ago, instead of running a costly
DELETE FROM ... WHERE year = ..., you can simply executeDROP TABLE landsat_bands_2014;. This operation is nearly instantaneous, generates no transactional overhead, and does not fragment indexes. -
Optimized Data Loading: It allows for more efficient data loading strategies and the creation of per-partition indexes, speeding up both writes and reads.
The process runs as follows when python main.py ingest is invoked:
- Start and Configuration:
main.pyreceives parameters and calls the ingestion logic. Configuration is loaded fromconfig/landsat_config.yamland secrets from.env. - Scene Search:
M2MClientauthenticates with the USGS API and searches for scenes intersecting the AOI and meeting the date/cloud filters. - Download: Bands from new scenes are downloaded as
.TIFfiles into thedata/temp/folder. - DB Ingestion (for each band):
a. Temporary Table:
raster2pgsqlis used to create a unique temporary table with the raster data from the.TIFfile. b. Insertion: AnINSERT INTO ... SELECT FROM ...command copies data from the temp table to the correct annual partition (e.g.,landsat_bands_2024), injecting key metadata. c. Cleanup: The temporary table is deleted. - Completion: The process logs a summary of the operation and cleans up temporary files and M2M lists.
- Python 3.12+
- PostgreSQL 16.4+ with PostGIS 3.4+ (including
postgis_raster). - Credentials for the USGS M2M API.
- Optional: Docker for a simpler database setup.
-
Clone the Repository:
git clone https://github.com/chachr81/landsat_data.git cd landsat_data -
Install Dependencies:
pip install -r requirements.txt
-
Set Up the Database:
- In your database, enable the extensions:
CREATE EXTENSION postgis; CREATE EXTENSION postgis_raster;
- Run the DDL script to create the structure:
psql -U your_user -d your_database -h your_host -f sql/schemas/01_bronze_raster.sql
- In your database, enable the extensions:
-
Configure Secrets (
.env):- Create a
.envfile in the project root and add your credentials:POSTGRES_HOST=localhost POSTGRES_USER=postgres POSTGRES_PASSWORD=secret POSTGRES_DB=gis_engine POSTGRES_PORT=5432 M2M_USERNAME=your_m2m_username M2M_PASSWORD=your_m2m_password # M2M_API_KEY=your_api_key_if_used_instead_of_password
- Create a
The single entry point is main.py.
| Argument | Required | Description |
|---|---|---|
--start |
Yes | Search start date (YYYY-MM-DD). |
--end |
Yes | Search end date (YYYY-MM-DD). |
--datasets |
No | Collections to process. Options are read from config.yaml. |
--clouds |
No | Maximum cloud cover percentage (0-100). |
--dry-run |
No | Simulates the run without downloading or writing to the DB. |
--log-level |
No | Logging level (DEBUG, INFO, WARNING). |
Example Executions:
# Ingest data for the first quarter of 2024
python main.py ingest --start 2024-01-01 --end 2024-04-01
# Ingest only Landsat 7 data with a max of 10% cloud cover
python main.py ingest --start 2022-01-01 --end 2023-01-01 --datasets landsat_7 --clouds 10Maintains the M2M API by removing old temporary scene lists.
| Argument | Required | Description |
|---|---|---|
--list-id |
Yes | One or more M2M List IDs to delete. |
--dry-run |
No | Simulates deletion. |
--force |
No | Skips confirmation prompt. |
Example:
python main.py cleanup-lists --list-id temp_list_12345 temp_list_67890-
For a detailed description of the Quality Assessment (QA) bands and how to interpret them, refer to the following document: