Plugin Structure and BoilerplateΒΆ
An ADL Plugin is fundamentally a folder named after the plugin. The folder should be a Django/Wagtail App.
This section explains the typical file/folder layout of an ADL plugin, what each file does, and how it plugs into the ADL Core runtime (Django + Wagtail + Celery + TimescaleDB).
Initialize your plugin from the plugin templateΒΆ
The plugin template is a cookiecutter template that generates a plugin with the required structure and files. This ensures that the plugin follows the expected structure and can be easily installed into the adl core application.
With the plugin boilerplate you can easily create a new plugin and setup a docker development environment that installs ADL as a dependency. This can easily be installed via cookiecutter.
To instantiate the template, execute the following commands from the directory where you want to create the plugin:
pip install cookiecutter
cookiecutter gh:wmo-raf/adl --directory plugin-boilerplate
Plugin Installation APIΒΆ
A adl docker image contains the following bash scripts that are used to install plugins. They can be used
to install a plugin into an existing adl container at runtime. install_plugin.sh can be used to install a
plugin from an url, a git repo or a local folder on the filesystem.
You can find these scripts in the following locations in the built images:
/adl/plugins/install_plugin.sh
On this repo, you can find the scripts in the deploy/plugins folder.
These scripts expect an adl plugin to follow the conventions described below:
Plugin File StructureΒΆ
The install_plugin.sh script expect your plugin to have a specific structure as follows:
βββ plugin_name
β βββ adl_plugin_info.json (A simple json file containing info about your plugin)
| βββ setup.py
| βββ build.sh (Called when installing the plugin in a Dockerfile/container)
| βββ runtime_setup.sh (Called on first runtime startup of the plugin)
| βββ uninstall.sh (Called when uninstalling the plugin in a container)
| βββ src/plugin_name/src/config/settings/settings.py (Optional Django setting file)
The folder contains three bash files which will be automatically called by adlβs plugin scripts during installation and uninstallation of the plugin. You can use these scripts to perform extra build steps, installation of packages and other docker container build steps required by your plugin.
build.sh: Called on container startup if a runtime installation is occurring.runtime_setup.sh: Called the first time a container starts up after the plugin has been installed, useful for running superuser commands on the container.uninstall.sh: Called on uninstall, the database will be available and so any backwards migrations should be run here.
The plugin info fileΒΆ
The adl_plugin_info.json file is a json file, in your root plugin folder, containing metadata about your
plugin. It should have the following JSON structure:
{
"name": "TODO",
"version": "TODO",
"description": "TODO",
"author": "TODO",
"author_url": "TODO",
"url": "TODO",
"license": "TODO",
"contact": "TODO"
}
Docker & Runtime IntegrationΒΆ
dev.DockerfileΒΆ
Base image:
FROM adl:latest(inherits ADL Core with Django/Celery/etc.).User/permissions: Uses
PLUGIN_BUILD_UID/GIDso mounted volumes are writable from your host.Installs dev deps: from
requirements/dev.txt(linters, build tools).Installs the plugin: Copies your code under
/adl/plugins/<module>/then runs/adl/plugins/install_plugin.sh --dev(provided by ADL Core) to install and register it in the ADL virtualenv.
docker-compose.ymlΒΆ
Defines a dev stack:
adl_db: TimescaleDB/PostgreSQL for time-series data.adl_redis: Redis for Celery broker/locks.adl: Django web app (ADL Core + your plugin); mounts your plugin for hot reload.adl_celery_worker: Celery worker.adl_celery_beat: Celery beat (schedules).
.env.sampleΒΆ
Template for runtime values. Copy to .env and set:
PLUGIN_BUILD_UID,PLUGIN_BUILD_GID(useid -u/id -g)ADL_DB_USER,ADL_DB_PASSWORD,ADL_DB_NAMEPORT
Packaging & MetadataΒΆ
setup.py / pyproject.tomlΒΆ
Standard Python packaging. setup.py points to the src/ layout and reads dependencies from requirements/base.txt.
MANIFEST.inΒΆ
Include non-Python assets (templates, static files, locales) in the distribution.
requirements/ (managed with pip-tools)ΒΆ
base.inβ compiled tobase.txt(runtime deps installed into the image)dev.inβ compiled todev.txt(linters, build/test tools)
adl_plugin_info.jsonΒΆ
Human-friendly plugin metadata used by ADL (name, version, author, URLs, license, contact).
Lifecycle ScriptsΒΆ
build.shβ Runs at plugin build time (during Docker image build or explicit plugin build). Put install-time tasks here (e.g., OS libs, non-Python tools). Do not touch the ADL data volume here.runtime_setup.shβ Runs on first container start with the plugin installed (must be idempotent). Put runtime tasks here that require the DB or data volume (e.g., creating extensions, seeding reference data). UsePLUGIN_RUNTIME_SETUP_MARKERto guard reruns.uninstall.shβ Runs when uninstalling the plugin. Reverse side effects not handled by Django migrations (custom DB schema, hypertables, etc.). Python package uninstall is handled by ADL automatically.
Django App Layer (src/<module>/)ΒΆ
apps.py β Register your pluginΒΆ
from django.apps import AppConfig
from adl.core.registries import plugin_registry
from .plugins import MyPlugin
class MyPluginConfig(AppConfig):
name = "my_plugin"
def ready(self):
plugin_registry.register(MyPlugin())
Registration makes your plugin discoverable via
NetworkConnection.plugin(stringtype).
plugins.py β The Plugin classΒΆ
Subclass ADLβs base Plugin and implement ingestion:
from adl.core.registries import Plugin
class MyPlugin(Plugin):
type = "my_plugin" # stable identifier
label = "My Plugin" # human-readable name
def get_station_data(self, station_link, start_date=None, end_date=None):
# Fetch provider data in [start_date, end_date) (station-local aware datetimes)
# Return iterable of dicts with at least "observation_time" + source fields
return [
{"observation_time": some_dt, "temp_K": 293.15, "rh": 75.0},
# ...
]
Best practice: Let the base class persist via
save_records()(it handles timezone normalization, unit conversion, and bulk upsert). You just return raw rows.
models.py β Plugin modelsΒΆ
Typical plugin models:
NetworkConnectionsubclass: provider credentials/config (API keys, base URL, batching, daily vs hourly).StationLinksubclass: per-station config (provider station code, timezone, first-collection date, variable mappings).Mapping model: map ADL parameters β provider field names + source units.
Recommended mapping attribute names (so save_records() works out of the box):
adl_parameterβDataParameterinstance (target variable in ADL)source_parameter_nameβ provider field key you will return inget_station_data()source_parameter_unitβUnitinstance representing providerβs unit
If your field names differ (e.g., tahmo_variable_shortcode), either:
expose properties that alias to the expected names, or
override
StationLink.get_variable_mappings()to yield objects with those attributes.
wagtail_hooks.py / views.py / widgets.pyΒΆ
Add admin endpoints (AJAX lists, metadata pages) or custom Wagtail form widgets to improve connection/station configuration UX.
Examples:
Endpoints listing provider stations/variables via your
client.py.Widgets that call those endpoints and render searchable selects.
client.pyΒΆ
Provider-specific connection logic (auth, retries, pagination, caching). Keep API logic here and call it from
get_station_data() or admin views.
The file can be named anything to match your provider connection mechanism. For example db.py for a database
connection, http.py for an HTTP API, ftp.py for an FTP server, etc.
config/settings/settings.pyΒΆ
Late-binding hook ADL calls during settings init:
def setup(settings):
# e.g., settings.INSTALLED_APPS += ["my_extra_dep"]
pass
migrations/ΒΆ
Include migrations if you define models (standard Django workflow).
How the pieces work togetherΒΆ
Django starts β
apps.py.ready()registers your plugin in the plugin registry.In the admin, a user creates a Network Connection and selects your plugin by
type.They create Station Links, define variable mappings (ADL parameter + source field + unit), and set per-station options (timezone, start date).
Celery (or a manual action) triggers
NetworkConnection.collect_data()β ADL callsplugin.run_process(conn).For each enabled Station Link, ADL computes a station-local time window and calls your
get_station_data().ADLβs base
save_records()performs unit conversion, timezone normalization, and bulk upsert intoObservationRecord.
Naming & ConventionsΒΆ
type(plugin ID) must be stable and globally unique (often your package name, e.g.,adl_tahmo_plugin).Keep source field names in your results exactly matched to mapping rowsβ
source_parameter_name.Return aware datetimes when possible; if naive, theyβre interpreted as station-local.
Use the module logger (
logging.getLogger(__name__)) for clear, prefixed logs.
Directory-by-Directory CheatsheetΒΆ
plugins/<module>/src/<module>/plugins.pyβ yourPluginsubclass; implementget_station_data().plugins/<module>/src/<module>/apps.pyβ register the plugin at startup.plugins/<module>/src/<module>/models.pyβNetworkConnection,StationLink, and mapping models.plugins/<module>/src/<module>/client.pyβ HTTP API wrapper(s). Could be nameddb.py,ftp.py, etc. for database, FTP, or other connections.plugins/<module>/src/<module>/wagtail_hooks.pyβ admin URLs & integration.plugins/<module>/src/<module>/views.pyβ admin AJAX endpoints or pages.plugins/<module>/src/<module>/widgets.pyβ custom admin widgets.plugins/<module>/src/<module>/validators.pyβ reusable validation helpers.plugins/<module>/src/<module>/config/settings/settings.pyβ late settings hook.plugins/<module>/requirements/β dependency specs (pip-tools).plugins/<module>/build.shβ build-time tasks.plugins/<module>/runtime_setup.shβ one-time runtime initialization.plugins/<module>/uninstall.shβ cleanup on uninstall.dev.Dockerfileβ dev image that installs your plugin into ADL.docker-compose.ymlβ local dev stack (DB, Redis, app, Celery)..env.sampleβ template env file for local runs.adl_plugin_info.jsonβ plugin metadata (name, version, URLs).
Minimal Working Set (if youβre in a hurry)ΒΆ
plugins/<module>/src/<module>/plugins.pyβ implementget_station_data()plugins/<module>/src/<module>/apps.pyβ register the pluginplugins/<module>/src/<module>/models.pyβ define aNetworkConnection,StationLink, and mappingsdev.Dockerfile,docker-compose.yml,.env.sampleβ to run locallysetup.py,MANIFEST.in,requirements/β to package/install
With just these, you can fetch, normalize, and store observations end-to-end.
Extensibility Hooks (optional)ΒΆ
Plugin.get_urls()β expose plugin-specific URLs (health checks, manual triggers).Plugin.get_default_start_date()/get_default_end_date()β customize cadence (e.g., daily at 00:00).StationLink.get_first_collection_date()β provider-specific history fallback.Dispatch channels (outside the plugin): push saved data to WIS2, MQTT, WIS2Box, etc.
Quick Validation ChecklistΒΆ
[ ]
apps.pyregisters one instance:plugin_registry.register(MyPlugin())[ ]
typeis unique and stable[ ]
get_station_data()returns dicts with"observation_time"+ mapped source fields[ ] Station-link mappings expose
.adl_parameter,.source_parameter_name,.source_parameter_unit[ ] Units are
Unitobjects; ADL converts to theDataParameterβs unit[ ] Timestamps are aware (preferred) or valid station-local naive
[ ] Use base
save_records()for persistence (bulk upsert)[ ] Secrets/base URLs live in
NetworkConnectionor.env[ ] Dev stack works:
docker compose build && docker compose up
In short: a plugin is a normal Django app packaged into the ADL image that registers a Plugin subclass. You
provide the fetch logic; ADL handles time-windowing, unit conversion, timezone normalization, and storage.