kopia lustrzana https://github.com/simonw/datasette
Merge b545b6a04e
into d4cc1374f4
commit
28068a62ad
|
@ -7,7 +7,7 @@
|
|||
[](https://docs.datasette.io/en/latest/?badge=latest)
|
||||
[](https://github.com/simonw/datasette/blob/main/LICENSE)
|
||||
[](https://hub.docker.com/r/datasetteproject/datasette)
|
||||
[](https://discord.gg/ktd74dm5mw)
|
||||
[](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.
|
||||
|
||||
|
|
|
@ -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()")
|
||||
|
|
|
@ -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,
|
||||
):
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~
|
||||
|
|
|
@ -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*
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue