{"ok": true, "next": null, "rows": [{"id": "internals:internals-csrf", "page": "internals", "ref": "internals-csrf", "title": "CSRF protection", "content": "Datasette protects against Cross-Site Request Forgery by inspecting the browser-set  Sec-Fetch-Site  and  Origin  headers on every unsafe (non- GET / HEAD / OPTIONS ) request, following the approach described in  Filippo Valsorda's article  and implemented in Go 1.25's  http.CrossOriginProtection . \n             A request is rejected with a  403  response if: \n             \n                 \n                     It carries  Sec-Fetch-Site  with any value other than  same-origin  or  none , or \n                 \n                 \n                     It has no  Sec-Fetch-Site  header but does carry an  Origin  header whose host does not match the request  Host . \n                 \n             \n             Requests from non-browser clients ( curl , server-to-server scripts, etc.) do not send  Sec-Fetch-Site  or  Origin  and pass through unchanged - CSRF is a browser-only attack. \n             No token, cookie, or hidden form field is needed. Any  <form method=\"POST\">  inside Datasette or a plugin will be accepted from the same origin without modification.", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[{\"href\": \"https://words.filippo.io/csrf/\", \"label\": \"Filippo Valsorda's article\"}]"}, {"id": "internals:internals-database", "page": "internals", "ref": "internals-database", "title": "Database class", "content": "Instances of the  Database  class can be used to execute queries against attached SQLite databases, and to run introspection against their schemas.", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[]"}, {"id": "internals:internals-datasette", "page": "internals", "ref": "internals-datasette", "title": "Datasette class", "content": "This object is an instance of the  Datasette  class, passed to many plugin hooks as an argument called  datasette . \n             You can create your own instance of this - for example to help write tests for a plugin - like so: \n             from datasette.app import Datasette\n\n# With no arguments a single in-memory database will be attached\ndatasette = Datasette()\n\n# The files= argument can load files from disk\ndatasette = Datasette(files=[\"/path/to/my-database.db\"])\n\n# Pass metadata as a JSON dictionary like this\ndatasette = Datasette(\n    files=[\"/path/to/my-database.db\"],\n    metadata={\n        \"databases\": {\n            \"my-database\": {\n                \"description\": \"This is my database\"\n            }\n        }\n    },\n) \n             Constructor parameters include: \n             \n                 \n                     files=[...]  - a list of database files to open \n                 \n                 \n                     immutables=[...]  - a list of database files to open in immutable mode \n                 \n                 \n                     metadata={...}  - a dictionary of  Metadata \n                 \n                 \n                     config_dir=...  - the  configuration directory  to use, stored in  datasette.config_dir", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[]"}, {"id": "internals:internals-formdata", "page": "internals", "ref": "internals-formdata", "title": "The FormData class", "content": "await request.form()  returns a  FormData  object - a dictionary-like object which provides access to form fields and uploaded files. It has a similar interface to  MultiParams . \n             \n                 \n                     form[key]  - string or UploadedFile \n                     \n                         Returns the first value for that key, or raises a  KeyError  if the key is missing. \n                     \n                 \n                 \n                     form.get(key)  - string, UploadedFile, or None \n                     \n                         Returns the first value for that key, or  None  if the key is missing. Pass a second argument to specify a different default. \n                     \n                 \n                 \n                     form.getlist(key)  - list \n                     \n                         Returns the list of values for that key. If the key is missing an empty list will be returned. \n                     \n                 \n                 \n                     form.keys()  - list of strings \n                     \n                         Returns the list of available keys. \n                     \n                 \n                 \n                     key in form  - True or False \n                     \n                         You can use  if key in form  to check if a key is present. \n                     \n                 \n                 \n                     for key in form  - iterator \n                     \n                         This lets you loop through every available key. \n                     \n                 \n                 \n                     len(form)  - integer \n                     \n                         Returns the total number of submitted values.", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[]"}, {"id": "internals:internals-internal", "page": "internals", "ref": "internals-internal", "title": "Datasette's internal database", "content": "Datasette maintains an \"internal\" SQLite database used for configuration, caching, and storage. Plugins can store configuration, settings, and other data inside this database. By default, Datasette will use a temporary in-memory SQLite database as the internal database, which is created at startup and destroyed at shutdown. Users of Datasette can optionally pass in a  --internal  flag to specify the path to a SQLite database to use as the internal database, which will persist internal data across Datasette instances. \n             Datasette maintains tables called  catalog_databases ,  catalog_tables ,  catalog_views ,  catalog_columns ,  catalog_indexes ,  catalog_foreign_keys  with details of the attached databases and their schemas. These tables should not be considered a stable API - they may change between Datasette releases. \n             Metadata is stored in tables  metadata_instance ,  metadata_databases ,  metadata_resources  and  metadata_columns . Plugins can interact with these tables via the  get_*_metadata() and set_*_metadata() methods . \n             The internal database is not exposed in the Datasette application by default, which means private data can safely be stored without worry of accidentally leaking information through the default Datasette interface and API. However, other plugins do have full read and write access to the internal database. \n             Plugins can access this database by calling  internal_db = datasette.get_internal_database()  and then executing queries using the  Database API . \n             Plugin authors are asked to practice good etiquette when using the internal database, as all plugins use the same database to store data. For example: \n             \n                 \n                     Use a unique prefix when creating tables, indices, and triggers in the internal database. If your plugin is called  datasette-xyz , then prefix names with  datasette_xyz_* . \n                 \n                 \n                     Avoid long-running write statements that may stall or block other plugins that are trying to write at the same time. \n                 \n                 \n                     Use temporary tables or shared in-memory attached databases when possible. \n                 \n                 \n                     Avoid implementing features that could expose private data stored in the internal database by other plugins.", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[]"}, {"id": "internals:internals-multiparams", "page": "internals", "ref": "internals-multiparams", "title": "The MultiParams class", "content": "request.args  is a  MultiParams  object - a dictionary-like object which provides access to query string parameters that may have multiple values. \n             Consider the query string  ?foo=1&foo=2&bar=3  - with two values for  foo  and one value for  bar . \n             \n                 \n                     request.args[key]  - string \n                     \n                         Returns the first value for that key, or raises a  KeyError  if the key is missing. For the above example  request.args[\"foo\"]  would return  \"1\" . \n                     \n                 \n                 \n                     request.args.get(key)  - string or None \n                     \n                         Returns the first value for that key, or  None  if the key is missing. Pass a second argument to specify a different default, e.g.  q = request.args.get(\"q\", \"\") . \n                     \n                 \n                 \n                     request.args.getlist(key)  - list of strings \n                     \n                         Returns the list of strings for that key.  request.args.getlist(\"foo\")  would return  [\"1\", \"2\"]  in the above example.  request.args.getlist(\"bar\")  would return  [\"3\"] . If the key is missing an empty list will be returned. \n                     \n                 \n                 \n                     request.args.keys()  - list of strings \n                     \n                         Returns the list of available keys - for the example this would be  [\"foo\", \"bar\"] . \n                     \n                 \n                 \n                     key in request.args  - True or False \n                     \n                         You can use  if key in request.args  to check if a key is present. \n                     \n                 \n                 \n                     for key in request.args  - iterator \n                     \n                         This lets you loop through every available key. \n                     \n                 \n                 \n                     len(request.args)  - integer \n                     \n                         Returns the number of keys.", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[]"}, {"id": "internals:internals-permission-classes", "page": "internals", "ref": "internals-permission-classes", "title": "Permission classes and utilities", "content": "", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[]"}, {"id": "internals:internals-request", "page": "internals", "ref": "internals-request", "title": "Request object", "content": "The request object is passed to various plugin hooks. It represents an incoming HTTP request. It has the following properties: \n             \n                 \n                     .scope  - dictionary \n                     \n                         The ASGI scope that was used to construct this request, described in the  ASGI HTTP connection scope  specification. \n                     \n                 \n                 \n                     .method  - string \n                     \n                         The HTTP method for this request, usually  GET  or  POST . \n                     \n                 \n                 \n                     .url  - string \n                     \n                         The full URL for this request, e.g.  https://latest.datasette.io/fixtures . \n                     \n                 \n                 \n                     .scheme  - string \n                     \n                         The request scheme - usually  https  or  http . \n                     \n                 \n                 \n                     .headers  - dictionary (str -> str) \n                     \n                         A dictionary of incoming HTTP request headers. Header names have been converted to lowercase. \n                     \n                 \n                 \n                     .cookies  - dictionary (str -> str) \n                     \n                         A dictionary of incoming cookies \n                     \n                 \n                 \n                     .host  - string \n                     \n                         The host header from the incoming request, e.g.  latest.datasette.io  or  localhost . \n                     \n                 \n                 \n                     .path  - string \n                     \n                         The path of the request excluding the query string, e.g.  /fixtures . \n                     \n                 \n                 \n                     .full_path  - string \n                     \n                         The path of the request including the query string if one is present, e.g.  /fixtures?sql=select+sqlite_version() . \n                     \n                 \n                 \n                     .query_string  - string \n                     \n                         The query string component of the request, without the  ?  - e.g.  name__contains=sam&age__gt=10 . \n                     \n                 \n                 \n                     .args  - MultiParams \n                     \n                         An object representing the parsed query string parameters, see below. \n                     \n                 \n                 \n                     .url_vars  - dictionary (str -> str) \n                     \n                         Variables extracted from the URL path, if that path was defined using a regular expression. See  register_routes(datasette) . \n                     \n                 \n                 \n                     .actor  - dictionary (str -> Any) or None \n                     \n                         The currently authenticated actor (see  actors ), or  None  if the request is unauthenticated. \n                     \n                 \n             \n             The object also has the following awaitable methods: \n             \n                 \n                     await request.form(files=False, ...)  - FormData \n                     \n                         Parses form data from the request body. Supports both  application/x-www-form-urlencoded  and  multipart/form-data  content types. \n                         Returns a  The FormData class  object with dict-like access to form fields and uploaded files. \n                         Requirements and errors: \n                         \n                             \n                                 A  Content-Type  header is required. Missing or unsupported content types raise  BadRequest . \n                             \n                             \n                                 For  multipart/form-data , the  boundary=...  parameter is required. \n                             \n                         \n                         Parameters: \n                         \n                             \n                                 files  (bool, default  False ): If  True , uploaded files are stored and accessible. If  False  (default), file content is discarded but form fields are still available. \n                             \n                             \n                                 max_file_size  (int, default 50MB): Maximum size per uploaded file in bytes. \n                             \n                             \n                                 max_request_size  (int, default 100MB): Maximum total request body size in bytes. \n                             \n                             \n                                 max_fields  (int, default 1000): Maximum number of form fields. \n                             \n                             \n                                 max_files  (int, default 100): Maximum number of uploaded files. \n                             \n                             \n                                 max_parts  (int, default  max_fields + max_files ): Maximum number of multipart parts in total. \n                             \n                             \n                                 max_field_size  (int, default 100KB): Maximum size of a text field value in bytes. \n                             \n                             \n                                 max_memory_file_size  (int, default 1MB): File size threshold before uploads spill to disk. \n                             \n                             \n                                 max_part_header_bytes  (int, default 16KB): Maximum total bytes allowed in part headers. \n                             \n                             \n                                 max_part_header_lines  (int, default 100): Maximum header lines per part. \n                             \n                             \n                                 min_free_disk_bytes  (int, default 50MB): Minimum free bytes required in the temp directory before accepting file uploads. \n                             \n                         \n                         Example usage: \n                         # Parse form fields only (files are discarded)\nform = await request.form()\nusername = form[\"username\"]\ntags = form.getlist(\"tags\")  # For multiple values\n\n# Parse form fields AND files\nform = await request.form(files=True)\nuploaded = form[\"avatar\"]\ncontent = await uploaded.read()\nprint(\n    uploaded.filename, uploaded.content_type, uploaded.size\n) \n                         Cleanup note: \n                         When using  files=True , call  await form.aclose()  once you are done with the uploads\n                            to ensure spooled temporary files are closed promptly. You can also use\n                             async with form: ...  for automatic cleanup. \n                         Don't forget to read about  CSRF protection ! \n                     \n                 \n                 \n                     await request.post_vars()  - dictionary \n                     \n                         Returns a dictionary of form variables that were submitted in the request body via  POST  using  application/x-www-form-urlencoded  encoding. For multipart forms or file uploads, use  request.form()  instead. \n                     \n                 \n                 \n                     await request.post_body()  - bytes \n                     \n                         Returns the un-parsed body of a request submitted by  POST  - useful for things like incoming JSON data. \n                     \n                 \n             \n             And a class method that can be used to create fake request objects for use in tests: \n             \n                 \n                     fake(path_with_query_string, method=\"GET\", scheme=\"http\", url_vars=None) \n                     \n                         Returns a  Request  instance for the specified path and method. For example: \n                         from datasette import Request\nfrom pprint import pprint\n\nrequest = Request.fake(\n    \"/fixtures/facetable/\",\n    url_vars={\"database\": \"fixtures\", \"table\": \"facetable\"},\n)\npprint(request.scope) \n                         This outputs: \n                         {'http_version': '1.1',\n 'method': 'GET',\n 'path': '/fixtures/facetable/',\n 'query_string': b'',\n 'raw_path': b'/fixtures/facetable/',\n 'scheme': 'http',\n 'type': 'http',\n 'url_route': {'kwargs': {'database': 'fixtures', 'table': 'facetable'}}}", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[{\"href\": \"https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope\", \"label\": \"ASGI HTTP connection scope\"}]"}, {"id": "internals:internals-response", "page": "internals", "ref": "internals-response", "title": "Response class", "content": "The  Response  class can be returned from view functions that have been registered using the  register_routes(datasette)  hook. \n             The  Response()  constructor takes the following arguments: \n             \n                 \n                     body  - string \n                     \n                         The body of the response. \n                     \n                 \n                 \n                     status  - integer (optional) \n                     \n                         The HTTP status - defaults to 200. \n                     \n                 \n                 \n                     headers  - dictionary (optional) \n                     \n                         A dictionary of extra HTTP headers, e.g.  {\"x-hello\": \"world\"} . \n                     \n                 \n                 \n                     content_type  - string (optional) \n                     \n                         The content-type for the response. Defaults to  text/plain . \n                     \n                 \n             \n             For example: \n             from datasette.utils.asgi import Response\n\nresponse = Response(\n    \"<xml>This is XML</xml>\",\n    content_type=\"application/xml; charset=utf-8\",\n) \n             The quickest way to create responses is using the  Response.text(...) ,  Response.html(...) ,  Response.json(...)  or  Response.redirect(...)  helper methods: \n             from datasette.utils.asgi import Response\n\nhtml_response = Response.html(\"This is HTML\")\njson_response = Response.json({\"this_is\": \"json\"})\ntext_response = Response.text(\n    \"This will become utf-8 encoded text\"\n)\n# Redirects are served as 302, unless you pass status=301:\nredirect_response = Response.redirect(\n    \"https://latest.datasette.io/\"\n) \n             Each of these responses will use the correct corresponding content-type -  text/html; charset=utf-8 ,  application/json; charset=utf-8  or  text/plain; charset=utf-8  respectively. \n             Each of the helper methods take optional  status=  and  headers=  arguments, documented above.", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[]"}, {"id": "internals:internals-shortcuts", "page": "internals", "ref": "internals-shortcuts", "title": "Import shortcuts", "content": "The following commonly used symbols can be imported directly from the  datasette  module: \n             from datasette import Response\nfrom datasette import Forbidden\nfrom datasette import NotFound\nfrom datasette import hookimpl\nfrom datasette import actor_matches_allow", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[]"}, {"id": "internals:internals-tracer", "page": "internals", "ref": "internals-tracer", "title": "datasette.tracer", "content": "Running Datasette with  --setting trace_debug 1  enables trace debug output, which can then be viewed by adding  ?_trace=1  to the query string for any page. \n             You can see an example of this at the bottom of  latest.datasette.io/fixtures/facetable?_trace=1 . The JSON output shows full details of every SQL query that was executed to generate the page. \n             The  datasette-pretty-traces  plugin can be installed to provide a more readable display of this information. You can see  a demo of that here . \n             You can add your own custom traces to the JSON output using the  trace()  context manager. This takes a string that identifies the type of trace being recorded, and records any keyword arguments as additional JSON keys on the resulting trace object. \n             The start and end time, duration and a traceback of where the trace was executed will be automatically attached to the JSON object. \n             This example uses trace to record the start, end and duration of any HTTP GET requests made using the function: \n             from datasette.tracer import trace\nimport httpx\n\n\nasync def fetch_url(url):\n    with trace(\"fetch-url\", url=url):\n        async with httpx.AsyncClient() as client:\n            return await client.get(url)", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[{\"href\": \"https://latest.datasette.io/fixtures/facetable?_trace=1\", \"label\": \"latest.datasette.io/fixtures/facetable?_trace=1\"}, {\"href\": \"https://datasette.io/plugins/datasette-pretty-traces\", \"label\": \"datasette-pretty-traces\"}, {\"href\": \"https://latest-with-plugins.datasette.io/github/commits?_trace=1\", \"label\": \"a demo of that here\"}]"}, {"id": "internals:internals-uploadedfile", "page": "internals", "ref": "internals-uploadedfile", "title": "The UploadedFile class", "content": "When parsing multipart form data with  files=True , file uploads are returned as  UploadedFile  objects with the following properties and methods: \n             \n                 \n                     uploaded_file.name  - string \n                     \n                         The form field name. \n                     \n                 \n                 \n                     uploaded_file.filename  - string \n                     \n                         The original filename provided by the client. Note: This is sanitized to remove path components for security. \n                     \n                 \n                 \n                     uploaded_file.content_type  - string or None \n                     \n                         The MIME type of the uploaded file, if provided by the client. \n                     \n                 \n                 \n                     uploaded_file.size  - integer \n                     \n                         The size of the uploaded file in bytes. \n                     \n                 \n                 \n                     await uploaded_file.read(size=-1)  - bytes \n                     \n                         Read and return up to  size  bytes from the file. If  size  is -1 (default), read the entire file. \n                     \n                 \n                 \n                     await uploaded_file.seek(offset, whence=0)  - integer \n                     \n                         Seek to the given position in the file. Returns the new position. \n                     \n                 \n                 \n                     await uploaded_file.close() \n                     \n                         Close the underlying file. This is called automatically when the object is garbage collected. \n                     \n                 \n             \n             Files smaller than 1MB are stored in memory. Larger files are automatically spilled to temporary files on disk and cleaned up when the request completes. \n             Example: \n             form = await request.form(files=True)\nuploaded = form[\"document\"]\n\n# Check file metadata\nprint(f\"Filename: {uploaded.filename}\")\nprint(f\"Content-Type: {uploaded.content_type}\")\nprint(f\"Size: {uploaded.size} bytes\")\n\n# Read file content\ncontent = await uploaded.read()\n\n# Or read in chunks\nawait uploaded.seek(0)\nwhile chunk := await uploaded.read(8192):\n    process_chunk(chunk)", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[]"}, {"id": "internals:internals-utils", "page": "internals", "ref": "internals-utils", "title": "The datasette.utils module", "content": "The  datasette.utils  module contains various utility functions used by Datasette. As a general rule you should consider anything in this module to be unstable - functions and classes here could change without warning or be removed entirely between Datasette releases, without being mentioned in the release notes. \n             The exception to this rule is anything that is documented here. If you find a need for an undocumented utility function in your own work, consider  opening an issue  requesting that the function you are using be upgraded to documented and supported status.", "breadcrumbs": "[\"Internals for plugins\"]", "references": "[{\"href\": \"https://github.com/simonw/datasette/issues/new\", \"label\": \"opening an issue\"}]"}], "truncated": false}