Plugin Development
This guide describes the plugin APIs that are currently implemented in Adapt.
Core Plugin Interfaces
Adapt plugin interfaces live in adapt/plugins/base.py.
PluginContext
PluginContext provides shared execution context:
enginerootreadonlylock_manager
ResourceDescriptor
A discovered resource has these attributes:
pathresource_typeschema_pathui_pathoptions_pathmetadata
Plugin Base Class
A plugin must implement:
detect(path)load(path)schema(resource)read(resource, request)write(resource, data, request, context)
Optional extension points:
apply_options(descriptor)get_route_configs(descriptor)index(resource)filter_for_user(resource, user, rows)default_ui(descriptor)generate_companion_files(descriptor)
How Discovery Works
Discovery scans the document root. The plugin_registry entry for the file
extension selects a candidate plugin.
Important behavior:
- Discovery ignores an extension without a registry entry.
- Discovery creates the candidate plugin and calls
detect(path). - If
detect(path)returnsFalse, discovery ignores the file. - If
detect(path)returnsTrue, discovery callsload(path).
Plugin Registration
Configure plugins in DOCROOT/.adapt/conf.json under plugin_registry.
Use dotted class paths (not module:Class):
{
"plugin_registry": {
".csv": "adapt.plugins.csv_plugin.CsvPlugin",
".myext": "my_plugin.plugin.MyPlugin"
}
}
Route Mounting Model
Plugins return route configs as (prefix, router) pairs.
Generated mounting combines prefix and namespace into routes such as:
/api/<namespace>/schema/<namespace>/ui/<namespace>/media/<namespace>
For each resource, Adapt mounts routes for both namespace forms. For reports.myext,
the API route exists at /api/reports/ and /api/reports.myext/.
Sub-namespaces follow the filename. For the Summary sheet in reports.xlsx,
the forms are /api/reports/Summary/ and /api/reports.xlsx/Summary/.
Implementing a Dataset-Style Plugin
Dataset-style plugins can inherit from DatasetPlugin to reuse schema, UI, and mutation patterns.
Set descriptor.metadata["readonly"] to True for a resource that cannot use
the shared mutation path. You can add a user-facing message in
descriptor.metadata["readonly_reason"]. The API returns this message with
status 405, and the default UI hides its edit controls.
Expected mutation contract:
POST /api/<resource>with{ "action": "create", "data": [...] }PATCH /api/<resource>with{ "action": "update", "data": { ... } }DELETE /api/<resource>with{ "action": "delete", "data": { ... } }
Mutations must:
- Respect
context.readonly - Use
lock_managerto avoid unsafe concurrent writes - Invalidate cache after successful changes
The schema returned by schema() supplies serialization hints, UI column
metadata, and validation rules for the shared dataset mutation path. Create and
update fields use the common string, integer, number, and boolean types
(plus corresponding pandas type names). Adapt normalizes numeric and boolean
strings. Adapt permits blank strings and null values. Unknown columns or
incompatible values return 422 before Adapt locks or writes the resource.
Custom schema types without defined validation semantics remain metadata only.
Adapt applies filter_for_user() on dataset reads. It is also available as a
row-filtering extension point. The shared mutation implementation does not
provide safe write-level row-security enforcement. It reads and rewrites the
row collection, and row identifiers can diverge after filtering. A plugin
that needs row-level write authorization must implement and test its own
write path instead of using this hook.
See Known Limitations.
Example Skeleton
from pathlib import Path
from typing import Any, Iterable, Sequence
from fastapi import Request
from fastapi.routing import APIRouter
from adapt.plugins.base import (
Plugin,
PluginContext,
ResourceDescriptor,
SearchDocument,
)
class MyPlugin(Plugin):
def detect(self, path: Path) -> bool:
return path.suffix.lower() == ".myext"
def load(
self, path: Path
) -> ResourceDescriptor | Sequence[ResourceDescriptor]:
return ResourceDescriptor(path=path, resource_type="myext")
def schema(self, resource: ResourceDescriptor) -> dict[str, Any]:
return {}
def read(self, resource: ResourceDescriptor, request: Request) -> Any:
return {"ok": True}
def write(
self,
resource: ResourceDescriptor,
data: Any,
request: Request,
context: PluginContext,
) -> Any:
if context.readonly:
raise RuntimeError("read-only mode")
return {"success": True}
def apply_options(self, descriptor: ResourceDescriptor) -> None:
pass
def get_route_configs(
self, descriptor: ResourceDescriptor
) -> list[tuple[str, APIRouter]]:
router = APIRouter()
@router.get("/")
def get_resource(request: Request) -> Any:
return self.read(descriptor, request)
return [("api", router)]
def index(
self, resource: ResourceDescriptor
) -> Iterable[SearchDocument]:
return []
Adapt mounts the returned router under both resource namespaces. It also adds the resource permission dependency to the router.
Python Handler Plugins
The built-in Python handler plugin (.py) loads modules and mounts an APIRouter named router under /api/<filename>.
If import fails, Adapt skips the handler.
Companion Files
Built-in dataset plugins generate companion files under .adapt/:
*.schema.json*.index.html
The companion UI filename always ends in .index.html. For example, the UI
for the Dashboard sheet in report.xlsx is
.adapt/report.Dashboard.index.html.
The media plugin uses ui_path differently. It writes JSON metadata to that
path, not an HTML template.
A generated *.schema.json carries "generated_by": "adapt". Adapt refreshes such a
file when the resource's shape changes. Adapt leaves any schema without that key alone.
As a result, Adapt never overwrites hand-written schemas. Adapt strips the key before
it serves the schema from /schema/<resource>.
Resource Options
You can also hand-write *.options.json alongside the generated files to override how
Adapt parses a resource. The naming matches the other companion files. For the sheet
Dashboard in report.xlsx, that is .adapt/report.Dashboard.options.json.
Supported keys:
| Key | Applies to | Meaning |
|---|---|---|
header_row |
.xlsx, .xls |
1-based row holding the column names. Defaults to 1. |
When a sheet opens with a title banner instead of column names, use header_row.
Without this option, Adapt parses the banner as the header:
{ "header_row": 3 }
Adapt ignores rows above the header row on both read and write operations. Adapt logs an unreadable options file or an invalid value and ignores it. Adapt does not raise an error. As a result, a typo cannot crash the server.
To consume options, a plugin overrides apply_options(descriptor). Discovery calls this
method after load() and before generate_companion_files(). load() sees only a path,
so it cannot locate the companion directory. Discovery calls apply_options() before
generate_companion_files(), so a derived schema reflects the options.
Testing Recommendations
When you create plugins, test the following:
- Discovery and load behavior
- Schema generation
- Read/write behavior
- Read-only mode behavior
- Lock conflict behavior
- Route registration and endpoint responses
Compatibility Notes
- Keep plugin class paths stable for
plugin_registryusers. - Prefer additive schema changes when possible.
- Document any plugin-specific configuration keys.
Manual navigation: Previous: Configuration | Index | Next: Architecture