laketower 0.6.0__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/web.py CHANGED
@@ -1,19 +1,24 @@
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
9
  from fastapi import APIRouter, FastAPI, File, Form, Query, Request, UploadFile
7
- from fastapi.responses import HTMLResponse, Response
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,
14
18
  ImportFileFormatEnum,
15
19
  ImportModeEnum,
16
20
  execute_query,
21
+ extract_query_parameter_names,
17
22
  generate_table_statistics_query,
18
23
  generate_table_query,
19
24
  import_file_to_table,
@@ -26,6 +31,12 @@ class Settings(pydantic_settings.BaseSettings):
26
31
  laketower_config_path: Path
27
32
 
28
33
 
34
+ @dataclass(frozen=True)
35
+ class AppMetadata:
36
+ app_name: str
37
+ app_version: str
38
+
39
+
29
40
  def current_path_with_args(request: Request, args: list[tuple[str, str]]) -> str:
30
41
  keys_to_update = set(arg[0] for arg in args)
31
42
  query_params = request.query_params.multi_items()
@@ -37,19 +48,28 @@ def current_path_with_args(request: Request, args: list[tuple[str, str]]) -> str
37
48
  return f"{request.url.path}?{query_string}"
38
49
 
39
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
+
40
57
  templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
41
58
  templates.env.filters["current_path_with_args"] = current_path_with_args
59
+ templates.env.filters["render_markdown"] = render_markdown
42
60
 
43
61
  router = APIRouter()
44
62
 
45
63
 
46
64
  @router.get("/", response_class=HTMLResponse)
47
65
  def index(request: Request) -> HTMLResponse:
66
+ app_metadata: AppMetadata = request.app.state.app_metadata
48
67
  config: Config = request.app.state.config
49
68
  return templates.TemplateResponse(
50
69
  request=request,
51
70
  name="index.html",
52
71
  context={
72
+ "app_metadata": app_metadata,
53
73
  "tables": config.tables,
54
74
  "queries": config.queries,
55
75
  },
@@ -58,11 +78,20 @@ def index(request: Request) -> HTMLResponse:
58
78
 
59
79
  @router.get("/tables/query", response_class=HTMLResponse)
60
80
  def get_tables_query(request: Request, sql: str) -> HTMLResponse:
81
+ app_metadata: AppMetadata = request.app.state.app_metadata
61
82
  config: Config = request.app.state.config
62
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
91
+ }
63
92
 
64
93
  try:
65
- results = execute_query(tables_dataset, sql)
94
+ results = execute_query(tables_dataset, sql, sql_params=sql_params)
66
95
  error = None
67
96
  except ValueError as e:
68
97
  error = {"message": str(e)}
@@ -72,10 +101,13 @@ def get_tables_query(request: Request, sql: str) -> HTMLResponse:
72
101
  request=request,
73
102
  name="tables/query.html",
74
103
  context={
104
+ "app_metadata": app_metadata,
75
105
  "tables": config.tables,
76
106
  "queries": config.queries,
77
107
  "table_results": results,
78
108
  "sql_query": sql,
109
+ "sql_schema": sql_schema,
110
+ "sql_params": sql_params,
79
111
  "error": error,
80
112
  },
81
113
  )
@@ -98,6 +130,7 @@ def export_tables_query_csv(request: Request, sql: str) -> Response:
98
130
 
99
131
  @router.get("/tables/{table_id}", response_class=HTMLResponse)
100
132
  def get_table_index(request: Request, table_id: str) -> HTMLResponse:
133
+ app_metadata: AppMetadata = request.app.state.app_metadata
101
134
  config: Config = request.app.state.config
102
135
  table_config = next(
103
136
  filter(lambda table_config: table_config.name == table_id, config.tables)
@@ -116,6 +149,7 @@ def get_table_index(request: Request, table_id: str) -> HTMLResponse:
116
149
  request=request,
117
150
  name="tables/index.html",
118
151
  context={
152
+ "app_metadata": app_metadata,
119
153
  "tables": config.tables,
120
154
  "queries": config.queries,
121
155
  "table_id": table_id,
@@ -128,6 +162,7 @@ def get_table_index(request: Request, table_id: str) -> HTMLResponse:
128
162
 
129
163
  @router.get("/tables/{table_id}/history", response_class=HTMLResponse)
130
164
  def get_table_history(request: Request, table_id: str) -> HTMLResponse:
165
+ app_metadata: AppMetadata = request.app.state.app_metadata
131
166
  config: Config = request.app.state.config
132
167
  table_config = next(
133
168
  filter(lambda table_config: table_config.name == table_id, config.tables)
@@ -144,6 +179,7 @@ def get_table_history(request: Request, table_id: str) -> HTMLResponse:
144
179
  request=request,
145
180
  name="tables/history.html",
146
181
  context={
182
+ "app_metadata": app_metadata,
147
183
  "tables": config.tables,
148
184
  "queries": config.queries,
149
185
  "table_id": table_id,
@@ -159,6 +195,7 @@ def get_table_statistics(
159
195
  table_id: str,
160
196
  version: int | None = None,
161
197
  ) -> HTMLResponse:
198
+ app_metadata: AppMetadata = request.app.state.app_metadata
162
199
  config: Config = request.app.state.config
163
200
  table_config = next(
164
201
  filter(lambda table_config: table_config.name == table_id, config.tables)
@@ -180,6 +217,7 @@ def get_table_statistics(
180
217
  request=request,
181
218
  name="tables/statistics.html",
182
219
  context={
220
+ "app_metadata": app_metadata,
183
221
  "tables": config.tables,
184
222
  "queries": config.queries,
185
223
  "table_id": table_id,
@@ -200,6 +238,7 @@ def get_table_view(
200
238
  sort_desc: str | None = None,
201
239
  version: int | None = None,
202
240
  ) -> HTMLResponse:
241
+ app_metadata: AppMetadata = request.app.state.app_metadata
203
242
  config: Config = request.app.state.config
204
243
  table_config = next(
205
244
  filter(lambda table_config: table_config.name == table_id, config.tables)
@@ -224,6 +263,7 @@ def get_table_view(
224
263
  request=request,
225
264
  name="tables/view.html",
226
265
  context={
266
+ "app_metadata": app_metadata,
227
267
  "tables": config.tables,
228
268
  "queries": config.queries,
229
269
  "table_id": table_id,
@@ -241,6 +281,7 @@ def get_table_import(
241
281
  request: Request,
242
282
  table_id: str,
243
283
  ) -> HTMLResponse:
284
+ app_metadata: AppMetadata = request.app.state.app_metadata
244
285
  config: Config = request.app.state.config
245
286
  table_config = next(
246
287
  filter(lambda table_config: table_config.name == table_id, config.tables)
@@ -257,6 +298,7 @@ def get_table_import(
257
298
  request=request,
258
299
  name="tables/import.html",
259
300
  context={
301
+ "app_metadata": app_metadata,
260
302
  "tables": config.tables,
261
303
  "queries": config.queries,
262
304
  "table_id": table_id,
@@ -276,6 +318,7 @@ def post_table_import(
276
318
  delimiter: Annotated[str, Form()],
277
319
  encoding: Annotated[str, Form()],
278
320
  ) -> HTMLResponse:
321
+ app_metadata: AppMetadata = request.app.state.app_metadata
279
322
  config: Config = request.app.state.config
280
323
  table_config = next(
281
324
  filter(lambda table_config: table_config.name == table_id, config.tables)
@@ -298,6 +341,7 @@ def post_table_import(
298
341
  request=request,
299
342
  name="tables/import.html",
300
343
  context={
344
+ "app_metadata": app_metadata,
301
345
  "tables": config.tables,
302
346
  "queries": config.queries,
303
347
  "table_id": table_id,
@@ -308,15 +352,37 @@ def post_table_import(
308
352
 
309
353
 
310
354
  @router.get("/queries/{query_id}/view", response_class=HTMLResponse)
311
- def get_query_view(request: Request, query_id: str) -> HTMLResponse:
355
+ def get_query_view(request: Request, query_id: str) -> Response:
356
+ app_metadata: AppMetadata = request.app.state.app_metadata
312
357
  config: Config = request.app.state.config
313
358
  query_config = next(
314
359
  filter(lambda query_config: query_config.name == query_id, config.queries)
315
360
  )
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
+
316
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
382
+ }
317
383
 
318
384
  try:
319
- results = execute_query(tables_dataset, query_config.sql)
385
+ results = execute_query(tables_dataset, query_config.sql, sql_params=sql_params)
320
386
  error = None
321
387
  except ValueError as e:
322
388
  error = {"message": str(e)}
@@ -326,10 +392,12 @@ def get_query_view(request: Request, query_id: str) -> HTMLResponse:
326
392
  request=request,
327
393
  name="queries/view.html",
328
394
  context={
395
+ "app_metadata": app_metadata,
329
396
  "tables": config.tables,
330
397
  "queries": config.queries,
331
398
  "query": query_config,
332
399
  "query_results": results,
400
+ "sql_params": sql_params,
333
401
  "error": error,
334
402
  },
335
403
  )
@@ -346,6 +414,9 @@ def create_app() -> FastAPI:
346
414
  name="static",
347
415
  )
348
416
  app.include_router(router)
417
+ app.state.app_metadata = AppMetadata(
418
+ app_name="Laketower", app_version=__about__.__version__
419
+ )
349
420
  app.state.config = config
350
421
 
351
422
  return app
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: laketower
3
- Version: 0.6.0
3
+ Version: 0.6.1
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
@@ -22,10 +22,12 @@ 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: bleach
25
26
  Requires-Dist: deltalake<2,>=1
26
27
  Requires-Dist: duckdb
27
28
  Requires-Dist: fastapi
28
29
  Requires-Dist: jinja2!=3.1.5,>=3
30
+ Requires-Dist: markdown
29
31
  Requires-Dist: pandas
30
32
  Requires-Dist: pyarrow!=19.0.0
31
33
  Requires-Dist: pydantic-settings>=2
@@ -98,6 +100,10 @@ tables:
98
100
  queries:
99
101
  - name: <query_name>
100
102
  title: <Query name>
103
+ description: <Query description>
104
+ parameters:
105
+ <param_name_1>:
106
+ default: <default_value>
101
107
  sql: <sql expression>
102
108
  ```
103
109
 
@@ -527,6 +533,20 @@ $ laketower -c demo/laketower.yml tables query "select date_trunc('day', time) a
527
533
  └───────────────────────────┴────────────────────┘
528
534
  ```
529
535
 
536
+ Use named parameters within a giving query (note: escape `$` prefixes properly!):
537
+
538
+ ```bash
539
+ $ laketower -c demo/laketower.yml tables query "select date_trunc('day', time) as day, avg(temperature_2m) as mean_temperature from weather where day between \$start_date and \$end_date group by day order by day desc" -p start_date 2025-01-29 -p end_date 2025-01-31
540
+
541
+ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
542
+ ┃ day ┃ mean_temperature ┃
543
+ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
544
+ │ 2025-01-31 00:00:00+01:00 │ 5.683333257834117 │
545
+ │ 2025-01-30 00:00:00+01:00 │ 8.900000015894571 │
546
+ │ 2025-01-29 00:00:00+01:00 │ 7.770833313465118 │
547
+ └───────────────────────────┴────────────────────┘
548
+ ```
549
+
530
550
  Export query results to CSV:
531
551
 
532
552
  ```bash
@@ -574,6 +594,22 @@ $ laketower -c demo/laketower.yml queries view daily_avg_temperature
574
594
  └───────────────────────────┴─────────────────┘
575
595
  ```
576
596
 
597
+ Executing a predefined query with parameters (here `start_date` and `end_date`):
598
+
599
+ ```bash
600
+ $ laketower -c demo/laketower.yml queries view daily_avg_temperature_params -p start_date 2025-02-01 -p end_date 2025-02-05
601
+
602
+ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
603
+ ┃ day ┃ avg_temperature ┃
604
+ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
605
+ │ 2025-02-01 00:00:00+01:00 │ 4.0 │
606
+ │ 2025-02-02 00:00:00+01:00 │ 4.0 │
607
+ │ 2025-02-03 00:00:00+01:00 │ 4.0 │
608
+ │ 2025-02-04 00:00:00+01:00 │ 3.0 │
609
+ │ 2025-02-05 00:00:00+01:00 │ 3.0 │
610
+ └───────────────────────────┴─────────────────┘
611
+ ```
612
+
577
613
  ## License
578
614
 
579
615
  Licensed under [Apache License 2.0](LICENSE)
@@ -0,0 +1,31 @@
1
+ laketower/__about__.py,sha256=baAcEjLSYFIeNZF51tOMmA_zAMhN8HvKael-UU-Ruec,22
2
+ laketower/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ laketower/__main__.py,sha256=czKxJKG8OfncnxWmpaOWx7b1JBwFnZNQi7wKSTncB4M,108
4
+ laketower/cli.py,sha256=QmfgqpQQIL5pyrgmnCvLiNUNcAKW6a1vnS6qTUfYcBk,16554
5
+ laketower/config.py,sha256=flRp9yb4DK4xQUJu5g6Mr_gSMtTyJ4_jBAb-08ROvqQ,3453
6
+ laketower/tables.py,sha256=PbbwEmN_MHF6BCVM4t2vEpKQus58-UOaDMYIVqsKvy0,10733
7
+ laketower/web.py,sha256=B77Zu2aUO3sLlaHoSSO8x8iV-BoVh2l5_Yg9bxu5l6A,13407
8
+ laketower/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ laketower/static/editor.bundle.js,sha256=Wa1bS0xWwKDjHKPTdXd-PX41JAGkCjd0VLBCF-SyXWk,1159343
10
+ laketower/static/editor.js,sha256=C8saQJH68X7CtdRyBmvrQxsDJ013YWoMfWF-6hzf_5s,15870
11
+ laketower/static/vendor/bootstrap/bootstrap.bundle.min.js,sha256=5P1JGBOIxI7FBAvT_mb1fCnI5n_NhQKzNUuW7Hq0fMc,80496
12
+ laketower/static/vendor/bootstrap-icons/bootstrap-icons.min.css,sha256=pdY4ejLKO67E0CM2tbPtq1DJ3VGDVVdqAR6j3ZwdiE4,87008
13
+ laketower/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff,sha256=9VUTt7WRy4SjuH_w406iTUgx1v7cIuVLkRymS1tUShU,180288
14
+ laketower/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2,sha256=bHVxA2ShylYEJncW9tKJl7JjGf2weM8R4LQqtm_y6mE,134044
15
+ laketower/static/vendor/halfmoon/halfmoon.min.css,sha256=RjeFzczeuZHCyS-Gvz-kleETzBF_o84ZRHukze_yv6o,369168
16
+ laketower/static/vendor/halfmoon/halfmoon.modern.css,sha256=DD6elX-jPmbFYPsGvzodUv2-9FHkxHlVtQi0_RJVULs,10576
17
+ laketower/templates/_base.html,sha256=2bVTAgdvZIop76oqzOGCkCf59KA14GBRWaIkEFcwZoI,3823
18
+ laketower/templates/index.html,sha256=dLF2Og0qgzBkvGyVRidRNzTv0u4o97ifOx1jVeig8Kg,59
19
+ laketower/templates/queries/view.html,sha256=pOx03hTR1MNycs9YfD8yFLR3VCLFStyd6CAwqdsJEEc,2551
20
+ laketower/templates/tables/_macros.html,sha256=sCI1TOFW0QA74oSXW87H6dNTudOs7n-FretnTPFcRh4,1174
21
+ laketower/templates/tables/history.html,sha256=a5GBLXCiLlbWno5eR0XT5i_oMAghylUBBFOpr27NB3Q,1853
22
+ laketower/templates/tables/import.html,sha256=bQZwRrv84tDBuf0AHJyc7L-PjW-XSoZhMHNDIo6TP4c,2604
23
+ laketower/templates/tables/index.html,sha256=saNdQbJAjMJAzayTk4rA5Mmw_bCXvor2WpghVmoWSAI,2507
24
+ laketower/templates/tables/query.html,sha256=GueaA2_JzqLWaopMGLoU8_axNZ1KtZKMt9Ku2ghHDCU,2602
25
+ laketower/templates/tables/statistics.html,sha256=h6TiQtFwiRWvPqDphcRRF1rZ886FP00UbJuMHuW5l6U,1827
26
+ laketower/templates/tables/view.html,sha256=ruiAX_S--wpodmgEbcQ-GT7BQzz-vzSCk4NpzlO3I80,3985
27
+ laketower-0.6.1.dist-info/METADATA,sha256=M4xxkXjq1zotCK6nFYcviaoAy2zefcqkPe4ZBYhYaIE,27664
28
+ laketower-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
+ laketower-0.6.1.dist-info/entry_points.txt,sha256=sJpQgRwdeZhRBudNqBTqtHPCE-uLC9YgFXJY2CTEyCk,53
30
+ laketower-0.6.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
31
+ laketower-0.6.1.dist-info/RECORD,,
@@ -1,23 +0,0 @@
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,,