pull/1823/merge
Simon Willison 2022-12-13 17:49:59 -08:00 zatwierdzone przez GitHub
commit 28068a62ad
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
5 zmienionych plików z 42 dodań i 30 usunięć

Wyświetl plik

@ -7,7 +7,7 @@
[![Documentation Status](https://readthedocs.org/projects/datasette/badge/?version=latest)](https://docs.datasette.io/en/latest/?badge=latest)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette/blob/main/LICENSE)
[![docker: datasette](https://img.shields.io/badge/docker-datasette-blue)](https://hub.docker.com/r/datasetteproject/datasette)
[![discord](https://img.shields.io/discord/823971286308356157?label=discord)](https://discord.gg/ktd74dm5mw)
[![discord](https://img.shields.io/discord/823971286308356157?label=discord)](https://datasette.io/discord)
*An open source multi-tool for exploring and publishing data*
@ -22,7 +22,7 @@ Datasette is aimed at data journalists, museum curators, archivists, local gover
* Comprehensive documentation: https://docs.datasette.io/
* Examples: https://datasette.io/examples
* Live demo of current `main` branch: https://latest.datasette.io/
* Questions, feedback or want to talk about the project? Join our [Discord](https://discord.gg/ktd74dm5mw)
* Questions, feedback or want to talk about the project? Join our [Discord](https://datasette.io/discord)
Want to stay up-to-date with the project? Subscribe to the [Datasette newsletter](https://datasette.substack.com/) for tips, tricks and news on what's new in the Datasette ecosystem.

Wyświetl plik

@ -221,6 +221,7 @@ class Datasette:
def __init__(
self,
files=None,
*,
immutables=None,
cache_headers=True,
cors=False,
@ -465,7 +466,7 @@ class Datasette:
def unsign(self, signed, namespace="default"):
return URLSafeSerializer(self._secret, namespace).loads(signed)
def get_database(self, name=None, route=None):
def get_database(self, name=None, *, route=None):
if route is not None:
matches = [db for db in self.databases.values() if db.route == route]
if not matches:
@ -476,7 +477,7 @@ class Datasette:
name = [key for key in self.databases.keys() if key != "_internal"][0]
return self.databases[name]
def add_database(self, db, name=None, route=None):
def add_database(self, db, name=None, *, route=None):
new_databases = self.databases.copy()
if name is None:
# Pick a unique name for this database
@ -521,7 +522,7 @@ class Datasette:
orig[key] = upd_value
return orig
def metadata(self, key=None, database=None, table=None, fallback=True):
def metadata(self, key=None, *, database=None, table=None, fallback=True):
"""
Looks up metadata, cascading backwards from specified level.
Returns None if metadata value is not found.
@ -573,7 +574,7 @@ class Datasette:
def _metadata(self):
return self.metadata()
def plugin_config(self, plugin_name, database=None, table=None, fallback=True):
def plugin_config(self, plugin_name, *, database=None, table=None, fallback=True):
"""Return config for plugin, falling back from specified database/table"""
plugins = self.metadata(
"plugins", database=database, table=table, fallback=fallback
@ -822,6 +823,7 @@ class Datasette:
db_name,
sql,
params=None,
*,
truncate=False,
custom_time_limit=None,
page_size=None,
@ -1051,7 +1053,7 @@ class Datasette:
)
async def render_template(
self, templates, context=None, request=None, view_name=None
self, templates, context=None, *, request=None, view_name=None
):
if not self._startup_invoked:
raise Exception("render_template() called before await ds.invoke_startup()")

Wyświetl plik

@ -139,7 +139,9 @@ class Request:
return dict(parse_qsl(body.decode("utf-8"), keep_blank_values=True))
@classmethod
def fake(cls, path_with_query_string, method="GET", scheme="http", url_vars=None):
def fake(
cls, path_with_query_string, *, method="GET", scheme="http", url_vars=None
):
"""Useful for constructing Request objects for tests"""
path, _, query_string = path_with_query_string.partition("?")
scope = {
@ -225,7 +227,7 @@ class AsgiWriter:
)
async def asgi_send_json(send, info, status=200, headers=None):
async def asgi_send_json(send, info, *, status=200, headers=None):
headers = headers or {}
await asgi_send(
send,
@ -236,7 +238,7 @@ async def asgi_send_json(send, info, status=200, headers=None):
)
async def asgi_send_html(send, html, status=200, headers=None):
async def asgi_send_html(send, html, *, status=200, headers=None):
headers = headers or {}
await asgi_send(
send,
@ -247,7 +249,7 @@ async def asgi_send_html(send, html, status=200, headers=None):
)
async def asgi_send_redirect(send, location, status=302):
async def asgi_send_redirect(send, location, *, status=302):
await asgi_send(
send,
"",
@ -257,12 +259,12 @@ async def asgi_send_redirect(send, location, status=302):
)
async def asgi_send(send, content, status, headers=None, content_type="text/plain"):
await asgi_start(send, status, headers, content_type)
async def asgi_send(send, content, status, *, headers=None, content_type="text/plain"):
await asgi_start(send, status=status, headers=headers, content_type=content_type)
await send({"type": "http.response.body", "body": content.encode("utf-8")})
async def asgi_start(send, status, headers=None, content_type="text/plain"):
async def asgi_start(send, status, *, headers=None, content_type="text/plain"):
headers = headers or {}
# Remove any existing content-type header
headers = {k: v for k, v in headers.items() if k.lower() != "content-type"}
@ -280,7 +282,7 @@ async def asgi_start(send, status, headers=None, content_type="text/plain"):
async def asgi_send_file(
send, filepath, filename=None, content_type=None, chunk_size=4096, headers=None
send, filepath, filename=None, *, content_type=None, chunk_size=4096, headers=None
):
headers = headers or {}
if filename:
@ -291,9 +293,11 @@ async def asgi_send_file(
if first:
await asgi_start(
send,
200,
headers,
content_type or guess_type(str(filepath))[0] or "text/plain",
status=200,
headers=headers,
content_type=content_type
or guess_type(str(filepath))[0]
or "text/plain",
)
first = False
more_body = True
@ -305,7 +309,7 @@ async def asgi_send_file(
)
def asgi_static(root_path, chunk_size=4096, headers=None, content_type=None):
def asgi_static(root_path, *, chunk_size=4096, headers=None, content_type=None):
root_path = Path(root_path)
async def inner_static(request, send):
@ -313,28 +317,32 @@ def asgi_static(root_path, chunk_size=4096, headers=None, content_type=None):
try:
full_path = (root_path / path).resolve().absolute()
except FileNotFoundError:
await asgi_send_html(send, "404: Directory not found", 404)
await asgi_send_html(send, "404: Directory not found", status=404)
return
if full_path.is_dir():
await asgi_send_html(send, "403: Directory listing is not allowed", 403)
await asgi_send_html(
send, "403: Directory listing is not allowed", status=403
)
return
# Ensure full_path is within root_path to avoid weird "../" tricks
try:
full_path.relative_to(root_path.resolve())
except ValueError:
await asgi_send_html(send, "404: Path not inside root path", 404)
await asgi_send_html(send, "404: Path not inside root path", status=404)
return
try:
await asgi_send_file(send, full_path, chunk_size=chunk_size)
except FileNotFoundError:
await asgi_send_html(send, "404: File not found", 404)
await asgi_send_html(send, "404: File not found", status=404)
return
return inner_static
class Response:
def __init__(self, body=None, status=200, headers=None, content_type="text/plain"):
def __init__(
self, body=None, *, status=200, headers=None, content_type="text/plain"
):
self.body = body
self.status = status
self.headers = headers or {}
@ -367,6 +375,7 @@ class Response:
self,
key,
value="",
*,
max_age=None,
expires=None,
path="/",
@ -395,7 +404,7 @@ class Response:
self._set_cookie_headers.append(cookie.output(header="").strip())
@classmethod
def html(cls, body, status=200, headers=None):
def html(cls, body, *, status=200, headers=None):
return cls(
body,
status=status,
@ -404,7 +413,7 @@ class Response:
)
@classmethod
def text(cls, body, status=200, headers=None):
def text(cls, body, *, status=200, headers=None):
return cls(
str(body),
status=status,
@ -413,7 +422,7 @@ class Response:
)
@classmethod
def json(cls, body, status=200, headers=None, default=None):
def json(cls, body, *, status=200, headers=None, default=None):
return cls(
json.dumps(body, default=default),
status=status,
@ -422,7 +431,7 @@ class Response:
)
@classmethod
def redirect(cls, path, status=302, headers=None):
def redirect(cls, path, *, status=302, headers=None):
headers = headers or {}
headers["Location"] = path
return cls("", status=status, headers=headers)
@ -433,6 +442,7 @@ class AsgiFileDownload:
self,
filepath,
filename=None,
*,
content_type="application/octet-stream",
headers=None,
):

Wyświetl plik

@ -109,7 +109,7 @@ Documentation
Datasette can now run entirely in your browser using WebAssembly. Try out `Datasette Lite <https://lite.datasette.io/>`__, take a look `at the code <https://github.com/simonw/datasette-lite>`__ or read more about it in `Datasette Lite: a server-side Python web application running in a browser <https://simonwillison.net/2022/May/4/datasette-lite/>`__.
Datasette now has a `Discord community <https://discord.gg/ktd74dm5mw>`__ for questions and discussions about Datasette and its ecosystem of projects.
Datasette now has a `Discord community <https://datasette.io/discord>`__ for questions and discussions about Datasette and its ecosystem of projects.
Features
~~~~~~~~

Wyświetl plik

@ -17,7 +17,7 @@ datasette| |discord|
.. |docker: datasette| image:: https://img.shields.io/badge/docker-datasette-blue
:target: https://hub.docker.com/r/datasetteproject/datasette
.. |discord| image:: https://img.shields.io/discord/823971286308356157?label=discord
:target: https://discord.gg/ktd74dm5mw
:target: https://datasette.io/discord
*An open source multi-tool for exploring and publishing data*