mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Removed units functionality and Pint dependency
Closes #2400, unblocks #2320
This commit is contained in:
parent
d444b6aad5
commit
39dfc7d7d7
14 changed files with 14 additions and 182 deletions
|
|
@ -37,7 +37,6 @@ from jinja2.exceptions import TemplateNotFound
|
||||||
|
|
||||||
from .events import Event
|
from .events import Event
|
||||||
from .views import Context
|
from .views import Context
|
||||||
from .views.base import ureg
|
|
||||||
from .views.database import database_download, DatabaseView, TableCreateView, QueryView
|
from .views.database import database_download, DatabaseView, TableCreateView, QueryView
|
||||||
from .views.index import IndexView
|
from .views.index import IndexView
|
||||||
from .views.special import (
|
from .views.special import (
|
||||||
|
|
|
||||||
|
|
@ -368,12 +368,8 @@ class Filters:
|
||||||
)
|
)
|
||||||
_filters_by_key = {f.key: f for f in _filters}
|
_filters_by_key = {f.key: f for f in _filters}
|
||||||
|
|
||||||
def __init__(self, pairs, units=None, ureg=None):
|
def __init__(self, pairs):
|
||||||
if units is None:
|
|
||||||
units = {}
|
|
||||||
self.pairs = pairs
|
self.pairs = pairs
|
||||||
self.units = units
|
|
||||||
self.ureg = ureg
|
|
||||||
|
|
||||||
def lookups(self):
|
def lookups(self):
|
||||||
"""Yields (lookup, display, no_argument) pairs"""
|
"""Yields (lookup, display, no_argument) pairs"""
|
||||||
|
|
@ -413,20 +409,6 @@ class Filters:
|
||||||
def has_selections(self):
|
def has_selections(self):
|
||||||
return bool(self.pairs)
|
return bool(self.pairs)
|
||||||
|
|
||||||
def convert_unit(self, column, value):
|
|
||||||
"""If the user has provided a unit in the query, convert it into the column unit, if present."""
|
|
||||||
if column not in self.units:
|
|
||||||
return value
|
|
||||||
|
|
||||||
# Try to interpret the value as a unit
|
|
||||||
value = self.ureg(value)
|
|
||||||
if isinstance(value, numbers.Number):
|
|
||||||
# It's just a bare number, assume it's the column unit
|
|
||||||
return value
|
|
||||||
|
|
||||||
column_unit = self.ureg(self.units[column])
|
|
||||||
return value.to(column_unit).magnitude
|
|
||||||
|
|
||||||
def build_where_clauses(self, table):
|
def build_where_clauses(self, table):
|
||||||
sql_bits = []
|
sql_bits = []
|
||||||
params = {}
|
params = {}
|
||||||
|
|
@ -434,9 +416,7 @@ class Filters:
|
||||||
for column, lookup, value in self.selections():
|
for column, lookup, value in self.selections():
|
||||||
filter = self._filters_by_key.get(lookup, None)
|
filter = self._filters_by_key.get(lookup, None)
|
||||||
if filter:
|
if filter:
|
||||||
sql_bit, param = filter.where_clause(
|
sql_bit, param = filter.where_clause(table, column, value, i)
|
||||||
table, column, self.convert_unit(column, value), i
|
|
||||||
)
|
|
||||||
sql_bits.append(sql_bit)
|
sql_bits.append(sql_bit)
|
||||||
if param is not None:
|
if param is not None:
|
||||||
if not isinstance(param, list):
|
if not isinstance(param, list):
|
||||||
|
|
|
||||||
|
|
@ -1368,7 +1368,6 @@ _table_config_keys = (
|
||||||
"fts_table",
|
"fts_table",
|
||||||
"fts_pk",
|
"fts_pk",
|
||||||
"searchmode",
|
"searchmode",
|
||||||
"units",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import urllib
|
||||||
from markupsafe import escape
|
from markupsafe import escape
|
||||||
|
|
||||||
|
|
||||||
import pint
|
|
||||||
|
|
||||||
from datasette.database import QueryInterrupted
|
from datasette.database import QueryInterrupted
|
||||||
from datasette.utils.asgi import Request
|
from datasette.utils.asgi import Request
|
||||||
from datasette.utils import (
|
from datasette.utils import (
|
||||||
|
|
@ -32,8 +30,6 @@ from datasette.utils.asgi import (
|
||||||
BadRequest,
|
BadRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
ureg = pint.UnitRegistry()
|
|
||||||
|
|
||||||
|
|
||||||
class DatasetteError(Exception):
|
class DatasetteError(Exception):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,6 @@ class RowView(DataView):
|
||||||
"columns": columns,
|
"columns": columns,
|
||||||
"primary_keys": resolved.pks,
|
"primary_keys": resolved.pks,
|
||||||
"primary_key_values": pk_values,
|
"primary_key_values": pk_values,
|
||||||
"units": (await self.ds.table_config(database, table)).get("units", {}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if "foreign_key_tables" in (request.args.get("_extras") or "").split(","):
|
if "foreign_key_tables" in (request.args.get("_extras") or "").split(","):
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ from datasette.utils import (
|
||||||
from datasette.utils.asgi import BadRequest, Forbidden, NotFound, Response
|
from datasette.utils.asgi import BadRequest, Forbidden, NotFound, Response
|
||||||
from datasette.filters import Filters
|
from datasette.filters import Filters
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
from .base import BaseView, DatasetteError, ureg, _error, stream_csv
|
from .base import BaseView, DatasetteError, _error, stream_csv
|
||||||
from .database import QueryView
|
from .database import QueryView
|
||||||
|
|
||||||
LINK_WITH_LABEL = (
|
LINK_WITH_LABEL = (
|
||||||
|
|
@ -292,14 +292,6 @@ async def display_columns_and_rows(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif column in table_config.get("units", {}) and value != "":
|
|
||||||
# Interpret units using pint
|
|
||||||
value = value * ureg(table_config["units"][column])
|
|
||||||
# Pint uses floating point which sometimes introduces errors in the compact
|
|
||||||
# representation, which we have to round off to avoid ugliness. In the vast
|
|
||||||
# majority of cases this rounding will be inconsequential. I hope.
|
|
||||||
value = round(value.to_compact(), 6)
|
|
||||||
display_value = markupsafe.Markup(f"{value:~P}".replace(" ", " "))
|
|
||||||
else:
|
else:
|
||||||
display_value = str(value)
|
display_value = str(value)
|
||||||
if truncate_cells and len(display_value) > truncate_cells:
|
if truncate_cells and len(display_value) > truncate_cells:
|
||||||
|
|
@ -1017,7 +1009,6 @@ async def table_view_data(
|
||||||
nofacet = True
|
nofacet = True
|
||||||
|
|
||||||
table_metadata = await datasette.table_config(database_name, table_name)
|
table_metadata = await datasette.table_config(database_name, table_name)
|
||||||
units = table_metadata.get("units", {})
|
|
||||||
|
|
||||||
# Arguments that start with _ and don't contain a __ are
|
# Arguments that start with _ and don't contain a __ are
|
||||||
# special - things like ?_search= - and should not be
|
# special - things like ?_search= - and should not be
|
||||||
|
|
@ -1029,7 +1020,7 @@ async def table_view_data(
|
||||||
filter_args.append((key, v))
|
filter_args.append((key, v))
|
||||||
|
|
||||||
# Build where clauses from query string arguments
|
# Build where clauses from query string arguments
|
||||||
filters = Filters(sorted(filter_args), units, ureg)
|
filters = Filters(sorted(filter_args))
|
||||||
where_clauses, params = filters.build_where_clauses(table_name)
|
where_clauses, params = filters.build_where_clauses(table_name)
|
||||||
|
|
||||||
# Execute filters_from_request plugin hooks - including the default
|
# Execute filters_from_request plugin hooks - including the default
|
||||||
|
|
|
||||||
|
|
@ -205,100 +205,6 @@ These will be displayed at the top of the table page, and will also show in the
|
||||||
|
|
||||||
You can see an example of how these look at `latest.datasette.io/fixtures/roadside_attractions <https://latest.datasette.io/fixtures/roadside_attractions>`__.
|
You can see an example of how these look at `latest.datasette.io/fixtures/roadside_attractions <https://latest.datasette.io/fixtures/roadside_attractions>`__.
|
||||||
|
|
||||||
Specifying units for a column
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
Datasette supports attaching units to a column, which will be used when displaying
|
|
||||||
values from that column. SI prefixes will be used where appropriate.
|
|
||||||
|
|
||||||
Column units are configured in the metadata like so:
|
|
||||||
|
|
||||||
.. [[[cog
|
|
||||||
metadata_example(cog, {
|
|
||||||
"databases": {
|
|
||||||
"database1": {
|
|
||||||
"tables": {
|
|
||||||
"example_table": {
|
|
||||||
"units": {
|
|
||||||
"column1": "metres",
|
|
||||||
"column2": "Hz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.. ]]]
|
|
||||||
|
|
||||||
.. tab:: metadata.yaml
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
databases:
|
|
||||||
database1:
|
|
||||||
tables:
|
|
||||||
example_table:
|
|
||||||
units:
|
|
||||||
column1: metres
|
|
||||||
column2: Hz
|
|
||||||
|
|
||||||
|
|
||||||
.. tab:: metadata.json
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"databases": {
|
|
||||||
"database1": {
|
|
||||||
"tables": {
|
|
||||||
"example_table": {
|
|
||||||
"units": {
|
|
||||||
"column1": "metres",
|
|
||||||
"column2": "Hz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.. [[[end]]]
|
|
||||||
|
|
||||||
|
|
||||||
Units are interpreted using Pint_, and you can see the full list of available units in
|
|
||||||
Pint's `unit registry`_. You can also add `custom units`_ to the metadata, which will be
|
|
||||||
registered with Pint:
|
|
||||||
|
|
||||||
.. [[[cog
|
|
||||||
metadata_example(cog, {
|
|
||||||
"custom_units": [
|
|
||||||
"decibel = [] = dB"
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.. ]]]
|
|
||||||
|
|
||||||
.. tab:: metadata.yaml
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
custom_units:
|
|
||||||
- decibel = [] = dB
|
|
||||||
|
|
||||||
|
|
||||||
.. tab:: metadata.json
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"custom_units": [
|
|
||||||
"decibel = [] = dB"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
.. [[[end]]]
|
|
||||||
|
|
||||||
.. _Pint: https://pint.readthedocs.io/
|
|
||||||
.. _unit registry: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt
|
|
||||||
.. _custom units: http://pint.readthedocs.io/en/latest/defining.html
|
|
||||||
|
|
||||||
.. _metadata_default_sort:
|
.. _metadata_default_sort:
|
||||||
|
|
||||||
Setting a default sort order
|
Setting a default sort order
|
||||||
|
|
|
||||||
1
setup.py
1
setup.py
|
|
@ -50,7 +50,6 @@ setup(
|
||||||
"httpx>=0.20",
|
"httpx>=0.20",
|
||||||
'importlib_resources>=1.3.1; python_version < "3.9"',
|
'importlib_resources>=1.3.1; python_version < "3.9"',
|
||||||
'importlib_metadata>=4.6; python_version < "3.10"',
|
'importlib_metadata>=4.6; python_version < "3.10"',
|
||||||
"pint>=0.9",
|
|
||||||
"pluggy>=1.0",
|
"pluggy>=1.0",
|
||||||
"uvicorn>=0.11",
|
"uvicorn>=0.11",
|
||||||
"aiofiles>=0.4",
|
"aiofiles>=0.4",
|
||||||
|
|
|
||||||
|
|
@ -379,7 +379,6 @@ METADATA = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"no_primary_key": {"sortable_columns": [], "hidden": True},
|
"no_primary_key": {"sortable_columns": [], "hidden": True},
|
||||||
"units": {"units": {"distance": "m", "frequency": "Hz"}},
|
|
||||||
"primary_key_multiple_columns_explicit_label": {
|
"primary_key_multiple_columns_explicit_label": {
|
||||||
"label_column": "content2"
|
"label_column": "content2"
|
||||||
},
|
},
|
||||||
|
|
@ -507,16 +506,6 @@ CREATE TABLE "custom_foreign_key_label" (
|
||||||
FOREIGN KEY ("foreign_key_with_custom_label") REFERENCES [primary_key_multiple_columns_explicit_label](id)
|
FOREIGN KEY ("foreign_key_with_custom_label") REFERENCES [primary_key_multiple_columns_explicit_label](id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE units (
|
|
||||||
pk integer primary key,
|
|
||||||
distance int,
|
|
||||||
frequency int
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO units VALUES (1, 1, 100);
|
|
||||||
INSERT INTO units VALUES (2, 5000, 2500);
|
|
||||||
INSERT INTO units VALUES (3, 100000, 75000);
|
|
||||||
|
|
||||||
CREATE TABLE tags (
|
CREATE TABLE tags (
|
||||||
tag TEXT PRIMARY KEY
|
tag TEXT PRIMARY KEY
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,21 @@ from datasette import tracer
|
||||||
from datasette.utils import path_with_added_args
|
from datasette.utils import path_with_added_args
|
||||||
from datasette.utils.asgi import asgi_send_json, Response
|
from datasette.utils.asgi import asgi_send_json, Response
|
||||||
import base64
|
import base64
|
||||||
import pint
|
|
||||||
import json
|
import json
|
||||||
import urllib
|
import urllib.parse
|
||||||
|
|
||||||
ureg = pint.UnitRegistry()
|
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def prepare_connection(conn, database, datasette):
|
def prepare_connection(conn, database, datasette):
|
||||||
def convert_units(amount, from_, to_):
|
def convert_units(amount, from_, to_):
|
||||||
"""select convert_units(100, 'm', 'ft');"""
|
"""select convert_units(100, 'm', 'ft');"""
|
||||||
return (amount * ureg(from_)).to(to_).to_tuple()[0]
|
# Convert meters to feet
|
||||||
|
if from_ == "m" and to_ == "ft":
|
||||||
|
return amount * 3.28084
|
||||||
|
# Convert feet to meters
|
||||||
|
if from_ == "ft" and to_ == "m":
|
||||||
|
return amount / 3.28084
|
||||||
|
assert False, "Unsupported conversion"
|
||||||
|
|
||||||
conn.create_function("convert_units", 3, convert_units)
|
conn.create_function("convert_units", 3, convert_units)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -528,16 +528,6 @@ async def test_database_page(ds_client):
|
||||||
},
|
},
|
||||||
"private": False,
|
"private": False,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "units",
|
|
||||||
"columns": ["pk", "distance", "frequency"],
|
|
||||||
"primary_keys": ["pk"],
|
|
||||||
"count": 3,
|
|
||||||
"hidden": False,
|
|
||||||
"fts_table": None,
|
|
||||||
"foreign_keys": {"incoming": [], "outgoing": []},
|
|
||||||
"private": False,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "no_primary_key",
|
"name": "no_primary_key",
|
||||||
"columns": ["content", "a", "b", "c"],
|
"columns": ["content", "a", "b", "c"],
|
||||||
|
|
@ -1133,7 +1123,6 @@ async def test_config_json(config, expected):
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"no_primary_key": {"sortable_columns": [], "hidden": True},
|
"no_primary_key": {"sortable_columns": [], "hidden": True},
|
||||||
"units": {"units": {"distance": "m", "frequency": "Hz"}},
|
|
||||||
"primary_key_multiple_columns_explicit_label": {
|
"primary_key_multiple_columns_explicit_label": {
|
||||||
"label_column": "content2"
|
"label_column": "content2"
|
||||||
},
|
},
|
||||||
|
|
@ -1168,7 +1157,6 @@ async def test_config_json(config, expected):
|
||||||
"text",
|
"text",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"units": {"units": {"distance": "m", "frequency": "Hz"}},
|
|
||||||
# These one get redacted:
|
# These one get redacted:
|
||||||
"no_primary_key": "***",
|
"no_primary_key": "***",
|
||||||
"primary_key_multiple_columns_explicit_label": "***",
|
"primary_key_multiple_columns_explicit_label": "***",
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,6 @@ async def test_table_names(db):
|
||||||
"table/with/slashes.csv",
|
"table/with/slashes.csv",
|
||||||
"complex_foreign_keys",
|
"complex_foreign_keys",
|
||||||
"custom_foreign_key_label",
|
"custom_foreign_key_label",
|
||||||
"units",
|
|
||||||
"tags",
|
"tags",
|
||||||
"searchable",
|
"searchable",
|
||||||
"searchable_tags",
|
"searchable_tags",
|
||||||
|
|
|
||||||
|
|
@ -424,8 +424,8 @@ def view_names_client(tmp_path_factory):
|
||||||
(
|
(
|
||||||
("/", "index"),
|
("/", "index"),
|
||||||
("/fixtures", "database"),
|
("/fixtures", "database"),
|
||||||
("/fixtures/units", "table"),
|
("/fixtures/facetable", "table"),
|
||||||
("/fixtures/units/1", "row"),
|
("/fixtures/facetable/1", "row"),
|
||||||
("/-/versions", "json_data"),
|
("/-/versions", "json_data"),
|
||||||
("/fixtures/-/query?sql=select+1", "database"),
|
("/fixtures/-/query?sql=select+1", "database"),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -720,22 +720,6 @@ async def test_view(ds_client):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_unit_filters(ds_client):
|
|
||||||
response = await ds_client.get(
|
|
||||||
"/fixtures/units.json?_shape=arrays&distance__lt=75km&frequency__gt=1kHz"
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
assert data["units"]["distance"] == "m"
|
|
||||||
assert data["units"]["frequency"] == "Hz"
|
|
||||||
|
|
||||||
assert len(data["rows"]) == 1
|
|
||||||
assert data["rows"][0][0] == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_page_size_matching_max_returned_rows(
|
def test_page_size_matching_max_returned_rows(
|
||||||
app_client_returned_rows_matches_page_size,
|
app_client_returned_rows_matches_page_size,
|
||||||
):
|
):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue