Working with earthdaily-agriculture🔗
Practical recipes for the most common day-to-day patterns: debugging an extractor that returns empty results, loading entities from a file vs. the platform, driving a single extractor vs. a full YAML workflow, wiring the cache, and the standard notebook bootstrap.
For getting started, see 01 - Quick_Start_Guide.md. For the architectural rationale behind WorkflowManager and the transform pattern, see 09 - Workflow_architecture.md.
Tip — Use log_level="DEBUG" to surface every API request and response🔗
Every extractor sprinkles logger.debug(...) calls on the API call path. At the default INFO level they stay silent; flipping to DEBUG makes them visible and gives you:
- The full API URL being called (endpoint + query string)
- The request payload (geometry, filters, parameters)
- The raw JSON response coming back
This is the fastest way to answer "why did I get empty results?" or "is my date actually making it to the API?".
Enabling DEBUG🔗
from earthdaily.agriculture.services.workflow_manager import WorkflowManager
manager = WorkflowManager(
"prod",
log_level="DEBUG", # flip from the default "INFO"
log_to_console=True, # keep stderr output so you see it live
)
Logs also land in logs/earthdaily_<YYYY-MM-DD>.log (daily rotation, 30-day retention) — useful for post-mortem inspection when you don't want console noise during a long run.
What DEBUG actually prints🔗
Sampling a few extractors:
| Extractor | Module | DEBUG output |
|---|---|---|
CoverageExtractor |
extractors.coverage_function |
API URL: …, Payload: …, API response: … |
cropidExtractor |
extractors.cropid_functions |
URL, filters (begin_year–end_year, mask_type), payload, response |
WeatherExtractor |
extractors.weather_functions |
Entity <id>: API URL: …, Entity <id>: API response: … |
Every other extractor follows the same pattern. If a call is silent at DEBUG, the extractor likely short-circuited before reaching the HTTP layer (e.g. missing required entity field).
When to turn it off🔗
DEBUG is verbose — each bulk run can produce tens of megabytes of logs. Leave it at INFO by default and flip to DEBUG only while diagnosing a specific entity or a failing extractor.
Scoping DEBUG to a single call🔗
If you want DEBUG just for one manual get_*_api() inspection without re-instantiating the manager, override loguru directly:
Then run your single call and restore INFO when done.
Using WorkflowManager🔗
WorkflowManager is the single entry point for any extraction: it handles authentication, entity loading, path resolution, cache configuration, and — optionally — orchestration of a multi-step pipeline defined in YAML.
Initialisation🔗
from earthdaily.agriculture.services.workflow_manager import WorkflowManager
manager = WorkflowManager(
env="prod", # "prod" | "preprod" (reads from ENVIRONMENT env var if omitted)
log_level="INFO", # "TRACE" | "DEBUG" | "INFO" | "WARNING" | "ERROR"
log_to_console=True, # stream loguru to stderr
project_root=None, # override the auto-detected project root
)
On construction the manager:
- Loads
.env, resolves credentials for the chosen environment. - Authenticates and stores
manager.bearer_token+manager.token_expiration. - Resolves workspace paths:
manager.config["output_result_dir"],partial_result_dir,cache_dir,logs_dir. - Sets up loguru file logging at
logs/earthdaily_<date>.log.
Key attributes you'll touch🔗
| Attribute | What it is |
|---|---|
manager.bearer_token / manager.token_expiration |
Current OAuth token — pass to extractors built manually |
manager.config |
Dict with env, paths, and optional cache settings (use_cache, cache_dir, cache_ttl_days) |
manager.sfd_list |
DataFrame of currently loaded entities (set by load_seasonfields* / load_sfd_list) |
manager.output_result_dir |
Final-results directory (absolute) |
manager.partial_result_dir |
Intermediate bulk partials directory (absolute) |
Example 1 — Direct extractor mode (most common)🔗
Use WorkflowManager purely for auth + config, then instantiate and run one extractor manually. This is the idiomatic pattern for notebook work and ad-hoc extractions.
from earthdaily.agriculture.services.workflow_manager import WorkflowManager
from earthdaily.agriculture.extractors.coverage_function import CoverageExtractor
manager = WorkflowManager("prod", log_level="INFO")
# Load entities from the EarthDaily platform
manager.load_seasonfields()
# Build the extractor — pass the manager as workflow_ref so token refresh is delegated
extractor = CoverageExtractor(
manager.bearer_token,
manager.token_expiration,
config=manager.config,
workflow_ref=manager,
)
extractor.setup_coverage_parameters(
vegetation_index="NDVI",
start_date="2025-01-01",
clear_cover_min=80,
filter="duplicate",
column_mapping={"crop": "crop.id"},
)
results = extractor.process_entity_coverage_bulk_parallel(
entity_list=manager.sfd_list.head(50),
max_workers=10,
output_path=manager.output_result_dir,
prefix="coverage",
)
df = results["results_df"]
print(df.shape, df.columns.tolist())
Example 2 — Loading entities from a file🔗
load_sfd_list() accepts shapefile, parquet, geojson, gpkg, and CSV. Populates manager.sfd_list.
from earthdaily.agriculture.core.geometry import load_geodataframe
manager = WorkflowManager("prod")
manager.sfd_list = load_geodataframe("inputs/my_fields.parquet")
print(f"Loaded {len(manager.sfd_list)} entities")
For very large platform extractions, use the batch variant which pages the MDM API 5 000 rows at a time:
manager.load_seasonfields_batch(
start_date="2025-01-01",
end_date="2025-12-31",
crop_id=123,
batch_size=5000,
)
Example 3 — Full YAML-driven workflow🔗
Write a multi-step pipeline in configuration/workflow.yml, load it, visualise it, run it.
# configuration/workflow.yml
workflow:
name: "Weather + NDVI pipeline"
settings:
max_workers: 10
partial_frequency: 50
column_mapping:
crop: crop.id
start_date: sowingDate
end_date: endDate
steps:
- name: weather_hist
extractor: WeatherExtractor
module: earthdaily.agriculture.extractors.weather_functions
setup:
method: setup_weather_parameters
params:
weather_type: HISTORICAL_DAILY
weather_parameters:
- Temperature.standardmax
- Temperature.standardmin
- precipitation.cumulative
run:
method: process_entity_weather_bulk_parallel
params:
prefix: weather_hist
skip_export: true
- name: mrts_ndvi
extractor: MRTSExtractor
module: earthdaily.agriculture.extractors.VTS_functions
setup:
method: setup_mrts_parameters
params:
vegetation_index: NDVI
clear_cover_min: 95
mode: full
run:
method: process_mrts_bulk_parallel
params:
prefix: mrts_ndvi
skip_export: true
- name: merge_report
depends_on:
- weather_hist
- mrts_ndvi
transform:
module: app.transforms
function: build_report
params:
weather_step: weather_hist
ndvi_step: mrts_ndvi
output_csv: results/report.csv
Driver code:
manager = WorkflowManager("prod")
manager.load_seasonfields()
manager.load_workflow("configuration/workflow.yml")
manager.visualize_workflow() # prints the dependency DAG
results = manager.run_workflow(entity_list=manager.sfd_list)
for step, payload in results.items():
df = payload.get("results_df")
errs = payload.get("global_errors", [])
print(f"{step:20s} rows={len(df):>6} errors={len(errs)}")
manager.run_workflow(...) executes steps in dependency order, feeds each extractor's output into the cache (if enabled), and hands upstream results to any transform step via upstream_results.
Example 4 — Enabling the shared cache across a workflow🔗
Set cache flags on manager.config before calling load_workflow() — every extractor the workflow instantiates inherits them.
manager = WorkflowManager("prod")
manager.config["use_cache"] = True
manager.config["cache_dir"] = "cache" # relative to project root
manager.config["cache_ttl_days"] = 7
manager.load_workflow("configuration/workflow.yml")
results = manager.run_workflow(entity_list=entities)
Re-running with the same entities replays from parquet; per-call override use_cache=False still forces a refresh for a single step. Full reference: 07 - Cache_design_context.md.
Example 5 — Transform-only step (no API call)🔗
Transforms are lightweight Python functions that reshape upstream results into a downstream input. They live in app/transforms.py and must follow the canonical signature:
# app/transforms.py
import pandas as pd
def drop_small_fields(entity_list, upstream_results, params) -> pd.DataFrame:
"""Keep only fields above a minimum area (from a prior coverage step)."""
min_area = params.get("min_area_sqm", 10_000)
coverage_df = upstream_results[params["coverage_step"]]["results_df"]
large_ids = coverage_df.loc[coverage_df["area_sqm"] >= min_area, "id"].unique()
return entity_list[entity_list["id"].isin(large_ids)].reset_index(drop=True)
Wire it in the YAML:
- name: filter_small
depends_on: coverage
transform:
module: app.transforms
function: drop_small_fields
params:
coverage_step: coverage
min_area_sqm: 20000
Downstream steps that depends_on: filter_small will receive the filtered entity list automatically.
Example 6 — Driving from a notebook with the standard bootstrap🔗
Every notebook starts with the same bootstrap cell followed by a WorkflowManager constructor:
# Cell 1 — bootstrap
from earthdaily.agriculture.notebook_setup import init
init()
# Cell 2 — manager + entities
from earthdaily.agriculture.services.workflow_manager import WorkflowManager
manager = WorkflowManager("prod", log_to_console=False, log_level="WARNING")
manager.load_seasonfields()
From here you can either go direct-extractor (Example 1) or load a YAML (Example 3).
Common pitfalls🔗
- Token expiry during long runs. Extractors built with
workflow_ref=managerautomatically refresh the token. Extractors built without it will fail after ~1 hour. - Entity DataFrame column names. If your data uses
sowingDate/crop.id(platform output) or anything custom, setcolumn_mappingeither atsetup_*_parameters()time or underworkflow.settings.column_mapping(applies to every step). See04 - Extractor_column_mapping_reference.md. - Empty results but no error. Flip
log_level="DEBUG"to see the actual API response. Often it reveals a geometry rejection or a missing required field (e.g. LAI extraction requirescrop). clear_cover_min=100is very restrictive — most extractions want 80–95.- Per-entity date overrides. Some extractors honour per-entity dates carried in the DataFrame; some don't. See
06 - Edagro_date_overwrite.mdfor the per-extractor matrix.