# 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"`)**: maps `processingLevel='S2MSI1C'` to collection `['sentinel-2-l1c']`. - **OData (`finder.provider="odata"`)**: queries collection `['SENTINEL-2']` with filter `productType 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`: ```python 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/`: ```text 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: ```python 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`: ```python 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: 1. **`get_finder(cls, task)`**: Instantiate and return `FinderCreodiasSentinel2L1C`. 2. **`create_task(cls, task)`** & **`upgrade_task(cls, task)`**: Call `inject_finder_keys(task)` so all `finder.*` search parameters are populated in `task.config`. 3. **`task_set(cls, task, key_values)`**: Resolve `master` (product 4-char hex ID or title) into `task.config['master']` (product title) and `task.config['master_path']` (relative path in `eodata`) via `finder.get_meta_record(value)`. 4. **`validate_task(cls, task, key)`**: Validate `eodata`, `master`, and `master_path`. 5. **`cli_task_mount(cls, cli_group)`**: Mount any custom CLI commands under `ocli task template`. ```python 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`): ```python 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=` 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 ```shell # 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= # 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 ```