{"id": "writing_plugins:writing-plugins-static-assets", "page": "writing_plugins", "ref": "writing-plugins-static-assets", "title": "Static assets", "content": "If your plugin has a static/ directory, Datasette will automatically configure itself to serve those static assets from the following path: \n /-/static-plugins/NAME_OF_PLUGIN_PACKAGE/yourfile.js \n Use the datasette.urls.static_plugins(plugin_name, path) method to generate URLs to that asset that take the base_url setting into account, see datasette.urls . \n To bundle the static assets for a plugin in the package that you publish to PyPI, add the following to the plugin's setup.py : \n package_data = (\n {\n \"datasette_plugin_name\": [\n \"static/plugin.js\",\n ],\n },\n) \n Where datasette_plugin_name is the name of the plugin package (note that it uses underscores, not hyphens) and static/plugin.js is the path within that package to the static file. \n datasette-cluster-map is a useful example of a plugin that includes packaged static assets in this way.", "breadcrumbs": "[\"Writing plugins\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette-cluster-map\", \"label\": \"datasette-cluster-map\"}]"} {"id": "writing_plugins:writing-plugins-packaging", "page": "writing_plugins", "ref": "writing-plugins-packaging", "title": "Packaging a plugin", "content": "Plugins can be packaged using Python setuptools. You can see an example of a packaged plugin at https://github.com/simonw/datasette-plugin-demos \n The example consists of two files: a setup.py file that defines the plugin: \n from setuptools import setup\n\nVERSION = \"0.1\"\n\nsetup(\n name=\"datasette-plugin-demos\",\n description=\"Examples of plugins for Datasette\",\n author=\"Simon Willison\",\n url=\"https://github.com/simonw/datasette-plugin-demos\",\n license=\"Apache License, Version 2.0\",\n version=VERSION,\n py_modules=[\"datasette_plugin_demos\"],\n entry_points={\n \"datasette\": [\n \"plugin_demos = datasette_plugin_demos\"\n ]\n },\n install_requires=[\"datasette\"],\n) \n And a Python module file, datasette_plugin_demos.py , that implements the plugin: \n from datasette import hookimpl\nimport random\n\n\n@hookimpl\ndef prepare_jinja2_environment(env):\n env.filters[\"uppercase\"] = lambda u: u.upper()\n\n\n@hookimpl\ndef prepare_connection(conn):\n conn.create_function(\n \"random_integer\", 2, random.randint\n ) \n Having built a plugin in this way you can turn it into an installable package using the following command: \n python3 setup.py sdist \n This will create a .tar.gz file in the dist/ directory. \n You can then install your new plugin into a Datasette virtual environment or Docker container using pip : \n pip install datasette-plugin-demos-0.1.tar.gz \n To learn how to upload your plugin to PyPI for use by other people, read the PyPA guide to Packaging and distributing projects .", "breadcrumbs": "[\"Writing plugins\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette-plugin-demos\", \"label\": \"https://github.com/simonw/datasette-plugin-demos\"}, {\"href\": \"https://pypi.org/\", \"label\": \"PyPI\"}, {\"href\": \"https://packaging.python.org/tutorials/distributing-packages/\", \"label\": \"Packaging and distributing projects\"}]"} {"id": "writing_plugins:writing-plugins-one-off", "page": "writing_plugins", "ref": "writing-plugins-one-off", "title": "Writing one-off plugins", "content": "The quickest way to start writing a plugin is to create a my_plugin.py file and drop it into your plugins/ directory. Here is an example plugin, which adds a new custom SQL function called hello_world() which takes no arguments and returns the string Hello world! . \n from datasette import hookimpl\n\n\n@hookimpl\ndef prepare_connection(conn):\n conn.create_function(\n \"hello_world\", 0, lambda: \"Hello world!\"\n ) \n If you save this in plugins/my_plugin.py you can then start Datasette like this: \n datasette serve mydb.db --plugins-dir=plugins/ \n Now you can navigate to http://localhost:8001/mydb and run this SQL: \n select hello_world(); \n To see the output of your plugin.", "breadcrumbs": "[\"Writing plugins\"]", "references": "[{\"href\": \"http://localhost:8001/mydb\", \"label\": \"http://localhost:8001/mydb\"}]"} {"id": "writing_plugins:writing-plugins-designing-urls", "page": "writing_plugins", "ref": "writing-plugins-designing-urls", "title": "Designing URLs for your plugin", "content": "You can register new URL routes within Datasette using the register_routes(datasette) plugin hook. \n Datasette's default URLs include these: \n \n \n /dbname - database page \n \n \n /dbname/tablename - table page \n \n \n /dbname/tablename/pk - row page \n \n \n See Pages and API endpoints and Introspection for more default URL routes. \n To avoid accidentally conflicting with a database file that may be loaded into Datasette, plugins should register URLs using a /-/ prefix. For example, if your plugin adds a new interface for uploading Excel files you might register a URL route like this one: \n \n \n /-/upload-excel \n \n \n Try to avoid registering URLs that clash with other plugins that your users might have installed. There is no central repository of reserved URL paths (yet) but you can review existing plugins by browsing the plugins directory . \n If your plugin includes functionality that relates to a specific database you could also register a URL route like this: \n \n \n /dbname/-/upload-excel \n \n \n Or for a specific table like this: \n \n \n /dbname/tablename/-/modify-table-schema \n \n \n Note that a row could have a primary key of - and this URL scheme will still work, because Datasette row pages do not ever have a trailing slash followed by additional path components.", "breadcrumbs": "[\"Writing plugins\"]", "references": "[]"} {"id": "writing_plugins:writing-plugins-custom-templates", "page": "writing_plugins", "ref": "writing-plugins-custom-templates", "title": "Custom templates", "content": "If your plugin has a templates/ directory, Datasette will attempt to load templates from that directory before it uses its own default templates. \n The priority order for template loading is: \n \n \n templates from the --template-dir argument, if specified \n \n \n templates from the templates/ directory in any installed plugins \n \n \n default templates that ship with Datasette \n \n \n See Custom pages and templates for more details on how to write custom templates, including which filenames to use to customize which parts of the Datasette UI. \n Templates should be bundled for distribution using the same package_data mechanism in setup.py described for static assets above, for example: \n package_data = (\n {\n \"datasette_plugin_name\": [\n \"templates/my_template.html\",\n ],\n },\n) \n You can also use wildcards here such as templates/*.html . See datasette-edit-schema for an example of this pattern.", "breadcrumbs": "[\"Writing plugins\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette-edit-schema\", \"label\": \"datasette-edit-schema\"}]"} {"id": "writing_plugins:writing-plugins-cookiecutter", "page": "writing_plugins", "ref": "writing-plugins-cookiecutter", "title": "Starting an installable plugin using cookiecutter", "content": "Plugins that can be installed should be written as Python packages using a setup.py file. \n The quickest way to start writing one an installable plugin is to use the datasette-plugin cookiecutter template. This creates a new plugin structure for you complete with an example test and GitHub Actions workflows for testing and publishing your plugin. \n Install cookiecutter and then run this command to start building a plugin using the template: \n cookiecutter gh:simonw/datasette-plugin \n Read a cookiecutter template for writing Datasette plugins for more information about this template.", "breadcrumbs": "[\"Writing plugins\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette-plugin\", \"label\": \"datasette-plugin\"}, {\"href\": \"https://cookiecutter.readthedocs.io/en/stable/installation.html\", \"label\": \"Install cookiecutter\"}, {\"href\": \"https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/\", \"label\": \"a cookiecutter template for writing Datasette plugins\"}]"} {"id": "writing_plugins:writing-plugins-configuration", "page": "writing_plugins", "ref": "writing-plugins-configuration", "title": "Writing plugins that accept configuration", "content": "When you are writing plugins, you can access plugin configuration like this using the datasette plugin_config() method. If you know you need plugin configuration for a specific table, you can access it like this: \n plugin_config = datasette.plugin_config(\n \"datasette-cluster-map\", database=\"sf-trees\", table=\"Street_Tree_List\"\n) \n This will return the {\"latitude_column\": \"lat\", \"longitude_column\": \"lng\"} in the above example. \n If there is no configuration for that plugin, the method will return None . \n If it cannot find the requested configuration at the table layer, it will fall back to the database layer and then the root layer. For example, a user may have set the plugin configuration option like so: \n {\n \"databases: {\n \"sf-trees\": {\n \"plugins\": {\n \"datasette-cluster-map\": {\n \"latitude_column\": \"xlat\",\n \"longitude_column\": \"xlng\"\n }\n }\n }\n }\n} \n In this case, the above code would return that configuration for ANY table within the sf-trees database. \n The plugin configuration could also be set at the top level of metadata.json : \n {\n \"title\": \"This is the top-level title in metadata.json\",\n \"plugins\": {\n \"datasette-cluster-map\": {\n \"latitude_column\": \"xlat\",\n \"longitude_column\": \"xlng\"\n }\n }\n} \n Now that datasette-cluster-map plugin configuration will apply to every table in every database.", "breadcrumbs": "[\"Writing plugins\"]", "references": "[]"} {"id": "writing_plugins:writing-plugins-building-urls", "page": "writing_plugins", "ref": "writing-plugins-building-urls", "title": "Building URLs within plugins", "content": "Plugins that define their own custom user interface elements may need to link to other pages within Datasette. \n This can be a bit tricky if the Datasette instance is using the base_url configuration setting to run behind a proxy, since that can cause Datasette's URLs to include an additional prefix. \n The datasette.urls object provides internal methods for correctly generating URLs to different pages within Datasette, taking any base_url configuration into account. \n This object is exposed in templates as the urls variable, which can be used like this: \n Back to the Homepage \n See datasette.urls for full details on this object.", "breadcrumbs": "[\"Writing plugins\"]", "references": "[]"} {"id": "writing_plugins:id1", "page": "writing_plugins", "ref": "id1", "title": "Writing plugins", "content": "You can write one-off plugins that apply to just one Datasette instance, or you can write plugins which can be installed using pip and can be shipped to the Python Package Index ( PyPI ) for other people to install. \n Want to start by looking at an example? The Datasette plugins directory lists more than 90 open source plugins with code you can explore. The plugin hooks page includes links to example plugins for each of the documented hooks.", "breadcrumbs": "[]", "references": "[{\"href\": \"https://pypi.org/\", \"label\": \"PyPI\"}, {\"href\": \"https://datasette.io/plugins\", \"label\": \"Datasette plugins directory\"}]"} {"id": "testing_plugins:testing-plugins-register-in-test", "page": "testing_plugins", "ref": "testing-plugins-register-in-test", "title": "Registering a plugin for the duration of a test", "content": "When writing tests for plugins you may find it useful to register a test plugin just for the duration of a single test. You can do this using pm.register() and pm.unregister() like this: \n from datasette import hookimpl\nfrom datasette.app import Datasette\nfrom datasette.plugins import pm\nimport pytest\n\n\n@pytest.mark.asyncio\nasync def test_using_test_plugin():\n class TestPlugin:\n __name__ = \"TestPlugin\"\n\n # Use hookimpl and method names to register hooks\n @hookimpl\n def register_routes(self):\n return [\n (r\"^/error$\", lambda: 1 / 0),\n ]\n\n pm.register(TestPlugin(), name=\"undo\")\n try:\n # The test implementation goes here\n datasette = Datasette()\n response = await datasette.client.get(\"/error\")\n assert response.status_code == 500\n finally:\n pm.unregister(name=\"undo\")", "breadcrumbs": "[\"Testing plugins\"]", "references": "[]"} {"id": "testing_plugins:testing-plugins-pytest-httpx", "page": "testing_plugins", "ref": "testing-plugins-pytest-httpx", "title": "Testing outbound HTTP calls with pytest-httpx", "content": "If your plugin makes outbound HTTP calls - for example datasette-auth-github or datasette-import-table - you may need to mock those HTTP requests in your tests. \n The pytest-httpx package is a useful library for mocking calls. It can be tricky to use with Datasette though since it mocks all HTTPX requests, and Datasette's own testing mechanism uses HTTPX internally. \n To avoid breaking your tests, you can return [\"localhost\"] from the non_mocked_hosts() fixture. \n As an example, here's a very simple plugin which executes an HTTP response and returns the resulting content: \n from datasette import hookimpl\nfrom datasette.utils.asgi import Response\nimport httpx\n\n\n@hookimpl\ndef register_routes():\n return [\n (r\"^/-/fetch-url$\", fetch_url),\n ]\n\n\nasync def fetch_url(datasette, request):\n if request.method == \"GET\":\n return Response.html(\n \"\"\"\n
\n \n \n
\"\"\".format(\n request.scope[\"csrftoken\"]()\n )\n )\n vars = await request.post_vars()\n url = vars[\"url\"]\n return Response.text(httpx.get(url).text) \n Here's a test for that plugin that mocks the HTTPX outbound request: \n from datasette.app import Datasette\nimport pytest\n\n\n@pytest.fixture\ndef non_mocked_hosts():\n # This ensures httpx-mock will not affect Datasette's own\n # httpx calls made in the tests by datasette.client:\n return [\"localhost\"]\n\n\nasync def test_outbound_http_call(httpx_mock):\n httpx_mock.add_response(\n url=\"https://www.example.com/\",\n text=\"Hello world\",\n )\n datasette = Datasette([], memory=True)\n response = await datasette.client.post(\n \"/-/fetch-url\",\n data={\"url\": \"https://www.example.com/\"},\n )\n assert response.text == \"Hello world\"\n\n outbound_request = httpx_mock.get_request()\n assert (\n outbound_request.url == \"https://www.example.com/\"\n )", "breadcrumbs": "[\"Testing plugins\"]", "references": "[{\"href\": \"https://pypi.org/project/pytest-httpx/\", \"label\": \"pytest-httpx\"}]"} {"id": "testing_plugins:testing-plugins-pdb", "page": "testing_plugins", "ref": "testing-plugins-pdb", "title": "Using pdb for errors thrown inside Datasette", "content": "If an exception occurs within Datasette itself during a test, the response returned to your plugin will have a response.status_code value of 500. \n You can add pdb=True to the Datasette constructor to drop into a Python debugger session inside your test run instead of getting back a 500 response code. This is equivalent to running the datasette command-line tool with the --pdb option. \n Here's what that looks like in a test function: \n def test_that_opens_the_debugger_or_errors():\n ds = Datasette([db_path], pdb=True)\n response = await ds.client.get(\"/\") \n If you use this pattern you will need to run pytest with the -s option to avoid capturing stdin/stdout in order to interact with the debugger prompt.", "breadcrumbs": "[\"Testing plugins\"]", "references": "[]"} {"id": "testing_plugins:testing-plugins-fixtures", "page": "testing_plugins", "ref": "testing-plugins-fixtures", "title": "Using pytest fixtures", "content": "Pytest fixtures can be used to create initial testable objects which can then be used by multiple tests. \n A common pattern for Datasette plugins is to create a fixture which sets up a temporary test database and wraps it in a Datasette instance. \n Here's an example that uses the sqlite-utils library to populate a temporary test database. It also sets the title of that table using a simulated metadata.json configuration: \n from datasette.app import Datasette\nimport pytest\nimport sqlite_utils\n\n\n@pytest.fixture(scope=\"session\")\ndef datasette(tmp_path_factory):\n db_directory = tmp_path_factory.mktemp(\"dbs\")\n db_path = db_directory / \"test.db\"\n db = sqlite_utils.Database(db_path)\n db[\"dogs\"].insert_all(\n [\n {\"id\": 1, \"name\": \"Cleo\", \"age\": 5},\n {\"id\": 2, \"name\": \"Pancakes\", \"age\": 4},\n ],\n pk=\"id\",\n )\n datasette = Datasette(\n [db_path],\n metadata={\n \"databases\": {\n \"test\": {\n \"tables\": {\n \"dogs\": {\"title\": \"Some dogs\"}\n }\n }\n }\n },\n )\n return datasette\n\n\n@pytest.mark.asyncio\nasync def test_example_table_json(datasette):\n response = await datasette.client.get(\n \"/test/dogs.json?_shape=array\"\n )\n assert response.status_code == 200\n assert response.json() == [\n {\"id\": 1, \"name\": \"Cleo\", \"age\": 5},\n {\"id\": 2, \"name\": \"Pancakes\", \"age\": 4},\n ]\n\n\n@pytest.mark.asyncio\nasync def test_example_table_html(datasette):\n response = await datasette.client.get(\"/test/dogs\")\n assert \">Some dogs\" in response.text \n Here the datasette() function defines the fixture, which is than automatically passed to the two test functions based on pytest automatically matching their datasette function parameters. \n The @pytest.fixture(scope=\"session\") line here ensures the fixture is reused for the full pytest execution session. This means that the temporary database file will be created once and reused for each test. \n If you want to create that test database repeatedly for every individual test function, write the fixture function like this instead. You may want to do this if your plugin modifies the database contents in some way: \n @pytest.fixture\ndef datasette(tmp_path_factory):\n # This fixture will be executed repeatedly for every test\n ...", "breadcrumbs": "[\"Testing plugins\"]", "references": "[{\"href\": \"https://docs.pytest.org/en/stable/fixture.html\", \"label\": \"Pytest fixtures\"}, {\"href\": \"https://sqlite-utils.datasette.io/en/stable/python-api.html\", \"label\": \"sqlite-utils library\"}]"} {"id": "testing_plugins:testing-plugins-datasette-test-instance", "page": "testing_plugins", "ref": "testing-plugins-datasette-test-instance", "title": "Setting up a Datasette test instance", "content": "The above example shows the easiest way to start writing tests against a Datasette instance: \n from datasette.app import Datasette\nimport pytest\n\n\n@pytest.mark.asyncio\nasync def test_plugin_is_installed():\n datasette = Datasette(memory=True)\n response = await datasette.client.get(\"/-/plugins.json\")\n assert response.status_code == 200 \n Creating a Datasette() instance like this as useful shortcut in tests, but there is one detail you need to be aware of. It's important to ensure that the async method .invoke_startup() is called on that instance. You can do that like this: \n datasette = Datasette(memory=True)\nawait datasette.invoke_startup() \n This method registers any startup(datasette) or prepare_jinja2_environment(env, datasette) plugins that might themselves need to make async calls. \n If you are using await datasette.client.get() and similar methods then you don't need to worry about this - Datasette automatically calls invoke_startup() the first time it handles a request.", "breadcrumbs": "[\"Testing plugins\"]", "references": "[]"} {"id": "testing_plugins:id1", "page": "testing_plugins", "ref": "id1", "title": "Testing plugins", "content": "We recommend using pytest to write automated tests for your plugins. \n If you use the template described in Starting an installable plugin using cookiecutter your plugin will start with a single test in your tests/ directory that looks like this: \n from datasette.app import Datasette\nimport pytest\n\n\n@pytest.mark.asyncio\nasync def test_plugin_is_installed():\n datasette = Datasette(memory=True)\n response = await datasette.client.get(\"/-/plugins.json\")\n assert response.status_code == 200\n installed_plugins = {p[\"name\"] for p in response.json()}\n assert (\n \"datasette-plugin-template-demo\"\n in installed_plugins\n ) \n This test uses the datasette.client object to exercise a test instance of Datasette. datasette.client is a wrapper around the HTTPX Python library which can imitate HTTP requests using ASGI. This is the recommended way to write tests against a Datasette instance. \n This test also uses the pytest-asyncio package to add support for async def test functions running under pytest. \n You can install these packages like so: \n pip install pytest pytest-asyncio \n If you are building an installable package you can add them as test dependencies to your setup.py module like this: \n setup(\n name=\"datasette-my-plugin\",\n # ...\n extras_require={\"test\": [\"pytest\", \"pytest-asyncio\"]},\n tests_require=[\"datasette-my-plugin[test]\"],\n) \n You can then install the test dependencies like so: \n pip install -e '.[test]' \n Then run the tests using pytest like so: \n pytest", "breadcrumbs": "[]", "references": "[{\"href\": \"https://docs.pytest.org/\", \"label\": \"pytest\"}, {\"href\": \"https://www.python-httpx.org/\", \"label\": \"HTTPX\"}, {\"href\": \"https://pypi.org/project/pytest-asyncio/\", \"label\": \"pytest-asyncio\"}]"} {"id": "sql_queries:sql-views", "page": "sql_queries", "ref": "sql-views", "title": "Views", "content": "If you want to bundle some pre-written SQL queries with your Datasette-hosted database you can do so in two ways. The first is to include SQL views in your database - Datasette will then list those views on your database index page. \n The quickest way to create views is with the SQLite command-line interface: \n $ sqlite3 sf-trees.db\nSQLite version 3.19.3 2017-06-27 16:48:08\nEnter \".help\" for usage hints.\nsqlite> CREATE VIEW demo_view AS select qSpecies from Street_Tree_List;\n", "breadcrumbs": "[\"Running SQL queries\"]", "references": "[]"} {"id": "sql_queries:sql-parameters", "page": "sql_queries", "ref": "sql-parameters", "title": "Named parameters", "content": "Datasette has special support for SQLite named parameters. Consider a SQL query like this: \n select * from Street_Tree_List\nwhere \"PermitNotes\" like :notes\nand \"qSpecies\" = :species \n If you execute this query using the custom query editor, Datasette will extract the two named parameters and use them to construct form fields for you to provide values. \n You can also provide values for these fields by constructing a URL: \n /mydatabase?sql=select...&species=44 \n SQLite string escaping rules will be applied to values passed using named parameters - they will be wrapped in quotes and their content will be correctly escaped. \n Values from named parameters are treated as SQLite strings. If you need to perform numeric comparisons on them you should cast them to an integer or float first using cast(:name as integer) or cast(:name as real) , for example: \n select * from Street_Tree_List\nwhere latitude > cast(:min_latitude as real)\nand latitude < cast(:max_latitude as real) \n Datasette disallows custom SQL queries containing the string PRAGMA (with a small number of exceptions ) as SQLite pragma statements can be used to change database settings at runtime. If you need to include the string \"pragma\" in a query you can do so safely using a named parameter.", "breadcrumbs": "[\"Running SQL queries\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette/issues/761\", \"label\": \"of exceptions\"}]"} {"id": "sql_queries:sql", "page": "sql_queries", "ref": "sql", "title": "Running SQL queries", "content": "Datasette treats SQLite database files as read-only and immutable. This means it is not possible to execute INSERT or UPDATE statements using Datasette, which allows us to expose SELECT statements to the outside world without needing to worry about SQL injection attacks. \n The easiest way to execute custom SQL against Datasette is through the web UI. The database index page includes a SQL editor that lets you run any SELECT query you like. You can also construct queries using the filter interface on the tables page, then click \"View and edit SQL\" to open that query in the custom SQL editor. \n Note that this interface is only available if the execute-sql permission is allowed. \n Any Datasette SQL query is reflected in the URL of the page, allowing you to bookmark them, share them with others and navigate through previous queries using your browser back button. \n You can also retrieve the results of any query as JSON by adding .json to the base URL.", "breadcrumbs": "[]", "references": "[]"} {"id": "sql_queries:id3", "page": "sql_queries", "ref": "id3", "title": "Cross-database queries", "content": "SQLite has the ability to run queries that join across multiple databases. Up to ten databases can be attached to a single SQLite connection and queried together. \n Datasette can execute joins across multiple databases if it is started with the --crossdb option: \n datasette fixtures.db extra_database.db --crossdb \n If it is started in this way, the /_memory page can be used to execute queries that join across multiple databases. \n References to tables in attached databases should be preceded by the database name and a period. \n For example, this query will show a list of tables across both of the above databases: \n select\n 'fixtures' as database, *\nfrom\n [fixtures].sqlite_master\nunion\nselect\n 'extra_database' as database, *\nfrom\n [extra_database].sqlite_master \n Try that out here .", "breadcrumbs": "[\"Running SQL queries\"]", "references": "[{\"href\": \"https://latest.datasette.io/_memory?sql=select%0D%0A++%27fixtures%27+as+database%2C+*%0D%0Afrom%0D%0A++%5Bfixtures%5D.sqlite_master%0D%0Aunion%0D%0Aselect%0D%0A++%27extra_database%27+as+database%2C+*%0D%0Afrom%0D%0A++%5Bextra_database%5D.sqlite_master\", \"label\": \"Try that out here\"}]"} {"id": "sql_queries:id2", "page": "sql_queries", "ref": "id2", "title": "Pagination", "content": "Datasette's default table pagination is designed to be extremely efficient. SQL OFFSET/LIMIT pagination can have a significant performance penalty once you get into multiple thousands of rows, as each page still requires the database to scan through every preceding row to find the correct offset. \n When paginating through tables, Datasette instead orders the rows in the table by their primary key and performs a WHERE clause against the last seen primary key for the previous page. For example: \n select rowid, * from Tree_List where rowid > 200 order by rowid limit 101 \n This represents page three for this particular table, with a page size of 100. \n Note that we request 101 items in the limit clause rather than 100. This allows us to detect if we are on the last page of the results: if the query returns less than 101 rows we know we have reached the end of the pagination set. Datasette will only return the first 100 rows - the 101st is used purely to detect if there should be another page. \n Since the where clause acts against the index on the primary key, the query is extremely fast even for records that are a long way into the overall pagination set.", "breadcrumbs": "[\"Running SQL queries\"]", "references": "[]"} {"id": "sql_queries:id1", "page": "sql_queries", "ref": "id1", "title": "Canned queries", "content": "As an alternative to adding views to your database, you can define canned queries inside your metadata.json file. Here's an example: \n {\n \"databases\": {\n \"sf-trees\": {\n \"queries\": {\n \"just_species\": {\n \"sql\": \"select qSpecies from Street_Tree_List\"\n }\n }\n }\n }\n} \n Then run Datasette like this: \n datasette sf-trees.db -m metadata.json \n Each canned query will be listed on the database index page, and will also get its own URL at: \n /database-name/canned-query-name \n For the above example, that URL would be: \n /sf-trees/just_species \n You can optionally include \"title\" and \"description\" keys to show a title and description on the canned query page. As with regular table metadata you can alternatively specify \"description_html\" to have your description rendered as HTML (rather than having HTML special characters escaped).", "breadcrumbs": "[\"Running SQL queries\"]", "references": "[]"} {"id": "sql_queries:hide-sql", "page": "sql_queries", "ref": "hide-sql", "title": "hide_sql", "content": "Canned queries default to displaying their SQL query at the top of the page. If the query is extremely long you may want to hide it by default, with a \"show\" link that can be used to make it visible. \n Add the \"hide_sql\": true option to hide the SQL query by default.", "breadcrumbs": "[\"Running SQL queries\", \"Canned queries\", \"Additional canned query options\"]", "references": "[]"} {"id": "sql_queries:fragment", "page": "sql_queries", "ref": "fragment", "title": "fragment", "content": "Some plugins, such as datasette-vega , can be configured by including additional data in the fragment hash of the URL - the bit that comes after a # symbol. \n You can set a default fragment hash that will be included in the link to the canned query from the database index page using the \"fragment\" key. \n This example demonstrates both fragment and hide_sql : \n {\n \"databases\": {\n \"fixtures\": {\n \"queries\": {\n \"neighborhood_search\": {\n \"sql\": \"select neighborhood, facet_cities.name, state\\nfrom facetable join facet_cities on facetable.city_id = facet_cities.id\\nwhere neighborhood like '%' || :text || '%' order by neighborhood;\",\n \"fragment\": \"fragment-goes-here\",\n \"hide_sql\": true\n }\n }\n }\n }\n} \n See here for a demo of this in action.", "breadcrumbs": "[\"Running SQL queries\", \"Canned queries\", \"Additional canned query options\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette-vega\", \"label\": \"datasette-vega\"}, {\"href\": \"https://latest.datasette.io/fixtures#queries\", \"label\": \"See here\"}]"} {"id": "sql_queries:canned-queries-writable", "page": "sql_queries", "ref": "canned-queries-writable", "title": "Writable canned queries", "content": "Canned queries by default are read-only. You can use the \"write\": true key to indicate that a canned query can write to the database. \n See Controlling access to specific canned queries for details on how to add permission checks to canned queries, using the \"allow\" key. \n {\n \"databases\": {\n \"mydatabase\": {\n \"queries\": {\n \"add_name\": {\n \"sql\": \"INSERT INTO names (name) VALUES (:name)\",\n \"write\": true\n }\n }\n }\n }\n} \n This configuration will create a page at /mydatabase/add_name displaying a form with a name field. Submitting that form will execute the configured INSERT query. \n You can customize how Datasette represents success and errors using the following optional properties: \n \n \n on_success_message - the message shown when a query is successful \n \n \n on_success_redirect - the path or URL the user is redirected to on success \n \n \n on_error_message - the message shown when a query throws an error \n \n \n on_error_redirect - the path or URL the user is redirected to on error \n \n \n For example: \n {\n \"databases\": {\n \"mydatabase\": {\n \"queries\": {\n \"add_name\": {\n \"sql\": \"INSERT INTO names (name) VALUES (:name)\",\n \"write\": true,\n \"on_success_message\": \"Name inserted\",\n \"on_success_redirect\": \"/mydatabase/names\",\n \"on_error_message\": \"Name insert failed\",\n \"on_error_redirect\": \"/mydatabase\"\n }\n }\n }\n }\n} \n You can use \"params\" to explicitly list the named parameters that should be displayed as form fields - otherwise they will be automatically detected. \n You can pre-populate form fields when the page first loads using a query string, e.g. /mydatabase/add_name?name=Prepopulated . The user will have to submit the form to execute the query.", "breadcrumbs": "[\"Running SQL queries\", \"Canned queries\"]", "references": "[]"} {"id": "sql_queries:canned-queries-options", "page": "sql_queries", "ref": "canned-queries-options", "title": "Additional canned query options", "content": "Additional options can be specified for canned queries in the YAML or JSON configuration.", "breadcrumbs": "[\"Running SQL queries\", \"Canned queries\"]", "references": "[]"} {"id": "sql_queries:canned-queries-named-parameters", "page": "sql_queries", "ref": "canned-queries-named-parameters", "title": "Canned query parameters", "content": "Canned queries support named parameters, so if you include those in the SQL you will then be able to enter them using the form fields on the canned query page or by adding them to the URL. This means canned queries can be used to create custom JSON APIs based on a carefully designed SQL statement. \n Here's an example of a canned query with a named parameter: \n select neighborhood, facet_cities.name, state\nfrom facetable\n join facet_cities on facetable.city_id = facet_cities.id\nwhere neighborhood like '%' || :text || '%'\norder by neighborhood; \n In the canned query metadata (here Using YAML for metadata as metadata.yaml ) it looks like this: \n databases:\n fixtures:\n queries:\n neighborhood_search:\n sql: |-\n select neighborhood, facet_cities.name, state\n from facetable\n join facet_cities on facetable.city_id = facet_cities.id\n where neighborhood like '%' || :text || '%'\n order by neighborhood\n title: Search neighborhoods \n Here's the equivalent using JSON (as metadata.json ): \n {\n \"databases\": {\n \"fixtures\": {\n \"queries\": {\n \"neighborhood_search\": {\n \"sql\": \"select neighborhood, facet_cities.name, state\\nfrom facetable\\n join facet_cities on facetable.city_id = facet_cities.id\\nwhere neighborhood like '%' || :text || '%'\\norder by neighborhood\",\n \"title\": \"Search neighborhoods\"\n }\n }\n }\n }\n} \n Note that we are using SQLite string concatenation here - the || operator - to add wildcard % characters to the string provided by the user. \n You can try this canned query out here:\n https://latest.datasette.io/fixtures/neighborhood_search?text=town \n In this example the :text named parameter is automatically extracted from the query using a regular expression. \n You can alternatively provide an explicit list of named parameters using the \"params\" key, like this: \n databases:\n fixtures:\n queries:\n neighborhood_search:\n params:\n - text\n sql: |-\n select neighborhood, facet_cities.name, state\n from facetable\n join facet_cities on facetable.city_id = facet_cities.id\n where neighborhood like '%' || :text || '%'\n order by neighborhood\n title: Search neighborhoods", "breadcrumbs": "[\"Running SQL queries\", \"Canned queries\"]", "references": "[{\"href\": \"https://latest.datasette.io/fixtures/neighborhood_search?text=town\", \"label\": \"https://latest.datasette.io/fixtures/neighborhood_search?text=town\"}]"} {"id": "sql_queries:canned-queries-magic-parameters", "page": "sql_queries", "ref": "canned-queries-magic-parameters", "title": "Magic parameters", "content": "Named parameters that start with an underscore are special: they can be used to automatically add values created by Datasette that are not contained in the incoming form fields or query string. \n These magic parameters are only supported for canned queries: to avoid security issues (such as queries that extract the user's private cookies) they are not available to SQL that is executed by the user as a custom SQL query. \n Available magic parameters are: \n \n \n _actor_* - e.g. _actor_id , _actor_name \n \n Fields from the currently authenticated Actors . \n \n \n \n _header_* - e.g. _header_user_agent \n \n Header from the incoming HTTP request. The key should be in lower case and with hyphens converted to underscores e.g. _header_user_agent or _header_accept_language . \n \n \n \n _cookie_* - e.g. _cookie_lang \n \n The value of the incoming cookie of that name. \n \n \n \n _now_epoch \n \n The number of seconds since the Unix epoch. \n \n \n \n _now_date_utc \n \n The date in UTC, e.g. 2020-06-01 \n \n \n \n _now_datetime_utc \n \n The ISO 8601 datetime in UTC, e.g. 2020-06-24T18:01:07Z \n \n \n \n _random_chars_* - e.g. _random_chars_128 \n \n A random string of characters of the specified length. \n \n \n \n Here's an example configuration (this time using metadata.yaml since that provides better support for multi-line SQL queries) that adds a message from the authenticated user, storing various pieces of additional metadata using magic parameters: \n databases:\n mydatabase:\n queries:\n add_message:\n allow:\n id: \"*\"\n sql: |-\n INSERT INTO messages (\n user_id, message, datetime\n ) VALUES (\n :_actor_id, :message, :_now_datetime_utc\n )\n write: true \n The form presented at /mydatabase/add_message will have just a field for message - the other parameters will be populated by the magic parameter mechanism. \n Additional custom magic parameters can be added by plugins using the register_magic_parameters(datasette) hook.", "breadcrumbs": "[\"Running SQL queries\", \"Canned queries\"]", "references": "[]"} {"id": "sql_queries:canned-queries-json-api", "page": "sql_queries", "ref": "canned-queries-json-api", "title": "JSON API for writable canned queries", "content": "Writable canned queries can also be accessed using a JSON API. You can POST data to them using JSON, and you can request that their response is returned to you as JSON. \n To submit JSON to a writable canned query, encode key/value parameters as a JSON document: \n POST /mydatabase/add_message\n\n{\"message\": \"Message goes here\"} \n You can also continue to submit data using regular form encoding, like so: \n POST /mydatabase/add_message\n\nmessage=Message+goes+here \n There are three options for specifying that you would like the response to your request to return JSON data, as opposed to an HTTP redirect to another page. \n \n \n Set an Accept: application/json header on your request \n \n \n Include ?_json=1 in the URL that you POST to \n \n \n Include \"_json\": 1 in your JSON body, or &_json=1 in your form encoded body \n \n \n The JSON response will look like this: \n {\n \"ok\": true,\n \"message\": \"Query executed, 1 row affected\",\n \"redirect\": \"/data/add_name\"\n} \n The \"message\" and \"redirect\" values here will take into account on_success_message , on_success_redirect , on_error_message and on_error_redirect , if they have been set.", "breadcrumbs": "[\"Running SQL queries\", \"Canned queries\"]", "references": "[]"} {"id": "spatialite:spatialite-warning", "page": "spatialite", "ref": "spatialite-warning", "title": "Warning", "content": "The SpatiaLite extension adds a large number of additional SQL functions , some of which are not be safe for untrusted users to execute: they may cause the Datasette server to crash. \n You should not expose a SpatiaLite-enabled Datasette instance to the public internet without taking extra measures to secure it against potentially harmful SQL queries. \n The following steps are recommended: \n \n \n Disable arbitrary SQL queries by untrusted users. See Controlling the ability to execute arbitrary SQL for ways to do this. The easiest is to start Datasette with the datasette --setting default_allow_sql off option. \n \n \n Define Canned queries with the SQL queries that use SpatiaLite functions that you want people to be able to execute. \n \n \n The Datasette SpatiaLite tutorial includes detailed instructions for running SpatiaLite safely using these techniques", "breadcrumbs": "[\"SpatiaLite\"]", "references": "[{\"href\": \"https://www.gaia-gis.it/gaia-sins/spatialite-sql-5.0.1.html\", \"label\": \"a large number of additional SQL functions\"}, {\"href\": \"https://datasette.io/tutorials/spatialite\", \"label\": \"Datasette SpatiaLite tutorial\"}]"} {"id": "spatialite:spatialite-installation", "page": "spatialite", "ref": "spatialite-installation", "title": "Installation", "content": "", "breadcrumbs": "[\"SpatiaLite\"]", "references": "[]"} {"id": "spatialite:spatial-indexing-latitude-longitude-columns", "page": "spatialite", "ref": "spatial-indexing-latitude-longitude-columns", "title": "Spatial indexing latitude/longitude columns", "content": "Here's a recipe for taking a table with existing latitude and longitude columns, adding a SpatiaLite POINT geometry column to that table, populating the new column and then populating a spatial index: \n import sqlite3\n\nconn = sqlite3.connect(\"museums.db\")\n# Lead the spatialite extension:\nconn.enable_load_extension(True)\nconn.load_extension(\"/usr/local/lib/mod_spatialite.dylib\")\n# Initialize spatial metadata for this database:\nconn.execute(\"select InitSpatialMetadata(1)\")\n# Add a geometry column called point_geom to our museums table:\nconn.execute(\n \"SELECT AddGeometryColumn('museums', 'point_geom', 4326, 'POINT', 2);\"\n)\n# Now update that geometry column with the lat/lon points\nconn.execute(\n \"\"\"\n UPDATE museums SET\n point_geom = GeomFromText('POINT('||\"longitude\"||' '||\"latitude\"||')',4326);\n\"\"\"\n)\n# Now add a spatial index to that column\nconn.execute(\n 'select CreateSpatialIndex(\"museums\", \"point_geom\");'\n)\n# If you don't commit your changes will not be persisted:\nconn.commit()\nconn.close()", "breadcrumbs": "[\"SpatiaLite\"]", "references": "[]"} {"id": "spatialite:querying-polygons-using-within", "page": "spatialite", "ref": "querying-polygons-using-within", "title": "Querying polygons using within()", "content": "The within() SQL function can be used to check if a point is within a geometry: \n select\n name\nfrom\n places\nwhere\n within(GeomFromText('POINT(-3.1724366 51.4704448)'), places.geom); \n The GeomFromText() function takes a string of well-known text. Note that the order used here is longitude then latitude . \n To run that same within() query in a way that benefits from the spatial index, use the following: \n select\n name\nfrom\n places\nwhere\n within(GeomFromText('POINT(-3.1724366 51.4704448)'), places.geom)\n and rowid in (\n SELECT pkid FROM idx_places_geom\n where xmin < -3.1724366\n and xmax > -3.1724366\n and ymin < 51.4704448\n and ymax > 51.4704448\n );", "breadcrumbs": "[\"SpatiaLite\"]", "references": "[]"} {"id": "spatialite:making-use-of-a-spatial-index", "page": "spatialite", "ref": "making-use-of-a-spatial-index", "title": "Making use of a spatial index", "content": "SpatiaLite spatial indexes are R*Trees. They allow you to run efficient bounding box queries using a sub-select, with a similar pattern to that used for Searches using custom SQL . \n In the above example, the resulting index will be called idx_museums_point_geom . This takes the form of a SQLite virtual table. You can inspect its contents using the following query: \n select * from idx_museums_point_geom limit 10; \n Here's a live example: timezones-api.datasette.io/timezones/idx_timezones_Geometry \n \n \n \n \n \n \n \n \n \n \n pkid \n \n \n xmin \n \n \n xmax \n \n \n ymin \n \n \n ymax \n \n \n \n \n \n \n 1 \n \n \n -8.601725578308105 \n \n \n -2.4930307865142822 \n \n \n 4.162120819091797 \n \n \n 10.74019718170166 \n \n \n \n \n 2 \n \n \n -3.2607860565185547 \n \n \n 1.27329421043396 \n \n \n 4.539252281188965 \n \n \n 11.174856185913086 \n \n \n \n \n 3 \n \n \n 32.997581481933594 \n \n \n 47.98238754272461 \n \n \n 3.3974475860595703 \n \n \n 14.894054412841797 \n \n \n \n \n 4 \n \n \n -8.66890811920166 \n \n \n 11.997337341308594 \n \n \n 18.9681453704834 \n \n \n 37.296207427978516 \n \n \n \n \n 5 \n \n \n 36.43336486816406 \n \n \n 43.300174713134766 \n \n \n 12.354820251464844 \n \n \n 18.070993423461914 \n \n \n \n \n \n You can now construct efficient bounding box queries that will make use of the index like this: \n select * from museums where museums.rowid in (\n SELECT pkid FROM idx_museums_point_geom\n -- left-hand-edge of point > left-hand-edge of bbox (minx)\n where xmin > :bbox_minx\n -- right-hand-edge of point < right-hand-edge of bbox (maxx)\n and xmax < :bbox_maxx\n -- bottom-edge of point > bottom-edge of bbox (miny)\n and ymin > :bbox_miny\n -- top-edge of point < top-edge of bbox (maxy)\n and ymax < :bbox_maxy\n); \n Spatial indexes can be created against polygon columns as well as point columns, in which case they will represent the minimum bounding rectangle of that polygon. This is useful for accelerating within queries, as seen in the Timezones API example.", "breadcrumbs": "[\"SpatiaLite\"]", "references": "[{\"href\": \"https://timezones-api.datasette.io/timezones/idx_timezones_Geometry\", \"label\": \"timezones-api.datasette.io/timezones/idx_timezones_Geometry\"}]"} {"id": "spatialite:installing-spatialite-on-os-x", "page": "spatialite", "ref": "installing-spatialite-on-os-x", "title": "Installing SpatiaLite on OS X", "content": "The easiest way to install SpatiaLite on OS X is to use Homebrew . \n brew update\nbrew install spatialite-tools \n This will install the spatialite command-line tool and the mod_spatialite dynamic library. \n You can now run Datasette like so: \n datasette --load-extension=spatialite", "breadcrumbs": "[\"SpatiaLite\", \"Installation\"]", "references": "[{\"href\": \"https://brew.sh/\", \"label\": \"Homebrew\"}]"} {"id": "spatialite:installing-spatialite-on-linux", "page": "spatialite", "ref": "installing-spatialite-on-linux", "title": "Installing SpatiaLite on Linux", "content": "SpatiaLite is packaged for most Linux distributions. \n apt install spatialite-bin libsqlite3-mod-spatialite \n Depending on your distribution, you should be able to run Datasette something like this: \n datasette --load-extension=/usr/lib/x86_64-linux-gnu/mod_spatialite.so \n If you are unsure of the location of the module, try running locate mod_spatialite and see what comes back.", "breadcrumbs": "[\"SpatiaLite\", \"Installation\"]", "references": "[]"} {"id": "spatialite:importing-shapefiles-into-spatialite", "page": "spatialite", "ref": "importing-shapefiles-into-spatialite", "title": "Importing shapefiles into SpatiaLite", "content": "The shapefile format is a common format for distributing geospatial data. You can use the spatialite command-line tool to create a new database table from a shapefile. \n Try it now with the North America shapefile available from the University of North Carolina Global River Database project. Download the file and unzip it (this will create files called narivs.dbf , narivs.prj , narivs.shp and narivs.shx in the current directory), then run the following: \n $ spatialite rivers-database.db\nSpatiaLite version ..: 4.3.0a Supported Extensions:\n...\nspatialite> .loadshp narivs rivers CP1252 23032\n========\nLoading shapefile at 'narivs' into SQLite table 'rivers'\n...\nInserted 467973 rows into 'rivers' from SHAPEFILE \n This will load the data from the narivs shapefile into a new database table called rivers . \n Exit out of spatialite (using Ctrl+D ) and run Datasette against your new database like this: \n datasette rivers-database.db \\\n --load-extension=/usr/local/lib/mod_spatialite.dylib \n If you browse to http://localhost:8001/rivers-database/rivers you will see the new table... but the Geometry column will contain unreadable binary data (SpatiaLite uses a custom format based on WKB ). \n The easiest way to turn this into semi-readable data is to use the SpatiaLite AsGeoJSON function. Try the following using the SQL query interface at http://localhost:8001/rivers-database : \n select *, AsGeoJSON(Geometry) from rivers limit 10; \n This will give you back an additional column of GeoJSON. You can copy and paste GeoJSON from this column into the debugging tool at geojson.io to visualize it on a map. \n To see a more interesting example, try ordering the records with the longest geometry first. Since there are 467,000 rows in the table you will first need to increase the SQL time limit imposed by Datasette: \n datasette rivers-database.db \\\n --load-extension=/usr/local/lib/mod_spatialite.dylib \\\n --setting sql_time_limit_ms 10000 \n Now try the following query: \n select *, AsGeoJSON(Geometry) from rivers\norder by length(Geometry) desc limit 10;", "breadcrumbs": "[\"SpatiaLite\"]", "references": "[{\"href\": \"https://en.wikipedia.org/wiki/Shapefile\", \"label\": \"shapefile format\"}, {\"href\": \"http://gaia.geosci.unc.edu/rivers/\", \"label\": \"Global River Database\"}, {\"href\": \"https://www.gaia-gis.it/gaia-sins/BLOB-Geometry.html\", \"label\": \"a custom format based on WKB\"}, {\"href\": \"https://geojson.io/\", \"label\": \"geojson.io\"}]"} {"id": "spatialite:importing-geojson-polygons-using-shapely", "page": "spatialite", "ref": "importing-geojson-polygons-using-shapely", "title": "Importing GeoJSON polygons using Shapely", "content": "Another common form of polygon data is the GeoJSON format. This can be imported into SpatiaLite directly, or by using the Shapely Python library. \n Who's On First is an excellent source of openly licensed GeoJSON polygons. Let's import the geographical polygon for Wales. First, we can use the Who's On First Spelunker tool to find the record for Wales: \n spelunker.whosonfirst.org/id/404227475 \n That page includes a link to the GeoJSON record, which can be accessed here: \n data.whosonfirst.org/404/227/475/404227475.geojson \n Here's Python code to create a SQLite database, enable SpatiaLite, create a places table and then add a record for Wales: \n import sqlite3\n\nconn = sqlite3.connect(\"places.db\")\n# Enable SpatialLite extension\nconn.enable_load_extension(True)\nconn.load_extension(\"/usr/local/lib/mod_spatialite.dylib\")\n# Create the masic countries table\nconn.execute(\"select InitSpatialMetadata(1)\")\nconn.execute(\n \"create table places (id integer primary key, name text);\"\n)\n# Add a MULTIPOLYGON Geometry column\nconn.execute(\n \"SELECT AddGeometryColumn('places', 'geom', 4326, 'MULTIPOLYGON', 2);\"\n)\n# Add a spatial index against the new column\nconn.execute(\"SELECT CreateSpatialIndex('places', 'geom');\")\n# Now populate the table\nfrom shapely.geometry.multipolygon import MultiPolygon\nfrom shapely.geometry import shape\nimport requests\n\ngeojson = requests.get(\n \"https://data.whosonfirst.org/404/227/475/404227475.geojson\"\n).json()\n# Convert to \"Well Known Text\" format\nwkt = shape(geojson[\"geometry\"]).wkt\n# Insert and commit the record\nconn.execute(\n \"INSERT INTO places (id, name, geom) VALUES(null, ?, GeomFromText(?, 4326))\",\n (\"Wales\", wkt),\n)\nconn.commit()", "breadcrumbs": "[\"SpatiaLite\"]", "references": "[{\"href\": \"https://pypi.org/project/Shapely/\", \"label\": \"Shapely\"}, {\"href\": \"https://whosonfirst.org/\", \"label\": \"Who's On First\"}, {\"href\": \"https://spelunker.whosonfirst.org/id/404227475/\", \"label\": \"spelunker.whosonfirst.org/id/404227475\"}, {\"href\": \"https://data.whosonfirst.org/404/227/475/404227475.geojson\", \"label\": \"data.whosonfirst.org/404/227/475/404227475.geojson\"}]"} {"id": "spatialite:id1", "page": "spatialite", "ref": "id1", "title": "SpatiaLite", "content": "The SpatiaLite module for SQLite adds features for handling geographic and spatial data. For an example of what you can do with it, see the tutorial Building a location to time zone API with SpatiaLite . \n To use it with Datasette, you need to install the mod_spatialite dynamic library. This can then be loaded into Datasette using the --load-extension command-line option. \n Datasette can look for SpatiaLite in common installation locations if you run it like this: \n datasette --load-extension=spatialite --setting default_allow_sql off \n If SpatiaLite is in another location, use the full path to the extension instead: \n datasette --setting default_allow_sql off \\\n --load-extension=/usr/local/lib/mod_spatialite.dylib", "breadcrumbs": "[]", "references": "[{\"href\": \"https://www.gaia-gis.it/fossil/libspatialite/index\", \"label\": \"SpatiaLite module\"}, {\"href\": \"https://datasette.io/tutorials/spatialite\", \"label\": \"Building a location to time zone API with SpatiaLite\"}]"} {"id": "settings:using-setting", "page": "settings", "ref": "using-setting", "title": "Using --setting", "content": "Datasette supports a number of settings. These can be set using the --setting name value option to datasette serve . \n You can set multiple settings at once like this: \n datasette mydatabase.db \\\n --setting default_page_size 50 \\\n --setting sql_time_limit_ms 3500 \\\n --setting max_returned_rows 2000", "breadcrumbs": "[\"Settings\"]", "references": "[]"} {"id": "settings:setting-truncate-cells-html", "page": "settings", "ref": "setting-truncate-cells-html", "title": "truncate_cells_html", "content": "In the HTML table view, truncate any strings that are longer than this value.\n The full value will still be available in CSV, JSON and on the individual row\n HTML page. Set this to 0 to disable truncation. \n datasette mydatabase.db --setting truncate_cells_html 0", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-trace-debug", "page": "settings", "ref": "setting-trace-debug", "title": "trace_debug", "content": "This setting enables appending ?_trace=1 to any page in order to see the SQL queries and other trace information that was used to generate that page. \n Enable it like this: \n datasette mydatabase.db --setting trace_debug 1 \n Some examples: \n \n \n https://latest.datasette.io/?_trace=1 \n \n \n https://latest.datasette.io/fixtures/roadside_attractions?_trace=1 \n \n \n See datasette.tracer for details on how to hook into this mechanism as a plugin author.", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[{\"href\": \"https://latest.datasette.io/?_trace=1\", \"label\": \"https://latest.datasette.io/?_trace=1\"}, {\"href\": \"https://latest.datasette.io/fixtures/roadside_attractions?_trace=1\", \"label\": \"https://latest.datasette.io/fixtures/roadside_attractions?_trace=1\"}]"} {"id": "settings:setting-template-debug", "page": "settings", "ref": "setting-template-debug", "title": "template_debug", "content": "This setting enables template context debug mode, which is useful to help understand what variables are available to custom templates when you are writing them. \n Enable it like this: \n datasette mydatabase.db --setting template_debug 1 \n Now you can add ?_context=1 or &_context=1 to any Datasette page to see the context that was passed to that template. \n Some examples: \n \n \n https://latest.datasette.io/?_context=1 \n \n \n https://latest.datasette.io/fixtures?_context=1 \n \n \n https://latest.datasette.io/fixtures/roadside_attractions?_context=1", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[{\"href\": \"https://latest.datasette.io/?_context=1\", \"label\": \"https://latest.datasette.io/?_context=1\"}, {\"href\": \"https://latest.datasette.io/fixtures?_context=1\", \"label\": \"https://latest.datasette.io/fixtures?_context=1\"}, {\"href\": \"https://latest.datasette.io/fixtures/roadside_attractions?_context=1\", \"label\": \"https://latest.datasette.io/fixtures/roadside_attractions?_context=1\"}]"} {"id": "settings:setting-suggest-facets", "page": "settings", "ref": "setting-suggest-facets", "title": "suggest_facets", "content": "Should Datasette calculate suggested facets? On by default, turn this off like so: \n datasette mydatabase.db --setting suggest_facets off", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-sql-time-limit-ms", "page": "settings", "ref": "setting-sql-time-limit-ms", "title": "sql_time_limit_ms", "content": "By default, queries have a time limit of one second. If a query takes longer than this to run Datasette will terminate the query and return an error. \n If this time limit is too short for you, you can customize it using the sql_time_limit_ms limit - for example, to increase it to 3.5 seconds: \n datasette mydatabase.db --setting sql_time_limit_ms 3500 \n You can optionally set a lower time limit for an individual query using the ?_timelimit=100 query string argument: \n /my-database/my-table?qSpecies=44&_timelimit=100 \n This would set the time limit to 100ms for that specific query. This feature is useful if you are working with databases of unknown size and complexity - a query that might make perfect sense for a smaller table could take too long to execute on a table with millions of rows. By setting custom time limits you can execute queries \"optimistically\" - e.g. give me an exact count of rows matching this query but only if it takes less than 100ms to calculate.", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-secret", "page": "settings", "ref": "setting-secret", "title": "Configuring the secret", "content": "Datasette uses a secret string to sign secure values such as cookies. \n If you do not provide a secret, Datasette will create one when it starts up. This secret will reset every time the Datasette server restarts though, so things like authentication cookies will not stay valid between restarts. \n You can pass a secret to Datasette in two ways: with the --secret command-line option or by setting a DATASETTE_SECRET environment variable. \n $ datasette mydb.db --secret=SECRET_VALUE_HERE \n Or: \n $ export DATASETTE_SECRET=SECRET_VALUE_HERE\n$ datasette mydb.db \n One way to generate a secure random secret is to use Python like this: \n $ python3 -c 'import secrets; print(secrets.token_hex(32))'\ncdb19e94283a20f9d42cca50c5a4871c0aa07392db308755d60a1a5b9bb0fa52 \n Plugin authors make use of this signing mechanism in their plugins using .sign(value, namespace=\"default\") and .unsign(value, namespace=\"default\") .", "breadcrumbs": "[\"Settings\"]", "references": "[]"} {"id": "settings:setting-publish-secrets", "page": "settings", "ref": "setting-publish-secrets", "title": "Using secrets with datasette publish", "content": "The datasette publish and datasette package commands both generate a secret for you automatically when Datasette is deployed. \n This means that every time you deploy a new version of a Datasette project, a new secret will be generated. This will cause signed cookies to become invalid on every fresh deploy. \n You can fix this by creating a secret that will be used for multiple deploys and passing it using the --secret option: \n datasette publish cloudrun mydb.db --service=my-service --secret=cdb19e94283a20f9d42cca5", "breadcrumbs": "[\"Settings\"]", "references": "[]"} {"id": "settings:setting-num-sql-threads", "page": "settings", "ref": "setting-num-sql-threads", "title": "num_sql_threads", "content": "Maximum number of threads in the thread pool Datasette uses to execute SQLite queries. Defaults to 3. \n datasette mydatabase.db --setting num_sql_threads 10 \n Setting this to 0 turns off threaded SQL queries entirely - useful for environments that do not support threading such as Pyodide .", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[{\"href\": \"https://pyodide.org/\", \"label\": \"Pyodide\"}]"} {"id": "settings:setting-max-returned-rows", "page": "settings", "ref": "setting-max-returned-rows", "title": "max_returned_rows", "content": "Datasette returns a maximum of 1,000 rows of data at a time. If you execute a query that returns more than 1,000 rows, Datasette will return the first 1,000 and include a warning that the result set has been truncated. You can use OFFSET/LIMIT or other methods in your SQL to implement pagination if you need to return more than 1,000 rows. \n You can increase or decrease this limit like so: \n datasette mydatabase.db --setting max_returned_rows 2000", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-max-csv-mb", "page": "settings", "ref": "setting-max-csv-mb", "title": "max_csv_mb", "content": "The maximum size of CSV that can be exported, in megabytes. Defaults to 100MB.\n You can disable the limit entirely by settings this to 0: \n datasette mydatabase.db --setting max_csv_mb 0", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-force-https-urls", "page": "settings", "ref": "setting-force-https-urls", "title": "force_https_urls", "content": "Forces self-referential URLs in the JSON output to always use the https:// \n protocol. This is useful for cases where the application itself is hosted using\n HTTP but is served to the outside world via a proxy that enables HTTPS. \n datasette mydatabase.db --setting force_https_urls 1", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-facet-time-limit-ms", "page": "settings", "ref": "setting-facet-time-limit-ms", "title": "facet_time_limit_ms", "content": "This is the time limit Datasette allows for calculating a facet, which defaults to 200ms: \n datasette mydatabase.db --setting facet_time_limit_ms 1000", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-facet-suggest-time-limit-ms", "page": "settings", "ref": "setting-facet-suggest-time-limit-ms", "title": "facet_suggest_time_limit_ms", "content": "When Datasette calculates suggested facets it needs to run a SQL query for every column in your table. The default for this time limit is 50ms to account for the fact that it needs to run once for every column. If the time limit is exceeded the column will not be suggested as a facet. \n You can increase this time limit like so: \n datasette mydatabase.db --setting facet_suggest_time_limit_ms 500", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-default-page-size", "page": "settings", "ref": "setting-default-page-size", "title": "default_page_size", "content": "The default number of rows returned by the table page. You can over-ride this on a per-page basis using the ?_size=80 query string parameter, provided you do not specify a value higher than the max_returned_rows setting. You can set this default using --setting like so: \n datasette mydatabase.db --setting default_page_size 50", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-default-facet-size", "page": "settings", "ref": "setting-default-facet-size", "title": "default_facet_size", "content": "The default number of unique rows returned by Facets is 30. You can customize it like this: \n datasette mydatabase.db --setting default_facet_size 50", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-default-cache-ttl", "page": "settings", "ref": "setting-default-cache-ttl", "title": "default_cache_ttl", "content": "Default HTTP caching max-age header in seconds, used for Cache-Control: max-age=X . Can be over-ridden on a per-request basis using the ?_ttl= query string parameter. Set this to 0 to disable HTTP caching entirely. Defaults to 5 seconds. \n datasette mydatabase.db --setting default_cache_ttl 60", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-default-allow-sql", "page": "settings", "ref": "setting-default-allow-sql", "title": "default_allow_sql", "content": "Should users be able to execute arbitrary SQL queries by default? \n Setting this to off causes permission checks for execute-sql to fail by default. \n datasette mydatabase.db --setting default_allow_sql off \n There are two ways to achieve this: the other is to add \"allow_sql\": false to your metadata.json file, as described in Controlling the ability to execute arbitrary SQL . This setting offers a more convenient way to do this.", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-cache-size-kb", "page": "settings", "ref": "setting-cache-size-kb", "title": "cache_size_kb", "content": "Sets the amount of memory SQLite uses for its per-connection cache , in KB. \n datasette mydatabase.db --setting cache_size_kb 5000", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[{\"href\": \"https://www.sqlite.org/pragma.html#pragma_cache_size\", \"label\": \"per-connection cache\"}]"} {"id": "settings:setting-base-url", "page": "settings", "ref": "setting-base-url", "title": "base_url", "content": "If you are running Datasette behind a proxy, it may be useful to change the root path used for the Datasette instance. \n For example, if you are sending traffic from https://www.example.com/tools/datasette/ through to a proxied Datasette instance you may wish Datasette to use /tools/datasette/ as its root URL. \n You can do that like so: \n datasette mydatabase.db --setting base_url /tools/datasette/", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-allow-facet", "page": "settings", "ref": "setting-allow-facet", "title": "allow_facet", "content": "Allow users to specify columns they would like to facet on using the ?_facet=COLNAME URL parameter to the table view. \n This is enabled by default. If disabled, facets will still be displayed if they have been specifically enabled in metadata.json configuration for the table. \n Here's how to disable this feature: \n datasette mydatabase.db --setting allow_facet off", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-allow-download", "page": "settings", "ref": "setting-allow-download", "title": "allow_download", "content": "Should users be able to download the original SQLite database using a link on the database index page? This is turned on by default. However, databases can only be downloaded if they are served in immutable mode and not in-memory. If downloading is unavailable for either of these reasons, the download link is hidden even if allow_download is on. To disable database downloads, use the following: \n datasette mydatabase.db --setting allow_download off", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:setting-allow-csv-stream", "page": "settings", "ref": "setting-allow-csv-stream", "title": "allow_csv_stream", "content": "Enables the CSV export feature where an entire table\n (potentially hundreds of thousands of rows) can be exported as a single CSV\n file. This is turned on by default - you can turn it off like this: \n datasette mydatabase.db --setting allow_csv_stream off", "breadcrumbs": "[\"Settings\", \"Settings\"]", "references": "[]"} {"id": "settings:id2", "page": "settings", "ref": "id2", "title": "Settings", "content": "The following options can be set using --setting name value , or by storing them in the settings.json file for use with Configuration directory mode .", "breadcrumbs": "[\"Settings\"]", "references": "[]"} {"id": "settings:id1", "page": "settings", "ref": "id1", "title": "Settings", "content": "", "breadcrumbs": "[]", "references": "[]"} {"id": "settings:config-dir", "page": "settings", "ref": "config-dir", "title": "Configuration directory mode", "content": "Normally you configure Datasette using command-line options. For a Datasette instance with custom templates, custom plugins, a static directory and several databases this can get quite verbose: \n $ datasette one.db two.db \\\n --metadata=metadata.json \\\n --template-dir=templates/ \\\n --plugins-dir=plugins \\\n --static css:css \n As an alternative to this, you can run Datasette in configuration directory mode. Create a directory with the following structure: \n # In a directory called my-app:\nmy-app/one.db\nmy-app/two.db\nmy-app/metadata.json\nmy-app/templates/index.html\nmy-app/plugins/my_plugin.py\nmy-app/static/my.css \n Now start Datasette by providing the path to that directory: \n $ datasette my-app/ \n Datasette will detect the files in that directory and automatically configure itself using them. It will serve all *.db files that it finds, will load metadata.json if it exists, and will load the templates , plugins and static folders if they are present. \n The files that can be included in this directory are as follows. All are optional. \n \n \n *.db (or *.sqlite3 or *.sqlite ) - SQLite database files that will be served by Datasette \n \n \n metadata.json - Metadata for those databases - metadata.yaml or metadata.yml can be used as well \n \n \n inspect-data.json - the result of running datasette inspect *.db --inspect-file=inspect-data.json from the configuration directory - any database files listed here will be treated as immutable, so they should not be changed while Datasette is running \n \n \n settings.json - settings that would normally be passed using --setting - here they should be stored as a JSON object of key/value pairs \n \n \n templates/ - a directory containing Custom templates \n \n \n plugins/ - a directory containing plugins, see Writing one-off plugins \n \n \n static/ - a directory containing static files - these will be served from /static/filename.txt , see Serving static files", "breadcrumbs": "[\"Settings\"]", "references": "[]"} {"id": "publish:publishing", "page": "publish", "ref": "publishing", "title": "Publishing data", "content": "Datasette includes tools for publishing and deploying your data to the internet. The datasette publish command will deploy a new Datasette instance containing your databases directly to a Heroku or Google Cloud hosting account. You can also use datasette package to create a Docker image that bundles your databases together with the datasette application that is used to serve them.", "breadcrumbs": "[]", "references": "[]"} {"id": "publish:publish-vercel", "page": "publish", "ref": "publish-vercel", "title": "Publishing to Vercel", "content": "Vercel - previously known as Zeit Now - provides a layer over AWS Lambda to allow for quick, scale-to-zero deployment. You can deploy Datasette instances to Vercel using the datasette-publish-vercel plugin. \n pip install datasette-publish-vercel\ndatasette publish vercel mydatabase.db --project my-database-project \n Not every feature is supported: consult the datasette-publish-vercel README for more details.", "breadcrumbs": "[\"Publishing data\", \"datasette publish\"]", "references": "[{\"href\": \"https://vercel.com/\", \"label\": \"Vercel\"}, {\"href\": \"https://github.com/simonw/datasette-publish-vercel\", \"label\": \"datasette-publish-vercel\"}, {\"href\": \"https://github.com/simonw/datasette-publish-vercel/blob/main/README.md\", \"label\": \"datasette-publish-vercel README\"}]"} {"id": "publish:publish-heroku", "page": "publish", "ref": "publish-heroku", "title": "Publishing to Heroku", "content": "To publish your data using Heroku , first create an account there and install and configure the Heroku CLI tool . \n You can publish one or more databases to Heroku using the following command: \n datasette publish heroku mydatabase.db \n This will output some details about the new deployment, including a URL like this one: \n https://limitless-reef-88278.herokuapp.com/ deployed to Heroku \n You can specify a custom app name by passing -n my-app-name to the publish command. This will also allow you to overwrite an existing app. \n Rather than deploying directly you can use the --generate-dir option to output the files that would be deployed to a directory: \n datasette publish heroku mydatabase.db --generate-dir=/tmp/deploy-this-to-heroku \n See datasette publish heroku for the full list of options for this command.", "breadcrumbs": "[\"Publishing data\", \"datasette publish\"]", "references": "[{\"href\": \"https://www.heroku.com/\", \"label\": \"Heroku\"}, {\"href\": \"https://devcenter.heroku.com/articles/heroku-cli\", \"label\": \"Heroku CLI tool\"}]"} {"id": "publish:publish-fly", "page": "publish", "ref": "publish-fly", "title": "Publishing to Fly", "content": "Fly is a competitively priced Docker-compatible hosting platform that supports running applications in globally distributed data centers close to your end users. You can deploy Datasette instances to Fly using the datasette-publish-fly plugin. \n pip install datasette-publish-fly\ndatasette publish fly mydatabase.db --app=\"my-app\" \n Consult the datasette-publish-fly README for more details.", "breadcrumbs": "[\"Publishing data\", \"datasette publish\"]", "references": "[{\"href\": \"https://fly.io/\", \"label\": \"Fly\"}, {\"href\": \"https://fly.io/docs/pricing/\", \"label\": \"competitively priced\"}, {\"href\": \"https://github.com/simonw/datasette-publish-fly\", \"label\": \"datasette-publish-fly\"}, {\"href\": \"https://github.com/simonw/datasette-publish-fly/blob/main/README.md\", \"label\": \"datasette-publish-fly README\"}]"} {"id": "publish:publish-custom-metadata-and-plugins", "page": "publish", "ref": "publish-custom-metadata-and-plugins", "title": "Custom metadata and plugins", "content": "datasette publish accepts a number of additional options which can be used to further customize your Datasette instance. \n You can define your own Metadata and deploy that with your instance like so: \n datasette publish cloudrun --service=my-service mydatabase.db -m metadata.json \n If you just want to set the title, license or source information you can do that directly using extra options to datasette publish : \n datasette publish cloudrun mydatabase.db --service=my-service \\\n --title=\"Title of my database\" \\\n --source=\"Where the data originated\" \\\n --source_url=\"http://www.example.com/\" \n You can also specify plugins you would like to install. For example, if you want to include the datasette-vega visualization plugin you can use the following: \n datasette publish cloudrun mydatabase.db --service=my-service --install=datasette-vega \n If a plugin has any Secret configuration values you can use the --plugin-secret option to set those secrets at publish time. For example, using Heroku with datasette-auth-github you might run the following command: \n $ datasette publish heroku my_database.db \\\n --name my-heroku-app-demo \\\n --install=datasette-auth-github \\\n --plugin-secret datasette-auth-github client_id your_client_id \\\n --plugin-secret datasette-auth-github client_secret your_client_secret", "breadcrumbs": "[\"Publishing data\", \"datasette publish\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette-vega\", \"label\": \"datasette-vega\"}, {\"href\": \"https://github.com/simonw/datasette-auth-github\", \"label\": \"datasette-auth-github\"}]"} {"id": "publish:publish-cloud-run", "page": "publish", "ref": "publish-cloud-run", "title": "Publishing to Google Cloud Run", "content": "Google Cloud Run allows you to publish data in a scale-to-zero environment, so your application will start running when the first request is received and will shut down again when traffic ceases. This means you only pay for time spent serving traffic. \n \n Cloud Run is a great option for inexpensively hosting small, low traffic projects - but costs can add up for projects that serve a lot of requests. \n Be particularly careful if your project has tables with large numbers of rows. Search engine crawlers that index a page for every row could result in a high bill. \n The datasette-block-robots plugin can be used to request search engine crawlers omit crawling your site, which can help avoid this issue. \n \n You will first need to install and configure the Google Cloud CLI tools by following these instructions . \n You can then publish one or more SQLite database files to Google Cloud Run using the following command: \n datasette publish cloudrun mydatabase.db --service=my-database \n A Cloud Run service is a single hosted application. The service name you specify will be used as part of the Cloud Run URL. If you deploy to a service name that you have used in the past your new deployment will replace the previous one. \n If you omit the --service option you will be asked to pick a service name interactively during the deploy. \n You may need to interact with prompts from the tool. Many of the prompts ask for values that can be set as properties for the Google Cloud SDK if you want to avoid the prompts. \n For example, the default region for the deployed instance can be set using the command: \n gcloud config set run/region us-central1 \n You should replace us-central1 with your desired region . Alternately, you can specify the region by setting the CLOUDSDK_RUN_REGION environment variable. \n Once it has finished it will output a URL like this one: \n Service [my-service] revision [my-service-00001] has been deployed\nand is serving traffic at https://my-service-j7hipcg4aq-uc.a.run.app \n Cloud Run provides a URL on the .run.app domain, but you can also point your own domain or subdomain at your Cloud Run service - see mapping custom domains in the Cloud Run documentation for details. \n See datasette publish cloudrun for the full list of options for this command.", "breadcrumbs": "[\"Publishing data\", \"datasette publish\"]", "references": "[{\"href\": \"https://cloud.google.com/run/\", \"label\": \"Google Cloud Run\"}, {\"href\": \"https://datasette.io/plugins/datasette-block-robots\", \"label\": \"datasette-block-robots\"}, {\"href\": \"https://cloud.google.com/sdk/\", \"label\": \"these instructions\"}, {\"href\": \"https://cloud.google.com/sdk/docs/properties\", \"label\": \"set as properties for the Google Cloud SDK\"}, {\"href\": \"https://cloud.google.com/about/locations\", \"label\": \"region\"}, {\"href\": \"https://cloud.google.com/run/docs/mapping-custom-domains\", \"label\": \"mapping custom domains\"}]"} {"id": "publish:cli-publish", "page": "publish", "ref": "cli-publish", "title": "datasette publish", "content": "Once you have created a SQLite database (e.g. using csvs-to-sqlite ) you can deploy it to a hosting account using a single command. \n You will need a hosting account with Heroku or Google Cloud . Once you have created your account you will need to install and configure the heroku or gcloud command-line tools.", "breadcrumbs": "[\"Publishing data\"]", "references": "[{\"href\": \"https://github.com/simonw/csvs-to-sqlite/\", \"label\": \"csvs-to-sqlite\"}, {\"href\": \"https://www.heroku.com/\", \"label\": \"Heroku\"}, {\"href\": \"https://cloud.google.com/\", \"label\": \"Google Cloud\"}]"} {"id": "publish:cli-package", "page": "publish", "ref": "cli-package", "title": "datasette package", "content": "If you have docker installed (e.g. using Docker for Mac ) you can use the datasette package command to create a new Docker image in your local repository containing the datasette app bundled together with one or more SQLite databases: \n datasette package mydatabase.db \n Here's example output for the package command: \n $ datasette package parlgov.db --extra-options=\"--setting sql_time_limit_ms 2500\"\nSending build context to Docker daemon 4.459MB\nStep 1/7 : FROM python:3.11.0-slim-bullseye\n ---> 79e1dc9af1c1\nStep 2/7 : COPY . /app\n ---> Using cache\n ---> cd4ec67de656\nStep 3/7 : WORKDIR /app\n ---> Using cache\n ---> 139699e91621\nStep 4/7 : RUN pip install datasette\n ---> Using cache\n ---> 340efa82bfd7\nStep 5/7 : RUN datasette inspect parlgov.db --inspect-file inspect-data.json\n ---> Using cache\n ---> 5fddbe990314\nStep 6/7 : EXPOSE 8001\n ---> Using cache\n ---> 8e83844b0fed\nStep 7/7 : CMD datasette serve parlgov.db --port 8001 --inspect-file inspect-data.json --setting sql_time_limit_ms 2500\n ---> Using cache\n ---> 1bd380ea8af3\nSuccessfully built 1bd380ea8af3 \n You can now run the resulting container like so: \n docker run -p 8081:8001 1bd380ea8af3 \n This exposes port 8001 inside the container as port 8081 on your host machine, so you can access the application at http://localhost:8081/ \n You can customize the port that is exposed by the container using the --port option: \n datasette package mydatabase.db --port 8080 \n A full list of options can be seen by running datasette package --help : \n See datasette package for the full list of options for this command.", "breadcrumbs": "[\"Publishing data\"]", "references": "[{\"href\": \"https://www.docker.com/docker-mac\", \"label\": \"Docker for Mac\"}]"} {"id": "plugins:plugins-installing", "page": "plugins", "ref": "plugins-installing", "title": "Installing plugins", "content": "If a plugin has been packaged for distribution using setuptools you can use the plugin by installing it alongside Datasette in the same virtual environment or Docker container. \n You can install plugins using the datasette install command: \n datasette install datasette-vega \n You can uninstall plugins with datasette uninstall : \n datasette uninstall datasette-vega \n You can upgrade plugins with datasette install --upgrade or datasette install -U : \n datasette install -U datasette-vega \n This command can also be used to upgrade Datasette itself to the latest released version: \n datasette install -U datasette \n These commands are thin wrappers around pip install and pip uninstall , which ensure they run pip in the same virtual environment as Datasette itself.", "breadcrumbs": "[\"Plugins\"]", "references": "[]"} {"id": "plugins:plugins-installed", "page": "plugins", "ref": "plugins-installed", "title": "Seeing what plugins are installed", "content": "You can see a list of installed plugins by navigating to the /-/plugins page of your Datasette instance - for example: https://fivethirtyeight.datasettes.com/-/plugins \n You can also use the datasette plugins command: \n $ datasette plugins\n[\n {\n \"name\": \"datasette_json_html\",\n \"static\": false,\n \"templates\": false,\n \"version\": \"0.4.0\"\n }\n] \n [[[cog\nfrom datasette import cli\nfrom click.testing import CliRunner\nimport textwrap, json\ncog.out(\"\\n\")\nresult = CliRunner().invoke(cli.cli, [\"plugins\", \"--all\"])\n# cog.out() with text containing newlines was unindenting for some reason\ncog.outl(\"If you run ``datasette plugins --all`` it will include default plugins that ship as part of Datasette::\\n\")\nplugins = [p for p in json.loads(result.output) if p[\"name\"].startswith(\"datasette.\")]\nindented = textwrap.indent(json.dumps(plugins, indent=4), \" \")\nfor line in indented.split(\"\\n\"):\n cog.outl(line)\ncog.out(\"\\n\\n\") \n ]]] \n If you run datasette plugins --all it will include default plugins that ship as part of Datasette: \n [\n {\n \"name\": \"datasette.actor_auth_cookie\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"actor_from_request\"\n ]\n },\n {\n \"name\": \"datasette.blob_renderer\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"register_output_renderer\"\n ]\n },\n {\n \"name\": \"datasette.default_magic_parameters\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"register_magic_parameters\"\n ]\n },\n {\n \"name\": \"datasette.default_menu_links\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"menu_links\"\n ]\n },\n {\n \"name\": \"datasette.default_permissions\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"permission_allowed\"\n ]\n },\n {\n \"name\": \"datasette.facets\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"register_facet_classes\"\n ]\n },\n {\n \"name\": \"datasette.filters\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"filters_from_request\"\n ]\n },\n {\n \"name\": \"datasette.forbidden\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"forbidden\"\n ]\n },\n {\n \"name\": \"datasette.handle_exception\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"handle_exception\"\n ]\n },\n {\n \"name\": \"datasette.publish.cloudrun\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"publish_subcommand\"\n ]\n },\n {\n \"name\": \"datasette.publish.heroku\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"publish_subcommand\"\n ]\n },\n {\n \"name\": \"datasette.sql_functions\",\n \"static\": false,\n \"templates\": false,\n \"version\": null,\n \"hooks\": [\n \"prepare_connection\"\n ]\n }\n] \n [[[end]]] \n You can add the --plugins-dir= option to include any plugins found in that directory.", "breadcrumbs": "[\"Plugins\"]", "references": "[{\"href\": \"https://fivethirtyeight.datasettes.com/-/plugins\", \"label\": \"https://fivethirtyeight.datasettes.com/-/plugins\"}]"} {"id": "plugins:plugins-configuration-secret", "page": "plugins", "ref": "plugins-configuration-secret", "title": "Secret configuration values", "content": "Any values embedded in metadata.json will be visible to anyone who views the /-/metadata page of your Datasette instance. Some plugins may need configuration that should stay secret - API keys for example. There are two ways in which you can store secret configuration values. \n As environment variables . If your secret lives in an environment variable that is available to the Datasette process, you can indicate that the configuration value should be read from that environment variable like so: \n {\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_secret\": {\n \"$env\": \"GITHUB_CLIENT_SECRET\"\n }\n }\n }\n} \n As values in separate files . Your secrets can also live in files on disk. To specify a secret should be read from a file, provide the full file path like this: \n {\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_secret\": {\n \"$file\": \"/secrets/client-secret\"\n }\n }\n }\n} \n If you are publishing your data using the datasette publish family of commands, you can use the --plugin-secret option to set these secrets at publish time. For example, using Heroku you might run the following command: \n $ datasette publish heroku my_database.db \\\n --name my-heroku-app-demo \\\n --install=datasette-auth-github \\\n --plugin-secret datasette-auth-github client_id your_client_id \\\n --plugin-secret datasette-auth-github client_secret your_client_secret \n This will set the necessary environment variables and add the following to the deployed metadata.json : \n {\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_id\": {\n \"$env\": \"DATASETTE_AUTH_GITHUB_CLIENT_ID\"\n },\n \"client_secret\": {\n \"$env\": \"DATASETTE_AUTH_GITHUB_CLIENT_SECRET\"\n }\n }\n }\n}", "breadcrumbs": "[\"Plugins\", \"Plugin configuration\"]", "references": "[]"} {"id": "plugins:plugins-configuration", "page": "plugins", "ref": "plugins-configuration", "title": "Plugin configuration", "content": "Plugins can have their own configuration, embedded in a Metadata file. Configuration options for plugins live within a \"plugins\" key in that file, which can be included at the root, database or table level. \n Here is an example of some plugin configuration for a specific table: \n {\n \"databases\": {\n \"sf-trees\": {\n \"tables\": {\n \"Street_Tree_List\": {\n \"plugins\": {\n \"datasette-cluster-map\": {\n \"latitude_column\": \"lat\",\n \"longitude_column\": \"lng\"\n }\n }\n }\n }\n }\n }\n} \n This tells the datasette-cluster-map column which latitude and longitude columns should be used for a table called Street_Tree_List inside a database file called sf-trees.db .", "breadcrumbs": "[\"Plugins\"]", "references": "[]"} {"id": "plugins:one-off-plugins-using-plugins-dir", "page": "plugins", "ref": "one-off-plugins-using-plugins-dir", "title": "One-off plugins using --plugins-dir", "content": "You can also define one-off per-project plugins by saving them as plugin_name.py functions in a plugins/ folder and then passing that folder to datasette using the --plugins-dir option: \n datasette mydb.db --plugins-dir=plugins/", "breadcrumbs": "[\"Plugins\", \"Installing plugins\"]", "references": "[]"} {"id": "plugins:id1", "page": "plugins", "ref": "id1", "title": "Plugins", "content": "Datasette's plugin system allows additional features to be implemented as Python\n code (or front-end JavaScript) which can be wrapped up in a separate Python\n package. The underlying mechanism uses pluggy . \n See the Datasette plugins directory for a list of existing plugins, or take a look at the\n datasette-plugin topic on GitHub. \n Things you can do with plugins include: \n \n \n Add visualizations to Datasette, for example\n datasette-cluster-map and\n datasette-vega . \n \n \n Make new custom SQL functions available for use within Datasette, for example\n datasette-haversine and\n datasette-jellyfish . \n \n \n Define custom output formats with custom extensions, for example datasette-atom and\n datasette-ics . \n \n \n Add template functions that can be called within your Jinja custom templates,\n for example datasette-render-markdown . \n \n \n Customize how database values are rendered in the Datasette interface, for example\n datasette-render-binary and\n datasette-pretty-json . \n \n \n Customize how Datasette's authentication and permissions systems work, for example datasette-auth-tokens and\n datasette-permissions-sql .", "breadcrumbs": "[]", "references": "[{\"href\": \"https://pluggy.readthedocs.io/\", \"label\": \"pluggy\"}, {\"href\": \"https://datasette.io/plugins\", \"label\": \"Datasette plugins directory\"}, {\"href\": \"https://github.com/topics/datasette-plugin\", \"label\": \"datasette-plugin\"}, {\"href\": \"https://github.com/simonw/datasette-cluster-map\", \"label\": \"datasette-cluster-map\"}, {\"href\": \"https://github.com/simonw/datasette-vega\", \"label\": \"datasette-vega\"}, {\"href\": \"https://github.com/simonw/datasette-haversine\", \"label\": \"datasette-haversine\"}, {\"href\": \"https://github.com/simonw/datasette-jellyfish\", \"label\": \"datasette-jellyfish\"}, {\"href\": \"https://github.com/simonw/datasette-atom\", \"label\": \"datasette-atom\"}, {\"href\": \"https://github.com/simonw/datasette-ics\", \"label\": \"datasette-ics\"}, {\"href\": \"https://github.com/simonw/datasette-render-markdown#markdown-in-templates\", \"label\": \"datasette-render-markdown\"}, {\"href\": \"https://github.com/simonw/datasette-render-binary\", \"label\": \"datasette-render-binary\"}, {\"href\": \"https://github.com/simonw/datasette-pretty-json\", \"label\": \"datasette-pretty-json\"}, {\"href\": \"https://github.com/simonw/datasette-auth-tokens\", \"label\": \"datasette-auth-tokens\"}, {\"href\": \"https://github.com/simonw/datasette-permissions-sql\", \"label\": \"datasette-permissions-sql\"}]"} {"id": "plugins:deploying-plugins-using-datasette-publish", "page": "plugins", "ref": "deploying-plugins-using-datasette-publish", "title": "Deploying plugins using datasette publish", "content": "The datasette publish and datasette package commands both take an optional --install argument. You can use this one or more times to tell Datasette to pip install specific plugins as part of the process: \n datasette publish cloudrun mydb.db --install=datasette-vega \n You can use the name of a package on PyPI or any of the other valid arguments to pip install such as a URL to a .zip file: \n datasette publish cloudrun mydb.db \\\n --install=https://url-to-my-package.zip", "breadcrumbs": "[\"Plugins\", \"Installing plugins\"]", "references": "[]"} {"id": "plugin_hooks:plugin-register-routes", "page": "plugin_hooks", "ref": "plugin-register-routes", "title": "register_routes(datasette)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) \n \n \n \n Register additional view functions to execute for specified URL routes. \n Return a list of (regex, view_function) pairs, something like this: \n from datasette import hookimpl, Response\nimport html\n\n\nasync def hello_from(request):\n name = request.url_vars[\"name\"]\n return Response.html(\n \"Hello from {}\".format(html.escape(name))\n )\n\n\n@hookimpl\ndef register_routes():\n return [(r\"^/hello-from/(?P.*)$\", hello_from)] \n The view functions can take a number of different optional arguments. The corresponding argument will be passed to your function depending on its named parameters - a form of dependency injection. \n The optional view function arguments are as follows: \n \n \n datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries. \n \n \n \n request - Request object \n \n The current HTTP request. \n \n \n \n scope - dictionary \n \n The incoming ASGI scope dictionary. \n \n \n \n send - function \n \n The ASGI send function. \n \n \n \n receive - function \n \n The ASGI receive function. \n \n \n \n The view function can be a regular function or an async def function, depending on if it needs to use any await APIs. \n The function can either return a Response class or it can return nothing and instead respond directly to the request using the ASGI send function (for advanced uses only). \n It can also raise the datasette.NotFound exception to return a 404 not found error, or the datasette.Forbidden exception for a 403 forbidden. \n See Designing URLs for your plugin for tips on designing the URL routes used by your plugin. \n Examples: datasette-auth-github , datasette-psutil", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://datasette.io/plugins/datasette-auth-github\", \"label\": \"datasette-auth-github\"}, {\"href\": \"https://datasette.io/plugins/datasette-psutil\", \"label\": \"datasette-psutil\"}]"} {"id": "plugin_hooks:plugin-register-output-renderer", "page": "plugin_hooks", "ref": "plugin-register-output-renderer", "title": "register_output_renderer(datasette)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) \n \n \n \n Registers a new output renderer, to output data in a custom format. The hook function should return a dictionary, or a list of dictionaries, of the following shape: \n @hookimpl\ndef register_output_renderer(datasette):\n return {\n \"extension\": \"test\",\n \"render\": render_demo,\n \"can_render\": can_render_demo, # Optional\n } \n This will register render_demo to be called when paths with the extension .test (for example /database.test , /database/table.test , or /database/table/row.test ) are requested. \n render_demo is a Python function. It can be a regular function or an async def render_demo() awaitable function, depending on if it needs to make any asynchronous calls. \n can_render_demo is a Python function (or async def function) which accepts the same arguments as render_demo but just returns True or False . It lets Datasette know if the current SQL query can be represented by the plugin - and hence influnce if a link to this output format is displayed in the user interface. If you omit the \"can_render\" key from the dictionary every query will be treated as being supported by the plugin. \n When a request is received, the \"render\" callback function is called with zero or more of the following arguments. Datasette will inspect your callback function and pass arguments that match its function signature. \n \n \n datasette - Datasette class \n \n For accessing plugin configuration and executing queries. \n \n \n \n columns - list of strings \n \n The names of the columns returned by this query. \n \n \n \n rows - list of sqlite3.Row objects \n \n The rows returned by the query. \n \n \n \n sql - string \n \n The SQL query that was executed. \n \n \n \n query_name - string or None \n \n If this was the execution of a canned query , the name of that query. \n \n \n \n database - string \n \n The name of the database. \n \n \n \n table - string or None \n \n The table or view, if one is being rendered. \n \n \n \n request - Request object \n \n The current HTTP request. \n \n \n \n view_name - string \n \n The name of the current view being called. index , database , table , and row are the most important ones. \n \n \n \n The callback function can return None , if it is unable to render the data, or a Response class that will be returned to the caller. \n It can also return a dictionary with the following keys. This format is deprecated as-of Datasette 0.49 and will be removed by Datasette 1.0. \n \n \n body - string or bytes, optional \n \n The response body, default empty \n \n \n \n content_type - string, optional \n \n The Content-Type header, default text/plain \n \n \n \n status_code - integer, optional \n \n The HTTP status code, default 200 \n \n \n \n headers - dictionary, optional \n \n Extra HTTP headers to be returned in the response. \n \n \n \n An example of an output renderer callback function: \n def render_demo():\n return Response.text(\"Hello World\") \n Here is a more complex example: \n async def render_demo(datasette, columns, rows):\n db = datasette.get_database()\n result = await db.execute(\"select sqlite_version()\")\n first_row = \" | \".join(columns)\n lines = [first_row]\n lines.append(\"=\" * len(first_row))\n for row in rows:\n lines.append(\" | \".join(row))\n return Response(\n \"\\n\".join(lines),\n content_type=\"text/plain; charset=utf-8\",\n headers={\"x-sqlite-version\": result.first()[0]},\n ) \n And here is an example can_render function which returns True only if the query results contain the columns atom_id , atom_title and atom_updated : \n def can_render_demo(columns):\n return {\n \"atom_id\",\n \"atom_title\",\n \"atom_updated\",\n }.issubset(columns) \n Examples: datasette-atom , datasette-ics , datasette-geojson , datasette-copyable", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://datasette.io/plugins/datasette-atom\", \"label\": \"datasette-atom\"}, {\"href\": \"https://datasette.io/plugins/datasette-ics\", \"label\": \"datasette-ics\"}, {\"href\": \"https://datasette.io/plugins/datasette-geojson\", \"label\": \"datasette-geojson\"}, {\"href\": \"https://datasette.io/plugins/datasette-copyable\", \"label\": \"datasette-copyable\"}]"} {"id": "plugin_hooks:plugin-register-facet-classes", "page": "plugin_hooks", "ref": "plugin-register-facet-classes", "title": "register_facet_classes()", "content": "Return a list of additional Facet subclasses to be registered. \n \n The design of this plugin hook is unstable and may change. See issue 830 . \n \n Each Facet subclass implements a new type of facet operation. The class should look like this: \n class SpecialFacet(Facet):\n # This key must be unique across all facet classes:\n type = \"special\"\n\n async def suggest(self):\n # Use self.sql and self.params to suggest some facets\n suggested_facets = []\n suggested_facets.append(\n {\n \"name\": column, # Or other unique name\n # Construct the URL that will enable this facet:\n \"toggle_url\": self.ds.absolute_url(\n self.request,\n path_with_added_args(\n self.request, {\"_facet\": column}\n ),\n ),\n }\n )\n return suggested_facets\n\n async def facet_results(self):\n # This should execute the facet operation and return results, again\n # using self.sql and self.params as the starting point\n facet_results = []\n facets_timed_out = []\n facet_size = self.get_facet_size()\n # Do some calculations here...\n for column in columns_selected_for_facet:\n try:\n facet_results_values = []\n # More calculations...\n facet_results_values.append(\n {\n \"value\": value,\n \"label\": label,\n \"count\": count,\n \"toggle_url\": self.ds.absolute_url(\n self.request, toggle_path\n ),\n \"selected\": selected,\n }\n )\n facet_results.append(\n {\n \"name\": column,\n \"results\": facet_results_values,\n \"truncated\": len(facet_rows_results)\n > facet_size,\n }\n )\n except QueryInterrupted:\n facets_timed_out.append(column)\n\n return facet_results, facets_timed_out \n See datasette/facets.py for examples of how these classes can work. \n The plugin hook can then be used to register the new facet class like this: \n @hookimpl\ndef register_facet_classes():\n return [SpecialFacet]", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette/issues/830\", \"label\": \"issue 830\"}, {\"href\": \"https://github.com/simonw/datasette/blob/main/datasette/facets.py\", \"label\": \"datasette/facets.py\"}]"} {"id": "plugin_hooks:plugin-hook-table-actions", "page": "plugin_hooks", "ref": "plugin-hook-table-actions", "title": "table_actions(datasette, actor, database, table, request)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries. \n \n \n \n actor - dictionary or None \n \n The currently authenticated actor . \n \n \n \n database - string \n \n The name of the database. \n \n \n \n table - string \n \n The name of the table. \n \n \n \n request - Request object or None \n \n The current HTTP request. This can be None if the request object is not available. \n \n \n \n This hook allows table actions to be displayed in a menu accessed via an action icon at the top of the table page. It should return a list of {\"href\": \"...\", \"label\": \"...\"} menu items. \n It can alternatively return an async def awaitable function which returns a list of menu items. \n This example adds a new table action if the signed in user is \"root\" : \n from datasette import hookimpl\n\n\n@hookimpl\ndef table_actions(datasette, actor, database, table):\n if actor and actor.get(\"id\") == \"root\":\n return [\n {\n \"href\": datasette.urls.path(\n \"/-/edit-schema/{}/{}\".format(\n database, table\n )\n ),\n \"label\": \"Edit schema for this table\",\n }\n ] \n Example: datasette-graphql", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://datasette.io/plugins/datasette-graphql\", \"label\": \"datasette-graphql\"}]"} {"id": "plugin_hooks:plugin-hook-startup", "page": "plugin_hooks", "ref": "plugin-hook-startup", "title": "startup(datasette)", "content": "This hook fires when the Datasette application server first starts up. You can implement a regular function, for example to validate required plugin configuration: \n @hookimpl\ndef startup(datasette):\n config = datasette.plugin_config(\"my-plugin\") or {}\n assert (\n \"required-setting\" in config\n ), \"my-plugin requires setting required-setting\" \n Or you can return an async function which will be awaited on startup. Use this option if you need to make any database queries: \n @hookimpl\ndef startup(datasette):\n async def inner():\n db = datasette.get_database()\n if \"my_table\" not in await db.table_names():\n await db.execute_write(\n \"\"\"\n create table my_table (mycol text)\n \"\"\"\n )\n\n return inner \n Potential use-cases: \n \n \n Run some initialization code for the plugin \n \n \n Create database tables that a plugin needs on startup \n \n \n Validate the metadata configuration for a plugin on startup, and raise an error if it is invalid \n \n \n \n If you are writing unit tests for a plugin that uses this hook and doesn't exercise Datasette by sending\n any simulated requests through it you will need to explicitly call await ds.invoke_startup() in your tests. An example: \n @pytest.mark.asyncio\nasync def test_my_plugin():\n ds = Datasette()\n await ds.invoke_startup()\n # Rest of test goes here \n \n Examples: datasette-saved-queries , datasette-init", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://datasette.io/plugins/datasette-saved-queries\", \"label\": \"datasette-saved-queries\"}, {\"href\": \"https://datasette.io/plugins/datasette-init\", \"label\": \"datasette-init\"}]"} {"id": "plugin_hooks:plugin-hook-skip-csrf", "page": "plugin_hooks", "ref": "plugin-hook-skip-csrf", "title": "skip_csrf(datasette, scope)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries. \n \n \n \n scope - dictionary \n \n The ASGI scope for the incoming HTTP request. \n \n \n \n This hook can be used to skip CSRF protection for a specific incoming request. For example, you might have a custom path at /submit-comment which is designed to accept comments from anywhere, whether or not the incoming request originated on the site and has an accompanying CSRF token. \n This example will disable CSRF protection for that specific URL path: \n from datasette import hookimpl\n\n\n@hookimpl\ndef skip_csrf(scope):\n return scope[\"path\"] == \"/submit-comment\" \n If any of the currently active skip_csrf() plugin hooks return True , CSRF protection will be skipped for the request.", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope\", \"label\": \"ASGI scope\"}]"} {"id": "plugin_hooks:plugin-hook-render-cell", "page": "plugin_hooks", "ref": "plugin-hook-render-cell", "title": "render_cell(row, value, column, table, database, datasette)", "content": "Lets you customize the display of values within table cells in the HTML table view. \n \n \n row - sqlite.Row \n \n The SQLite row object that the value being rendered is part of \n \n \n \n value - string, integer, float, bytes or None \n \n The value that was loaded from the database \n \n \n \n column - string \n \n The name of the column being rendered \n \n \n \n table - string or None \n \n The name of the table - or None if this is a custom SQL query \n \n \n \n database - string \n \n The name of the database \n \n \n \n datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries. \n \n \n \n If your hook returns None , it will be ignored. Use this to indicate that your hook is not able to custom render this particular value. \n If the hook returns a string, that string will be rendered in the table cell. \n If you want to return HTML markup you can do so by returning a jinja2.Markup object. \n You can also return an awaitable function which returns a value. \n Datasette will loop through all available render_cell hooks and display the value returned by the first one that does not return None . \n Here is an example of a custom render_cell() plugin which looks for values that are a JSON string matching the following format: \n {\"href\": \"https://www.example.com/\", \"label\": \"Name\"} \n If the value matches that pattern, the plugin returns an HTML link element: \n from datasette import hookimpl\nimport markupsafe\nimport json\n\n\n@hookimpl\ndef render_cell(value):\n # Render {\"href\": \"...\", \"label\": \"...\"} as link\n if not isinstance(value, str):\n return None\n stripped = value.strip()\n if not (\n stripped.startswith(\"{\") and stripped.endswith(\"}\")\n ):\n return None\n try:\n data = json.loads(value)\n except ValueError:\n return None\n if not isinstance(data, dict):\n return None\n if set(data.keys()) != {\"href\", \"label\"}:\n return None\n href = data[\"href\"]\n if not (\n href.startswith(\"/\")\n or href.startswith(\"http://\")\n or href.startswith(\"https://\")\n ):\n return None\n return markupsafe.Markup(\n '{label}'.format(\n href=markupsafe.escape(data[\"href\"]),\n label=markupsafe.escape(data[\"label\"] or \"\")\n or \" \",\n )\n ) \n Examples: datasette-render-binary , datasette-render-markdown , datasette-json-html", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://datasette.io/plugins/datasette-render-binary\", \"label\": \"datasette-render-binary\"}, {\"href\": \"https://datasette.io/plugins/datasette-render-markdown\", \"label\": \"datasette-render-markdown\"}, {\"href\": \"https://datasette.io/plugins/datasette-json-html\", \"label\": \"datasette-json-html\"}]"} {"id": "plugin_hooks:plugin-hook-register-magic-parameters", "page": "plugin_hooks", "ref": "plugin-hook-register-magic-parameters", "title": "register_magic_parameters(datasette)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) . \n \n \n \n Magic parameters can be used to add automatic parameters to canned queries . This plugin hook allows additional magic parameters to be defined by plugins. \n Magic parameters all take this format: _prefix_rest_of_parameter . The prefix indicates which magic parameter function should be called - the rest of the parameter is passed as an argument to that function. \n To register a new function, return it as a tuple of (string prefix, function) from this hook. The function you register should take two arguments: key and request , where key is the rest_of_parameter portion of the parameter and request is the current Request object . \n This example registers two new magic parameters: :_request_http_version returning the HTTP version of the current request, and :_uuid_new which returns a new UUID: \n from uuid import uuid4\n\n\ndef uuid(key, request):\n if key == \"new\":\n return str(uuid4())\n else:\n raise KeyError\n\n\ndef request(key, request):\n if key == \"http_version\":\n return request.scope[\"http_version\"]\n else:\n raise KeyError\n\n\n@hookimpl\ndef register_magic_parameters(datasette):\n return [\n (\"request\", request),\n (\"uuid\", uuid),\n ]", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"} {"id": "plugin_hooks:plugin-hook-register-commands", "page": "plugin_hooks", "ref": "plugin-hook-register-commands", "title": "register_commands(cli)", "content": "cli - the root Datasette Click command group \n \n Use this to register additional CLI commands \n \n \n \n Register additional CLI commands that can be run using datsette yourcommand ... . This provides a mechanism by which plugins can add new CLI commands to Datasette. \n This example registers a new datasette verify file1.db file2.db command that checks if the provided file paths are valid SQLite databases: \n from datasette import hookimpl\nimport click\nimport sqlite3\n\n\n@hookimpl\ndef register_commands(cli):\n @cli.command()\n @click.argument(\n \"files\", type=click.Path(exists=True), nargs=-1\n )\n def verify(files):\n \"Verify that files can be opened by Datasette\"\n for file in files:\n conn = sqlite3.connect(str(file))\n try:\n conn.execute(\"select * from sqlite_master\")\n except sqlite3.DatabaseError:\n raise click.ClickException(\n \"Invalid database: {}\".format(file)\n ) \n The new command can then be executed like so: \n datasette verify fixtures.db \n Help text (from the docstring for the function plus any defined Click arguments or options) will become available using: \n datasette verify --help \n Plugins can register multiple commands by making multiple calls to the @cli.command() decorator. Consult the Click documentation for full details on how to build a CLI command, including how to define arguments and options. \n Note that register_commands() plugins cannot used with the --plugins-dir mechanism - they need to be installed into the same virtual environment as Datasette using pip install . Provided it has a setup.py file (see Packaging a plugin ) you can run pip install directly against the directory in which you are developing your plugin like so: \n pip install -e path/to/my/datasette-plugin \n Examples: datasette-auth-passwords , datasette-verify", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://click.palletsprojects.com/en/latest/commands/#callback-invocation\", \"label\": \"Click command group\"}, {\"href\": \"https://click.palletsprojects.com/\", \"label\": \"Click documentation\"}, {\"href\": \"https://datasette.io/plugins/datasette-auth-passwords\", \"label\": \"datasette-auth-passwords\"}, {\"href\": \"https://datasette.io/plugins/datasette-verify\", \"label\": \"datasette-verify\"}]"} {"id": "plugin_hooks:plugin-hook-publish-subcommand", "page": "plugin_hooks", "ref": "plugin-hook-publish-subcommand", "title": "publish_subcommand(publish)", "content": "publish - Click publish command group \n \n The Click command group for the datasette publish subcommand \n \n \n \n This hook allows you to create new providers for the datasette publish \n command. Datasette uses this hook internally to implement the default cloudrun \n and heroku subcommands, so you can read\n their source \n to see examples of this hook in action. \n Let's say you want to build a plugin that adds a datasette publish my_hosting_provider --api_key=xxx mydatabase.db publish command. Your implementation would start like this: \n from datasette import hookimpl\nfrom datasette.publish.common import (\n add_common_publish_arguments_and_options,\n)\nimport click\n\n\n@hookimpl\ndef publish_subcommand(publish):\n @publish.command()\n @add_common_publish_arguments_and_options\n @click.option(\n \"-k\",\n \"--api_key\",\n help=\"API key for talking to my hosting provider\",\n )\n def my_hosting_provider(\n files,\n metadata,\n extra_options,\n branch,\n template_dir,\n plugins_dir,\n static,\n install,\n plugin_secret,\n version_note,\n secret,\n title,\n license,\n license_url,\n source,\n source_url,\n about,\n about_url,\n api_key,\n ):\n ... \n Examples: datasette-publish-fly , datasette-publish-vercel", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette/tree/main/datasette/publish\", \"label\": \"their source\"}, {\"href\": \"https://datasette.io/plugins/datasette-publish-fly\", \"label\": \"datasette-publish-fly\"}, {\"href\": \"https://datasette.io/plugins/datasette-publish-vercel\", \"label\": \"datasette-publish-vercel\"}]"} {"id": "plugin_hooks:plugin-hook-prepare-jinja2-environment", "page": "plugin_hooks", "ref": "plugin-hook-prepare-jinja2-environment", "title": "prepare_jinja2_environment(env, datasette)", "content": "env - jinja2 Environment \n \n The template environment that is being prepared \n \n \n \n datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) \n \n \n \n This hook is called with the Jinja2 environment that is used to evaluate\n Datasette HTML templates. You can use it to do things like register custom\n template filters , for\n example: \n from datasette import hookimpl\n\n\n@hookimpl\ndef prepare_jinja2_environment(env):\n env.filters[\"uppercase\"] = lambda u: u.upper() \n You can now use this filter in your custom templates like so: \n Table name: {{ table|uppercase }} \n This function can return an awaitable function if it needs to run any async code. \n Examples: datasette-edit-templates", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"http://jinja.pocoo.org/docs/2.10/api/#custom-filters\", \"label\": \"register custom\\n template filters\"}, {\"href\": \"https://datasette.io/plugins/datasette-edit-templates\", \"label\": \"datasette-edit-templates\"}]"} {"id": "plugin_hooks:plugin-hook-prepare-connection", "page": "plugin_hooks", "ref": "plugin-hook-prepare-connection", "title": "prepare_connection(conn, database, datasette)", "content": "conn - sqlite3 connection object \n \n The connection that is being opened \n \n \n \n database - string \n \n The name of the database \n \n \n \n datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) \n \n \n \n This hook is called when a new SQLite database connection is created. You can\n use it to register custom SQL functions ,\n aggregates and collations. For example: \n from datasette import hookimpl\nimport random\n\n\n@hookimpl\ndef prepare_connection(conn):\n conn.create_function(\n \"random_integer\", 2, random.randint\n ) \n This registers a SQL function called random_integer which takes two\n arguments and can be called like this: \n select random_integer(1, 10); \n Examples: datasette-jellyfish , datasette-jq , datasette-haversine , datasette-rure", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://docs.python.org/2/library/sqlite3.html#sqlite3.Connection.create_function\", \"label\": \"register custom SQL functions\"}, {\"href\": \"https://datasette.io/plugins/datasette-jellyfish\", \"label\": \"datasette-jellyfish\"}, {\"href\": \"https://datasette.io/plugins/datasette-jq\", \"label\": \"datasette-jq\"}, {\"href\": \"https://datasette.io/plugins/datasette-haversine\", \"label\": \"datasette-haversine\"}, {\"href\": \"https://datasette.io/plugins/datasette-rure\", \"label\": \"datasette-rure\"}]"} {"id": "plugin_hooks:plugin-hook-permission-allowed", "page": "plugin_hooks", "ref": "plugin-hook-permission-allowed", "title": "permission_allowed(datasette, actor, action, resource)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries. \n \n \n \n actor - dictionary \n \n The current actor, as decided by actor_from_request(datasette, request) . \n \n \n \n action - string \n \n The action to be performed, e.g. \"edit-table\" . \n \n \n \n resource - string or None \n \n An identifier for the individual resource, e.g. the name of the table. \n \n \n \n Called to check that an actor has permission to perform an action on a resource. Can return True if the action is allowed, False if the action is not allowed or None if the plugin does not have an opinion one way or the other. \n Here's an example plugin which randomly selects if a permission should be allowed or denied, except for view-instance which always uses the default permission scheme instead. \n from datasette import hookimpl\nimport random\n\n\n@hookimpl\ndef permission_allowed(action):\n if action != \"view-instance\":\n # Return True or False at random\n return random.random() > 0.5\n # Returning None falls back to default permissions \n This function can alternatively return an awaitable function which itself returns True , False or None . You can use this option if you need to execute additional database queries using await datasette.execute(...) . \n Here's an example that allows users to view the admin_log table only if their actor id is present in the admin_users table. It aso disallows arbitrary SQL queries for the staff.db database for all users. \n @hookimpl\ndef permission_allowed(datasette, actor, action, resource):\n async def inner():\n if action == \"execute-sql\" and resource == \"staff\":\n return False\n if action == \"view-table\" and resource == (\n \"staff\",\n \"admin_log\",\n ):\n if not actor:\n return False\n user_id = actor[\"id\"]\n return await datasette.get_database(\n \"staff\"\n ).execute(\n \"select count(*) from admin_users where user_id = :user_id\",\n {\"user_id\": user_id},\n )\n\n return inner \n See built-in permissions for a full list of permissions that are included in Datasette core. \n Example: datasette-permissions-sql", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://datasette.io/plugins/datasette-permissions-sql\", \"label\": \"datasette-permissions-sql\"}]"} {"id": "plugin_hooks:plugin-hook-menu-links", "page": "plugin_hooks", "ref": "plugin-hook-menu-links", "title": "menu_links(datasette, actor, request)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries. \n \n \n \n actor - dictionary or None \n \n The currently authenticated actor . \n \n \n \n request - Request object or None \n \n The current HTTP request. This can be None if the request object is not available. \n \n \n \n This hook allows additional items to be included in the menu displayed by Datasette's top right menu icon. \n The hook should return a list of {\"href\": \"...\", \"label\": \"...\"} menu items. These will be added to the menu. \n It can alternatively return an async def awaitable function which returns a list of menu items. \n This example adds a new menu item but only if the signed in user is \"root\" : \n from datasette import hookimpl\n\n\n@hookimpl\ndef menu_links(datasette, actor):\n if actor and actor.get(\"id\") == \"root\":\n return [\n {\n \"href\": datasette.urls.path(\n \"/-/edit-schema\"\n ),\n \"label\": \"Edit schema\",\n },\n ] \n Using datasette.urls here ensures that links in the menu will take the base_url setting into account. \n Examples: datasette-search-all , datasette-graphql", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://datasette.io/plugins/datasette-search-all\", \"label\": \"datasette-search-all\"}, {\"href\": \"https://datasette.io/plugins/datasette-graphql\", \"label\": \"datasette-graphql\"}]"} {"id": "plugin_hooks:plugin-hook-handle-exception", "page": "plugin_hooks", "ref": "plugin-hook-handle-exception", "title": "handle_exception(datasette, request, exception)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to render templates or execute SQL queries. \n \n \n \n request - Request object \n \n The current HTTP request. \n \n \n \n exception - Exception \n \n The exception that was raised. \n \n \n \n This hook is called any time an unexpected exception is raised. You can use it to record the exception. \n If your handler returns a Response object it will be returned to the client in place of the default Datasette error page. \n The handler can return a response directly, or it can return return an awaitable function that returns a response. \n This example logs an error to Sentry and then renders a custom error page: \n from datasette import hookimpl, Response\nimport sentry_sdk\n\n\n@hookimpl\ndef handle_exception(datasette, exception):\n sentry_sdk.capture_exception(exception)\n\n async def inner():\n return Response.html(\n await datasette.render_template(\n \"custom_error.html\", request=request\n )\n )\n\n return inner \n Example: datasette-sentry", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://sentry.io/\", \"label\": \"Sentry\"}, {\"href\": \"https://datasette.io/plugins/datasette-sentry\", \"label\": \"datasette-sentry\"}]"} {"id": "plugin_hooks:plugin-hook-get-metadata", "page": "plugin_hooks", "ref": "plugin-hook-get-metadata", "title": "get_metadata(datasette, key, database, table)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) . \n \n \n \n actor - dictionary or None \n \n The currently authenticated actor . \n \n \n \n database - string or None \n \n The name of the database metadata is being asked for. \n \n \n \n table - string or None \n \n The name of the table. \n \n \n \n key - string or None \n \n The name of the key for which data is being asked for. \n \n \n \n This hook is responsible for returning a dictionary corresponding to Datasette Metadata . This function is passed the database , table and key which were passed to the upstream internal request for metadata. Regardless, it is important to return a global metadata object, where \"databases\": [] would be a top-level key. The dictionary returned here, will be merged with, and overwritten by, the contents of the physical metadata.yaml if one is present. \n \n The design of this plugin hook does not currently provide a mechanism for interacting with async code, and may change in the future. See issue 1384 . \n \n @hookimpl\ndef get_metadata(datasette, key, database, table):\n metadata = {\n \"title\": \"This will be the Datasette landing page title!\",\n \"description\": get_instance_description(datasette),\n \"databases\": [],\n }\n for db_name, db_data_dict in get_my_database_meta(\n datasette, database, table, key\n ):\n metadata[\"databases\"][db_name] = db_data_dict\n # whatever we return here will be merged with any other plugins using this hook and\n # will be overwritten by a local metadata.yaml if one exists!\n return metadata \n Example: datasette-remote-metadata plugin", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette/issues/1384\", \"label\": \"issue 1384\"}, {\"href\": \"https://datasette.io/plugins/datasette-remote-metadata\", \"label\": \"datasette-remote-metadata plugin\"}]"} {"id": "plugin_hooks:plugin-hook-forbidden", "page": "plugin_hooks", "ref": "plugin-hook-forbidden", "title": "forbidden(datasette, request, message)", "content": "datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to render templates or execute SQL queries. \n \n \n \n request - Request object \n \n The current HTTP request. \n \n \n \n message - string \n \n A message hinting at why the request was forbidden. \n \n \n \n Plugins can use this to customize how Datasette responds when a 403 Forbidden error occurs - usually because a page failed a permission check, see Permissions . \n If a plugin hook wishes to react to the error, it should return a Response object . \n This example returns a redirect to a /-/login page: \n from datasette import hookimpl\nfrom urllib.parse import urlencode\n\n\n@hookimpl\ndef forbidden(request, message):\n return Response.redirect(\n \"/-/login?=\" + urlencode({\"message\": message})\n ) \n The function can alternatively return an awaitable function if it needs to make any asynchronous method calls. This example renders a template: \n from datasette import hookimpl, Response\n\n\n@hookimpl\ndef forbidden(datasette):\n async def inner():\n return Response.html(\n await datasette.render_template(\n \"render_message.html\", request=request\n )\n )\n\n return inner", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"} {"id": "plugin_hooks:plugin-hook-filters-from-request", "page": "plugin_hooks", "ref": "plugin-hook-filters-from-request", "title": "filters_from_request(request, database, table, datasette)", "content": "request - Request object \n \n The current HTTP request. \n \n \n \n database - string \n \n The name of the database. \n \n \n \n table - string \n \n The name of the table. \n \n \n \n datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries. \n \n \n \n This hook runs on the table page, and can influence the where clause of the SQL query used to populate that page, based on query string arguments on the incoming request. \n The hook should return an instance of datasette.filters.FilterArguments which has one required and three optional arguments: \n return FilterArguments(\n where_clauses=[\"id > :max_id\"],\n params={\"max_id\": 5},\n human_descriptions=[\"max_id is greater than 5\"],\n extra_context={},\n) \n The arguments to the FilterArguments class constructor are as follows: \n \n \n where_clauses - list of strings, required \n \n A list of SQL fragments that will be inserted into the SQL query, joined by the and operator. These can include :named parameters which will be populated using data in params . \n \n \n \n params - dictionary, optional \n \n Additional keyword arguments to be used when the query is executed. These should match any :arguments in the where clauses. \n \n \n \n human_descriptions - list of strings, optional \n \n These strings will be included in the human-readable description at the top of the page and the page . \n \n \n \n extra_context - dictionary, optional \n \n Additional context variables that should be made available to the table.html template when it is rendered. \n \n \n \n This example plugin causes 0 results to be returned if ?_nothing=1 is added to the URL: \n from datasette import hookimpl\nfrom datasette.filters import FilterArguments\n\n\n@hookimpl\ndef filters_from_request(self, request):\n if request.args.get(\"_nothing\"):\n return FilterArguments(\n [\"1 = 0\"], human_descriptions=[\"NOTHING\"]\n ) \n Example: datasette-leaflet-freedraw", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://datasette.io/plugins/datasette-leaflet-freedraw\", \"label\": \"datasette-leaflet-freedraw\"}]"} {"id": "plugin_hooks:plugin-hook-extra-template-vars", "page": "plugin_hooks", "ref": "plugin-hook-extra-template-vars", "title": "extra_template_vars(template, database, table, columns, view_name, request, datasette)", "content": "Extra template variables that should be made available in the rendered template context. \n \n \n template - string \n \n The template that is being rendered, e.g. database.html \n \n \n \n database - string or None \n \n The name of the database, or None if the page does not correspond to a database (e.g. the root page) \n \n \n \n table - string or None \n \n The name of the table, or None if the page does not correct to a table \n \n \n \n columns - list of strings or None \n \n The names of the database columns that will be displayed on this page. None if the page does not contain a table. \n \n \n \n view_name - string \n \n The name of the view being displayed. ( index , database , table , and row are the most important ones.) \n \n \n \n request - Request object or None \n \n The current HTTP request. This can be None if the request object is not available. \n \n \n \n datasette - Datasette class \n \n You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) \n \n \n \n This hook can return one of three different types: \n \n \n Dictionary \n \n If you return a dictionary its keys and values will be merged into the template context. \n \n \n \n Function that returns a dictionary \n \n If you return a function it will be executed. If it returns a dictionary those values will will be merged into the template context. \n \n \n \n Function that returns an awaitable function that returns a dictionary \n \n You can also return a function which returns an awaitable function which returns a dictionary. \n \n \n \n Datasette runs Jinja2 in async mode , which means you can add awaitable functions to the template scope and they will be automatically awaited when they are rendered by the template. \n Here's an example plugin that adds a \"user_agent\" variable to the template context containing the current request's User-Agent header: \n @hookimpl\ndef extra_template_vars(request):\n return {\"user_agent\": request.headers.get(\"user-agent\")} \n This example returns an awaitable function which adds a list of hidden_table_names to the context: \n @hookimpl\ndef extra_template_vars(datasette, database):\n async def hidden_table_names():\n if database:\n db = datasette.databases[database]\n return {\n \"hidden_table_names\": await db.hidden_table_names()\n }\n else:\n return {}\n\n return hidden_table_names \n And here's an example which adds a sql_first(sql_query) function which executes a SQL statement and returns the first column of the first row of results: \n @hookimpl\ndef extra_template_vars(datasette, database):\n async def sql_first(sql, dbname=None):\n dbname = (\n dbname\n or database\n or next(iter(datasette.databases.keys()))\n )\n result = await datasette.execute(dbname, sql)\n return result.rows[0][0]\n\n return {\"sql_first\": sql_first} \n You can then use the new function in a template like so: \n SQLite version: {{ sql_first(\"select sqlite_version()\") }} \n Examples: datasette-search-all , datasette-template-sql", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://jinja.palletsprojects.com/en/2.10.x/api/#async-support\", \"label\": \"async mode\"}, {\"href\": \"https://datasette.io/plugins/datasette-search-all\", \"label\": \"datasette-search-all\"}, {\"href\": \"https://datasette.io/plugins/datasette-template-sql\", \"label\": \"datasette-template-sql\"}]"} {"id": "plugin_hooks:plugin-hook-extra-js-urls", "page": "plugin_hooks", "ref": "plugin-hook-extra-js-urls", "title": "extra_js_urls(template, database, table, columns, view_name, request, datasette)", "content": "This takes the same arguments as extra_template_vars(...) \n This works in the same way as extra_css_urls() but for JavaScript. You can\n return a list of URLs, a list of dictionaries or an awaitable function that returns those things: \n from datasette import hookimpl\n\n\n@hookimpl\ndef extra_js_urls():\n return [\n {\n \"url\": \"https://code.jquery.com/jquery-3.3.1.slim.min.js\",\n \"sri\": \"sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo\",\n }\n ] \n You can also return URLs to files from your plugin's static/ directory, if\n you have one: \n @hookimpl\ndef extra_js_urls():\n return [\"/-/static-plugins/your-plugin/app.js\"] \n Note that your-plugin here should be the hyphenated plugin name - the name that is displayed in the list on the /-/plugins debug page. \n If your code uses JavaScript modules you should include the \"module\": True key. See Custom CSS and JavaScript for more details. \n @hookimpl\ndef extra_js_urls():\n return [\n {\n \"url\": \"/-/static-plugins/your-plugin/app.js\",\n \"module\": True,\n }\n ] \n Examples: datasette-cluster-map , datasette-vega", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules\", \"label\": \"JavaScript modules\"}, {\"href\": \"https://datasette.io/plugins/datasette-cluster-map\", \"label\": \"datasette-cluster-map\"}, {\"href\": \"https://datasette.io/plugins/datasette-vega\", \"label\": \"datasette-vega\"}]"} {"id": "plugin_hooks:plugin-hook-extra-css-urls", "page": "plugin_hooks", "ref": "plugin-hook-extra-css-urls", "title": "extra_css_urls(template, database, table, columns, view_name, request, datasette)", "content": "This takes the same arguments as extra_template_vars(...) \n Return a list of extra CSS URLs that should be included on the page. These can\n take advantage of the CSS class hooks described in Custom pages and templates . \n This can be a list of URLs: \n from datasette import hookimpl\n\n\n@hookimpl\ndef extra_css_urls():\n return [\n \"https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css\"\n ] \n Or a list of dictionaries defining both a URL and an\n SRI hash : \n @hookimpl\ndef extra_css_urls():\n return [\n {\n \"url\": \"https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css\",\n \"sri\": \"sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4\",\n }\n ] \n This function can also return an awaitable function, useful if it needs to run any async code: \n @hookimpl\ndef extra_css_urls(datasette):\n async def inner():\n db = datasette.get_database()\n results = await db.execute(\n \"select url from css_files\"\n )\n return [r[0] for r in results]\n\n return inner \n Examples: datasette-cluster-map , datasette-vega", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[{\"href\": \"https://www.srihash.org/\", \"label\": \"SRI hash\"}, {\"href\": \"https://datasette.io/plugins/datasette-cluster-map\", \"label\": \"datasette-cluster-map\"}, {\"href\": \"https://datasette.io/plugins/datasette-vega\", \"label\": \"datasette-vega\"}]"}