laketower 0.5.1__py3-none-any.whl → 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of laketower might be problematic. Click here for more details.
- laketower/__about__.py +1 -1
- laketower/cli.py +221 -91
- laketower/config.py +86 -14
- laketower/static/editor.bundle.js +27433 -0
- laketower/static/editor.js +74 -0
- laketower/static/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- laketower/static/vendor/bootstrap-icons/bootstrap-icons.min.css +5 -0
- laketower/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- laketower/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- laketower/static/vendor/halfmoon/halfmoon.min.css +22 -0
- laketower/static/vendor/halfmoon/halfmoon.modern.css +282 -0
- laketower/tables.py +183 -13
- laketower/templates/_base.html +22 -19
- laketower/templates/queries/view.html +24 -4
- laketower/templates/tables/_macros.html +3 -0
- laketower/templates/tables/history.html +6 -0
- laketower/templates/tables/import.html +71 -0
- laketower/templates/tables/index.html +6 -0
- laketower/templates/tables/query.html +31 -1
- laketower/templates/tables/statistics.html +6 -0
- laketower/templates/tables/view.html +6 -0
- laketower/web.py +216 -30
- {laketower-0.5.1.dist-info → laketower-0.6.1.dist-info}/METADATA +173 -2
- laketower-0.6.1.dist-info/RECORD +31 -0
- laketower-0.6.1.dist-info/entry_points.txt +2 -0
- laketower-0.5.1.dist-info/RECORD +0 -22
- laketower-0.5.1.dist-info/entry_points.txt +0 -2
- {laketower-0.5.1.dist-info → laketower-0.6.1.dist-info}/WHEEL +0 -0
- {laketower-0.5.1.dist-info → laketower-0.6.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
{% import 'tables/_macros.html' as table_macros %}
|
|
3
3
|
|
|
4
4
|
{% block body %}
|
|
5
|
+
{% if error %}
|
|
6
|
+
<div class="alert alert-danger" role="alert">
|
|
7
|
+
{{ error.message }}
|
|
8
|
+
</div>
|
|
9
|
+
{% else %}
|
|
5
10
|
{{ table_macros.table_nav(table_id, 'history') }}
|
|
6
11
|
|
|
7
12
|
<div class="row">
|
|
@@ -39,4 +44,5 @@
|
|
|
39
44
|
{% endfor %}
|
|
40
45
|
</div>
|
|
41
46
|
</div>
|
|
47
|
+
{% endif %}
|
|
42
48
|
{% endblock %}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{% extends "_base.html" %}
|
|
2
|
+
{% import 'tables/_macros.html' as table_macros %}
|
|
3
|
+
|
|
4
|
+
{% block body %}
|
|
5
|
+
{{ table_macros.table_nav(table_id, 'import') }}
|
|
6
|
+
|
|
7
|
+
{% if message %}
|
|
8
|
+
{% if message.type == 'success' %}{% set alert_type = 'success' %}
|
|
9
|
+
{% elif message.type == 'error' %}{% set alert_type = 'danger' %}
|
|
10
|
+
{% else %}{% set alert_type = 'primary' %}
|
|
11
|
+
{% endif %}
|
|
12
|
+
|
|
13
|
+
<div class="alert alert-{{ alert_type }}" role="alert">
|
|
14
|
+
{{ message.body }}
|
|
15
|
+
</div>
|
|
16
|
+
{% endif %}
|
|
17
|
+
|
|
18
|
+
<div class="row justify-content-center">
|
|
19
|
+
<div class="col-12 col-md-8 col-lg-4">
|
|
20
|
+
<form action="{{ request.url.path }}" method="post" enctype="multipart/form-data">
|
|
21
|
+
<div class="mb-3">
|
|
22
|
+
<label for="import-file-input" class="form-label">Input file</label>
|
|
23
|
+
<input id="import-file-input" class="form-control" name="input_file" type="file" accept=".csv" required>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="mb-3">
|
|
27
|
+
<label class="form-label">Mode</label>
|
|
28
|
+
|
|
29
|
+
<div class="form-check">
|
|
30
|
+
<input id="import-mode-append" class="form-check-input" name="mode" type="radio" value="append" checked>
|
|
31
|
+
<label for="import-mode-append" class="form-check-label">Append</label>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-check">
|
|
34
|
+
<input id="import-mode-overwrite" class="form-check-input" name="mode" type="radio" value="overwrite">
|
|
35
|
+
<label for="import-mode-overwrite" class="form-check-label">Overwrite</label>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="mb-3">
|
|
40
|
+
<label for="import-file-format" class="form-label">File format</label>
|
|
41
|
+
<select id="import-file-format" class="form-select" name="file_format">
|
|
42
|
+
<option value="csv" selected>CSV</option>
|
|
43
|
+
</select>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="mb-3">
|
|
47
|
+
<label for="import-delimiter" class="form-label">Delimiter</label>
|
|
48
|
+
<input id="import-delimiter" class="form-control" name="delimiter" value="," required>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="mb-3">
|
|
52
|
+
<label for="import-encoding" class="form-label">Encoding</label>
|
|
53
|
+
<select id="import-encoding" class="form-select" name="encoding">
|
|
54
|
+
<option value="utf-8" selected>UTF-8</option>
|
|
55
|
+
<option value="utf-16">UTF-16</option>
|
|
56
|
+
<option value="utf-32">UTF-32</option>
|
|
57
|
+
<option value="latin-1">Latin-1</option>
|
|
58
|
+
</select>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="mb-3">
|
|
62
|
+
<div class="d-flex justify-content-end">
|
|
63
|
+
<button type="submit" class="btn btn-primary">
|
|
64
|
+
<i class="bi-upload" aria-hidden="true"></i> Import Data
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</form>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
{% endblock %}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
{% import 'tables/_macros.html' as table_macros %}
|
|
3
3
|
|
|
4
4
|
{% block body %}
|
|
5
|
+
{% if error %}
|
|
6
|
+
<div class="alert alert-danger" role="alert">
|
|
7
|
+
{{ error.message }}
|
|
8
|
+
</div>
|
|
9
|
+
{% else %}
|
|
5
10
|
{{ table_macros.table_nav(table_id, 'overview') }}
|
|
6
11
|
|
|
7
12
|
<div class="row row-cols-1 row-cols-md-2 g-4">
|
|
@@ -81,4 +86,5 @@
|
|
|
81
86
|
</div>
|
|
82
87
|
</div>
|
|
83
88
|
</div>
|
|
89
|
+
{% endif %}
|
|
84
90
|
{% endblock %}
|
|
@@ -8,9 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
<form action="{{ request.url.path }}" method="get">
|
|
10
10
|
<div class="mb-3">
|
|
11
|
-
<textarea name="sql" rows="5" class="form-control">{{ sql_query }}</textarea>
|
|
11
|
+
<textarea id="sql-editor" name="sql" rows="5" class="form-control">{{ sql_query }}</textarea>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
+
{% if sql_params|length %}
|
|
15
|
+
<h3 class="mb-3">Parameters</h3>
|
|
16
|
+
{% for param_name, param_value in sql_params.items() %}
|
|
17
|
+
<div class="row mb-3">
|
|
18
|
+
<label for="param-{{ param_name }}" class="col-form-label col-sm-2">{{ param_name }}</label>
|
|
19
|
+
<div class="col-sm-4">
|
|
20
|
+
<input id="param-{{ param_name }}" class="form-control" name="{{ param_name }}" value="{{ param_value }}">
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
{% endfor %}
|
|
24
|
+
{% endif %}
|
|
25
|
+
|
|
14
26
|
<div class="mb-3">
|
|
15
27
|
<div class="d-flex justify-content-end">
|
|
16
28
|
<button type="submit" class="btn btn-primary">
|
|
@@ -25,6 +37,12 @@
|
|
|
25
37
|
{{ error.message }}
|
|
26
38
|
</div>
|
|
27
39
|
{% else %}
|
|
40
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
41
|
+
<h3>Results</h3>
|
|
42
|
+
<a href="/tables/query/csv?sql={{ sql_query | urlencode }}" class="btn btn-outline-secondary btn-sm">
|
|
43
|
+
<i class="bi-download" aria-hidden="true"></i> Export CSV
|
|
44
|
+
</a>
|
|
45
|
+
</div>
|
|
28
46
|
<div class="table-responsive">
|
|
29
47
|
<table class="table table-sm table-bordered table-striped table-hover">
|
|
30
48
|
<thead>
|
|
@@ -48,4 +66,16 @@
|
|
|
48
66
|
{% endif %}
|
|
49
67
|
</div>
|
|
50
68
|
</div>
|
|
69
|
+
{% endblock %}
|
|
70
|
+
|
|
71
|
+
{% block extra_scripts %}
|
|
72
|
+
<script src="{{ url_for('static', path='/editor.bundle.js') }}"></script>
|
|
73
|
+
<script>
|
|
74
|
+
window.addEventListener("DOMContentLoaded", () => {
|
|
75
|
+
const textArea = document.querySelector("textarea#sql-editor")
|
|
76
|
+
textArea.style.display = "none";
|
|
77
|
+
const sqlSchema = {{ sql_schema | tojson }}
|
|
78
|
+
const sqlEditor = editor.createEditor(textArea, { readOnly: false, dialect: 'duckdb', schema: sqlSchema})
|
|
79
|
+
})
|
|
80
|
+
</script>
|
|
51
81
|
{% endblock %}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
{% import 'tables/_macros.html' as table_macros %}
|
|
3
3
|
|
|
4
4
|
{% block body %}
|
|
5
|
+
{% if error %}
|
|
6
|
+
<div class="alert alert-danger" role="alert">
|
|
7
|
+
{{ error.message }}
|
|
8
|
+
</div>
|
|
9
|
+
{% else %}
|
|
5
10
|
{{ table_macros.table_nav(table_id, 'statistics') }}
|
|
6
11
|
|
|
7
12
|
<div class="row">
|
|
@@ -53,4 +58,5 @@
|
|
|
53
58
|
</div>
|
|
54
59
|
</div>
|
|
55
60
|
</div>
|
|
61
|
+
{% endif %}
|
|
56
62
|
{% endblock %}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
{% import 'tables/_macros.html' as table_macros %}
|
|
3
3
|
|
|
4
4
|
{% block body %}
|
|
5
|
+
{% if error %}
|
|
6
|
+
<div class="alert alert-danger" role="alert">
|
|
7
|
+
{{ error.message }}
|
|
8
|
+
</div>
|
|
9
|
+
{% else %}
|
|
5
10
|
{{ table_macros.table_nav(table_id, 'view') }}
|
|
6
11
|
|
|
7
12
|
<div class="row">
|
|
@@ -93,4 +98,5 @@
|
|
|
93
98
|
</div>
|
|
94
99
|
</div>
|
|
95
100
|
</div>
|
|
101
|
+
{% endif %}
|
|
96
102
|
{% endblock %}
|
laketower/web.py
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import urllib.parse
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Annotated
|
|
4
5
|
|
|
6
|
+
import bleach
|
|
7
|
+
import markdown
|
|
5
8
|
import pydantic_settings
|
|
6
|
-
from fastapi import APIRouter, FastAPI, Query, Request
|
|
7
|
-
from fastapi.responses import HTMLResponse
|
|
9
|
+
from fastapi import APIRouter, FastAPI, File, Form, Query, Request, UploadFile
|
|
10
|
+
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
|
8
11
|
from fastapi.staticfiles import StaticFiles
|
|
9
12
|
from fastapi.templating import Jinja2Templates
|
|
10
13
|
|
|
14
|
+
from laketower import __about__
|
|
11
15
|
from laketower.config import Config, load_yaml_config
|
|
12
16
|
from laketower.tables import (
|
|
13
17
|
DEFAULT_LIMIT,
|
|
18
|
+
ImportFileFormatEnum,
|
|
19
|
+
ImportModeEnum,
|
|
14
20
|
execute_query,
|
|
21
|
+
extract_query_parameter_names,
|
|
15
22
|
generate_table_statistics_query,
|
|
16
23
|
generate_table_query,
|
|
24
|
+
import_file_to_table,
|
|
25
|
+
load_datasets,
|
|
17
26
|
load_table,
|
|
18
27
|
)
|
|
19
28
|
|
|
@@ -22,6 +31,12 @@ class Settings(pydantic_settings.BaseSettings):
|
|
|
22
31
|
laketower_config_path: Path
|
|
23
32
|
|
|
24
33
|
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class AppMetadata:
|
|
36
|
+
app_name: str
|
|
37
|
+
app_version: str
|
|
38
|
+
|
|
39
|
+
|
|
25
40
|
def current_path_with_args(request: Request, args: list[tuple[str, str]]) -> str:
|
|
26
41
|
keys_to_update = set(arg[0] for arg in args)
|
|
27
42
|
query_params = request.query_params.multi_items()
|
|
@@ -33,19 +48,28 @@ def current_path_with_args(request: Request, args: list[tuple[str, str]]) -> str
|
|
|
33
48
|
return f"{request.url.path}?{query_string}"
|
|
34
49
|
|
|
35
50
|
|
|
51
|
+
def render_markdown(md_text: str) -> str:
|
|
52
|
+
return bleach.clean(
|
|
53
|
+
markdown.markdown(md_text), tags=bleach.sanitizer.ALLOWED_TAGS | {"p"}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
36
57
|
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
|
|
37
58
|
templates.env.filters["current_path_with_args"] = current_path_with_args
|
|
59
|
+
templates.env.filters["render_markdown"] = render_markdown
|
|
38
60
|
|
|
39
61
|
router = APIRouter()
|
|
40
62
|
|
|
41
63
|
|
|
42
64
|
@router.get("/", response_class=HTMLResponse)
|
|
43
65
|
def index(request: Request) -> HTMLResponse:
|
|
66
|
+
app_metadata: AppMetadata = request.app.state.app_metadata
|
|
44
67
|
config: Config = request.app.state.config
|
|
45
68
|
return templates.TemplateResponse(
|
|
46
69
|
request=request,
|
|
47
70
|
name="index.html",
|
|
48
71
|
context={
|
|
72
|
+
"app_metadata": app_metadata,
|
|
49
73
|
"tables": config.tables,
|
|
50
74
|
"queries": config.queries,
|
|
51
75
|
},
|
|
@@ -54,14 +78,20 @@ def index(request: Request) -> HTMLResponse:
|
|
|
54
78
|
|
|
55
79
|
@router.get("/tables/query", response_class=HTMLResponse)
|
|
56
80
|
def get_tables_query(request: Request, sql: str) -> HTMLResponse:
|
|
81
|
+
app_metadata: AppMetadata = request.app.state.app_metadata
|
|
57
82
|
config: Config = request.app.state.config
|
|
58
|
-
tables_dataset =
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
tables_dataset = load_datasets(config.tables)
|
|
84
|
+
sql_schema = {
|
|
85
|
+
table_name: dataset.schema.names
|
|
86
|
+
for table_name, dataset in tables_dataset.items()
|
|
87
|
+
}
|
|
88
|
+
sql_param_names = extract_query_parameter_names(sql)
|
|
89
|
+
sql_params = {
|
|
90
|
+
name: request.query_params.get(name) or "" for name in sql_param_names
|
|
61
91
|
}
|
|
62
92
|
|
|
63
93
|
try:
|
|
64
|
-
results = execute_query(tables_dataset, sql)
|
|
94
|
+
results = execute_query(tables_dataset, sql, sql_params=sql_params)
|
|
65
95
|
error = None
|
|
66
96
|
except ValueError as e:
|
|
67
97
|
error = {"message": str(e)}
|
|
@@ -71,52 +101,90 @@ def get_tables_query(request: Request, sql: str) -> HTMLResponse:
|
|
|
71
101
|
request=request,
|
|
72
102
|
name="tables/query.html",
|
|
73
103
|
context={
|
|
104
|
+
"app_metadata": app_metadata,
|
|
74
105
|
"tables": config.tables,
|
|
75
106
|
"queries": config.queries,
|
|
76
107
|
"table_results": results,
|
|
77
108
|
"sql_query": sql,
|
|
109
|
+
"sql_schema": sql_schema,
|
|
110
|
+
"sql_params": sql_params,
|
|
78
111
|
"error": error,
|
|
79
112
|
},
|
|
80
113
|
)
|
|
81
114
|
|
|
82
115
|
|
|
116
|
+
@router.get("/tables/query/csv")
|
|
117
|
+
def export_tables_query_csv(request: Request, sql: str) -> Response:
|
|
118
|
+
config: Config = request.app.state.config
|
|
119
|
+
tables_dataset = load_datasets(config.tables)
|
|
120
|
+
|
|
121
|
+
results = execute_query(tables_dataset, sql)
|
|
122
|
+
csv_content = results.to_csv(header=True, index=False, sep=",")
|
|
123
|
+
|
|
124
|
+
return Response(
|
|
125
|
+
content=csv_content,
|
|
126
|
+
media_type="text/csv",
|
|
127
|
+
headers={"Content-Disposition": "attachment; filename=query_results.csv"},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
83
131
|
@router.get("/tables/{table_id}", response_class=HTMLResponse)
|
|
84
132
|
def get_table_index(request: Request, table_id: str) -> HTMLResponse:
|
|
133
|
+
app_metadata: AppMetadata = request.app.state.app_metadata
|
|
85
134
|
config: Config = request.app.state.config
|
|
86
135
|
table_config = next(
|
|
87
136
|
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
88
137
|
)
|
|
89
|
-
|
|
138
|
+
try:
|
|
139
|
+
table = load_table(table_config)
|
|
140
|
+
table_metadata = table.metadata()
|
|
141
|
+
table_schema = table.schema()
|
|
142
|
+
error = None
|
|
143
|
+
except ValueError as e:
|
|
144
|
+
error = {"message": str(e)}
|
|
145
|
+
table_metadata = None
|
|
146
|
+
table_schema = None
|
|
90
147
|
|
|
91
148
|
return templates.TemplateResponse(
|
|
92
149
|
request=request,
|
|
93
150
|
name="tables/index.html",
|
|
94
151
|
context={
|
|
152
|
+
"app_metadata": app_metadata,
|
|
95
153
|
"tables": config.tables,
|
|
96
154
|
"queries": config.queries,
|
|
97
155
|
"table_id": table_id,
|
|
98
|
-
"table_metadata":
|
|
99
|
-
"table_schema":
|
|
156
|
+
"table_metadata": table_metadata,
|
|
157
|
+
"table_schema": table_schema,
|
|
158
|
+
"error": error,
|
|
100
159
|
},
|
|
101
160
|
)
|
|
102
161
|
|
|
103
162
|
|
|
104
163
|
@router.get("/tables/{table_id}/history", response_class=HTMLResponse)
|
|
105
164
|
def get_table_history(request: Request, table_id: str) -> HTMLResponse:
|
|
165
|
+
app_metadata: AppMetadata = request.app.state.app_metadata
|
|
106
166
|
config: Config = request.app.state.config
|
|
107
167
|
table_config = next(
|
|
108
168
|
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
109
169
|
)
|
|
110
|
-
|
|
170
|
+
try:
|
|
171
|
+
table = load_table(table_config)
|
|
172
|
+
table_history = table.history()
|
|
173
|
+
error = None
|
|
174
|
+
except ValueError as e:
|
|
175
|
+
error = {"message": str(e)}
|
|
176
|
+
table_history = None
|
|
111
177
|
|
|
112
178
|
return templates.TemplateResponse(
|
|
113
179
|
request=request,
|
|
114
180
|
name="tables/history.html",
|
|
115
181
|
context={
|
|
182
|
+
"app_metadata": app_metadata,
|
|
116
183
|
"tables": config.tables,
|
|
117
184
|
"queries": config.queries,
|
|
118
185
|
"table_id": table_id,
|
|
119
|
-
"table_history":
|
|
186
|
+
"table_history": table_history,
|
|
187
|
+
"error": error,
|
|
120
188
|
},
|
|
121
189
|
)
|
|
122
190
|
|
|
@@ -127,26 +195,35 @@ def get_table_statistics(
|
|
|
127
195
|
table_id: str,
|
|
128
196
|
version: int | None = None,
|
|
129
197
|
) -> HTMLResponse:
|
|
198
|
+
app_metadata: AppMetadata = request.app.state.app_metadata
|
|
130
199
|
config: Config = request.app.state.config
|
|
131
200
|
table_config = next(
|
|
132
201
|
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
133
202
|
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
203
|
+
try:
|
|
204
|
+
table = load_table(table_config)
|
|
205
|
+
table_name = table_config.name
|
|
206
|
+
table_metadata = table.metadata()
|
|
207
|
+
table_dataset = table.dataset(version=version)
|
|
208
|
+
sql_query = generate_table_statistics_query(table_name)
|
|
209
|
+
query_results = execute_query({table_name: table_dataset}, sql_query)
|
|
210
|
+
error = None
|
|
211
|
+
except ValueError as e:
|
|
212
|
+
error = {"message": str(e)}
|
|
213
|
+
table_metadata = None
|
|
214
|
+
query_results = None
|
|
140
215
|
|
|
141
216
|
return templates.TemplateResponse(
|
|
142
217
|
request=request,
|
|
143
218
|
name="tables/statistics.html",
|
|
144
219
|
context={
|
|
220
|
+
"app_metadata": app_metadata,
|
|
145
221
|
"tables": config.tables,
|
|
146
222
|
"queries": config.queries,
|
|
147
223
|
"table_id": table_id,
|
|
148
224
|
"table_metadata": table_metadata,
|
|
149
225
|
"table_results": query_results,
|
|
226
|
+
"error": error,
|
|
150
227
|
},
|
|
151
228
|
)
|
|
152
229
|
|
|
@@ -161,23 +238,32 @@ def get_table_view(
|
|
|
161
238
|
sort_desc: str | None = None,
|
|
162
239
|
version: int | None = None,
|
|
163
240
|
) -> HTMLResponse:
|
|
241
|
+
app_metadata: AppMetadata = request.app.state.app_metadata
|
|
164
242
|
config: Config = request.app.state.config
|
|
165
243
|
table_config = next(
|
|
166
244
|
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
167
245
|
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
246
|
+
try:
|
|
247
|
+
table = load_table(table_config)
|
|
248
|
+
table_name = table_config.name
|
|
249
|
+
table_metadata = table.metadata()
|
|
250
|
+
table_dataset = table.dataset(version=version)
|
|
251
|
+
sql_query = generate_table_query(
|
|
252
|
+
table_name, limit=limit, cols=cols, sort_asc=sort_asc, sort_desc=sort_desc
|
|
253
|
+
)
|
|
254
|
+
results = execute_query({table_name: table_dataset}, sql_query)
|
|
255
|
+
error = None
|
|
256
|
+
except ValueError as e:
|
|
257
|
+
error = {"message": str(e)}
|
|
258
|
+
table_metadata = None
|
|
259
|
+
sql_query = None
|
|
260
|
+
results = None
|
|
176
261
|
|
|
177
262
|
return templates.TemplateResponse(
|
|
178
263
|
request=request,
|
|
179
264
|
name="tables/view.html",
|
|
180
265
|
context={
|
|
266
|
+
"app_metadata": app_metadata,
|
|
181
267
|
"tables": config.tables,
|
|
182
268
|
"queries": config.queries,
|
|
183
269
|
"table_id": table_id,
|
|
@@ -185,23 +271,118 @@ def get_table_view(
|
|
|
185
271
|
"table_results": results,
|
|
186
272
|
"sql_query": sql_query,
|
|
187
273
|
"default_limit": DEFAULT_LIMIT,
|
|
274
|
+
"error": error,
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@router.get("/tables/{table_id}/import", response_class=HTMLResponse)
|
|
280
|
+
def get_table_import(
|
|
281
|
+
request: Request,
|
|
282
|
+
table_id: str,
|
|
283
|
+
) -> HTMLResponse:
|
|
284
|
+
app_metadata: AppMetadata = request.app.state.app_metadata
|
|
285
|
+
config: Config = request.app.state.config
|
|
286
|
+
table_config = next(
|
|
287
|
+
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
288
|
+
)
|
|
289
|
+
try:
|
|
290
|
+
table = load_table(table_config)
|
|
291
|
+
table_metadata = table.metadata()
|
|
292
|
+
message = None
|
|
293
|
+
except ValueError as e:
|
|
294
|
+
message = {"type": "error", "body": str(e)}
|
|
295
|
+
table_metadata = None
|
|
296
|
+
|
|
297
|
+
return templates.TemplateResponse(
|
|
298
|
+
request=request,
|
|
299
|
+
name="tables/import.html",
|
|
300
|
+
context={
|
|
301
|
+
"app_metadata": app_metadata,
|
|
302
|
+
"tables": config.tables,
|
|
303
|
+
"queries": config.queries,
|
|
304
|
+
"table_id": table_id,
|
|
305
|
+
"table_metadata": table_metadata,
|
|
306
|
+
"message": message,
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@router.post("/tables/{table_id}/import", response_class=HTMLResponse)
|
|
312
|
+
def post_table_import(
|
|
313
|
+
request: Request,
|
|
314
|
+
table_id: str,
|
|
315
|
+
input_file: Annotated[UploadFile, File()],
|
|
316
|
+
mode: Annotated[ImportModeEnum, Form()],
|
|
317
|
+
file_format: Annotated[ImportFileFormatEnum, Form()],
|
|
318
|
+
delimiter: Annotated[str, Form()],
|
|
319
|
+
encoding: Annotated[str, Form()],
|
|
320
|
+
) -> HTMLResponse:
|
|
321
|
+
app_metadata: AppMetadata = request.app.state.app_metadata
|
|
322
|
+
config: Config = request.app.state.config
|
|
323
|
+
table_config = next(
|
|
324
|
+
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
325
|
+
)
|
|
326
|
+
try:
|
|
327
|
+
table = load_table(table_config)
|
|
328
|
+
table_metadata = table.metadata()
|
|
329
|
+
rows_imported = import_file_to_table(
|
|
330
|
+
table_config, input_file.file, mode, file_format, delimiter, encoding
|
|
331
|
+
)
|
|
332
|
+
message = {
|
|
333
|
+
"type": "success",
|
|
334
|
+
"body": f"Successfully imported {rows_imported} rows",
|
|
335
|
+
}
|
|
336
|
+
except Exception as e:
|
|
337
|
+
message = {"type": "error", "body": str(e)}
|
|
338
|
+
table_metadata = None
|
|
339
|
+
|
|
340
|
+
return templates.TemplateResponse(
|
|
341
|
+
request=request,
|
|
342
|
+
name="tables/import.html",
|
|
343
|
+
context={
|
|
344
|
+
"app_metadata": app_metadata,
|
|
345
|
+
"tables": config.tables,
|
|
346
|
+
"queries": config.queries,
|
|
347
|
+
"table_id": table_id,
|
|
348
|
+
"table_metadata": table_metadata,
|
|
349
|
+
"message": message,
|
|
188
350
|
},
|
|
189
351
|
)
|
|
190
352
|
|
|
191
353
|
|
|
192
354
|
@router.get("/queries/{query_id}/view", response_class=HTMLResponse)
|
|
193
|
-
def get_query_view(request: Request, query_id: str) ->
|
|
355
|
+
def get_query_view(request: Request, query_id: str) -> Response:
|
|
356
|
+
app_metadata: AppMetadata = request.app.state.app_metadata
|
|
194
357
|
config: Config = request.app.state.config
|
|
195
358
|
query_config = next(
|
|
196
359
|
filter(lambda query_config: query_config.name == query_id, config.queries)
|
|
197
360
|
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
len(request.query_params.keys()) == 0
|
|
364
|
+
and len(query_config.parameters.keys()) > 0
|
|
365
|
+
):
|
|
366
|
+
default_parameters = {k: v.default for k, v in query_config.parameters.items()}
|
|
367
|
+
url = request.url_for("get_query_view", query_id=query_id)
|
|
368
|
+
query_params = urllib.parse.urlencode(default_parameters)
|
|
369
|
+
return RedirectResponse(f"{url}?{query_params}")
|
|
370
|
+
|
|
371
|
+
tables_dataset = load_datasets(config.tables)
|
|
372
|
+
sql_param_names = extract_query_parameter_names(query_config.sql)
|
|
373
|
+
sql_params = {
|
|
374
|
+
name: request.query_params.get(name)
|
|
375
|
+
or (
|
|
376
|
+
query_param.default
|
|
377
|
+
if (query_param := query_config.parameters.get(name))
|
|
378
|
+
else None
|
|
379
|
+
)
|
|
380
|
+
or ""
|
|
381
|
+
for name in sql_param_names
|
|
201
382
|
}
|
|
202
383
|
|
|
203
384
|
try:
|
|
204
|
-
results = execute_query(tables_dataset, query_config.sql)
|
|
385
|
+
results = execute_query(tables_dataset, query_config.sql, sql_params=sql_params)
|
|
205
386
|
error = None
|
|
206
387
|
except ValueError as e:
|
|
207
388
|
error = {"message": str(e)}
|
|
@@ -211,10 +392,12 @@ def get_query_view(request: Request, query_id: str) -> HTMLResponse:
|
|
|
211
392
|
request=request,
|
|
212
393
|
name="queries/view.html",
|
|
213
394
|
context={
|
|
395
|
+
"app_metadata": app_metadata,
|
|
214
396
|
"tables": config.tables,
|
|
215
397
|
"queries": config.queries,
|
|
216
398
|
"query": query_config,
|
|
217
399
|
"query_results": results,
|
|
400
|
+
"sql_params": sql_params,
|
|
218
401
|
"error": error,
|
|
219
402
|
},
|
|
220
403
|
)
|
|
@@ -231,6 +414,9 @@ def create_app() -> FastAPI:
|
|
|
231
414
|
name="static",
|
|
232
415
|
)
|
|
233
416
|
app.include_router(router)
|
|
417
|
+
app.state.app_metadata = AppMetadata(
|
|
418
|
+
app_name="Laketower", app_version=__about__.__version__
|
|
419
|
+
)
|
|
234
420
|
app.state.config = config
|
|
235
421
|
|
|
236
422
|
return app
|