2023-09-21 13:26:13 -07:00
|
|
|
import importlib
|
2026-07-25 15:47:08 -07:00
|
|
|
import importlib.metadata as importlib_metadata
|
|
|
|
|
import importlib.resources as importlib_resources
|
2023-08-30 15:12:24 -07:00
|
|
|
import os
|
2019-05-01 22:09:03 -07:00
|
|
|
import sys
|
2026-07-25 15:47:08 -07:00
|
|
|
from pprint import pprint
|
2018-08-04 17:14:56 -07:00
|
|
|
|
2026-07-25 15:47:08 -07:00
|
|
|
import pluggy
|
2023-09-21 12:42:15 -07:00
|
|
|
|
2026-07-25 15:47:08 -07:00
|
|
|
from . import hookspecs
|
2023-09-21 12:42:15 -07:00
|
|
|
|
Extract facet code out into a new plugin hook, closes #427 (#445)
Datasette previously only supported one type of faceting: exact column value counting.
With this change, faceting logic is extracted out into one or more separate classes which can implement other patterns of faceting - this is discussed in #427, but potential upcoming facet types include facet-by-date, facet-by-JSON-array, facet-by-many-2-many and more.
A new plugin hook, register_facet_classes, can be used by plugins to add in additional facet classes.
Each class must implement two methods: suggest(), which scans columns in the table to decide if they might be worth suggesting for faceting, and facet_results(), which executes the facet operation and returns results ready to be displayed in the UI.
2019-05-02 17:11:26 -07:00
|
|
|
DEFAULT_PLUGINS = (
|
|
|
|
|
"datasette.publish.heroku",
|
2019-05-03 15:59:01 +02:00
|
|
|
"datasette.publish.cloudrun",
|
Extract facet code out into a new plugin hook, closes #427 (#445)
Datasette previously only supported one type of faceting: exact column value counting.
With this change, faceting logic is extracted out into one or more separate classes which can implement other patterns of faceting - this is discussed in #427, but potential upcoming facet types include facet-by-date, facet-by-JSON-array, facet-by-many-2-many and more.
A new plugin hook, register_facet_classes, can be used by plugins to add in additional facet classes.
Each class must implement two methods: suggest(), which scans columns in the table to decide if they might be worth suggesting for faceting, and facet_results(), which executes the facet operation and returns results ready to be displayed in the UI.
2019-05-02 17:11:26 -07:00
|
|
|
"datasette.facets",
|
2021-12-17 11:02:14 -08:00
|
|
|
"datasette.filters",
|
2019-12-29 18:48:13 +00:00
|
|
|
"datasette.sql_functions",
|
2020-05-31 18:03:17 -07:00
|
|
|
"datasette.actor_auth_cookie",
|
2020-05-31 22:00:36 -07:00
|
|
|
"datasette.default_permissions",
|
register_token_handler() plugin hook for custom API token backends (#2650)
Closes #2649
* Add register_token_handler plugin hook for pluggable token backends
Adds a new register_token_handler hook that allows plugins to provide
custom token creation and verification backends. This enables plugins
like datasette-oauth to issue tokens without depending on specific
backend plugins like datasette-auth-tokens.
Key changes:
- New datasette/tokens.py with TokenHandler base class and SignedTokenHandler
(the default signed-token implementation moved here)
- New register_token_handler hookspec in hookspecs.py
- Datasette.create_token() is now async and delegates to token handlers
- New Datasette.verify_token() method tries all handlers in sequence
- handler= parameter on create_token() to select a specific backend
- TokenHandler exported from datasette package for plugin use
- Fixed actor_from_request loop to await all coroutines (avoids warnings)
* Add documentation and hook test for register_token_handler
Fixes CI failures: the new hook needs a section in docs/plugin_hooks.rst
(checked by test_plugin_hooks_are_documented) and a test_hook_* function
in test_plugins.py (checked by test_plugin_hooks_have_tests).
* Register tokens module as separate default plugin
Instead of re-exporting hookimpls from default_permissions/__init__.py,
register datasette.default_permissions.tokens as its own DEFAULT_PLUGINS
entry. Cleaner and avoids confusing import-for-side-effect patterns.
* Replace restrict_x params with TokenRestrictions dataclass
Consolidates the three separate restrict_all, restrict_database, and
restrict_resource parameters into a single TokenRestrictions dataclass.
Cleaner API surface for both Datasette.create_token() and
TokenHandler.create_token().
Also clarifies docs re: default handler selection via pluggy ordering.
* Add builder methods to TokenRestrictions
Adds allow_all(), allow_database(), and allow_resource() methods that
return self for chaining. Callers no longer need to manipulate nested
dicts directly:
restrictions = (TokenRestrictions()
.allow_all("view-instance")
.allow_database("mydb", "create-table")
.allow_resource("mydb", "mytable", "insert-row"))
* docs: add 1.0a25 upgrade guide section for create_token() signature change
Ref: https://github.com/simonw/datasette/issues/2649#issuecomment-3962639393
* docs: note that create_token() is now async in upgrade guide
* docs: update internals, plugin_hooks, authentication for new token API
- internals.rst: new async create_token() signature with restrictions
and handler params, add TokenRestrictions reference docs
- plugin_hooks.rst: show full create_token signature in TokenHandler
example, note list returns and error cases
- authentication.rst: cross-reference TokenRestrictions from the
restrictions section
* style: apply black formatting to token handler files
* docs: fix RST heading underline length in internals.rst
* tests: add restrictions round-trip and expiration tests for token handler
Covers allow_database/allow_resource builders, _r payload encoding,
and token_expires in verified actors. Coverage 76% -> 90%.
* tests: add test for signed tokens disabled
* fix: add TokenRestrictions TYPE_CHECKING import to fix ruff F821
* docs: regenerate plugins.rst with cog
* docs: reformat code blocks in plugin_hooks.rst with blacken-docs
* docs: add await .verify_token() to internals.rst
* tests: rewrite register_token_handler test to use real plugin handler
Adds a HardcodedTokenHandler to the test plugins dir that creates
tokens like dstok_hardcoded_token_1. The test now exercises creating
tokens via the default handler (which is the plugin's hardcoded one),
by explicitly naming the hardcoded handler, and by explicitly naming
the signed handler -- then verifies each token round-trips correctly.
* tests: clarify test_token_handler_via_http tests the default signed handler
* fix: use handler="signed" explicitly where signed tokens are expected
The HardcodedTokenHandler in my_plugin.py gets globally registered,
so create_token() without a handler name picks it up as the default.
Fix the create-token view, CLI, and tests to explicitly request the
signed handler where they depend on signed token behavior.
* fix: use handler="signed" in test_create_table_permissions
https://claude.ai/code/session_013cQFiDQjYRrRBH2biFfKuS
2026-02-25 16:32:45 -08:00
|
|
|
"datasette.default_permissions.tokens",
|
Implement resource-based permission system with SQL-driven access control
This introduces a new hierarchical permission system that uses SQL queries
for efficient permission checking across resources. The system replaces the
older permission_allowed() pattern with a more flexible resource-based
approach.
Core changes:
- New Resource ABC and Action dataclass in datasette/permissions.py
* Resources represent hierarchical entities (instance, database, table)
* Each resource type implements resources_sql() to list all instances
* Actions define operations on resources with cascading rules
- New plugin hook: register_actions(datasette)
* Plugins register actions with their associated resource types
* Replaces register_permissions() and register_resource_types()
* See docs/plugin_hooks.rst for full documentation
- Three new Datasette methods for permission checks:
* allowed_resources(action, actor) - returns list[Resource]
* allowed_resources_with_reasons(action, actor) - for debugging
* allowed(action, resource, actor) - checks single resource
* All use SQL for filtering, never Python iteration
- New /-/tables endpoint (TablesView)
* Returns JSON list of tables user can view
* Supports ?q= parameter for regex filtering
* Format: {"matches": [{"name": "db/table", "url": "/db/table"}]}
* Respects all permission rules from configuration and plugins
- SQL-based permission evaluation (datasette/utils/actions_sql.py)
* Cascading rules: child-level → parent-level → global-level
* DENY beats ALLOW at same specificity
* Uses CTEs for efficient SQL-only filtering
* Combines permission_resources_sql() hook results
- Default actions in datasette/default_actions.py
* InstanceResource, DatabaseResource, TableResource, QueryResource
* Core actions: view-instance, view-database, view-table, etc.
- Fixed default_permissions.py to handle database-level allow blocks
* Now creates parent-level rules for view-table action
* Fixes: datasette ... -s databases.fixtures.allow.id root
Documentation:
- Comprehensive register_actions() hook documentation
- Detailed resources_sql() method explanation
- /-/tables endpoint documentation in docs/introspection.rst
- Deprecated register_permissions() with migration guide
Tests:
- tests/test_actions_sql.py: 7 tests for core permission API
- tests/test_tables_endpoint.py: 13 tests for /-/tables endpoint
- All 118 documentation tests pass
- Tests verify SQL does filtering (not Python)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 15:59:37 -07:00
|
|
|
"datasette.default_actions",
|
Add column types system for semantic column annotations
Implements the column types feature that lets Datasette and plugins annotate
columns with semantic types beyond SQLite storage types (e.g. markdown, email,
url, json, file, point). This enables type-appropriate rendering, validation,
form widgets, and API behavior.
Key changes:
- New `column_types` internal DB table for storing assignments
- `ColumnType` dataclass in datasette/column_types.py with render_cell,
validate, and transform_value methods
- `register_column_types` plugin hook for registering types
- Built-in url, email, and json column types
- Datasette API methods: get/set/remove_column_type(s),
get_column_type_class
- Config loading from datasette.json `column_types` table config key
- `column_types` extra on the table JSON endpoint
- Column type info in display_columns extra
- Column type render_cell gets priority in rendering pipeline
- column_type/column_type_config args added to render_cell hookspec
- Write-path validation on insert and update
https://claude.ai/code/session_01SvPEPqHgURTWESRp28pTC3
2026-03-17 02:40:37 +00:00
|
|
|
"datasette.default_column_types",
|
2020-06-27 19:58:16 -07:00
|
|
|
"datasette.default_magic_parameters",
|
2020-10-29 15:01:38 -07:00
|
|
|
"datasette.blob_renderer",
|
2026-05-23 16:57:09 -07:00
|
|
|
"datasette.default_debug_menu",
|
2026-05-23 09:10:25 -07:00
|
|
|
"datasette.default_jump_items",
|
2026-05-25 08:49:18 -07:00
|
|
|
"datasette.default_database_actions",
|
2026-06-17 09:21:09 -07:00
|
|
|
"datasette.default_table_actions",
|
2026-06-08 20:19:47 -07:00
|
|
|
"datasette.default_query_actions",
|
2022-07-17 16:24:39 -07:00
|
|
|
"datasette.handle_exception",
|
|
|
|
|
"datasette.forbidden",
|
2024-01-31 15:21:40 -08:00
|
|
|
"datasette.events",
|
Extract facet code out into a new plugin hook, closes #427 (#445)
Datasette previously only supported one type of faceting: exact column value counting.
With this change, faceting logic is extracted out into one or more separate classes which can implement other patterns of faceting - this is discussed in #427, but potential upcoming facet types include facet-by-date, facet-by-JSON-array, facet-by-many-2-many and more.
A new plugin hook, register_facet_classes, can be used by plugins to add in additional facet classes.
Each class must implement two methods: suggest(), which scans columns in the table to decide if they might be worth suggesting for faceting, and facet_results(), which executes the facet operation and returns results ready to be displayed in the UI.
2019-05-02 17:11:26 -07:00
|
|
|
)
|
2018-08-04 17:14:56 -07:00
|
|
|
|
|
|
|
|
pm = pluggy.PluginManager("datasette")
|
|
|
|
|
pm.add_hookspecs(hookspecs)
|
2019-05-01 22:09:03 -07:00
|
|
|
|
2024-02-16 12:56:39 -08:00
|
|
|
DATASETTE_TRACE_PLUGINS = os.environ.get("DATASETTE_TRACE_PLUGINS", None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def before(hook_name, hook_impls, kwargs):
|
|
|
|
|
print(file=sys.stderr)
|
|
|
|
|
print(f"{hook_name}:", file=sys.stderr)
|
|
|
|
|
pprint(kwargs, width=40, indent=4, stream=sys.stderr)
|
|
|
|
|
print("Hook implementations:", file=sys.stderr)
|
|
|
|
|
pprint(hook_impls, width=40, indent=4, stream=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def after(outcome, hook_name, hook_impls, kwargs):
|
|
|
|
|
results = outcome.get_result()
|
|
|
|
|
if not isinstance(results, list):
|
|
|
|
|
results = [results]
|
2025-10-26 15:52:36 -07:00
|
|
|
print("Results:", file=sys.stderr)
|
2024-02-16 12:56:39 -08:00
|
|
|
pprint(results, width=40, indent=4, stream=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if DATASETTE_TRACE_PLUGINS:
|
|
|
|
|
pm.add_hookcall_monitoring(before, after)
|
|
|
|
|
|
|
|
|
|
|
2023-08-30 15:12:24 -07:00
|
|
|
DATASETTE_LOAD_PLUGINS = os.environ.get("DATASETTE_LOAD_PLUGINS", None)
|
|
|
|
|
|
|
|
|
|
if not hasattr(sys, "_called_from_test") and DATASETTE_LOAD_PLUGINS is None:
|
2019-05-01 22:09:03 -07:00
|
|
|
# Only load plugins if not running tests
|
|
|
|
|
pm.load_setuptools_entrypoints("datasette")
|
2018-08-04 17:14:56 -07:00
|
|
|
|
2023-08-30 15:12:24 -07:00
|
|
|
# Load any plugins specified in DATASETTE_LOAD_PLUGINS")
|
|
|
|
|
if DATASETTE_LOAD_PLUGINS is not None:
|
|
|
|
|
for package_name in [
|
|
|
|
|
name for name in DATASETTE_LOAD_PLUGINS.split(",") if name.strip()
|
|
|
|
|
]:
|
|
|
|
|
try:
|
2023-09-21 13:26:13 -07:00
|
|
|
distribution = importlib_metadata.distribution(package_name)
|
2023-09-21 12:11:35 -07:00
|
|
|
entry_points = distribution.entry_points
|
|
|
|
|
for entry_point in entry_points:
|
|
|
|
|
if entry_point.group == "datasette":
|
2023-08-30 15:12:24 -07:00
|
|
|
mod = entry_point.load()
|
|
|
|
|
pm.register(mod, name=entry_point.name)
|
|
|
|
|
# Ensure name can be found in plugin_to_distinfo later:
|
|
|
|
|
pm._plugin_distinfo.append((mod, distribution))
|
2023-09-21 13:26:13 -07:00
|
|
|
except importlib_metadata.PackageNotFoundError:
|
2026-07-25 15:47:08 -07:00
|
|
|
sys.stderr.write(f"Plugin {package_name} could not be found\n")
|
2023-08-30 15:12:24 -07:00
|
|
|
|
|
|
|
|
|
2018-08-04 17:14:56 -07:00
|
|
|
# Load default plugins
|
2018-08-28 00:36:22 -07:00
|
|
|
for plugin in DEFAULT_PLUGINS:
|
2018-08-04 17:14:56 -07:00
|
|
|
mod = importlib.import_module(plugin)
|
|
|
|
|
pm.register(mod, plugin)
|
2020-03-08 16:09:31 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_plugins():
|
|
|
|
|
plugins = []
|
|
|
|
|
plugin_to_distinfo = dict(pm.list_plugin_distinfo())
|
|
|
|
|
for plugin in pm.get_plugins():
|
|
|
|
|
static_path = None
|
|
|
|
|
templates_path = None
|
2025-11-13 10:31:03 -08:00
|
|
|
plugin_name = (
|
|
|
|
|
plugin.__name__
|
|
|
|
|
if hasattr(plugin, "__name__")
|
|
|
|
|
else plugin.__class__.__name__
|
|
|
|
|
)
|
|
|
|
|
if plugin_name not in DEFAULT_PLUGINS:
|
2020-03-08 16:09:31 -07:00
|
|
|
try:
|
2025-11-13 10:31:03 -08:00
|
|
|
if (importlib_resources.files(plugin_name) / "static").is_dir():
|
|
|
|
|
static_path = str(importlib_resources.files(plugin_name) / "static")
|
|
|
|
|
if (importlib_resources.files(plugin_name) / "templates").is_dir():
|
2023-09-21 12:11:35 -07:00
|
|
|
templates_path = str(
|
2025-11-13 10:31:03 -08:00
|
|
|
importlib_resources.files(plugin_name) / "templates"
|
2020-03-08 16:09:31 -07:00
|
|
|
)
|
2023-09-21 12:11:35 -07:00
|
|
|
except (TypeError, ModuleNotFoundError):
|
|
|
|
|
# Caused by --plugins_dir= plugins
|
2020-03-08 16:09:31 -07:00
|
|
|
pass
|
|
|
|
|
plugin_info = {
|
2025-11-13 10:31:03 -08:00
|
|
|
"name": plugin_name,
|
2020-03-08 16:09:31 -07:00
|
|
|
"static_path": static_path,
|
|
|
|
|
"templates_path": templates_path,
|
2020-06-02 14:49:28 -07:00
|
|
|
"hooks": [h.name for h in pm.get_hookcallers(plugin)],
|
2020-03-08 16:09:31 -07:00
|
|
|
}
|
|
|
|
|
distinfo = plugin_to_distinfo.get(plugin)
|
|
|
|
|
if distinfo:
|
|
|
|
|
plugin_info["version"] = distinfo.version
|
2023-09-21 13:26:13 -07:00
|
|
|
plugin_info["name"] = distinfo.name or distinfo.project_name
|
2020-03-08 16:09:31 -07:00
|
|
|
plugins.append(plugin_info)
|
|
|
|
|
return plugins
|