id,page,ref,title,content,breadcrumbs,references upgrade-1.0a20:fixing-async-with-httpx-asyncclient-app-app,upgrade-1.0a20,fixing-async-with-httpx-asyncclient-app-app,Fixing async with httpx.AsyncClient(app=app),"Some older plugins may use the following pattern in their tests, which is no longer supported: app = Datasette([], memory=True).app() async with httpx.AsyncClient(app=app) as client: response = await client.get(""http://localhost/path"") The new pattern is to use ds.client like this: ds = Datasette([], memory=True) response = await ds.client.get(""/path"")","[""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade-1.0a20:migrating-from-metadata-to-config,upgrade-1.0a20,migrating-from-metadata-to-config,Migrating from metadata= to config=,"Datasette 1.0 separates metadata (titles, descriptions, licenses) from configuration (settings, plugins, queries, permissions). Plugin tests and code need to be updated accordingly.","[""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade-1.0a20:permissions-are-now-actions,upgrade-1.0a20,permissions-are-now-actions,Permissions are now actions,"The register_permissions() hook shoud be replaced with register_actions() . Old code: @hookimpl def register_permissions(datasette): return [ Permission( name=""explain-sql"", abbr=None, description=""Can explain SQL queries"", takes_database=True, takes_resource=False, default=False, ), Permission( name=""annotate-rows"", abbr=None, description=""Can annotate rows"", takes_database=True, takes_resource=True, default=False, ), Permission( name=""view-debug-info"", abbr=None, description=""Can view debug information"", takes_database=False, takes_resource=False, default=False, ), ] The new Action does not have a default= parameter. Here's the equivalent new code: from datasette import hookimpl from datasette.permissions import Action from datasette.resources import DatabaseResource, TableResource @hookimpl def register_actions(datasette): return [ Action( name=""explain-sql"", description=""Explain SQL queries"", resource_class=DatabaseResource, ), Action( name=""annotate-rows"", description=""Annotate rows"", resource_class=TableResource, ), Action( name=""view-debug-info"", description=""View debug information"", ), ] The abbr= is now optional and defaults to None . For actions that apply to specific resources (like databases or tables), specify the resource_class instead of takes_parent and takes_child . Note that view-debug-info does not specify a resource_class because it applies globally.","[""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade-1.0a20:root-enabled-instances-during-testing,upgrade-1.0a20,root-enabled-instances-during-testing,Root-enabled instances during testing,"When writing tests that exercise root-only functionality, make sure to set datasette.root_enabled = True on the Datasette instance. Root permissions are only granted automatically when Datasette is started with datasette --root or when the flag is enabled directly in tests.","[""Datasette 1.0a20 plugin upgrade guide"", ""Root user checks are no longer necessary""]",[] upgrade-1.0a20:root-user-checks-are-no-longer-necessary,upgrade-1.0a20,root-user-checks-are-no-longer-necessary,Root user checks are no longer necessary,"Some plugins would introduce their own custom permission and then ensure the ""root"" actor had access to it using a pattern like this: @hookimpl def register_permissions(datasette): return [ Permission( name=""upload-dbs"", abbr=None, description=""Upload SQLite database files"", takes_database=False, takes_resource=False, default=False, ) ] @hookimpl def permission_allowed(actor, action): if action == ""upload-dbs"" and actor and actor.get(""id"") == ""root"": return True This is no longer necessary in Datasette 1.0a20 - the ""root"" actor automatically has all permissions when Datasette is started with the datasette --root option. The permission_allowed() hook in this example can be entirely removed.","[""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade-1.0a20:target-the-new-apis-exclusively,upgrade-1.0a20,target-the-new-apis-exclusively,Target the new APIs exclusively,"Datasette 1.0a20’s permission system is substantially different from previous releases. Attempting to keep plugin code compatible with both the old permission_allowed() and the new allowed() interfaces leads to brittle workarounds. Prefer to adopt the 1.0a20 APIs ( register_actions , permission_resources_sql() , and datasette.allowed() ) outright and drop legacy fallbacks.","[""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade-1.0a20:update-datasette-metadata-calls,upgrade-1.0a20,update-datasette-metadata-calls,Update datasette.metadata() calls,"The datasette.metadata() method has been removed. Use these methods instead: Old code: try: title = datasette.metadata(database=database)[""queries""][query_name][""title""] except (KeyError, TypeError): pass New code: try: query_info = await datasette.get_canned_query(database, query_name, request.actor) if query_info and ""title"" in query_info: title = query_info[""title""] except (KeyError, TypeError): pass","[""Datasette 1.0a20 plugin upgrade guide"", ""Migrating from metadata= to config=""]",[] upgrade-1.0a20:update-query-urls-in-tests,upgrade-1.0a20,update-query-urls-in-tests,Update query URLs in tests,"Datasette now redirects ?sql= parameters from database pages to the query view: Old code: response = await ds.client.get(""/_memory.atom?sql=select+1"") New code: response = await ds.client.get(""/_memory/-/query.atom?sql=select+1"")","[""Datasette 1.0a20 plugin upgrade guide"", ""Migrating from metadata= to config=""]",[] upgrade-1.0a20:update-render-functions-to-async,upgrade-1.0a20,update-render-functions-to-async,Update render functions to async,"If your plugin's render function needs to call datasette.get_canned_query() or other async Datasette methods, it must be declared as async: Old code: def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data): # ... if query_name: title = datasette.metadata(database=database)[""queries""][query_name][""title""] New code: async def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data): # ... if query_name: query_info = await datasette.get_canned_query(database, query_name, request.actor) if query_info and ""title"" in query_info: title = query_info[""title""]","[""Datasette 1.0a20 plugin upgrade guide"", ""Migrating from metadata= to config=""]",[] upgrade-1.0a20:update-test-constructors,upgrade-1.0a20,update-test-constructors,Update test constructors,"Old code: ds = Datasette( memory=True, metadata={ ""databases"": { ""_memory"": {""queries"": {""my_query"": {""sql"": ""select 1"", ""title"": ""My Query""}}} }, ""plugins"": { ""my-plugin"": {""setting"": ""value""} } } ) New code: ds = Datasette( memory=True, config={ ""databases"": { ""_memory"": {""queries"": {""my_query"": {""sql"": ""select 1"", ""title"": ""My Query""}}} }, ""plugins"": { ""my-plugin"": {""setting"": ""value""} } } )","[""Datasette 1.0a20 plugin upgrade guide"", ""Migrating from metadata= to config=""]",[] upgrade-1.0a20:using-datasette-allowed-to-check-permissions-instead-of-datasette-permission-allowed,upgrade-1.0a20,using-datasette-allowed-to-check-permissions-instead-of-datasette-permission-allowed,Using datasette.allowed() to check permissions instead of datasette.permission_allowed(),"The internal method datasette.permission_allowed() has been replaced by datasette.allowed() . The old method looked like this: can_debug = await datasette.permission_allowed( request.actor, ""view-debug-info"", ) can_explain_sql = await datasette.permission_allowed( request.actor, ""explain-sql"", resource=""database_name"", ) can_annotate_rows = await datasette.permission_allowed( request.actor, ""annotate-rows"", resource=(database_name, table_name), ) Note the confusing design here where resource could be either a string or a tuple depending on the permission being checked. The new keyword-only design makes this a lot more clear: from datasette.resources import DatabaseResource, TableResource can_debug = await datasette.allowed( actor=request.actor, action=""view-debug-info"", ) can_explain_sql = await datasette.allowed( actor=request.actor, action=""explain-sql"", resource=DatabaseResource(database_name), ) can_annotate_rows = await datasette.allowed( actor=request.actor, action=""annotate-rows"", resource=TableResource(database_name, table_name), )","[""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade_guide:fixing-async-with-httpx-asyncclient-app-app,upgrade_guide,fixing-async-with-httpx-asyncclient-app-app,Fixing async with httpx.AsyncClient(app=app),"Some older plugins may use the following pattern in their tests, which is no longer supported: app = Datasette([], memory=True).app() async with httpx.AsyncClient(app=app) as client: response = await client.get(""http://localhost/path"") The new pattern is to use ds.client like this: ds = Datasette([], memory=True) response = await ds.client.get(""/path"")","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade_guide:migrating-from-metadata-to-config,upgrade_guide,migrating-from-metadata-to-config,Migrating from metadata= to config=,"Datasette 1.0 separates metadata (titles, descriptions, licenses) from configuration (settings, plugins, queries, permissions). Plugin tests and code need to be updated accordingly.","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade_guide:permissions-are-now-actions,upgrade_guide,permissions-are-now-actions,Permissions are now actions,"The register_permissions() hook shoud be replaced with register_actions() . Old code: @hookimpl def register_permissions(datasette): return [ Permission( name=""explain-sql"", abbr=None, description=""Can explain SQL queries"", takes_database=True, takes_resource=False, default=False, ), Permission( name=""annotate-rows"", abbr=None, description=""Can annotate rows"", takes_database=True, takes_resource=True, default=False, ), Permission( name=""view-debug-info"", abbr=None, description=""Can view debug information"", takes_database=False, takes_resource=False, default=False, ), ] The new Action does not have a default= parameter. Here's the equivalent new code: from datasette import hookimpl from datasette.permissions import Action from datasette.resources import DatabaseResource, TableResource @hookimpl def register_actions(datasette): return [ Action( name=""explain-sql"", description=""Explain SQL queries"", resource_class=DatabaseResource, ), Action( name=""annotate-rows"", description=""Annotate rows"", resource_class=TableResource, ), Action( name=""view-debug-info"", description=""View debug information"", ), ] The abbr= is now optional and defaults to None . For actions that apply to specific resources (like databases or tables), specify the resource_class instead of takes_parent and takes_child . Note that view-debug-info does not specify a resource_class because it applies globally.","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade_guide:root-enabled-instances-during-testing,upgrade_guide,root-enabled-instances-during-testing,Root-enabled instances during testing,"When writing tests that exercise root-only functionality, make sure to set datasette.root_enabled = True on the Datasette instance. Root permissions are only granted automatically when Datasette is started with datasette --root or when the flag is enabled directly in tests.","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide"", ""Root user checks are no longer necessary""]",[] upgrade_guide:root-user-checks-are-no-longer-necessary,upgrade_guide,root-user-checks-are-no-longer-necessary,Root user checks are no longer necessary,"Some plugins would introduce their own custom permission and then ensure the ""root"" actor had access to it using a pattern like this: @hookimpl def register_permissions(datasette): return [ Permission( name=""upload-dbs"", abbr=None, description=""Upload SQLite database files"", takes_database=False, takes_resource=False, default=False, ) ] @hookimpl def permission_allowed(actor, action): if action == ""upload-dbs"" and actor and actor.get(""id"") == ""root"": return True This is no longer necessary in Datasette 1.0a20 - the ""root"" actor automatically has all permissions when Datasette is started with the datasette --root option. The permission_allowed() hook in this example can be entirely removed.","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade_guide:target-the-new-apis-exclusively,upgrade_guide,target-the-new-apis-exclusively,Target the new APIs exclusively,"Datasette 1.0a20’s permission system is substantially different from previous releases. Attempting to keep plugin code compatible with both the old permission_allowed() and the new allowed() interfaces leads to brittle workarounds. Prefer to adopt the 1.0a20 APIs ( register_actions , permission_resources_sql() , and datasette.allowed() ) outright and drop legacy fallbacks.","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade_guide:update-datasette-metadata-calls,upgrade_guide,update-datasette-metadata-calls,Update datasette.metadata() calls,"The datasette.metadata() method has been removed. Use these methods instead: Old code: try: title = datasette.metadata(database=database)[""queries""][query_name][""title""] except (KeyError, TypeError): pass New code: try: query_info = await datasette.get_canned_query(database, query_name, request.actor) if query_info and ""title"" in query_info: title = query_info[""title""] except (KeyError, TypeError): pass","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide"", ""Migrating from metadata= to config=""]",[] upgrade_guide:update-query-urls-in-tests,upgrade_guide,update-query-urls-in-tests,Update query URLs in tests,"Datasette now redirects ?sql= parameters from database pages to the query view: Old code: response = await ds.client.get(""/_memory.atom?sql=select+1"") New code: response = await ds.client.get(""/_memory/-/query.atom?sql=select+1"")","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide"", ""Migrating from metadata= to config=""]",[] upgrade_guide:update-render-functions-to-async,upgrade_guide,update-render-functions-to-async,Update render functions to async,"If your plugin's render function needs to call datasette.get_canned_query() or other async Datasette methods, it must be declared as async: Old code: def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data): # ... if query_name: title = datasette.metadata(database=database)[""queries""][query_name][""title""] New code: async def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data): # ... if query_name: query_info = await datasette.get_canned_query(database, query_name, request.actor) if query_info and ""title"" in query_info: title = query_info[""title""]","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide"", ""Migrating from metadata= to config=""]",[] upgrade_guide:update-test-constructors,upgrade_guide,update-test-constructors,Update test constructors,"Old code: ds = Datasette( memory=True, metadata={ ""databases"": { ""_memory"": {""queries"": {""my_query"": {""sql"": ""select 1"", ""title"": ""My Query""}}} }, ""plugins"": { ""my-plugin"": {""setting"": ""value""} } } ) New code: ds = Datasette( memory=True, config={ ""databases"": { ""_memory"": {""queries"": {""my_query"": {""sql"": ""select 1"", ""title"": ""My Query""}}} }, ""plugins"": { ""my-plugin"": {""setting"": ""value""} } } )","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide"", ""Migrating from metadata= to config=""]",[] upgrade_guide:upgrade-guide-v1-a25,upgrade_guide,upgrade-guide-v1-a25,Datasette 1.0a25: ,"datasette.create_token() is now an async method (previously it was synchronous). The restrict_all , restrict_database , and restrict_resource keyword arguments have been replaced by a single restrictions parameter that accepts a TokenRestrictions object. Old code: token = datasette.create_token( actor_id=""user1"", restrict_all=[""view-instance"", ""view-table""], restrict_database={""docs"": [""view-query""]}, restrict_resource={ ""docs"": { ""attachments"": [""insert-row"", ""update-row""] } }, ) New code: from datasette.tokens import TokenRestrictions token = await datasette.create_token( actor_id=""user1"", restrictions=( TokenRestrictions() .allow_all(""view-instance"") .allow_all(""view-table"") .allow_database(""docs"", ""view-query"") .allow_resource(""docs"", ""attachments"", ""insert-row"") .allow_resource(""docs"", ""attachments"", ""update-row"") ), ) The datasette create-token CLI command is unchanged.","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide""]",[] upgrade_guide:using-datasette-allowed-to-check-permissions-instead-of-datasette-permission-allowed,upgrade_guide,using-datasette-allowed-to-check-permissions-instead-of-datasette-permission-allowed,Using datasette.allowed() to check permissions instead of datasette.permission_allowed(),"The internal method datasette.permission_allowed() has been replaced by datasette.allowed() . The old method looked like this: can_debug = await datasette.permission_allowed( request.actor, ""view-debug-info"", ) can_explain_sql = await datasette.permission_allowed( request.actor, ""explain-sql"", resource=""database_name"", ) can_annotate_rows = await datasette.permission_allowed( request.actor, ""annotate-rows"", resource=(database_name, table_name), ) Note the confusing design here where resource could be either a string or a tuple depending on the permission being checked. The new keyword-only design makes this a lot more clear: from datasette.resources import DatabaseResource, TableResource can_debug = await datasette.allowed( actor=request.actor, action=""view-debug-info"", ) can_explain_sql = await datasette.allowed( actor=request.actor, action=""explain-sql"", resource=DatabaseResource(database_name), ) can_annotate_rows = await datasette.allowed( actor=request.actor, action=""annotate-rows"", resource=TableResource(database_name, table_name), )","[""Upgrade guide"", ""Datasette 1.0a20 plugin upgrade guide""]",[]