mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Compare commits
4 commits
main
...
encode-dec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07912ba46d | ||
|
|
77789d0ff4 | ||
|
|
7e756ad097 | ||
|
|
0f3b35d4e8 |
14 changed files with 75 additions and 34 deletions
|
|
@ -27,6 +27,7 @@ from .views.table import RowView, TableView
|
||||||
from .utils import (
|
from .utils import (
|
||||||
InterruptedError,
|
InterruptedError,
|
||||||
Results,
|
Results,
|
||||||
|
encode_table_name,
|
||||||
escape_css_string,
|
escape_css_string,
|
||||||
escape_sqlite,
|
escape_sqlite,
|
||||||
get_plugins,
|
get_plugins,
|
||||||
|
|
@ -465,6 +466,7 @@ class Datasette:
|
||||||
self.jinja_env = Environment(loader=template_loader, autoescape=True)
|
self.jinja_env = Environment(loader=template_loader, autoescape=True)
|
||||||
self.jinja_env.filters["escape_css_string"] = escape_css_string
|
self.jinja_env.filters["escape_css_string"] = escape_css_string
|
||||||
self.jinja_env.filters["quote_plus"] = lambda u: urllib.parse.quote_plus(u)
|
self.jinja_env.filters["quote_plus"] = lambda u: urllib.parse.quote_plus(u)
|
||||||
|
self.jinja_env.filters["encode_table_name"] = encode_table_name
|
||||||
self.jinja_env.filters["escape_sqlite"] = escape_sqlite
|
self.jinja_env.filters["escape_sqlite"] = escape_sqlite
|
||||||
self.jinja_env.filters["to_css_class"] = to_css_class
|
self.jinja_env.filters["to_css_class"] = to_css_class
|
||||||
pm.hook.prepare_jinja2_environment(env=self.jinja_env)
|
pm.hook.prepare_jinja2_environment(env=self.jinja_env)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ def publish_subcommand(publish):
|
||||||
help="Application name to use when deploying",
|
help="Application name to use when deploying",
|
||||||
)
|
)
|
||||||
@click.option("--force", is_flag=True, help="Pass --force option to now")
|
@click.option("--force", is_flag=True, help="Pass --force option to now")
|
||||||
@click.option("--token", help="Auth token to use for deploy (Now only)")
|
@click.option("--token", help="Auth token to use for deploy")
|
||||||
|
@click.option("--alias", help="Desired alias e.g. yoursite.now.sh")
|
||||||
@click.option("--spatialite", is_flag=True, help="Enable SpatialLite extension")
|
@click.option("--spatialite", is_flag=True, help="Enable SpatialLite extension")
|
||||||
def now(
|
def now(
|
||||||
files,
|
files,
|
||||||
|
|
@ -41,6 +42,7 @@ def publish_subcommand(publish):
|
||||||
name,
|
name,
|
||||||
force,
|
force,
|
||||||
token,
|
token,
|
||||||
|
alias,
|
||||||
spatialite,
|
spatialite,
|
||||||
):
|
):
|
||||||
fail_if_publish_binary_not_installed("now", "Zeit Now", "https://zeit.co/now")
|
fail_if_publish_binary_not_installed("now", "Zeit Now", "https://zeit.co/now")
|
||||||
|
|
@ -70,11 +72,12 @@ def publish_subcommand(publish):
|
||||||
"source_url": source_url,
|
"source_url": source_url,
|
||||||
},
|
},
|
||||||
):
|
):
|
||||||
open("now.json", "w").write(json.dumps({
|
now_json = {
|
||||||
"features": {
|
"version": 1
|
||||||
"cloud": "v1"
|
|
||||||
}
|
}
|
||||||
}))
|
if alias:
|
||||||
|
now_json["alias"] = alias
|
||||||
|
open("now.json", "w").write(json.dumps(now_json))
|
||||||
args = []
|
args = []
|
||||||
if force:
|
if force:
|
||||||
args.append("--force")
|
args.append("--force")
|
||||||
|
|
@ -84,3 +87,5 @@ def publish_subcommand(publish):
|
||||||
call(["now"] + args)
|
call(["now"] + args)
|
||||||
else:
|
else:
|
||||||
call("now")
|
call("now")
|
||||||
|
if alias:
|
||||||
|
call(["now", "alias"])
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
{% for table in tables %}
|
{% for table in tables %}
|
||||||
{% if show_hidden or not table.hidden %}
|
{% if show_hidden or not table.hidden %}
|
||||||
<div class="db-table">
|
<div class="db-table">
|
||||||
<h2><a href="/{{ database }}-{{ database_hash }}/{{ table.name|quote_plus }}">{{ table.name }}</a>{% if table.hidden %}<em> (hidden)</em>{% endif %}</h2>
|
<h2><a href="/{{ database }}-{{ database_hash }}/{{ table.name|encode_table_name }}">{{ table.name }}</a>{% if table.hidden %}<em> (hidden)</em>{% endif %}</h2>
|
||||||
<p><em>{% for column in table.columns[:9] %}{{ column }}{% if not loop.last %}, {% endif %}{% endfor %}{% if table.columns|length > 9 %}...{% endif %}</em></p>
|
<p><em>{% for column in table.columns[:9] %}{{ column }}{% if not loop.last %}, {% endif %}{% endfor %}{% if table.columns|length > 9 %}...{% endif %}</em></p>
|
||||||
<p>{{ "{:,}".format(table.count) }} row{% if table.count == 1 %}{% else %}s{% endif %}</p>
|
<p>{{ "{:,}".format(table.count) }} row{% if table.count == 1 %}{% else %}s{% endif %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
{{ "{:,}".format(database.views_count) }} view{% if database.views_count != 1 %}s{% endif %}
|
{{ "{:,}".format(database.views_count) }} view{% if database.views_count != 1 %}s{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p>{% for table in database.tables_truncated %}<a href="{{ database.path }}/{{ table.name|quote_plus }}" title="{{ table.count }} rows">{{ table.name }}</a>{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_more %}, <a href="{{ database.path }}">...</a>{% endif %}</p>
|
<p>{% for table in database.tables_truncated %}<a href="{{ database.path }}/{{ table.name|encode_table_name }}" title="{{ table.count }} rows">{{ table.name }}</a>{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_more %}, <a href="{{ database.path }}">...</a>{% endif %}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
{% block body_class %}row db-{{ database|to_css_class }} table-{{ table|to_css_class }}{% endblock %}
|
{% block body_class %}row db-{{ database|to_css_class }} table-{{ table|to_css_class }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="hd"><a href="/">home</a> / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a> / <a href="/{{ database }}-{{ database_hash }}/{{ table|quote_plus }}">{{ table }}</a></div>
|
<div class="hd"><a href="/">home</a> / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a> / <a href="/{{ database }}-{{ database_hash }}/{{ table|encode_table_name }}">{{ table }}</a></div>
|
||||||
|
|
||||||
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_hash[:6] }}">{{ table }}: {{ ', '.join(primary_key_values) }}</a></h1>
|
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_hash[:6] }}">{{ table }}: {{ ', '.join(primary_key_values) }}</a></h1>
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
{% for other in foreign_key_tables %}
|
{% for other in foreign_key_tables %}
|
||||||
<li>
|
<li>
|
||||||
<a href="/{{ database }}-{{ database_hash }}/{{ other.other_table|quote_plus }}?{{ other.other_column }}={{ ', '.join(primary_key_values) }}">
|
<a href="/{{ database }}-{{ database_hash }}/{{ other.other_table|encode_table_name }}?{{ other.other_column }}={{ ', '.join(primary_key_values) }}">
|
||||||
{{ "{:,}".format(other.count) }} row{% if other.count == 1 %}{% else %}s{% endif %}</a>
|
{{ "{:,}".format(other.count) }} row{% if other.count == 1 %}{% else %}s{% endif %}</a>
|
||||||
from {{ other.other_column }} in {{ other.other_table }}
|
from {{ other.other_column }} in {{ other.other_table }}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
</h3>
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form class="filters" action="/{{ database }}-{{ database_hash }}/{{ table|quote_plus }}" method="get">
|
<form class="filters" action="/{{ database }}-{{ database_hash }}/{{ table|encode_table_name }}" method="get">
|
||||||
{% if supports_search %}
|
{% if supports_search %}
|
||||||
<div class="search-row"><label for="_search">Search:</label><input id="_search" type="search" name="_search" value="{{ search }}"></div>
|
<div class="search-row"><label for="_search">Search:</label><input id="_search" type="search" name="_search" value="{{ search }}"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,23 @@ def urlsafe_components(token):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
_decode_table_name_re = re.compile(r"U\+([\da-h]{4})", re.IGNORECASE)
|
||||||
|
_encode_table_name_re = re.compile("[{}]".format(''.join(re.escape(c) for c in (
|
||||||
|
";", "/", "?", ":", "@", "&", "=", "+", "$", ",", "~"
|
||||||
|
))))
|
||||||
|
|
||||||
|
|
||||||
|
def decode_table_name(table_name):
|
||||||
|
return _decode_table_name_re.sub(lambda m: chr(int(m.group(1), 16)), table_name)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_table_name(table_name):
|
||||||
|
return _encode_table_name_re.sub(
|
||||||
|
lambda m: "U+{0:0{1}x}".format(ord(m.group(0)), 4).upper(),
|
||||||
|
table_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def path_from_row_pks(row, pks, use_rowid, quote=True):
|
def path_from_row_pks(row, pks, use_rowid, quote=True):
|
||||||
""" Generate an optionally URL-quoted unique identifier
|
""" Generate an optionally URL-quoted unique identifier
|
||||||
for a row from its primary keys."""
|
for a row from its primary keys."""
|
||||||
|
|
@ -867,13 +884,13 @@ class LimitedWriter:
|
||||||
self.limit_bytes = limit_mb * 1024 * 1024
|
self.limit_bytes = limit_mb * 1024 * 1024
|
||||||
self.bytes_count = 0
|
self.bytes_count = 0
|
||||||
|
|
||||||
def write(self, bytes):
|
async def write(self, bytes):
|
||||||
self.bytes_count += len(bytes)
|
self.bytes_count += len(bytes)
|
||||||
if self.limit_bytes and (self.bytes_count > self.limit_bytes):
|
if self.limit_bytes and (self.bytes_count > self.limit_bytes):
|
||||||
raise WriteLimitExceeded("CSV contains more than {} bytes".format(
|
raise WriteLimitExceeded("CSV contains more than {} bytes".format(
|
||||||
self.limit_bytes
|
self.limit_bytes
|
||||||
))
|
))
|
||||||
self.writer.write(bytes)
|
await self.writer.write(bytes)
|
||||||
|
|
||||||
|
|
||||||
_infinities = {float("inf"), float("-inf")}
|
_infinities = {float("inf"), float("-inf")}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ from datasette.utils import (
|
||||||
InterruptedError,
|
InterruptedError,
|
||||||
InvalidSql,
|
InvalidSql,
|
||||||
LimitedWriter,
|
LimitedWriter,
|
||||||
|
encode_table_name,
|
||||||
|
decode_table_name,
|
||||||
is_url,
|
is_url,
|
||||||
path_from_row_pks,
|
path_from_row_pks,
|
||||||
path_with_added_args,
|
path_with_added_args,
|
||||||
|
|
@ -161,7 +163,7 @@ class BaseView(RenderMixin):
|
||||||
if expected != hash:
|
if expected != hash:
|
||||||
if "table_and_format" in kwargs:
|
if "table_and_format" in kwargs:
|
||||||
table, _format = resolve_table_and_format(
|
table, _format = resolve_table_and_format(
|
||||||
table_and_format=urllib.parse.unquote_plus(
|
table_and_format=decode_table_name(
|
||||||
kwargs["table_and_format"]
|
kwargs["table_and_format"]
|
||||||
),
|
),
|
||||||
table_exists=lambda t: self.ds.table_exists(name, t)
|
table_exists=lambda t: self.ds.table_exists(name, t)
|
||||||
|
|
@ -170,13 +172,13 @@ class BaseView(RenderMixin):
|
||||||
if _format:
|
if _format:
|
||||||
kwargs["as_format"] = ".{}".format(_format)
|
kwargs["as_format"] = ".{}".format(_format)
|
||||||
elif "table" in kwargs:
|
elif "table" in kwargs:
|
||||||
kwargs["table"] = urllib.parse.unquote_plus(
|
kwargs["table"] = decode_table_name(
|
||||||
kwargs["table"]
|
kwargs["table"]
|
||||||
)
|
)
|
||||||
|
|
||||||
should_redirect = "/{}-{}".format(name, expected)
|
should_redirect = "/{}-{}".format(name, expected)
|
||||||
if "table" in kwargs:
|
if "table" in kwargs:
|
||||||
should_redirect += "/" + urllib.parse.quote_plus(
|
should_redirect += "/" + encode_table_name(
|
||||||
kwargs["table"]
|
kwargs["table"]
|
||||||
)
|
)
|
||||||
if "pk_path" in kwargs:
|
if "pk_path" in kwargs:
|
||||||
|
|
@ -259,13 +261,13 @@ class BaseView(RenderMixin):
|
||||||
request, database, hash, **kwargs
|
request, database, hash, **kwargs
|
||||||
)
|
)
|
||||||
if first:
|
if first:
|
||||||
writer.writerow(headings)
|
await writer.writerow(headings)
|
||||||
first = False
|
first = False
|
||||||
next = data.get("next")
|
next = data.get("next")
|
||||||
for row in data["rows"]:
|
for row in data["rows"]:
|
||||||
if not expanded_columns:
|
if not expanded_columns:
|
||||||
# Simple path
|
# Simple path
|
||||||
writer.writerow(row)
|
await writer.writerow(row)
|
||||||
else:
|
else:
|
||||||
# Look for {"value": "label": } dicts and expand
|
# Look for {"value": "label": } dicts and expand
|
||||||
new_row = []
|
new_row = []
|
||||||
|
|
@ -275,10 +277,10 @@ class BaseView(RenderMixin):
|
||||||
new_row.append(cell["label"])
|
new_row.append(cell["label"])
|
||||||
else:
|
else:
|
||||||
new_row.append(cell)
|
new_row.append(cell)
|
||||||
writer.writerow(new_row)
|
await writer.writerow(new_row)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('caught this', e)
|
print('caught this', e)
|
||||||
r.write(str(e))
|
await r.write(str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
content_type = "text/plain; charset=utf-8"
|
content_type = "text/plain; charset=utf-8"
|
||||||
|
|
@ -305,7 +307,7 @@ class BaseView(RenderMixin):
|
||||||
_format = (kwargs.pop("as_format", None) or "").lstrip(".")
|
_format = (kwargs.pop("as_format", None) or "").lstrip(".")
|
||||||
if "table_and_format" in kwargs:
|
if "table_and_format" in kwargs:
|
||||||
table, _ext_format = resolve_table_and_format(
|
table, _ext_format = resolve_table_and_format(
|
||||||
table_and_format=urllib.parse.unquote_plus(
|
table_and_format=decode_table_name(
|
||||||
kwargs["table_and_format"]
|
kwargs["table_and_format"]
|
||||||
),
|
),
|
||||||
table_exists=lambda t: self.ds.table_exists(database, t)
|
table_exists=lambda t: self.ds.table_exists(database, t)
|
||||||
|
|
@ -314,7 +316,7 @@ class BaseView(RenderMixin):
|
||||||
kwargs["table"] = table
|
kwargs["table"] = table
|
||||||
del kwargs["table_and_format"]
|
del kwargs["table_and_format"]
|
||||||
elif "table" in kwargs:
|
elif "table" in kwargs:
|
||||||
kwargs["table"] = urllib.parse.unquote_plus(
|
kwargs["table"] = decode_table_name(
|
||||||
kwargs["table"]
|
kwargs["table"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from datasette.utils import (
|
||||||
InterruptedError,
|
InterruptedError,
|
||||||
append_querystring,
|
append_querystring,
|
||||||
compound_keys_after_sql,
|
compound_keys_after_sql,
|
||||||
|
encode_table_name,
|
||||||
escape_sqlite,
|
escape_sqlite,
|
||||||
filters_should_redirect,
|
filters_should_redirect,
|
||||||
is_url,
|
is_url,
|
||||||
|
|
@ -147,7 +148,7 @@ class RowTableShared(BaseView):
|
||||||
"value": jinja2.Markup(
|
"value": jinja2.Markup(
|
||||||
'<a href="/{database}/{table}/{flat_pks_quoted}">{flat_pks}</a>'.format(
|
'<a href="/{database}/{table}/{flat_pks_quoted}">{flat_pks}</a>'.format(
|
||||||
database=database,
|
database=database,
|
||||||
table=urllib.parse.quote_plus(table),
|
table=encode_table_name(table),
|
||||||
flat_pks=str(
|
flat_pks=str(
|
||||||
jinja2.escape(
|
jinja2.escape(
|
||||||
path_from_row_pks(row, pks, not pks, False)
|
path_from_row_pks(row, pks, not pks, False)
|
||||||
|
|
@ -187,7 +188,7 @@ class RowTableShared(BaseView):
|
||||||
)
|
)
|
||||||
display_value = jinja2.Markup(link_template.format(
|
display_value = jinja2.Markup(link_template.format(
|
||||||
database=database,
|
database=database,
|
||||||
table=urllib.parse.quote_plus(other_table),
|
table=encode_table_name(other_table),
|
||||||
link_id=urllib.parse.quote_plus(str(value)),
|
link_id=urllib.parse.quote_plus(str(value)),
|
||||||
id=str(jinja2.escape(value)),
|
id=str(jinja2.escape(value)),
|
||||||
label=str(jinja2.escape(label)),
|
label=str(jinja2.escape(label)),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ Options:
|
||||||
--source_url TEXT Source URL for metadata
|
--source_url TEXT Source URL for metadata
|
||||||
-n, --name TEXT Application name to use when deploying
|
-n, --name TEXT Application name to use when deploying
|
||||||
--force Pass --force option to now
|
--force Pass --force option to now
|
||||||
--token TEXT Auth token to use for deploy (Now only)
|
--token TEXT Auth token to use for deploy
|
||||||
|
--alias TEXT Desired alias e.g. yoursite.now.sh
|
||||||
--spatialite Enable SpatialLite extension
|
--spatialite Enable SpatialLite extension
|
||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
|
|
|
||||||
6
setup.py
6
setup.py
|
|
@ -36,7 +36,7 @@ setup(
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'click==6.7',
|
'click==6.7',
|
||||||
'click-default-group==1.2',
|
'click-default-group==1.2',
|
||||||
'Sanic==0.7.0',
|
'sanic==18.12.0',
|
||||||
'Jinja2==2.10',
|
'Jinja2==2.10',
|
||||||
'hupper==1.0',
|
'hupper==1.0',
|
||||||
'pint==0.8.1',
|
'pint==0.8.1',
|
||||||
|
|
@ -50,8 +50,8 @@ setup(
|
||||||
extras_require={
|
extras_require={
|
||||||
'test': [
|
'test': [
|
||||||
'pytest==4.0.2',
|
'pytest==4.0.2',
|
||||||
'aiohttp==3.3.2',
|
'aiohttp==3.5.1',
|
||||||
'beautifulsoup4==4.6.1',
|
'beautifulsoup4==4.6.3',
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
tests_require=[
|
tests_require=[
|
||||||
|
|
|
||||||
|
|
@ -597,7 +597,7 @@ def test_table_shape_object_compound_primary_Key(app_client):
|
||||||
|
|
||||||
|
|
||||||
def test_table_with_slashes_in_name(app_client):
|
def test_table_with_slashes_in_name(app_client):
|
||||||
response = app_client.get('/fixtures/table%2Fwith%2Fslashes.csv?_shape=objects&_format=json')
|
response = app_client.get('/fixtures/tableU+002FwithU+002Fslashes.csv?_shape=objects&_format=json')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
data = response.json
|
data = response.json
|
||||||
assert data['rows'] == [{
|
assert data['rows'] == [{
|
||||||
|
|
@ -897,7 +897,7 @@ def test_row(app_client):
|
||||||
|
|
||||||
|
|
||||||
def test_row_strange_table_name(app_client):
|
def test_row_strange_table_name(app_client):
|
||||||
response = app_client.get('/fixtures/table%2Fwith%2Fslashes.csv/3.json?_shape=objects')
|
response = app_client.get('/fixtures/tableU+002FwithU+002Fslashes.csv/3.json?_shape=objects')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert [{'pk': '3', 'content': 'hey'}] == response.json['rows']
|
assert [{'pk': '3', 'content': 'hey'}] == response.json['rows']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,14 @@ def test_row(app_client):
|
||||||
|
|
||||||
def test_row_strange_table_name(app_client):
|
def test_row_strange_table_name(app_client):
|
||||||
response = app_client.get(
|
response = app_client.get(
|
||||||
'/fixtures/table%2Fwith%2Fslashes.csv/3',
|
'/fixtures/tableU+002FwithU+002Fslashes.csv/3',
|
||||||
allow_redirects=False
|
allow_redirects=False
|
||||||
)
|
)
|
||||||
assert response.status == 302
|
assert response.status == 302
|
||||||
assert response.headers['Location'].endswith(
|
assert response.headers['Location'].endswith(
|
||||||
'/table%2Fwith%2Fslashes.csv/3'
|
'/tableU+002FwithU+002Fslashes.csv/3'
|
||||||
)
|
)
|
||||||
response = app_client.get('/fixtures/table%2Fwith%2Fslashes.csv/3')
|
response = app_client.get('/fixtures/tableU+002FwithU+002Fslashes.csv/3')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -358,7 +358,7 @@ def test_facets_persist_through_filter_form(app_client):
|
||||||
('/fixtures/simple_primary_key', [
|
('/fixtures/simple_primary_key', [
|
||||||
'table', 'db-fixtures', 'table-simple_primary_key'
|
'table', 'db-fixtures', 'table-simple_primary_key'
|
||||||
]),
|
]),
|
||||||
('/fixtures/table%2Fwith%2Fslashes.csv', [
|
('/fixtures/tableU+002FwithU+002Fslashes.csv', [
|
||||||
'table', 'db-fixtures', 'table-tablewithslashescsv-fa7563'
|
'table', 'db-fixtures', 'table-tablewithslashescsv-fa7563'
|
||||||
]),
|
]),
|
||||||
('/fixtures/simple_primary_key/1', [
|
('/fixtures/simple_primary_key/1', [
|
||||||
|
|
|
||||||
|
|
@ -374,3 +374,16 @@ def test_path_with_format(path, format, extra_qs, expected):
|
||||||
)
|
)
|
||||||
actual = utils.path_with_format(request, format, extra_qs)
|
actual = utils.path_with_format(request, format, extra_qs)
|
||||||
assert expected == actual
|
assert expected == actual
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name,expected", [
|
||||||
|
("table", "table"),
|
||||||
|
("table/and/slashes", "tableU+002FandU+002Fslashes"),
|
||||||
|
("~table", "U+007Etable"),
|
||||||
|
("+bobcats!", "U+002Bbobcats!"),
|
||||||
|
("U+007Etable", "UU+002B007Etable"),
|
||||||
|
])
|
||||||
|
def test_encode_decode_table_name(name, expected):
|
||||||
|
encoded = utils.encode_table_name(name)
|
||||||
|
assert encoded == expected
|
||||||
|
assert name == utils.decode_table_name(encoded)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue