diff --git a/docs/development/rfc/6.md b/docs/development/rfc/6.md new file mode 100644 index 00000000..1833b071 --- /dev/null +++ b/docs/development/rfc/6.md @@ -0,0 +1,104 @@ +--- +title: RFC6 - Add an optional plugin context and remove pydantic from core +--- + +# RFC - Add an optional plugin context and remove pydantic from core + +- date: 2026-03-28 +- author: Francesco Bartoli +- contact: xbartolone@gmail.com +- status: draft +- modified: 2026-03-29 + +## Overview + +This RFC describes the introduction of a `PluginContext` dataclass to inject optional dependencies in the plugin system and the migration of core pydantic models to standard library dataclasses. + +Currently, pygeoapi plugins rely on global state for runtime dependencies (logger, locales) and use pydantic `BaseModel` for configuration and metadata validation in core modules. The following issues arise from this design: + +- plugins cannot receive per-instance dependencies, making unit testing difficult +- pydantic is a required core dependency even though most providers do not use its validation features +- the pydantic v1/v2 compatibility layer adds maintenance burden +- downstream projects cannot easily extend or override validation behaviour + +## Proposed solution + +### Optional PluginContext class to add more dependencies to plugins + +An optional `context` parameter is added to `load_plugin()` and the base classes (`BaseProvider`, `BaseProcessor`, `BaseManager`): + +```python +@dataclass +class PluginContext: + config: Dict[str, Any] + logger: Optional[Any] = None + locales: Optional[List[str]] = None + base_url: Optional[str] = None +``` + +Plugins that accept `context` gain access to injected dependencies. Plugins that do not are unaffected -- the parameter defaults to `None` and all base classes fall back to global state. + +### Dataclasses replacing pydantic in core + +All pydantic `BaseModel` classes in `pygeoapi/models/` are replaced with `@dataclass` equivalents that: + +- validate types at construction via a shared `validate_type()` utility +- expose a `model_dump(exclude_none=False)` method for serialization +- can be overridden downstream via duck typing (any class with the same interface satisfies a Protocol check) + +The affected models are: + +- `APIRules` in `pygeoapi/models/config.py` +- `OAPIFormat` in `pygeoapi/models/openapi.py` +- `TileMatrixSetEnumType`, `TileMatrixLimitsType`, `TwoDBoundingBoxType`, `LinkType`, `GeospatialDataType`, `StyleType`, `TilePointType`, `TileSetMetadata` in `pygeoapi/models/provider/base.py` +- `VectorLayers`, `MVTTilesJson` in `pygeoapi/models/provider/mvt.py` + +Pydantic remains fully usable at the plugin level. Providers that need pydantic validation (e.g. MVT providers for tile metadata) can continue using it as a plugin-level dependency. + +## Implementation + +### PluginContext + +- `PluginContext` dataclass is added to `pygeoapi/plugin.py` +- `load_plugin()` accepts an optional `context` keyword argument and forwards it to the plugin constructor +- `BaseProvider`, `BaseProcessor`, `BaseManager` accept `context: Optional[PluginContext] = None` with typed parameter +- base classes use injected logger/locales when provided, falling back to module-level globals otherwise + +### Dataclass migration + +- a `validate_type()` function is added to `pygeoapi/models/validation.py` providing runtime type checking equivalent to pydantic's behaviour +- each `BaseModel` is replaced with a `@dataclass` that calls `validate_type(self)` in `__post_init__` +- `model_dump()` is implemented on each dataclass to maintain serialization compatibility +- `MVTTilesJson` accepts and silently ignores unknown kwargs and coerces string values to int where needed, matching pydantic's behaviour when instantiated from arbitrary JSON metadata + +### Backwards Compatibility Issues + +This change is designed for full backwards compatibility: + +- `context` defaults to `None` -- existing plugins work unchanged +- `model_dump()` method signature and behaviour are preserved +- `APIRules.create()` factory method is preserved +- all existing tests pass without modification (except `test_models.py` which switches from `ModelFactory` to `DataclassFactory` for polyfactory) + +The `api/` module continues to call `load_plugin()` without a context. Wiring `PluginContext` into the API layer is a natural follow-up but intentionally out of scope to keep this change focused and low-risk. + +### Testing + +- unit tests for all dataclass models (creation, validation, type errors, `model_dump`, `exclude_none`) +- unit tests for `PluginContext` and dependency injection in plugin loading +- integration tests verifying tile metadata serialization (`test_tiles.py`) +- integration tests verifying `APIRules` through `get_api_rules()` and framework apps + +### Documentation + +Documentation will be updated to reflect the new `PluginContext` parameter and the dataclass-based models. + +## Issue and Pull Request tracking + +Issue: [Summit discussion](https://github.com/geopython/pygeoapi/wiki/Code-Sprint-2024-02-29-to-2024-03-01#notes), [meeting](https://github.com/geopython/pygeoapi/wiki/Meeting-2025-05-02) + +Pull Request: [#2307](https://github.com/geopython/pygeoapi/pull/2307) + +## Voting History + +TBD diff --git a/docs/development/rfc/index.md b/docs/development/rfc/index.md index 1e64e8e4..b5d3d753 100644 --- a/docs/development/rfc/index.md +++ b/docs/development/rfc/index.md @@ -15,3 +15,4 @@ the project. * [RFC3: Architectural Decision Records](3) * [RFC4: GitHub Issue and Pull Request management](4) * [RFC5: Enhanced data limit handling](5) +* [RFC6: Add an optional plugin context and remove pydantic from core](6)