laketower 0.5.0__py3-none-any.whl → 0.6.0__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 +181 -87
- laketower/config.py +82 -16
- laketower/tables.py +166 -12
- laketower/templates/queries/view.html +6 -0
- 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 +6 -0
- laketower/templates/tables/statistics.html +6 -0
- laketower/templates/tables/view.html +6 -0
- laketower/web.py +144 -29
- {laketower-0.5.0.dist-info → laketower-0.6.0.dist-info}/METADATA +145 -10
- laketower-0.6.0.dist-info/RECORD +23 -0
- laketower-0.6.0.dist-info/entry_points.txt +2 -0
- laketower-0.5.0.dist-info/RECORD +0 -22
- laketower-0.5.0.dist-info/entry_points.txt +0 -2
- {laketower-0.5.0.dist-info → laketower-0.6.0.dist-info}/WHEEL +0 -0
- {laketower-0.5.0.dist-info → laketower-0.6.0.dist-info}/licenses/LICENSE +0 -0
laketower/web.py
CHANGED
|
@@ -3,17 +3,21 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Annotated
|
|
4
4
|
|
|
5
5
|
import pydantic_settings
|
|
6
|
-
from fastapi import APIRouter, FastAPI, Query, Request
|
|
7
|
-
from fastapi.responses import HTMLResponse
|
|
6
|
+
from fastapi import APIRouter, FastAPI, File, Form, Query, Request, UploadFile
|
|
7
|
+
from fastapi.responses import HTMLResponse, Response
|
|
8
8
|
from fastapi.staticfiles import StaticFiles
|
|
9
9
|
from fastapi.templating import Jinja2Templates
|
|
10
10
|
|
|
11
11
|
from laketower.config import Config, load_yaml_config
|
|
12
12
|
from laketower.tables import (
|
|
13
13
|
DEFAULT_LIMIT,
|
|
14
|
+
ImportFileFormatEnum,
|
|
15
|
+
ImportModeEnum,
|
|
14
16
|
execute_query,
|
|
15
17
|
generate_table_statistics_query,
|
|
16
18
|
generate_table_query,
|
|
19
|
+
import_file_to_table,
|
|
20
|
+
load_datasets,
|
|
17
21
|
load_table,
|
|
18
22
|
)
|
|
19
23
|
|
|
@@ -55,10 +59,7 @@ def index(request: Request) -> HTMLResponse:
|
|
|
55
59
|
@router.get("/tables/query", response_class=HTMLResponse)
|
|
56
60
|
def get_tables_query(request: Request, sql: str) -> HTMLResponse:
|
|
57
61
|
config: Config = request.app.state.config
|
|
58
|
-
tables_dataset =
|
|
59
|
-
table_config.name: load_table(table_config).dataset()
|
|
60
|
-
for table_config in config.tables
|
|
61
|
-
}
|
|
62
|
+
tables_dataset = load_datasets(config.tables)
|
|
62
63
|
|
|
63
64
|
try:
|
|
64
65
|
results = execute_query(tables_dataset, sql)
|
|
@@ -80,13 +81,36 @@ def get_tables_query(request: Request, sql: str) -> HTMLResponse:
|
|
|
80
81
|
)
|
|
81
82
|
|
|
82
83
|
|
|
84
|
+
@router.get("/tables/query/csv")
|
|
85
|
+
def export_tables_query_csv(request: Request, sql: str) -> Response:
|
|
86
|
+
config: Config = request.app.state.config
|
|
87
|
+
tables_dataset = load_datasets(config.tables)
|
|
88
|
+
|
|
89
|
+
results = execute_query(tables_dataset, sql)
|
|
90
|
+
csv_content = results.to_csv(header=True, index=False, sep=",")
|
|
91
|
+
|
|
92
|
+
return Response(
|
|
93
|
+
content=csv_content,
|
|
94
|
+
media_type="text/csv",
|
|
95
|
+
headers={"Content-Disposition": "attachment; filename=query_results.csv"},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
83
99
|
@router.get("/tables/{table_id}", response_class=HTMLResponse)
|
|
84
100
|
def get_table_index(request: Request, table_id: str) -> HTMLResponse:
|
|
85
101
|
config: Config = request.app.state.config
|
|
86
102
|
table_config = next(
|
|
87
103
|
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
88
104
|
)
|
|
89
|
-
|
|
105
|
+
try:
|
|
106
|
+
table = load_table(table_config)
|
|
107
|
+
table_metadata = table.metadata()
|
|
108
|
+
table_schema = table.schema()
|
|
109
|
+
error = None
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
error = {"message": str(e)}
|
|
112
|
+
table_metadata = None
|
|
113
|
+
table_schema = None
|
|
90
114
|
|
|
91
115
|
return templates.TemplateResponse(
|
|
92
116
|
request=request,
|
|
@@ -95,8 +119,9 @@ def get_table_index(request: Request, table_id: str) -> HTMLResponse:
|
|
|
95
119
|
"tables": config.tables,
|
|
96
120
|
"queries": config.queries,
|
|
97
121
|
"table_id": table_id,
|
|
98
|
-
"table_metadata":
|
|
99
|
-
"table_schema":
|
|
122
|
+
"table_metadata": table_metadata,
|
|
123
|
+
"table_schema": table_schema,
|
|
124
|
+
"error": error,
|
|
100
125
|
},
|
|
101
126
|
)
|
|
102
127
|
|
|
@@ -107,7 +132,13 @@ def get_table_history(request: Request, table_id: str) -> HTMLResponse:
|
|
|
107
132
|
table_config = next(
|
|
108
133
|
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
109
134
|
)
|
|
110
|
-
|
|
135
|
+
try:
|
|
136
|
+
table = load_table(table_config)
|
|
137
|
+
table_history = table.history()
|
|
138
|
+
error = None
|
|
139
|
+
except ValueError as e:
|
|
140
|
+
error = {"message": str(e)}
|
|
141
|
+
table_history = None
|
|
111
142
|
|
|
112
143
|
return templates.TemplateResponse(
|
|
113
144
|
request=request,
|
|
@@ -116,7 +147,8 @@ def get_table_history(request: Request, table_id: str) -> HTMLResponse:
|
|
|
116
147
|
"tables": config.tables,
|
|
117
148
|
"queries": config.queries,
|
|
118
149
|
"table_id": table_id,
|
|
119
|
-
"table_history":
|
|
150
|
+
"table_history": table_history,
|
|
151
|
+
"error": error,
|
|
120
152
|
},
|
|
121
153
|
)
|
|
122
154
|
|
|
@@ -131,12 +163,18 @@ def get_table_statistics(
|
|
|
131
163
|
table_config = next(
|
|
132
164
|
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
133
165
|
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
166
|
+
try:
|
|
167
|
+
table = load_table(table_config)
|
|
168
|
+
table_name = table_config.name
|
|
169
|
+
table_metadata = table.metadata()
|
|
170
|
+
table_dataset = table.dataset(version=version)
|
|
171
|
+
sql_query = generate_table_statistics_query(table_name)
|
|
172
|
+
query_results = execute_query({table_name: table_dataset}, sql_query)
|
|
173
|
+
error = None
|
|
174
|
+
except ValueError as e:
|
|
175
|
+
error = {"message": str(e)}
|
|
176
|
+
table_metadata = None
|
|
177
|
+
query_results = None
|
|
140
178
|
|
|
141
179
|
return templates.TemplateResponse(
|
|
142
180
|
request=request,
|
|
@@ -147,6 +185,7 @@ def get_table_statistics(
|
|
|
147
185
|
"table_id": table_id,
|
|
148
186
|
"table_metadata": table_metadata,
|
|
149
187
|
"table_results": query_results,
|
|
188
|
+
"error": error,
|
|
150
189
|
},
|
|
151
190
|
)
|
|
152
191
|
|
|
@@ -165,14 +204,21 @@ def get_table_view(
|
|
|
165
204
|
table_config = next(
|
|
166
205
|
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
167
206
|
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
207
|
+
try:
|
|
208
|
+
table = load_table(table_config)
|
|
209
|
+
table_name = table_config.name
|
|
210
|
+
table_metadata = table.metadata()
|
|
211
|
+
table_dataset = table.dataset(version=version)
|
|
212
|
+
sql_query = generate_table_query(
|
|
213
|
+
table_name, limit=limit, cols=cols, sort_asc=sort_asc, sort_desc=sort_desc
|
|
214
|
+
)
|
|
215
|
+
results = execute_query({table_name: table_dataset}, sql_query)
|
|
216
|
+
error = None
|
|
217
|
+
except ValueError as e:
|
|
218
|
+
error = {"message": str(e)}
|
|
219
|
+
table_metadata = None
|
|
220
|
+
sql_query = None
|
|
221
|
+
results = None
|
|
176
222
|
|
|
177
223
|
return templates.TemplateResponse(
|
|
178
224
|
request=request,
|
|
@@ -185,6 +231,78 @@ def get_table_view(
|
|
|
185
231
|
"table_results": results,
|
|
186
232
|
"sql_query": sql_query,
|
|
187
233
|
"default_limit": DEFAULT_LIMIT,
|
|
234
|
+
"error": error,
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@router.get("/tables/{table_id}/import", response_class=HTMLResponse)
|
|
240
|
+
def get_table_import(
|
|
241
|
+
request: Request,
|
|
242
|
+
table_id: str,
|
|
243
|
+
) -> HTMLResponse:
|
|
244
|
+
config: Config = request.app.state.config
|
|
245
|
+
table_config = next(
|
|
246
|
+
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
247
|
+
)
|
|
248
|
+
try:
|
|
249
|
+
table = load_table(table_config)
|
|
250
|
+
table_metadata = table.metadata()
|
|
251
|
+
message = None
|
|
252
|
+
except ValueError as e:
|
|
253
|
+
message = {"type": "error", "body": str(e)}
|
|
254
|
+
table_metadata = None
|
|
255
|
+
|
|
256
|
+
return templates.TemplateResponse(
|
|
257
|
+
request=request,
|
|
258
|
+
name="tables/import.html",
|
|
259
|
+
context={
|
|
260
|
+
"tables": config.tables,
|
|
261
|
+
"queries": config.queries,
|
|
262
|
+
"table_id": table_id,
|
|
263
|
+
"table_metadata": table_metadata,
|
|
264
|
+
"message": message,
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@router.post("/tables/{table_id}/import", response_class=HTMLResponse)
|
|
270
|
+
def post_table_import(
|
|
271
|
+
request: Request,
|
|
272
|
+
table_id: str,
|
|
273
|
+
input_file: Annotated[UploadFile, File()],
|
|
274
|
+
mode: Annotated[ImportModeEnum, Form()],
|
|
275
|
+
file_format: Annotated[ImportFileFormatEnum, Form()],
|
|
276
|
+
delimiter: Annotated[str, Form()],
|
|
277
|
+
encoding: Annotated[str, Form()],
|
|
278
|
+
) -> HTMLResponse:
|
|
279
|
+
config: Config = request.app.state.config
|
|
280
|
+
table_config = next(
|
|
281
|
+
filter(lambda table_config: table_config.name == table_id, config.tables)
|
|
282
|
+
)
|
|
283
|
+
try:
|
|
284
|
+
table = load_table(table_config)
|
|
285
|
+
table_metadata = table.metadata()
|
|
286
|
+
rows_imported = import_file_to_table(
|
|
287
|
+
table_config, input_file.file, mode, file_format, delimiter, encoding
|
|
288
|
+
)
|
|
289
|
+
message = {
|
|
290
|
+
"type": "success",
|
|
291
|
+
"body": f"Successfully imported {rows_imported} rows",
|
|
292
|
+
}
|
|
293
|
+
except Exception 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
|
+
"tables": config.tables,
|
|
302
|
+
"queries": config.queries,
|
|
303
|
+
"table_id": table_id,
|
|
304
|
+
"table_metadata": table_metadata,
|
|
305
|
+
"message": message,
|
|
188
306
|
},
|
|
189
307
|
)
|
|
190
308
|
|
|
@@ -195,10 +313,7 @@ def get_query_view(request: Request, query_id: str) -> HTMLResponse:
|
|
|
195
313
|
query_config = next(
|
|
196
314
|
filter(lambda query_config: query_config.name == query_id, config.queries)
|
|
197
315
|
)
|
|
198
|
-
tables_dataset =
|
|
199
|
-
table_config.name: load_table(table_config).dataset()
|
|
200
|
-
for table_config in config.tables
|
|
201
|
-
}
|
|
316
|
+
tables_dataset = load_datasets(config.tables)
|
|
202
317
|
|
|
203
318
|
try:
|
|
204
319
|
results = execute_query(tables_dataset, query_config.sql)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: laketower
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Oversee your lakehouse
|
|
5
5
|
Project-URL: Repository, https://github.com/datalpia/laketower
|
|
6
6
|
Project-URL: Issues, https://github.com/datalpia/laketower/issues
|
|
7
|
-
Project-URL: Changelog, https://github.com/datalpia/laketower/blob/
|
|
7
|
+
Project-URL: Changelog, https://github.com/datalpia/laketower/blob/main/CHANGELOG.md
|
|
8
8
|
Author-email: Romain Clement <git@romain-clement.net>
|
|
9
9
|
License: Apache-2.0
|
|
10
10
|
License-File: LICENSE
|
|
@@ -22,7 +22,7 @@ Classifier: Topic :: Database
|
|
|
22
22
|
Classifier: Topic :: Software Development
|
|
23
23
|
Classifier: Topic :: Utilities
|
|
24
24
|
Requires-Python: <3.14,>=3.10
|
|
25
|
-
Requires-Dist: deltalake
|
|
25
|
+
Requires-Dist: deltalake<2,>=1
|
|
26
26
|
Requires-Dist: duckdb
|
|
27
27
|
Requires-Dist: fastapi
|
|
28
28
|
Requires-Dist: jinja2!=3.1.5,>=3
|
|
@@ -30,6 +30,7 @@ Requires-Dist: pandas
|
|
|
30
30
|
Requires-Dist: pyarrow!=19.0.0
|
|
31
31
|
Requires-Dist: pydantic-settings>=2
|
|
32
32
|
Requires-Dist: pydantic>=2
|
|
33
|
+
Requires-Dist: python-multipart
|
|
33
34
|
Requires-Dist: pyyaml
|
|
34
35
|
Requires-Dist: rich
|
|
35
36
|
Requires-Dist: sqlglot
|
|
@@ -50,13 +51,16 @@ Utility application to explore and manage tables in your data lakehouse, especia
|
|
|
50
51
|
## Features
|
|
51
52
|
|
|
52
53
|
- Delta Lake table format support
|
|
54
|
+
- Remote tables support (S3, ADLS)
|
|
53
55
|
- Inspect table metadata
|
|
54
56
|
- Inspect table schema
|
|
55
57
|
- Inspect table history
|
|
56
58
|
- Get table statistics
|
|
59
|
+
- Import data into a table from CSV files
|
|
57
60
|
- View table content with a simple query builder
|
|
58
61
|
- Query all registered tables with DuckDB SQL dialect
|
|
59
62
|
- Execute saved queries
|
|
63
|
+
- Export query results to CSV files
|
|
60
64
|
- Static and versionable YAML configuration
|
|
61
65
|
- Web application
|
|
62
66
|
- CLI application
|
|
@@ -99,7 +103,9 @@ queries:
|
|
|
99
103
|
|
|
100
104
|
Current limitations:
|
|
101
105
|
|
|
102
|
-
- `tables.uri`:
|
|
106
|
+
- `tables.uri`:
|
|
107
|
+
- Local paths are supported (`./path/to/table`, `/abs/path/to/table`, `file:///abs/path/to/table`)
|
|
108
|
+
- Remote paths to S3 (`s3://<bucket>/<path>`) and ADLS (`abfss://<container>/<path>`)
|
|
103
109
|
- `tables.format`: only `delta` is allowed
|
|
104
110
|
|
|
105
111
|
Example from the provided demo:
|
|
@@ -138,6 +144,103 @@ queries:
|
|
|
138
144
|
day asc
|
|
139
145
|
```
|
|
140
146
|
|
|
147
|
+
Support for environment variables substitution is also supported within the YAML
|
|
148
|
+
configuration using a object containing a single key `env` with the name of the
|
|
149
|
+
environment variable to be injected. The value of the variable can contain JSON
|
|
150
|
+
and will be decoded in a best effort manner (default to string value). For instance:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
# export TABLE_URI=path/to/table
|
|
154
|
+
|
|
155
|
+
tables:
|
|
156
|
+
- name: sample_table
|
|
157
|
+
uri:
|
|
158
|
+
env: TABLE_URI
|
|
159
|
+
format: delta
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### Remote S3 Tables
|
|
163
|
+
|
|
164
|
+
Configuring S3 tables (AWS, MinIO, Cloudflare R2):
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
tables:
|
|
168
|
+
- name: delta_table_s3
|
|
169
|
+
uri: s3://<bucket>/path/to/table
|
|
170
|
+
format: delta
|
|
171
|
+
connection:
|
|
172
|
+
s3:
|
|
173
|
+
s3_access_key_id: access-key-id
|
|
174
|
+
s3_secret_access_key: secret-access-key
|
|
175
|
+
s3_region: s3-region
|
|
176
|
+
s3_endpoint_url: http://s3.domain.com
|
|
177
|
+
s3_allow_http: false
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Depending on your object storage location and configuration, one might have to
|
|
181
|
+
set part or all the available `connection.s3` parameters. The only required ones
|
|
182
|
+
are `s3_access_key_id` and `s3_secret_access_key`.
|
|
183
|
+
|
|
184
|
+
Also as a security best practice, it is best not to write secrets directly in
|
|
185
|
+
static configuration files, so one can use environment variables to all dynamic substitution,
|
|
186
|
+
e.g.
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
tables:
|
|
190
|
+
- name: delta_table_s3
|
|
191
|
+
uri: s3://<bucket>/path/to/table
|
|
192
|
+
format: delta
|
|
193
|
+
connection:
|
|
194
|
+
s3:
|
|
195
|
+
s3_access_key_id: access-key-id
|
|
196
|
+
s3_secret_access_key:
|
|
197
|
+
env: S3_SECRET_ACCESS_KEY
|
|
198
|
+
s3_region: s3-region
|
|
199
|
+
s3_endpoint_url: http://s3.domain.com
|
|
200
|
+
s3_allow_http: false
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Remote ADLS Tables
|
|
204
|
+
|
|
205
|
+
Configuring Azure ADLS tables:
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
tables:
|
|
209
|
+
- name: delta_table_adls
|
|
210
|
+
uri: abfss://<container>/path/to/table
|
|
211
|
+
format: delta
|
|
212
|
+
connection:
|
|
213
|
+
adls:
|
|
214
|
+
adls_account_name: adls-account-name
|
|
215
|
+
adls_access_key: adls-access-key
|
|
216
|
+
adls_sas_key: adls-sas-key
|
|
217
|
+
adls_tenant_id: adls-tenant-id
|
|
218
|
+
adls_client_id: adls-client-id
|
|
219
|
+
adls_client_secret: adls-client-secret
|
|
220
|
+
azure_msi_endpoint: https://msi.azure.com
|
|
221
|
+
use_azure_cli: false
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Depending on your object storage location and configuration, one might have to
|
|
225
|
+
set part or all the available `connection.adls` parameters. The only required one
|
|
226
|
+
is `adls_account_name`.
|
|
227
|
+
|
|
228
|
+
Also as a security best practice, it is best not to write secrets directly in
|
|
229
|
+
static configuration files, so one can use environment variables to all dynamic substitution,
|
|
230
|
+
e.g.
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
tables:
|
|
234
|
+
- name: delta_table_adls
|
|
235
|
+
uri: abfss://<container>/path/to/table
|
|
236
|
+
format: delta
|
|
237
|
+
connection:
|
|
238
|
+
adls:
|
|
239
|
+
adls_account_name: adls-account-name
|
|
240
|
+
adls_access_key:
|
|
241
|
+
env: ADLS_ACCESS_KEY
|
|
242
|
+
```
|
|
243
|
+
|
|
141
244
|
### Web Application
|
|
142
245
|
|
|
143
246
|
The easiest way to get started is to launch the Laketower web application:
|
|
@@ -148,12 +251,13 @@ $ laketower -c demo/laketower.yml web
|
|
|
148
251
|
|
|
149
252
|
#### Screenshots
|
|
150
253
|
|
|
151
|
-

|
|
152
|
-

|
|
153
|
-

|
|
154
|
-

|
|
155
|
-

|
|
255
|
+

|
|
256
|
+

|
|
257
|
+

|
|
258
|
+

|
|
259
|
+

|
|
260
|
+

|
|
157
261
|
|
|
158
262
|
### CLI
|
|
159
263
|
|
|
@@ -321,6 +425,29 @@ $ laketower -c demo/laketower.yml tables statistics --version 0 weather
|
|
|
321
425
|
└──────────────────────┴───────┴──────┴──────┴──────┴──────┘
|
|
322
426
|
```
|
|
323
427
|
|
|
428
|
+
#### Import data into a given table
|
|
429
|
+
|
|
430
|
+
Import a CSV dataset into a table in append mode:
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
$ laketower -c demo/laketower.yml tables import weather --file data.csv --mode append --format csv --delimiter ',' --encoding 'utf-8'
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`--mode` argument can be one of:
|
|
437
|
+
- `append`: append rows to the table (default)
|
|
438
|
+
- `overwrite`: replace all rows with the ones from the input file
|
|
439
|
+
|
|
440
|
+
`--format` argument can be one of:
|
|
441
|
+
- `csv`: CSV file format (default)
|
|
442
|
+
|
|
443
|
+
`--delimiter` argument can be:
|
|
444
|
+
- Any single character (only valid for CSV file format)
|
|
445
|
+
- Default is _comma_ (`','`)
|
|
446
|
+
|
|
447
|
+
`--encoding` argument can be:
|
|
448
|
+
- Any [standard Python encoding](https://docs.python.org/3/library/codecs.html#standard-encodings),
|
|
449
|
+
- Default is `'utf-8'`
|
|
450
|
+
|
|
324
451
|
#### View a given table
|
|
325
452
|
|
|
326
453
|
Using a simple query builder, the content of a table can be displayed.
|
|
@@ -400,6 +527,14 @@ $ laketower -c demo/laketower.yml tables query "select date_trunc('day', time) a
|
|
|
400
527
|
└───────────────────────────┴────────────────────┘
|
|
401
528
|
```
|
|
402
529
|
|
|
530
|
+
Export query results to CSV:
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
$ laketower -c demo/laketower.yml tables query --output results.csv "select date_trunc('day', time) as day, avg(temperature_2m) as mean_temperature from weather group by day order by day desc limit 3"
|
|
534
|
+
|
|
535
|
+
Query results written to: results.csv
|
|
536
|
+
```
|
|
537
|
+
|
|
403
538
|
#### List saved queries
|
|
404
539
|
|
|
405
540
|
```bash
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
laketower/__about__.py,sha256=cID1jLnC_vj48GgMN6Yb1FA3JsQ95zNmCHmRYE8TFhY,22
|
|
2
|
+
laketower/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
laketower/__main__.py,sha256=czKxJKG8OfncnxWmpaOWx7b1JBwFnZNQi7wKSTncB4M,108
|
|
4
|
+
laketower/cli.py,sha256=tvCr90q4jRVAoP2qzwhTLG7PUsae0QOGQwJRid3GVLc,15324
|
|
5
|
+
laketower/config.py,sha256=uIQSE1MjEuA-kp0TwA0QREwPbaNGL9hLGmKWqNaA8VY,3298
|
|
6
|
+
laketower/tables.py,sha256=gs4klWJkyyS7_oIDz1HKFicXAF6jbGfvzJWrDw8r-rQ,10235
|
|
7
|
+
laketower/web.py,sha256=-cXg_8pUCdhU5QF6WH_3luXi545F0Et9rpSC05J63Ng,10736
|
|
8
|
+
laketower/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
laketower/templates/_base.html,sha256=S-8kjAfYBx3Btb4FwzM2qyfkGYrOBHhpvCWR32mCvOw,3729
|
|
10
|
+
laketower/templates/index.html,sha256=dLF2Og0qgzBkvGyVRidRNzTv0u4o97ifOx1jVeig8Kg,59
|
|
11
|
+
laketower/templates/queries/view.html,sha256=naqU3XGyVVW6Er8wQ95-DYlp38Czolvh-h5noIAWs84,1978
|
|
12
|
+
laketower/templates/tables/_macros.html,sha256=sCI1TOFW0QA74oSXW87H6dNTudOs7n-FretnTPFcRh4,1174
|
|
13
|
+
laketower/templates/tables/history.html,sha256=a5GBLXCiLlbWno5eR0XT5i_oMAghylUBBFOpr27NB3Q,1853
|
|
14
|
+
laketower/templates/tables/import.html,sha256=bQZwRrv84tDBuf0AHJyc7L-PjW-XSoZhMHNDIo6TP4c,2604
|
|
15
|
+
laketower/templates/tables/index.html,sha256=saNdQbJAjMJAzayTk4rA5Mmw_bCXvor2WpghVmoWSAI,2507
|
|
16
|
+
laketower/templates/tables/query.html,sha256=ymWcqZj4TtJgUeCIMseJD0PIOqy0gf1SVzrQzN9UD5Q,1652
|
|
17
|
+
laketower/templates/tables/statistics.html,sha256=h6TiQtFwiRWvPqDphcRRF1rZ886FP00UbJuMHuW5l6U,1827
|
|
18
|
+
laketower/templates/tables/view.html,sha256=ruiAX_S--wpodmgEbcQ-GT7BQzz-vzSCk4NpzlO3I80,3985
|
|
19
|
+
laketower-0.6.0.dist-info/METADATA,sha256=HMP4sBtBKVgvO0Ay_KIJd6que4EKaICfXLXYA9TQ12o,25496
|
|
20
|
+
laketower-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
laketower-0.6.0.dist-info/entry_points.txt,sha256=sJpQgRwdeZhRBudNqBTqtHPCE-uLC9YgFXJY2CTEyCk,53
|
|
22
|
+
laketower-0.6.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
23
|
+
laketower-0.6.0.dist-info/RECORD,,
|
laketower-0.5.0.dist-info/RECORD
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
laketower/__about__.py,sha256=LBK46heutvn3KmsCrKIYu8RQikbfnjZaj2xFrXaeCzQ,22
|
|
2
|
-
laketower/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
laketower/__main__.py,sha256=czKxJKG8OfncnxWmpaOWx7b1JBwFnZNQi7wKSTncB4M,108
|
|
4
|
-
laketower/cli.py,sha256=U4gI12egcOs51wxjmQlU70XhA2QGcowc0AmTYpUKEFE,11962
|
|
5
|
-
laketower/config.py,sha256=NdUDF7lr2hEW9Gujp0OpkOKcDP46ju1y_r0IM4Hrx2M,1100
|
|
6
|
-
laketower/tables.py,sha256=QwqoK73Q9pDRzyuoN9pwmwP_WWj3Rg2qPJaIcCdnJbw,4402
|
|
7
|
-
laketower/web.py,sha256=5NMKj26aVz3cKnUAe-3sLDJ_4Ue3u0VXhATrDQ8GVF8,7205
|
|
8
|
-
laketower/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
laketower/templates/_base.html,sha256=S-8kjAfYBx3Btb4FwzM2qyfkGYrOBHhpvCWR32mCvOw,3729
|
|
10
|
-
laketower/templates/index.html,sha256=dLF2Og0qgzBkvGyVRidRNzTv0u4o97ifOx1jVeig8Kg,59
|
|
11
|
-
laketower/templates/queries/view.html,sha256=2jW08X-PflXmsfy7u9ibX1BF8G5dx9X_n_Eoq3jzizA,1686
|
|
12
|
-
laketower/templates/tables/_macros.html,sha256=fnj_8nBco0iS6mlBmGmfT2PZhI2Y82yP0cm8tRxchpU,965
|
|
13
|
-
laketower/templates/tables/history.html,sha256=yAW0xw9_Uxp0QZYKje6qhcbpeznxI3fb740hfNyILZ8,1740
|
|
14
|
-
laketower/templates/tables/index.html,sha256=oY13l_p8qozlLONanLpga1WhEo4oTP92pRf9sBSuFZI,2394
|
|
15
|
-
laketower/templates/tables/query.html,sha256=YAFnW8Q5abDsbeglFHHZmJfGJXPjI2s4Nxf6gF_-Eg0,1360
|
|
16
|
-
laketower/templates/tables/statistics.html,sha256=rgIOuF2PlHo2jvcYDAnxa5ObNortwyALlrURpM7qxMw,1714
|
|
17
|
-
laketower/templates/tables/view.html,sha256=psfeRKkN19Q3Ko5Sm2570qRhehvuoEBPG89zFU5KQlc,3872
|
|
18
|
-
laketower-0.5.0.dist-info/METADATA,sha256=AKQgJ1YSUgNA3bXyyBweGY3BAMDtR2Mlr2SzQFZZ8P8,20935
|
|
19
|
-
laketower-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
20
|
-
laketower-0.5.0.dist-info/entry_points.txt,sha256=OL_4klopvyEzasJOFJ-sKu54lv24Jvomni32h1WVUjk,48
|
|
21
|
-
laketower-0.5.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
22
|
-
laketower-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|