How to access Sentinel-2 L1C via finder in custom AIKP
This guide explains how to access Sentinel-2 Level-1C (S2MSI1C) products from the Creodias API (STAC and OData) using OCLI’s Finder framework, how to derive a custom Finder class, and how to integrate it into a custom AIKP template (using ocli.aikp.boilerplate.sentinel_2_l1c as a reference POC).
1. Two Ways to Access Sentinel-2 L1C in an AIKP
Option A: Zero-Subclass Configuration (FinderCreodiasSentinel2)
FinderCreodiasSentinel2 (ocli/ocli/finders/finder_creodias_sentinel_2.py) supports both S2MSI2A (Level-2A, default) and S2MSI1C (Level-1C):
STAC (
finder.provider="stac"): mapsprocessingLevel='S2MSI1C'to collection['sentinel-2-l1c'].OData (
finder.provider="odata"): queries collection['SENTINEL-2']with filterproductType eq 'S2MSI1C'.Local EODATA validation (
check_product_in_eodata): detects both L2A (GRANULE/*/IMG_DATA/*/*.jp2) and L1C (GRANULE/*/IMG_DATA/*.jp2) band layouts.
If you only need standard Sentinel-2 Creodias search defaulted to L1C, set 'sat_family_name': 's2' and 'finder.processingLevel': 'S2MSI1C' in your AIKP’s TASK_DEFAULTS:
TASK_DEFAULTS = {
...
'sat_family_name': 's2',
'eodata': '/eodata',
'master': None,
'master_path': None,
'finder.processingLevel': 'S2MSI1C',
}
Option B: Deriving a Custom Finder Subclass and Attaching via Template.get_finder(task)
When your custom AIKP requires custom search defaults, custom STAC/OData collection routing, custom feature parsing, or an isolated metadata cache file (.gpkg), derive a subclass from FinderCreodiasSentinel2 (or FinderCreodias / Finder) and return it from Template.get_finder(cls, task).
finder_by_task(task) (ocli/ocli/finders/__init__.py) first checks template_class.get_finder(task) before falling back to task.get_sat_family_name(), and then merges any finder.* task parameters into the returned finder’s search defaults.
2. Step-by-Step POC: ocli.aikp.boilerplate.sentinel_2_l1c
The reference implementation lives in ocli/ocli/aikp/boilerplate/sentinel_2_l1c/:
ocli/ocli/aikp/boilerplate/sentinel_2_l1c/
├── __init__.py # Exposes class Template(TemplateBoilerplateSentinel2L1C)
├── config.py # TASK_DEFAULTS and RECIPE_DEFAULTS for the L1C AIKP
├── finder.py # Derived FinderCreodiasSentinel2L1C class
├── template.py # TemplateBoilerplateSentinel2L1C with Finder lifecycle hooks
├── cli.py # Custom CLI command (`ocli task template inspect_l1c`)
└── README.md # Module documentation
Step 1: Derive FinderCreodiasSentinel2L1C (finder.py)
Subclass FinderCreodiasSentinel2 to set 'processingLevel' default to 'S2MSI1C', route stac_collections to ['sentinel-2-l1c'], and store cached metadata in a dedicated GeoPackage (finders/creodias_s2_l1c.gpkg) so it does not collide with L2A tasks in the same project:
import copy
import os
from typing import Optional
from ocli.core import restricted_value as rv
from ocli.finders.finder_creodias_models import LegacySearchParams
from ocli.finders.finder_creodias_sentinel_2 import (
FinderCreodiasSentinel2,
SatDefaultsSentinel2,
)
class FinderCreodiasSentinel2L1C(FinderCreodiasSentinel2):
"""Derived Creodias Sentinel-2 Finder pre-configured for Level-1C (S2MSI1C) products."""
_s2_l1c_search_defaults = {
**SatDefaultsSentinel2.default_search_params,
'processingLevel': rv.ENUM((None, 'S2MSI1C', 'S2MSI2A'), 'S2MSI1C', 'processingLevel'),
}
def __init__(
self,
eodata_home: str,
cache_dir: str,
provider: Optional[rv.ENUM(("stac", "odata"), "stac")] = None,
):
super().__init__(eodata_home=eodata_home, cache_dir=cache_dir, provider=provider)
self.search_defaults.update(copy.deepcopy(self._s2_l1c_search_defaults))
self.cache_path = os.path.join(cache_dir, 'finders', 'creodias_s2_l1c.gpkg')
def stac_collections(self, params: LegacySearchParams) -> list[str] | None:
level = params.processingLevel
if not level:
return ['sentinel-2-l1c']
return super().stac_collections(params)
Step 2: Configure Task & Recipe Defaults (config.py)
Add sat_family_name, eodata, master, master_path, and finder.processingLevel to TASK_DEFAULTS:
from ocli.aikp.boilerplate.config import (
RECIPE_DEFAULTS as BASE_RECIPE_DEFAULTS,
TASK_DEFAULTS as BASE_TASK_DEFAULTS,
)
RECIPE_DEFAULTS = {
**BASE_RECIPE_DEFAULTS,
}
TASK_DEFAULTS = {
**BASE_TASK_DEFAULTS,
'sat_family_name': 's2',
'eodata': '/eodata',
'master': None,
'master_path': None,
'finder.processingLevel': 'S2MSI1C',
}
Step 3: Wire the Finder Lifecycle in Template (template.py)
In your TaskTemplate subclass, implement five hooks:
get_finder(cls, task): Instantiate and returnFinderCreodiasSentinel2L1C.create_task(cls, task)&upgrade_task(cls, task): Callinject_finder_keys(task)so allfinder.*search parameters are populated intask.config.task_set(cls, task, key_values): Resolvemaster(product 4-char hex ID or title) intotask.config['master'](product title) andtask.config['master_path'](relative path ineodata) viafinder.get_meta_record(value).validate_task(cls, task, key): Validateeodata,master, andmaster_path.cli_task_mount(cls, cli_group): Mount any custom CLI commands underocli task template.
from pathlib import Path
from typing import Dict, List
from ocli.aikp.boilerplate.template import TemplateBoilerplate
from ocli.core.exception import OCLIException
from ocli.core.logging_tools import log
from ocli.finders import finder_by_task, inject_finder_keys
from ocli.helpers.update_visible_metadata import update_visible_metadata
from ocli.project import _local_eodata_relative_path
from .config import RECIPE_DEFAULTS, TASK_DEFAULTS
from .finder import FinderCreodiasSentinel2L1C
class TemplateBoilerplateSentinel2L1C(TemplateBoilerplate):
__version__ = 2.0
template_recipe_conf = RECIPE_DEFAULTS
template_task_conf = TASK_DEFAULTS
REQUIRED = "Required by Boilerplate Sentinel-2 L1C Template AIKP"
@classmethod
def get_finder(cls, task) -> FinderCreodiasSentinel2L1C:
return FinderCreodiasSentinel2L1C(
eodata_home=task.get_eodata_home_path(),
cache_dir=task.get_project_cache_path(),
)
@classmethod
def create_task(cls, task):
super().create_task(task)
inject_finder_keys(task)
@classmethod
def upgrade_task(cls, task) -> bool:
inject_finder_keys(task)
return super().upgrade_task(task)
@classmethod
def task_set(cls, task, key_values: Dict):
for k in ('master',):
if k not in key_values:
continue
value = key_values[k]
try:
finder = finder_by_task(task)
record = finder.get_meta_record(value)
task.config[k] = record['title']
task.config[k + '_path'] = record['product_eodata_path']
key_values.pop(k, None)
except OCLIException as e:
log.warning(f'Could not resolve product {value}: {e}')
super().task_set(task, key_values)
Step 4: Expose Custom CLI Commands (cli.py)
Use finder_by_task(task) and _local_eodata_relative_path(eodata, master_path) inside your custom CLI command to access product metadata and .SAFE files (MTD_MSIL1C.xml and GRANULE/*/IMG_DATA/*.jp2):
import click
from ocli.classes.task import Task
from ocli.cli.state import pass_task
from ocli.cli.task import ensure_task_resolved
from ocli.core import output
from ocli.core.exception import OCLIException
from ocli.finders import finder_by_task
@click.command('inspect_l1c')
@pass_task
@ensure_task_resolved
def inspect_l1c(task: Task):
master = task.config.get('master')
master_path = task.config.get('master_path')
if not master:
raise OCLIException('No master product set in task. Run `task set master=<product_id>` first.')
finder = finder_by_task(task)
record = finder.get_meta_record(master)
output.info(f"Title: {record['title']}")
output.info(f"Product ID: {record['productId']}")
output.info(f"Processing Level: {record['processingLevel']}")
output.info(f"EODATA Relative Path: {master_path or record['product_eodata_path']}")
3. End-to-End CLI Usage
# 1. Create a project and add an ROI
ocli create -n demo_project -a
ocli roi add -n demo_roi /path/to/roi.geojson
# 2. Create a task using the custom Sentinel-2 L1C AIKP template
ocli task create -t ocli.aikp.boilerplate.sentinel_2_l1c -r demo_roi -n s2_l1c_task --activate
# 3. Verify that finder.processingLevel defaults to S2MSI1C
ocli task show -k finder.processingLevel
# 4. Set search dates (and optionally provider: stac or odata) and query Creodias
ocli task set finder.startDate=2024-07-28 finder.completionDate=2024-08-01
ocli product load
# 5. List discovered Sentinel-2 L1C products and assign master by 4-char productId or title
ocli product list
ocli task set master=<productId>
# 6. Download product into local EODATA (when connected to EODATA storage)
ocli task get-data -m --data
# 7. Run the custom AIKP command to inspect the L1C product
ocli task template inspect_l1c