mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Compare commits
20 commits
main
...
prototype-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6822378416 | ||
|
|
007294008d |
||
|
|
8ea00e038d | ||
|
|
94882aa72b | ||
|
|
0805771061 | ||
|
|
3f39fba7ea | ||
|
|
59c52b5874 | ||
|
|
01353c7ee8 | ||
|
|
305655c816 | ||
|
|
3a7be0c5b1 | ||
|
|
bbbfdb034c | ||
|
|
6ae5312158 | ||
|
|
fdb141f622 | ||
|
|
6f903d5a98 | ||
|
|
3304fd43a2 | ||
|
|
8b86fb7fb4 | ||
|
|
a706f34b92 | ||
|
|
026429fadd | ||
|
|
40dc5f5c50 | ||
|
|
7b41521b33 |
12 changed files with 1259 additions and 14 deletions
|
|
@ -34,7 +34,12 @@ from jinja2.environment import Template
|
|||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
from .views.base import ureg
|
||||
from .views.database import DatabaseDownload, DatabaseView, TableCreateView
|
||||
from .views.database import (
|
||||
DatabaseDownload,
|
||||
DatabaseView,
|
||||
TableCreateView,
|
||||
database_view,
|
||||
)
|
||||
from .views.index import IndexView
|
||||
from .views.special import (
|
||||
JsonDataView,
|
||||
|
|
@ -1366,8 +1371,12 @@ class Datasette:
|
|||
r"/-/patterns$",
|
||||
)
|
||||
add_route(DatabaseDownload.as_view(self), r"/(?P<database>[^\/\.]+)\.db$")
|
||||
# add_route(
|
||||
# DatabaseView.as_view(self), r"/(?P<database>[^\/\.]+)(\.(?P<format>\w+))?$"
|
||||
# )
|
||||
add_route(
|
||||
DatabaseView.as_view(self), r"/(?P<database>[^\/\.]+)(\.(?P<format>\w+))?$"
|
||||
wrap_view(database_view, self),
|
||||
r"/(?P<database>[^\/\.]+)(\.(?P<format>\w+))?$",
|
||||
)
|
||||
add_route(TableCreateView.as_view(self), r"/(?P<database>[^\/\.]+)/-/create$")
|
||||
add_route(
|
||||
|
|
|
|||
97
datasette/context.py
Normal file
97
datasette/context.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
from typing import List, Dict, Optional, Any
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
def doc(documentation):
|
||||
return field(metadata={"doc": documentation})
|
||||
|
||||
|
||||
def is_builtin_type(obj):
|
||||
return isinstance(
|
||||
obj,
|
||||
tuple(
|
||||
x.__class__
|
||||
for x in (int, float, str, bool, bytes, list, tuple, dict, set, frozenset)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def rst_docs_for_dataclass(klass: Any) -> str:
|
||||
"""Generate reStructuredText (reST) docs for a dataclass."""
|
||||
docs = []
|
||||
|
||||
# Class name and docstring
|
||||
docs.append(klass.__name__)
|
||||
docs.append("-" * len(klass.__name__))
|
||||
docs.append("")
|
||||
if klass.__doc__:
|
||||
docs.append(klass.__doc__)
|
||||
docs.append("")
|
||||
|
||||
# Dataclass fields
|
||||
docs.append("Fields")
|
||||
docs.append("~~~~~~")
|
||||
docs.append("")
|
||||
|
||||
for name, field_info in klass.__dataclass_fields__.items():
|
||||
if is_builtin_type(field_info.type):
|
||||
# <class 'int'>
|
||||
type_name = field_info.type.__name__
|
||||
else:
|
||||
# List[str]
|
||||
type_name = str(field_info.type).replace("typing.", "")
|
||||
docs.append(f':{name} - ``{type_name}``: {field_info.metadata.get("doc", "")}')
|
||||
|
||||
return "\n".join(docs)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForeignKey:
|
||||
incoming: List[Dict]
|
||||
outgoing: List[Dict]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Table:
|
||||
"A table is a useful thing"
|
||||
name: str = doc("The name of the table")
|
||||
columns: List[str] = doc("List of column names in the table")
|
||||
primary_keys: List[str] = doc("List of column names that are primary keys")
|
||||
count: int = doc("Number of rows in the table")
|
||||
hidden: bool = doc(
|
||||
"Should this table default to being hidden in the main database UI?"
|
||||
)
|
||||
fts_table: Optional[str] = doc(
|
||||
"If this table has FTS support, the accompanying FTS table name"
|
||||
)
|
||||
foreign_keys: ForeignKey = doc("List of foreign keys for this table")
|
||||
private: bool = doc("Private tables are not visible to signed-out anonymous users")
|
||||
|
||||
|
||||
@dataclass
|
||||
class View:
|
||||
name: str
|
||||
private: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class Query:
|
||||
title: str
|
||||
sql: str
|
||||
name: str
|
||||
private: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class Database:
|
||||
content: str
|
||||
private: bool
|
||||
path: str
|
||||
size: int
|
||||
tables: List[Table]
|
||||
hidden_count: int
|
||||
views: List[View]
|
||||
queries: List[Query]
|
||||
allow_execute_sql: bool
|
||||
table_columns: Dict[str, List[str]]
|
||||
query_ms: float
|
||||
|
|
@ -16,6 +16,9 @@ class TestResponse:
|
|||
def status(self):
|
||||
return self.httpx_response.status_code
|
||||
|
||||
def __repr__(self):
|
||||
return "<TestResponse {} [{}]>".format(self.httpx_response.url, self.status)
|
||||
|
||||
# Supports both for test-writing convenience
|
||||
@property
|
||||
def status_code(self):
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,6 @@ import markupsafe
|
|||
from datasette.plugins import pm
|
||||
from datasette.database import QueryInterrupted
|
||||
from datasette import tracer
|
||||
from datasette.renderer import json_renderer
|
||||
from datasette.utils import (
|
||||
add_cors_headers,
|
||||
await_me_maybe,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,12 @@
|
|||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ["sphinx.ext.extlinks", "sphinx.ext.autodoc", "sphinx_copybutton"]
|
||||
extensions = [
|
||||
"sphinx.ext.extlinks",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx_copybutton",
|
||||
"jsoncontext",
|
||||
]
|
||||
|
||||
extlinks = {
|
||||
"issue": ("https://github.com/simonw/datasette/issues/%s", "#%s"),
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ Contents
|
|||
settings
|
||||
introspection
|
||||
custom_templates
|
||||
template_context
|
||||
plugins
|
||||
writing_plugins
|
||||
plugin_hooks
|
||||
|
|
|
|||
28
docs/jsoncontext.py
Normal file
28
docs/jsoncontext.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from docutils import nodes
|
||||
from sphinx.util.docutils import SphinxDirective
|
||||
from importlib import import_module
|
||||
import json
|
||||
|
||||
|
||||
class JSONContextDirective(SphinxDirective):
|
||||
required_arguments = 1
|
||||
|
||||
def run(self):
|
||||
module_path, class_name = self.arguments[0].rsplit(".", 1)
|
||||
try:
|
||||
module = import_module(module_path)
|
||||
dataclass = getattr(module, class_name)
|
||||
except ImportError:
|
||||
warning = f"Unable to import {self.arguments[0]}"
|
||||
return [nodes.error(None, nodes.paragraph(text=warning))]
|
||||
|
||||
doc = json.dumps(
|
||||
dataclass.__annotations__, indent=4, sort_keys=True, default=repr
|
||||
)
|
||||
doc_node = nodes.literal_block(text=doc)
|
||||
|
||||
return [doc_node]
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_directive("jsoncontext", JSONContextDirective)
|
||||
29
docs/template_context.rst
Normal file
29
docs/template_context.rst
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
.. _template_context:
|
||||
|
||||
Template context
|
||||
================
|
||||
|
||||
This page describes the variables made available to templates used by Datasette to render different pages of the application.
|
||||
|
||||
|
||||
.. [[[cog
|
||||
from datasette.context import rst_docs_for_dataclass, Table
|
||||
cog.out(rst_docs_for_dataclass(Table))
|
||||
.. ]]]
|
||||
Table
|
||||
-----
|
||||
|
||||
A table is a useful thing
|
||||
|
||||
Fields
|
||||
~~~~~~
|
||||
|
||||
:name - ``str``: The name of the table
|
||||
:columns - ``List[str]``: List of column names in the table
|
||||
:primary_keys - ``List[str]``: List of column names that are primary keys
|
||||
:count - ``int``: Number of rows in the table
|
||||
:hidden - ``bool``: Should this table default to being hidden in the main database UI?
|
||||
:fts_table - ``Optional[str]``: If this table has FTS support, the accompanying FTS table name
|
||||
:foreign_keys - ``ForeignKey``: List of foreign keys for this table
|
||||
:private - ``bool``: Private tables are not visible to signed-out anonymous users
|
||||
.. [[[end]]]
|
||||
2
setup.py
2
setup.py
|
|
@ -58,7 +58,7 @@ setup(
|
|||
"mergedeep>=1.1.1",
|
||||
"itsdangerous>=1.1",
|
||||
"sqlite-utils>=3.30",
|
||||
"asyncinject>=0.5",
|
||||
"asyncinject>=0.6",
|
||||
"setuptools",
|
||||
"pip",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -643,9 +643,6 @@ async def test_custom_sql(ds_client):
|
|||
"/fixtures.json?sql=select+content+from+simple_primary_key&_shape=objects"
|
||||
)
|
||||
data = response.json()
|
||||
assert {"sql": "select content from simple_primary_key", "params": {}} == data[
|
||||
"query"
|
||||
]
|
||||
assert [
|
||||
{"content": "hello"},
|
||||
{"content": "world"},
|
||||
|
|
@ -653,8 +650,6 @@ async def test_custom_sql(ds_client):
|
|||
{"content": "RENDER_CELL_DEMO"},
|
||||
{"content": "RENDER_CELL_ASYNC"},
|
||||
] == data["rows"]
|
||||
assert ["content"] == data["columns"]
|
||||
assert "fixtures" == data["database"]
|
||||
assert not data["truncated"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from datasette.cli import cli, serve
|
||||
from datasette.plugins import pm
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import ANY
|
||||
import textwrap
|
||||
import json
|
||||
|
||||
|
|
@ -35,11 +36,11 @@ def test_serve_with_get(tmp_path_factory):
|
|||
],
|
||||
)
|
||||
assert 0 == result.exit_code, result.output
|
||||
assert {
|
||||
"database": "_memory",
|
||||
assert json.loads(result.output) == {
|
||||
"ok": True,
|
||||
"rows": [{"sqlite_version()": ANY}],
|
||||
"truncated": False,
|
||||
"columns": ["sqlite_version()"],
|
||||
}.items() <= json.loads(result.output).items()
|
||||
}
|
||||
|
||||
# The plugin should have created hello.txt
|
||||
assert (plugins_dir / "hello.txt").read_text() == "hello"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue