Compare commits

...

20 commits

Author SHA1 Message Date
Simon Willison
6822378416 Prototype of rst_docs_for_dataclass mechanism, refs #1510 2023-06-27 19:00:33 -07:00
Simon Willison
007294008d
Merge branch 'main' into json-extras-query 2023-05-25 17:25:56 -07:00
Simon Willison
8ea00e038d New View base class (#2080)
* New View base class, closes #2078
* Use new View subclass for PatternPortfolioView
2023-05-25 17:23:53 -07:00
Simon Willison
94882aa72b --cors Access-Control-Max-Age: 3600, closes #2079 2023-05-25 17:23:53 -07:00
Simon Willison
0805771061 Rename callable.py to check_callable.py, refs #2078 2023-05-25 17:23:53 -07:00
Simon Willison
3f39fba7ea datasette.utils.check_callable(obj) - refs #2078 2023-05-25 17:23:53 -07:00
Simon Willison
59c52b5874 Action: Deploy a Datasette branch preview to Vercel
Closes #2070
2023-05-25 17:23:53 -07:00
Simon Willison
01353c7ee8 Build docs with 3.11 on ReadTheDocs
Inspired by https://github.com/simonw/sqlite-utils/issues/540
2023-05-25 17:23:53 -07:00
Simon Willison
305655c816 Add pip as a dependency too, for Rye - refs #2065 2023-05-25 17:23:53 -07:00
Simon Willison
3a7be0c5b1 Hopeful fix for Python 3.7 httpx failure, refs #2066 2023-05-25 17:23:53 -07:00
Simon Willison
bbbfdb034c Add setuptools to dependencies
Refs #2065
2023-05-25 17:23:52 -07:00
Simon Willison
6ae5312158 ?sql=... now displays HTML 2023-05-22 18:44:07 -07:00
Simon Willison
fdb141f622 shape_arrayfirst for query view 2023-05-08 17:52:22 -07:00
Simon Willison
6f903d5a98 Fixed a test 2023-05-08 17:51:19 -07:00
Simon Willison
3304fd43a2 refresh_schemas() on database view 2023-05-08 17:51:05 -07:00
Simon Willison
8b86fb7fb4 Better debugging 2023-05-08 17:50:12 -07:00
Simon Willison
a706f34b92 Remove debug lines 2023-04-26 22:07:05 -07:00
Simon Willison
026429fadd Work in progress on query view, refs #2049 2023-04-26 20:47:03 -07:00
Simon Willison
40dc5f5c50 WIP 2023-04-12 17:04:26 -07:00
Simon Willison
7b41521b33 WIP new JSON for queries, refs #2049 2023-04-05 16:25:29 -07:00
12 changed files with 1259 additions and 14 deletions

View file

@ -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
View 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

View file

@ -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

View file

@ -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,

View file

@ -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"),

View file

@ -57,6 +57,7 @@ Contents
settings
introspection
custom_templates
template_context
plugins
writing_plugins
plugin_hooks

28
docs/jsoncontext.py Normal file
View 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
View 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]]]

View file

@ -58,7 +58,7 @@ setup(
"mergedeep>=1.1.1",
"itsdangerous>=1.1",
"sqlite-utils>=3.30",
"asyncinject>=0.5",
"asyncinject>=0.6",
"setuptools",
"pip",
],

View file

@ -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"]

View file

@ -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"