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.

@@ -0,0 +1,282 @@
1
+
2
+ /*!
3
+ * ----------------------------------------------------------------------------
4
+ * Halfmoon CSS - Modern theme
5
+ * Copyright (c) 2023, Tahmid Khan | MIT License | https://www.gethalfmoon.com
6
+ * ----------------------------------------------------------------------------
7
+ * The above notice must be included in its entirety when this file is used.
8
+ */
9
+
10
+ /* Color palette */
11
+
12
+ [data-bs-core=modern] {
13
+ /* Gray */
14
+
15
+ --bs-slate-hue: 216;
16
+ --bs-slate-saturation: 20%;
17
+
18
+ /* Light gray */
19
+
20
+ --bs-lightgray-hue: var(--bs-slate-hue);
21
+ --bs-lightgray-saturation: var(--bs-slate-saturation);
22
+
23
+ /* Sable (almost black) */
24
+
25
+ --bs-sable-hue: var(--bs-darkgray-hue);
26
+ --bs-sable-saturation: var(--bs-darkgray-saturation);
27
+ --bs-sable-100-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 31%;
28
+ --bs-sable-200-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 29%;
29
+ --bs-sable-300-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 27%;
30
+ --bs-sable-400-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 25%;
31
+ --bs-sable-500-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 23%;
32
+ --bs-sable-600-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 21%;
33
+ --bs-sable-700-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 19%;
34
+ --bs-sable-800-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 17%;
35
+ --bs-sable-900-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 15%;
36
+ --bs-sable-100: hsl(var(--bs-sable-100-hsl));
37
+ --bs-sable-200: hsl(var(--bs-sable-200-hsl));
38
+ --bs-sable-300: hsl(var(--bs-sable-300-hsl));
39
+ --bs-sable-400: hsl(var(--bs-sable-400-hsl));
40
+ --bs-sable-500: hsl(var(--bs-sable-500-hsl));
41
+ --bs-sable-600: hsl(var(--bs-sable-600-hsl));
42
+ --bs-sable-700: hsl(var(--bs-sable-700-hsl));
43
+ --bs-sable-800: hsl(var(--bs-sable-800-hsl));
44
+ --bs-sable-900: hsl(var(--bs-sable-900-hsl));
45
+ --bs-sable-hsl: var(--bs-sable-500-hsl);
46
+ --bs-sable: hsl(var(--bs-sable-hsl));
47
+ --bs-sable-foreground-hsl: var(--bs-white-hsl);
48
+ --bs-sable-foreground: hsl(var(--bs-sable-foreground-hsl));
49
+ --bs-sable-text-emphasis-hsl: var(--bs-sable-600-hsl);
50
+ --bs-sable-text-emphasis: hsl(var(--bs-sable-text-emphasis-hsl));
51
+ --bs-sable-hover-bg: var(--bs-sable-600);
52
+ --bs-sable-active-bg: var(--bs-sable-700);
53
+ --bs-sable-bg-subtle: hsl(var(--bs-sable-hue), var(--bs-sable-saturation), 70%);
54
+ --bs-sable-border-subtle: var(--bs-sable-400);
55
+ --bs-sable-checkbox-svg: var(--bs-checkbox-svg-light);
56
+ --bs-sable-dash-svg: var(--bs-dash-svg-light);
57
+ --bs-sable-radio-svg: var(--bs-radio-svg-light);
58
+ --bs-sable-switch-svg: var(--bs-switch-svg-light);
59
+
60
+ /* Primary */
61
+
62
+ --bs-primary-hue: var(--bs-navy-hue);
63
+ --bs-primary-saturation: var(--bs-navy-saturation);
64
+ --bs-primary-100-hsl: var(--bs-navy-100-hsl);
65
+ --bs-primary-200-hsl: var(--bs-navy-200-hsl);
66
+ --bs-primary-300-hsl: var(--bs-navy-300-hsl);
67
+ --bs-primary-400-hsl: var(--bs-navy-400-hsl);
68
+ --bs-primary-500-hsl: var(--bs-navy-500-hsl);
69
+ --bs-primary-600-hsl: var(--bs-navy-600-hsl);
70
+ --bs-primary-700-hsl: var(--bs-navy-700-hsl);
71
+ --bs-primary-800-hsl: var(--bs-navy-800-hsl);
72
+ --bs-primary-900-hsl: var(--bs-navy-900-hsl);
73
+ --bs-primary-100: var(--bs-navy-100);
74
+ --bs-primary-200: var(--bs-navy-200);
75
+ --bs-primary-300: var(--bs-navy-300);
76
+ --bs-primary-400: var(--bs-navy-400);
77
+ --bs-primary-500: var(--bs-navy-500);
78
+ --bs-primary-600: var(--bs-navy-600);
79
+ --bs-primary-700: var(--bs-navy-700);
80
+ --bs-primary-800: var(--bs-navy-800);
81
+ --bs-primary-900: var(--bs-navy-900);
82
+ --bs-primary-hsl: var(--bs-navy-hsl);
83
+ --bs-primary: var(--bs-navy);
84
+ --bs-primary-foreground-hsl: var(--bs-navy-foreground-hsl);
85
+ --bs-primary-foreground: var(--bs-navy-foreground);
86
+ --bs-primary-text-emphasis-hsl: var(--bs-navy-text-emphasis-hsl);
87
+ --bs-primary-text-emphasis: var(--bs-navy-text-emphasis);
88
+ --bs-primary-hover-bg: var(--bs-navy-hover-bg);
89
+ --bs-primary-active-bg: var(--bs-navy-active-bg);
90
+ --bs-primary-bg-subtle: var(--bs-navy-bg-subtle);
91
+ --bs-primary-border-subtle: var(--bs-navy-border-subtle);
92
+ --bs-primary-checkbox-svg: var(--bs-navy-checkbox-svg);
93
+ --bs-primary-dash-svg: var(--bs-navy-dash-svg);
94
+ --bs-primary-radio-svg: var(--bs-navy-radio-svg);
95
+ --bs-primary-switch-svg: var(--bs-navy-switch-svg);
96
+
97
+ /* Info */
98
+
99
+ --bs-info-hue: var(--bs-blue-hue);
100
+ --bs-info-saturation: var(--bs-blue-saturation);
101
+ --bs-info-100-hsl: var(--bs-blue-100-hsl);
102
+ --bs-info-200-hsl: var(--bs-blue-200-hsl);
103
+ --bs-info-300-hsl: var(--bs-blue-300-hsl);
104
+ --bs-info-400-hsl: var(--bs-blue-400-hsl);
105
+ --bs-info-500-hsl: var(--bs-blue-500-hsl);
106
+ --bs-info-600-hsl: var(--bs-blue-600-hsl);
107
+ --bs-info-700-hsl: var(--bs-blue-700-hsl);
108
+ --bs-info-800-hsl: var(--bs-blue-800-hsl);
109
+ --bs-info-900-hsl: var(--bs-blue-900-hsl);
110
+ --bs-info-100: var(--bs-blue-100);
111
+ --bs-info-200: var(--bs-blue-200);
112
+ --bs-info-300: var(--bs-blue-300);
113
+ --bs-info-400: var(--bs-blue-400);
114
+ --bs-info-500: var(--bs-blue-500);
115
+ --bs-info-600: var(--bs-blue-600);
116
+ --bs-info-700: var(--bs-blue-700);
117
+ --bs-info-800: var(--bs-blue-800);
118
+ --bs-info-900: var(--bs-blue-900);
119
+ --bs-info-hsl: var(--bs-blue-hsl);
120
+ --bs-info: var(--bs-blue);
121
+ --bs-info-foreground-hsl: var(--bs-blue-foreground-hsl);
122
+ --bs-info-foreground: var(--bs-blue-foreground);
123
+ --bs-info-text-emphasis-hsl: var(--bs-blue-text-emphasis-hsl);
124
+ --bs-info-text-emphasis: var(--bs-blue-text-emphasis);
125
+ --bs-info-hover-bg: var(--bs-blue-hover-bg);
126
+ --bs-info-active-bg: var(--bs-blue-active-bg);
127
+ --bs-info-bg-subtle: var(--bs-blue-bg-subtle);
128
+ --bs-info-border-subtle: var(--bs-blue-border-subtle);
129
+ --bs-info-checkbox-svg: var(--bs-blue-checkbox-svg);
130
+ --bs-info-dash-svg: var(--bs-blue-dash-svg);
131
+ --bs-info-radio-svg: var(--bs-blue-radio-svg);
132
+ --bs-info-switch-svg: var(--bs-blue-switch-svg);
133
+ }
134
+
135
+ [data-bs-core=modern][data-bs-theme=dark] {
136
+ /* Dark gray */
137
+
138
+ --bs-darkgray-text-emphasis-hsl: var(--bs-darkgray-200-hsl);
139
+ --bs-darkgray-text-emphasis: hsl(var(--bs-darkgray-text-emphasis-hsl));
140
+
141
+ /* Sable (black) */
142
+
143
+ --bs-sable-text-emphasis-hsl: var(--bs-sable-400-hsl);
144
+ --bs-sable-text-emphasis: hsl(var(--bs-sable-text-emphasis-hsl));
145
+ --bs-sable-bg-subtle: hsl(var(--bs-sable-hue), var(--bs-sable-saturation), 14%);
146
+ --bs-sable-border-subtle: var(--bs-sable-600);
147
+
148
+ /* Blue */
149
+
150
+ --bs-blue-text-emphasis-hsl: var(--bs-blue-300-hsl);
151
+ --bs-blue-text-emphasis: hsl(var(--bs-blue-text-emphasis-hsl));
152
+
153
+ /* Primary */
154
+
155
+ --bs-primary-hue: var(--bs-sky-hue);
156
+ --bs-primary-saturation: var(--bs-sky-saturation);
157
+ --bs-primary-100-hsl: var(--bs-sky-100-hsl);
158
+ --bs-primary-200-hsl: var(--bs-sky-200-hsl);
159
+ --bs-primary-300-hsl: var(--bs-sky-300-hsl);
160
+ --bs-primary-400-hsl: var(--bs-sky-400-hsl);
161
+ --bs-primary-500-hsl: var(--bs-sky-500-hsl);
162
+ --bs-primary-600-hsl: var(--bs-sky-600-hsl);
163
+ --bs-primary-700-hsl: var(--bs-sky-700-hsl);
164
+ --bs-primary-800-hsl: var(--bs-sky-800-hsl);
165
+ --bs-primary-900-hsl: var(--bs-sky-900-hsl);
166
+ --bs-primary-100: var(--bs-sky-100);
167
+ --bs-primary-200: var(--bs-sky-200);
168
+ --bs-primary-300: var(--bs-sky-300);
169
+ --bs-primary-400: var(--bs-sky-400);
170
+ --bs-primary-500: var(--bs-sky-500);
171
+ --bs-primary-600: var(--bs-sky-600);
172
+ --bs-primary-700: var(--bs-sky-700);
173
+ --bs-primary-800: var(--bs-sky-800);
174
+ --bs-primary-900: var(--bs-sky-900);
175
+ --bs-primary-hsl: var(--bs-sky-hsl);
176
+ --bs-primary: var(--bs-sky);
177
+ --bs-primary-foreground-hsl: var(--bs-sky-foreground-hsl);
178
+ --bs-primary-foreground: var(--bs-sky-foreground);
179
+ --bs-primary-text-emphasis-hsl: var(--bs-sky-text-emphasis-hsl);
180
+ --bs-primary-text-emphasis: var(--bs-sky-text-emphasis);
181
+ --bs-primary-hover-bg: var(--bs-sky-hover-bg);
182
+ --bs-primary-active-bg: var(--bs-sky-active-bg);
183
+ --bs-primary-bg-subtle: var(--bs-sky-bg-subtle);
184
+ --bs-primary-border-subtle: var(--bs-sky-border-subtle);
185
+ --bs-primary-checkbox-svg: var(--bs-sky-checkbox-svg);
186
+ --bs-primary-dash-svg: var(--bs-sky-dash-svg);
187
+ --bs-primary-radio-svg: var(--bs-sky-radio-svg);
188
+ --bs-primary-switch-svg: var(--bs-sky-switch-svg);
189
+
190
+ /* Info */
191
+
192
+ --bs-info-text-emphasis-hsl: var(--bs-blue-text-emphasis-hsl);
193
+ --bs-info-text-emphasis: var(--bs-blue-text-emphasis);
194
+ --bs-info-bg-subtle: var(--bs-blue-bg-subtle);
195
+ --bs-info-border-subtle: var(--bs-blue-border-subtle);
196
+ }
197
+
198
+ /* Variables */
199
+
200
+ [data-bs-core=modern] {
201
+ /* Link */
202
+
203
+ --bs-link-color-hsl: var(--bs-info-text-emphasis-hsl);
204
+ --bs-link-hover-color-hsl: var(--bs-info-hsl);
205
+
206
+ /* Content (used as needed in cards, panels, menus, etc.) */
207
+
208
+ --bs-content-bg-hsl: var(--bs-body-bg-hsl);
209
+ --bs-content-border-color: var(--bs-border-color);
210
+
211
+ /* Form */
212
+
213
+ --bs-form-focus-border-color: var(--bs-info-border-subtle);
214
+ --bs-form-focus-shadow-hsl: var(--bs-info-hsl);
215
+ --bs-form-check-focus-border-color: var(--bs-info-border-subtle);
216
+ }
217
+
218
+ [data-bs-core=modern]:not([data-bs-theme=dark]) {
219
+ /* Background */
220
+
221
+ --bs-body-bg-hsl: var(--bs-white-hsl);
222
+ --bs-secondary-bg-hsl: var(--bs-lightgray-hue), var(--bs-lightgray-saturation), 98.75%;
223
+ --bs-tertiary-bg-hsl: var(--bs-lightgray-hue), var(--bs-lightgray-saturation), 97.5%;
224
+
225
+ /* Border */
226
+
227
+ --bs-border-color: var(--bs-lightgray-700);
228
+ --bs-border-color-light: var(--bs-lightgray-500);
229
+ }
230
+
231
+ [data-bs-core=modern][data-bs-theme=dark] {
232
+ /* Background */
233
+
234
+ --bs-body-bg-hsl: var(--bs-sable-900-hsl);
235
+ --bs-secondary-bg-hsl: var(--bs-sable-800-hsl);
236
+ --bs-tertiary-bg-hsl: var(--bs-sable-700-hsl);
237
+
238
+ /* Border */
239
+
240
+ --bs-border-color: var(--bs-gray-900);
241
+
242
+ /* Content (used as needed in cards, panels, menus, etc.) */
243
+
244
+ --bs-content-floating-bg-hsl: var(--bs-sable-hue), var(--bs-sable-saturation), 16.5%;
245
+
246
+ /* Action (used as needed in buttons, inputs, menu items, page links, etc.) */
247
+
248
+ --bs-action-border-color: var(--bs-border-color);
249
+
250
+ /* Contextual buttons */
251
+
252
+ --bs-ctx-btn-border-color: transparent;
253
+ --bs-ctx-btn-bg-clip: border-box;
254
+
255
+ /* Action bar (used as needed in range, progress, etc.) */
256
+
257
+ --bs-actionbar-border-color: hsla(var(--bs-white-hsl), 0.075);
258
+ --bs-progresstrack-border-width: 0;
259
+ --bs-progresstrack-box-shadow: inset 0 0 0 var(--bs-border-width) var(--bs-actionbar-border-color);
260
+ --bs-progresstrack-bg-clip: border-box;
261
+ }
262
+
263
+ /* Sidebar */
264
+
265
+ [data-bs-core=modern] .sidebar {
266
+ --bs-sidebar-item-padding-x: 1rem;
267
+ --bs-sidebar-item-padding-y: 0.25rem;
268
+ --bs-sidebar-header-font-weight: var(--bs-font-weight-bold);
269
+ --bs-sidebar-divider-bg: var(--bs-sidebar-bg);
270
+ }
271
+
272
+ [data-bs-core=modern] .sidebar-nav .nav-link {
273
+ border-left: var(--bs-border-width) solid var(--bs-border-color-light);
274
+ }
275
+
276
+ [data-bs-core=modern] .sidebar-nav .nav-link.active,
277
+ [data-bs-core=modern] .sidebar-nav .nav-link.show {
278
+ font-weight: var(--bs-font-weight-bold);
279
+ border-color: currentColor;
280
+ -webkit-font-smoothing: antialiased;
281
+ -moz-osx-font-smoothing: grayscale;
282
+ }
laketower/tables.py CHANGED
@@ -1,5 +1,6 @@
1
+ import enum
1
2
  from datetime import datetime, timezone
2
- from typing import Any, Protocol
3
+ from typing import Any, BinaryIO, Protocol, TextIO
3
4
 
4
5
  import deltalake
5
6
  import duckdb
@@ -17,6 +18,15 @@ from laketower.config import ConfigTable, TableFormats
17
18
  DEFAULT_LIMIT = 10
18
19
 
19
20
 
21
+ class ImportModeEnum(str, enum.Enum):
22
+ append = "append"
23
+ overwrite = "overwrite"
24
+
25
+
26
+ class ImportFileFormatEnum(str, enum.Enum):
27
+ csv = "csv"
28
+
29
+
20
30
  class TableMetadata(pydantic.BaseModel):
21
31
  table_format: TableFormats
22
32
  name: str | None = None
@@ -43,17 +53,112 @@ class TableHistory(pydantic.BaseModel):
43
53
 
44
54
 
45
55
  class TableProtocol(Protocol): # pragma: no cover
56
+ @classmethod
57
+ def is_valid(cls, table_config: ConfigTable) -> bool: ...
58
+ def __init__(self, table_config: ConfigTable) -> None: ...
46
59
  def metadata(self) -> TableMetadata: ...
47
60
  def schema(self) -> pa.Schema: ...
48
61
  def history(self) -> TableHistory: ...
49
62
  def dataset(self, version: int | str | None = None) -> padataset.Dataset: ...
63
+ def import_data(
64
+ self, data: pd.DataFrame, mode: ImportModeEnum = ImportModeEnum.append
65
+ ) -> None: ...
50
66
 
51
67
 
52
68
  class DeltaTable:
53
69
  def __init__(self, table_config: ConfigTable):
54
70
  super().__init__()
55
71
  self.table_config = table_config
56
- self._impl = deltalake.DeltaTable(table_config.uri)
72
+ storage_options = self._generate_storage_options(table_config)
73
+ self._impl = deltalake.DeltaTable(
74
+ table_config.uri, storage_options=storage_options
75
+ )
76
+
77
+ @classmethod
78
+ def _generate_storage_options(
79
+ cls, table_config: ConfigTable
80
+ ) -> dict[str, str] | None:
81
+ # documentation from `object-store` Rust crate:
82
+ # - s3: https://docs.rs/object_store/latest/object_store/aws/enum.AmazonS3ConfigKey.html
83
+ # - adls: https://docs.rs/object_store/latest/object_store/azure/enum.AzureConfigKey.html
84
+ storage_options = None
85
+ conn_s3 = (
86
+ table_config.connection.s3
87
+ if table_config.connection and table_config.connection.s3
88
+ else None
89
+ )
90
+ conn_adls = (
91
+ table_config.connection.adls
92
+ if table_config.connection and table_config.connection.adls
93
+ else None
94
+ )
95
+ if conn_s3:
96
+ storage_options = (
97
+ {
98
+ "aws_access_key_id": conn_s3.s3_access_key_id,
99
+ "aws_secret_access_key": conn_s3.s3_secret_access_key.get_secret_value(),
100
+ "aws_allow_http": str(conn_s3.s3_allow_http).lower(),
101
+ }
102
+ | ({"aws_region": conn_s3.s3_region} if conn_s3.s3_region else {})
103
+ | (
104
+ {"aws_endpoint_url": str(conn_s3.s3_endpoint_url).rstrip("/")}
105
+ if conn_s3.s3_endpoint_url
106
+ else {}
107
+ )
108
+ )
109
+ elif conn_adls:
110
+ storage_options = (
111
+ {
112
+ "azure_storage_account_name": conn_adls.adls_account_name,
113
+ "azure_use_azure_cli": str(conn_adls.use_azure_cli).lower(),
114
+ }
115
+ | (
116
+ {
117
+ "azure_storage_access_key": conn_adls.adls_access_key.get_secret_value()
118
+ }
119
+ if conn_adls.adls_access_key
120
+ else {}
121
+ )
122
+ | (
123
+ {"azure_storage_sas_key": conn_adls.adls_sas_key.get_secret_value()}
124
+ if conn_adls.adls_sas_key
125
+ else {}
126
+ )
127
+ | (
128
+ {"azure_storage_tenant_id": conn_adls.adls_tenant_id}
129
+ if conn_adls.adls_tenant_id
130
+ else {}
131
+ )
132
+ | (
133
+ {"azure_storage_client_id": conn_adls.adls_client_id}
134
+ if conn_adls.adls_client_id
135
+ else {}
136
+ )
137
+ | (
138
+ {
139
+ "azure_storage_client_secret": conn_adls.adls_client_secret.get_secret_value()
140
+ }
141
+ if conn_adls.adls_client_secret
142
+ else {}
143
+ )
144
+ | (
145
+ {
146
+ "azure_msi_endpoint": str(conn_adls.azure_msi_endpoint).rstrip(
147
+ "/"
148
+ )
149
+ }
150
+ if conn_adls.azure_msi_endpoint
151
+ else {}
152
+ )
153
+ )
154
+ return storage_options
155
+
156
+ @classmethod
157
+ def is_valid(cls, table_config: ConfigTable) -> bool:
158
+ storage_options = cls._generate_storage_options(table_config)
159
+ return deltalake.DeltaTable.is_deltatable(
160
+ table_config.uri, storage_options=storage_options
161
+ )
57
162
 
58
163
  def metadata(self) -> TableMetadata:
59
164
  metadata = self._impl.metadata()
@@ -96,10 +201,43 @@ class DeltaTable:
96
201
  self._impl.load_as_version(version)
97
202
  return self._impl.to_pyarrow_dataset()
98
203
 
204
+ def import_data(
205
+ self, data: pd.DataFrame, mode: ImportModeEnum = ImportModeEnum.append
206
+ ) -> None:
207
+ deltalake.write_deltalake(
208
+ self.table_config.uri, data, mode=mode.value, schema_mode="merge"
209
+ )
210
+
99
211
 
100
212
  def load_table(table_config: ConfigTable) -> TableProtocol:
101
- format_handler = {TableFormats.delta: DeltaTable}
102
- return format_handler[table_config.table_format](table_config)
213
+ format_handler: dict[TableFormats, type[TableProtocol]] = {
214
+ TableFormats.delta: DeltaTable
215
+ }
216
+ table_handler = format_handler[table_config.table_format]
217
+ if not table_handler.is_valid(table_config):
218
+ raise ValueError(f"Invalid table: {table_config.uri}")
219
+ return table_handler(table_config)
220
+
221
+
222
+ def load_datasets(table_configs: list[ConfigTable]) -> dict[str, padataset.Dataset]:
223
+ tables_dataset = {}
224
+ for table_config in table_configs:
225
+ try:
226
+ tables_dataset[table_config.name] = load_table(table_config).dataset()
227
+ except ValueError:
228
+ pass
229
+ return tables_dataset
230
+
231
+
232
+ def extract_query_parameter_names(sql: str) -> set[str]:
233
+ parsed_sql = sqlglot.parse(sql, dialect=sqlglot.dialects.duckdb.DuckDB)
234
+ return {
235
+ str(node.this)
236
+ for statement in parsed_sql
237
+ if statement is not None
238
+ for node in statement.walk()
239
+ if isinstance(node, sqlglot.expressions.Placeholder)
240
+ }
103
241
 
104
242
 
105
243
  def generate_table_query(
@@ -110,32 +248,64 @@ def generate_table_query(
110
248
  sort_desc: str | None = None,
111
249
  ) -> str:
112
250
  query_expr = (
113
- sqlglot.select(*(cols or ["*"])).from_(table_name).limit(limit or DEFAULT_LIMIT)
251
+ sqlglot.select(*([f'"{col}"' for col in cols] if cols else ["*"]))
252
+ .from_(f'"{table_name}"')
253
+ .limit(limit or DEFAULT_LIMIT)
114
254
  )
115
255
  if sort_asc:
116
256
  query_expr = query_expr.order_by(f"{sort_asc} asc")
117
257
  elif sort_desc:
118
258
  query_expr = query_expr.order_by(f"{sort_desc} desc")
119
- return sqlglot.Generator(dialect=sqlglot.dialects.duckdb.DuckDB).generate(
120
- query_expr
121
- )
259
+ return query_expr.sql(dialect=sqlglot.dialects.duckdb.DuckDB, identify="always")
122
260
 
123
261
 
124
262
  def generate_table_statistics_query(table_name: str) -> str:
125
- return (
126
- f"SELECT column_name, count, avg, std, min, max FROM (SUMMARIZE {table_name})" # nosec B608
263
+ summarize_expr = sqlglot.expressions.Summarize(
264
+ this=sqlglot.expressions.Table(this=f'"{table_name}"')
127
265
  )
266
+ subquery_expr = sqlglot.expressions.Subquery(this=summarize_expr)
267
+ query_expr = sqlglot.select(
268
+ "column_name", "count", "avg", "std", "min", "max"
269
+ ).from_(subquery_expr)
270
+ return query_expr.sql(dialect=sqlglot.dialects.duckdb.DuckDB, identify="always")
128
271
 
129
272
 
130
273
  def execute_query(
131
- tables_datasets: dict[str, padataset.Dataset], sql_query: str
274
+ tables_datasets: dict[str, padataset.Dataset],
275
+ sql_query: str,
276
+ sql_params: dict[str, str] = {},
132
277
  ) -> pd.DataFrame:
278
+ if not sql_query:
279
+ raise ValueError("Error: Cannot execute empty SQL query")
280
+
133
281
  try:
134
282
  conn = duckdb.connect()
135
283
  for table_name, table_dataset in tables_datasets.items():
284
+ # ATTACH IF NOT EXISTS ':memory:' AS {catalog.name};
285
+ # CREATE SCHEMA IF NOT EXISTS {catalog.name}.{database.name};
286
+ # USE {catalog.name}.{database.name};
287
+ # CREATE VIEW IF NOT EXISTS {table.name} AS FROM {table.name}_dataset;
288
+
136
289
  view_name = f"{table_name}_view"
137
290
  conn.register(view_name, table_dataset)
138
- conn.execute(f"create table {table_name} as select * from {view_name}") # nosec B608
139
- return conn.execute(sql_query).df()
291
+ conn.execute(f'create table "{table_name}" as select * from "{view_name}"') # nosec B608
292
+ return conn.execute(sql_query, parameters=sql_params).df()
140
293
  except duckdb.Error as e:
141
294
  raise ValueError(str(e)) from e
295
+
296
+
297
+ def import_file_to_table(
298
+ table_config: ConfigTable,
299
+ file_path: BinaryIO | TextIO,
300
+ mode: ImportModeEnum = ImportModeEnum.append,
301
+ file_format: ImportFileFormatEnum = ImportFileFormatEnum.csv,
302
+ delimiter: str = ",",
303
+ encoding: str = "utf-8",
304
+ ) -> int:
305
+ file_format_handler = {
306
+ ImportFileFormatEnum.csv: lambda f, d, e: pd.read_csv(f, sep=d, encoding=e)
307
+ }
308
+ table = load_table(table_config)
309
+ df = file_format_handler[file_format](file_path, delimiter, encoding)
310
+ table.import_data(df, mode=mode)
311
+ return len(df)
@@ -4,12 +4,10 @@
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <title>Laketower</title>
8
- <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
9
- <link href="https://cdn.jsdelivr.net/npm/halfmoon@2.0.2/css/halfmoon.min.css" rel="stylesheet"
10
- integrity="sha256-RjeFzczeuZHCyS+Gvz+kleETzBF/o84ZRHukze/yv6o=" crossorigin="anonymous">
11
- <link href="https://cdn.jsdelivr.net/npm/halfmoon@2.0.2/css/cores/halfmoon.modern.css" rel="stylesheet"
12
- integrity="sha256-DD6elX+jPmbFYPsGvzodUv2+9FHkxHlVtQi0/RJVULs=" crossorigin="anonymous">
7
+ <title>{{ app_metadata.app_name }}</title>
8
+ <link href="{{ url_for('static', path='/vendor/bootstrap-icons/bootstrap-icons.min.css') }}" rel="stylesheet">
9
+ <link href="{{ url_for('static', path='/vendor/halfmoon/halfmoon.min.css') }}" rel="stylesheet">
10
+ <link href="{{ url_for('static', path='/vendor/halfmoon/halfmoon.modern.css') }}" rel="stylesheet">
13
11
  </head>
14
12
 
15
13
  <body class="ps-md-sbwidth">
@@ -17,7 +15,7 @@
17
15
  <nav class="sidebar offcanvas-start offcanvas-md" tabindex="-1" id="sidebar">
18
16
  <div class="offcanvas-header border-bottom">
19
17
  <a class="sidebar-brand" href="/">
20
- Laketower
18
+ {{ app_metadata.app_name }}
21
19
  </a>
22
20
  <button
23
21
  type="button"
@@ -28,8 +26,8 @@
28
26
  >
29
27
  </button>
30
28
  </div>
31
- <div class="offcanvas-body">
32
- <ul class="sidebar-nav">
29
+ <div class="offcanvas-body d-flex flex-column">
30
+ <ul class="sidebar-nav flex-grow-1 overflow-auto">
33
31
  <li>
34
32
  <h6 class="sidebar-header">Tables</h6>
35
33
  </li>
@@ -68,6 +66,17 @@
68
66
  </li>
69
67
  {% endfor %}
70
68
  </ul>
69
+
70
+ <div class="mt-auto pt-3 border-top flex-shrink-0">
71
+ <small class="text-muted px-3 d-block">
72
+ <a href="https://github.com/datalpia/laketower/releases/tag/{{ app_metadata.app_version }}"
73
+ target="_blank"
74
+ class="text-muted text-decoration-none">
75
+ v{{ app_metadata.app_version }}
76
+ <i class="bi-box-arrow-up-right ms-1" style="font-size: 0.7em;"></i>
77
+ </a>
78
+ </small>
79
+ </div>
71
80
  </div>
72
81
  </nav>
73
82
 
@@ -78,8 +87,8 @@
78
87
  </button>
79
88
 
80
89
  <a href="/" class="navbar-brand d-flex align-items-center d-md-none ms-auto ms-md-0">
81
- Laketower
82
- </a>
90
+ {{ app_metadata.app_name }}
91
+ </a>
83
92
  </div>
84
93
  </nav>
85
94
  </header>
@@ -90,14 +99,8 @@
90
99
  </div>
91
100
  </main>
92
101
 
93
- <footer></footer>
94
-
95
- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
96
- integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
97
- crossorigin="anonymous"></script>
98
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
99
- integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
100
- crossorigin="anonymous"></script>
102
+ <script src="{{ url_for('static', path='/vendor/bootstrap/bootstrap.bundle.min.js') }}"></script>
103
+ {% block extra_scripts %}{% endblock %}
101
104
  </body>
102
105
 
103
106
  </html>
@@ -5,16 +5,30 @@
5
5
  <div class="col">
6
6
  <h2 class="mb-3">{{ query.title }}</h2>
7
7
 
8
+ {% if query.description %}
9
+ <div>
10
+ {{ query.description | render_markdown | safe }}
11
+ </div>
12
+ {% endif %}
13
+
8
14
  <form action="{{ request.url.path }}" method="get">
9
- <div class="mb-3">
10
- <textarea disabled name="sql" rows="5" class="form-control">{{ query.sql }}</textarea>
15
+ {% if sql_params|length %}
16
+ <h3 class="mb-3">Parameters</h3>
17
+ {% for param_name, param_value in sql_params.items() %}
18
+ <div class="row mb-3">
19
+ <label for="param-{{ param_name }}" class="col-form-label col-sm-2">{{ param_name }}</label>
20
+ <div class="col-sm-4">
21
+ <input id="param-{{ param_name }}" class="form-control" name="{{ param_name }}" value="{{ param_value }}">
22
+ </div>
11
23
  </div>
24
+ {% endfor %}
25
+ {% endif %}
12
26
 
13
27
  <div class="mb-3">
14
28
  <div class="d-flex justify-content-end">
15
29
  <div class="row">
16
30
  <div class="col">
17
- <a href="/tables/query?sql={{ query.sql | urlencode }}" class="btn btn-secondary" type="button" >
31
+ <a href="/tables/query?sql={{ query.sql | urlencode }}{% if request.query_params | length > 0 %}&{{ request.query_params.multi_items() | urlencode}}{% endif %}" class="btn btn-secondary" type="button">
18
32
  <i class="bi-code" aria-hidden="true"></i> Edit SQL
19
33
  </a>
20
34
  </div>
@@ -34,6 +48,12 @@
34
48
  {{ error.message }}
35
49
  </div>
36
50
  {% else %}
51
+ <div class="d-flex justify-content-between align-items-center mb-2">
52
+ <h3>Results</h3>
53
+ <a href="/tables/query/csv?sql={{ query.sql | urlencode }}" class="btn btn-outline-secondary btn-sm">
54
+ <i class="bi-download" aria-hidden="true"></i> Export CSV
55
+ </a>
56
+ </div>
37
57
  <div class="table-responsive">
38
58
  <table class="table table-sm table-bordered table-striped table-hover">
39
59
  <thead>
@@ -57,4 +77,4 @@
57
77
  {% endif %}
58
78
  </div>
59
79
  </div>
60
- {% endblock %}
80
+ {% endblock %}
@@ -12,5 +12,8 @@
12
12
  <li class="nav-item">
13
13
  <a class="nav-link{% if current == 'history' %} active{% endif %}"{% if current == 'history' %} aria-current="true"{% endif %} href="/tables/{{ table_id }}/history">History</a>
14
14
  </li>
15
+ <li class="nav-item">
16
+ <a class="nav-link{% if current == 'import' %} active{% endif %}"{% if current == 'import' %} aria-current="true"{% endif %} href="/tables/{{ table_id }}/import">Import</a>
17
+ </li>
15
18
  </ul>
16
19
  {%- endmacro %}