meerschaum 3.0.5__py3-none-any.whl → 3.0.6__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.
@@ -192,6 +192,16 @@ STATIC_CONFIG: Dict[str, Any] = {
192
192
  'requirements': {'database'},
193
193
  'defaults': {},
194
194
  },
195
+ 'geopackage': {
196
+ 'engine': 'sqlite',
197
+ 'create_engine': _default_create_engine_args,
198
+ 'omit_create_engine': {'method',},
199
+ 'to_sql': {
200
+ 'method': 'multi',
201
+ },
202
+ 'requirements': {'database'},
203
+ 'defaults': {},
204
+ },
195
205
  'duckdb': {
196
206
  'engine': 'duckdb',
197
207
  'create_engine': {},
@@ -99,7 +99,7 @@ def _copy_pipes(
99
99
  new_pipe = Pipe(
100
100
  ck, mk, lk,
101
101
  instance=instance_keys,
102
- parameters=pipe.parameters.copy(),
102
+ parameters=pipe.get_parameters(apply_symlinks=False),
103
103
  )
104
104
 
105
105
  if new_pipe.get_id(debug=debug) is not None:
@@ -349,7 +349,7 @@ def _delete_connectors(
349
349
  for c in to_delete:
350
350
  try:
351
351
  ### Remove database files.
352
- if c.flavor in ('sqlite', 'duckdb'):
352
+ if c.flavor in ('sqlite', 'duckdb', 'geopackage'):
353
353
  if ':memory:' not in c.database and pathlib.Path(c.database).exists():
354
354
  if force or yes_no(
355
355
  f"Detected '{c.flavor}' database '{c.database}'. "
@@ -51,7 +51,7 @@ from meerschaum.utils.misc import filter_keywords, flatten_list, string_to_dict
51
51
  from meerschaum.utils.yaml import yaml
52
52
  from meerschaum.actions import get_subactions, actions
53
53
  from meerschaum._internal.arguments._parser import parser
54
- from meerschaum.connectors.sql._fetch import set_pipe_query
54
+ from meerschaum.connectors.sql._fetch import set_pipe_query, get_pipe_query
55
55
  dash = attempt_import('dash', lazy=False, check_update=CHECK_UPDATE)
56
56
  dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
57
57
  dcc, html = import_dcc(check_update=CHECK_UPDATE), import_html(check_update=CHECK_UPDATE)
@@ -919,14 +919,31 @@ def update_pipe_sql_click(n_clicks, sql_editor_text):
919
919
  success, msg = False, f"Unable to update SQL definition for {pipe}."
920
920
  else:
921
921
  try:
922
- set_pipe_query(pipe, sql_editor_text)
923
- success, msg = pipe.edit(debug=debug)
922
+ success, msg = set_pipe_query(pipe, sql_editor_text)
924
923
  except Exception as e:
925
924
  success, msg = False, f"Invalid SQL query:\n{e}"
926
925
 
927
926
  return alert_from_success_tuple((success, msg))
928
927
 
929
928
 
929
+ @dash_app.callback(
930
+ Output({'type': 'sql-editor', 'index': MATCH}, 'value'),
931
+ Input({'type': 'resolve-symlinks-switch', 'index': MATCH}, 'value'),
932
+ prevent_initial_call=True,
933
+ )
934
+ def toggle_sql_symlinks(value):
935
+ triggered = dash.callback_context.triggered
936
+ if triggered[0]['value'] is None:
937
+ raise PreventUpdate
938
+
939
+ pipe = pipe_from_ctx(triggered, 'value')
940
+ if pipe is None:
941
+ raise PreventUpdate
942
+
943
+ query = get_pipe_query(pipe, apply_symlinks=value)
944
+ return query
945
+
946
+
930
947
  @dash_app.callback(
931
948
  Output({'type': 'sync-success-div', 'index': MATCH}, 'children'),
932
949
  Input({'type': 'update-sync-button', 'index': MATCH}, 'n_clicks'),
@@ -690,16 +690,32 @@ def accordion_items_from_pipe(
690
690
  items_bodies['sql'] = html.Div([
691
691
  sql_editor,
692
692
  html.Br(),
693
- dbc.Row([
694
- dbc.Col([update_sql_button], width=2),
695
- dbc.Col([
696
- html.Div(
697
- id={'type': 'update-sql-success-div', 'index': pipe_meta_str}
698
- )
693
+ dbc.Row(
694
+ [
695
+ dbc.Col(update_sql_button, width='auto'),
696
+ dbc.Col(
697
+ dbc.Switch(
698
+ label="Resolve symlinks",
699
+ value=True,
700
+ id={'type': 'resolve-symlinks-switch', 'index': pipe_meta_str},
701
+ ),
702
+ width='auto',
703
+ ),
699
704
  ],
700
- width=True,
701
- )
702
- ]),
705
+ justify='between',
706
+ align='center',
707
+ ),
708
+ dbc.Row(
709
+ dbc.Col(
710
+ html.Div(
711
+ id={
712
+ 'type': 'update-sql-success-div',
713
+ 'index': pipe_meta_str,
714
+ },
715
+ ),
716
+ width=True,
717
+ ),
718
+ ),
703
719
  ])
704
720
 
705
721
  if 'recent-data' in active_items:
@@ -2,4 +2,4 @@
2
2
  Specify the Meerschaum release version.
3
3
  """
4
4
 
5
- __version__ = "3.0.5"
5
+ __version__ = "3.0.6"
@@ -36,6 +36,7 @@ class SQLConnector(InstanceConnector):
36
36
  exec_queries,
37
37
  get_connection,
38
38
  _cleanup_connections,
39
+ _init_geopackage_table,
39
40
  )
40
41
  from meerschaum.utils.sql import test_connection
41
42
  from ._fetch import fetch, get_pipe_metadef
@@ -176,7 +177,7 @@ class SQLConnector(InstanceConnector):
176
177
  **kw
177
178
  )
178
179
 
179
- if self.__dict__.get('flavor', None) == 'sqlite':
180
+ if self.__dict__.get('flavor', None) in ('sqlite', 'geopackage'):
180
181
  self._reset_attributes()
181
182
  self._set_attributes(
182
183
  'sql',
@@ -187,7 +188,7 @@ class SQLConnector(InstanceConnector):
187
188
  ### For backwards compatability reasons, set the path for sql:local if its missing.
188
189
  if self.label == 'local' and not self.__dict__.get('database', None):
189
190
  from meerschaum.config._paths import SQLITE_DB_PATH
190
- self.database = str(SQLITE_DB_PATH)
191
+ self.database = SQLITE_DB_PATH.as_posix()
191
192
 
192
193
  ### ensure flavor and label are set accordingly
193
194
  if 'flavor' not in self.__dict__:
@@ -291,7 +292,7 @@ class SQLConnector(InstanceConnector):
291
292
  """
292
293
  if self.flavor in ('duckdb', 'oracle'):
293
294
  return False
294
- if self.flavor == 'sqlite':
295
+ if self.flavor in ('sqlite', 'geopackage'):
295
296
  return ':memory:' not in self.URI
296
297
  return True
297
298
 
@@ -24,6 +24,7 @@ flavor_clis = {
24
24
  'mariadb' : 'mycli',
25
25
  'percona' : 'mycli',
26
26
  'sqlite' : 'litecli',
27
+ 'geopackage' : 'litecli',
27
28
  'mssql' : 'mssqlcli',
28
29
  'duckdb' : 'gadwall',
29
30
  }
@@ -105,7 +106,7 @@ def _cli_exit(
105
106
  ### NOTE: The `DATABASE_URL` property must be initialized first in case the database is not
106
107
  ### yet defined (e.g. 'sql:local').
107
108
  cli_arg_str = self.DATABASE_URL
108
- if self.flavor in ('sqlite', 'duckdb'):
109
+ if self.flavor in ('sqlite', 'duckdb', 'geopackage'):
109
110
  cli_arg_str = (
110
111
  str(self.database)
111
112
  if 'database' in self.__dict__
@@ -19,6 +19,7 @@ from meerschaum._internal.static import STATIC_CONFIG
19
19
  flavor_configs = STATIC_CONFIG['sql']['create_engine_flavors']
20
20
  install_flavor_drivers = {
21
21
  'sqlite': ['aiosqlite'],
22
+ 'geopackage': ['aiosqlite'],
22
23
  'duckdb': ['duckdb', 'duckdb_engine'],
23
24
  'mysql': ['pymysql'],
24
25
  'mariadb': ['pymysql'],
@@ -91,7 +92,7 @@ def create_engine(
91
92
  sqlalchemy.dialects.registry.register(*flavor_dialects[self.flavor])
92
93
 
93
94
  ### self._sys_config was deepcopied and can be updated safely
94
- if self.flavor in ("sqlite", "duckdb"):
95
+ if self.flavor in ("sqlite", "duckdb", "geopackage"):
95
96
  engine_str = f"{_engine}:///{_database}" if not _uri else _uri
96
97
  if 'create_engine' not in self._sys_config:
97
98
  self._sys_config['create_engine'] = {}
@@ -221,7 +221,11 @@ def get_pipe_metadef(
221
221
  return meta_def.rstrip()
222
222
 
223
223
 
224
- def get_pipe_query(pipe: mrsm.Pipe, warn: bool = True) -> Union[str, None]:
224
+ def get_pipe_query(
225
+ pipe: mrsm.Pipe,
226
+ apply_symlinks: bool = True,
227
+ warn: bool = True,
228
+ ) -> Union[str, None]:
225
229
  """
226
230
  Run through the possible keys for a pipe's query and return the first match.
227
231
 
@@ -233,14 +237,17 @@ def get_pipe_query(pipe: mrsm.Pipe, warn: bool = True) -> Union[str, None]:
233
237
  import textwrap
234
238
  from meerschaum.utils.warnings import warn as _warn
235
239
  from meerschaum.utils.pipes import replace_pipes_syntax
236
- if pipe.parameters.get('fetch', {}).get('definition', None):
237
- definition = pipe.parameters['fetch']['definition']
240
+
241
+ parameters = pipe.get_parameters(apply_symlinks=apply_symlinks)
242
+
243
+ if parameters.get('fetch', {}).get('definition', None):
244
+ definition = parameters['fetch']['definition']
238
245
  elif pipe.parameters.get('definition', None):
239
- definition = pipe.parameters['definition']
246
+ definition = parameters['definition']
240
247
  elif pipe.parameters.get('query', None):
241
- definition = pipe.parameters['query']
248
+ definition = parameters['query']
242
249
  elif pipe.parameters.get('sql', None):
243
- definition = pipe.parameters['sql']
250
+ definition = parameters['sql']
244
251
  else:
245
252
  if warn:
246
253
  _warn(
@@ -249,11 +256,12 @@ def get_pipe_query(pipe: mrsm.Pipe, warn: bool = True) -> Union[str, None]:
249
256
  )
250
257
  return None
251
258
 
252
- definition = replace_pipes_syntax(definition)
259
+ if apply_symlinks:
260
+ definition = replace_pipes_syntax(definition)
253
261
  return textwrap.dedent(definition.lstrip().rstrip())
254
262
 
255
263
 
256
- def set_pipe_query(pipe: mrsm.Pipe, query: str) -> None:
264
+ def set_pipe_query(pipe: mrsm.Pipe, query: str) -> mrsm.SuccessTuple:
257
265
  """
258
266
  Run through the possible keys for a pipe's query and set the first match.
259
267
 
@@ -262,22 +270,17 @@ def set_pipe_query(pipe: mrsm.Pipe, query: str) -> None:
262
270
  - query
263
271
  - sql
264
272
  """
265
- if 'fetch' in pipe.parameters and 'definition' in pipe.parameters['fetch']:
266
- if pipe.parameters.get('fetch', None) is None:
267
- pipe.parameters['fetch'] = {}
268
- dict_to_set = pipe.parameters['fetch']
269
- key_to_set = 'definition'
270
- elif 'definition' in pipe.parameters:
271
- dict_to_set = pipe.parameters
272
- key_to_set = 'definition'
273
- elif 'query' in pipe.parameters:
274
- dict_to_set = pipe.parameters
275
- key_to_set = 'query'
273
+ parameters = pipe.get_parameters()
274
+ if 'fetch' in parameters and 'definition' in parameters['fetch']:
275
+ patch_dict = {'fetch': {'defintion': query}}
276
+ elif 'definition' in parameters:
277
+ patch_dict = {'definition': query}
278
+ elif 'query' in parameters:
279
+ patch_dict = {'query': query}
276
280
  else:
277
- dict_to_set = pipe.parameters
278
- key_to_set = 'sql'
281
+ patch_dict = {'sql': query}
279
282
 
280
- dict_to_set[key_to_set] = query
283
+ return pipe.update_parameters(patch_dict)
281
284
 
282
285
 
283
286
  def _simple_fetch_query(
@@ -706,7 +706,7 @@ def get_create_index_queries(
706
706
  ),
707
707
  ])
708
708
  elif not autoincrement and primary_key in existing_cols_pd_types:
709
- if self.flavor == 'sqlite':
709
+ if self.flavor in ('sqlite', 'geopackage'):
710
710
  new_table_name = sql_item_name(
711
711
  f'_new_{pipe.target}',
712
712
  self.flavor,
@@ -888,7 +888,7 @@ def get_create_index_queries(
888
888
  f"CREATE UNIQUE INDEX {unique_index_name} ON {_pipe_name} ({unique_index_cols_str})"
889
889
  )
890
890
  constraint_queries = [create_unique_index_query]
891
- if self.flavor != 'sqlite':
891
+ if self.flavor not in ('sqlite', 'geopackage'):
892
892
  constraint_queries.append(add_constraint_query)
893
893
  if upsert and indices_cols_str:
894
894
  index_queries[unique_index_name] = constraint_queries
@@ -977,7 +977,12 @@ def get_drop_index_queries(
977
977
  if ix_unquoted.lower() not in existing_indices:
978
978
  continue
979
979
 
980
- if ix_key == 'unique' and upsert and self.flavor not in ('sqlite',) and not is_hypertable:
980
+ if (
981
+ ix_key == 'unique'
982
+ and upsert
983
+ and self.flavor not in ('sqlite', 'geopackage')
984
+ and not is_hypertable
985
+ ):
981
986
  constraint_name_unquoted = ix_unquoted.replace('IX_', 'UQ_')
982
987
  constraint_name = sql_item_name(constraint_name_unquoted, self.flavor)
983
988
  constraint_or_index = (
@@ -1558,6 +1563,11 @@ def create_pipe_table_from_df(
1558
1563
  get_create_schema_if_not_exists_queries,
1559
1564
  )
1560
1565
  from meerschaum.utils.dtypes.sql import get_db_type_from_pd_type
1566
+ if self.flavor == 'geopackage':
1567
+ init_success, init_msg = self._init_geopackage_table(df, pipe.target, debug=debug)
1568
+ if not init_success:
1569
+ return init_success, init_msg
1570
+
1561
1571
  primary_key = pipe.columns.get('primary', None)
1562
1572
  primary_key_typ = (
1563
1573
  pipe.dtypes.get(primary_key, str(df.dtypes.get(primary_key, 'int')))
@@ -1614,6 +1624,9 @@ def create_pipe_table_from_df(
1614
1624
  if success
1615
1625
  else f"Failed to create {target_name}."
1616
1626
  )
1627
+ if success and self.flavor == 'geopackage':
1628
+ return self._init_geopackage_table(df, target, debug=debug)
1629
+
1617
1630
  return success, msg
1618
1631
 
1619
1632
 
@@ -3078,7 +3091,7 @@ def get_pipe_columns_types(
3078
3091
  if not pipe.exists(debug=debug):
3079
3092
  return {}
3080
3093
 
3081
- if self.flavor not in ('oracle', 'mysql', 'mariadb', 'sqlite'):
3094
+ if self.flavor not in ('oracle', 'mysql', 'mariadb', 'sqlite', 'geopackage'):
3082
3095
  return get_table_cols_types(
3083
3096
  pipe.target,
3084
3097
  self,
@@ -3436,7 +3449,7 @@ def get_alter_columns_queries(
3436
3449
  for col, (db_typ, typ) in altered_cols.items()
3437
3450
  }
3438
3451
 
3439
- if self.flavor == 'sqlite':
3452
+ if self.flavor in ('sqlite', 'geopackage'):
3440
3453
  temp_table_name = '-' + session_id + '_' + target
3441
3454
  rename_query = (
3442
3455
  "ALTER TABLE "
@@ -3884,7 +3897,7 @@ def get_pipe_schema(self, pipe: mrsm.Pipe) -> Union[str, None]:
3884
3897
  -------
3885
3898
  A schema string or `None` if nothing is configured.
3886
3899
  """
3887
- if self.flavor == 'sqlite':
3900
+ if self.flavor in ('sqlite', 'geopackage'):
3888
3901
  return self.schema
3889
3902
  return pipe.parameters.get('schema', self.schema)
3890
3903
 
@@ -27,7 +27,7 @@ _bulk_flavors = {
27
27
  }
28
28
  ### flavors that do not support chunks
29
29
  _disallow_chunks_flavors = ['duckdb']
30
- _max_chunks_flavors = {'sqlite': 1000}
30
+ _max_chunks_flavors = {'sqlite': 1000, 'geopackage': 1000}
31
31
  SKIP_READ_TRANSACTION_FLAVORS: list[str] = ['mssql']
32
32
 
33
33
 
@@ -1347,3 +1347,33 @@ def _cleanup_connections(self) -> None:
1347
1347
  connection.close()
1348
1348
  except Exception:
1349
1349
  pass
1350
+
1351
+
1352
+ def _init_geopackage_table(
1353
+ self,
1354
+ df: 'pd.DataFrame',
1355
+ table: str,
1356
+ debug: bool = False,
1357
+ ) -> SuccessTuple:
1358
+ """
1359
+ Initialize the geopackage schema tables from a DataFrame.
1360
+ """
1361
+ import pathlib
1362
+ database = self.__dict__.get('database', self.parse_uri(self.URI).get('database', None))
1363
+ if not database:
1364
+ return False, f"Could not determine database for '{self}'."
1365
+
1366
+ database_path = pathlib.Path(database)
1367
+ mode = 'w' if not database_path.exists() else 'a'
1368
+
1369
+ try:
1370
+ df.head(0).to_file(
1371
+ database_path.as_posix(),
1372
+ layer=table,
1373
+ driver='GPKG',
1374
+ index=False,
1375
+ mode=mode,
1376
+ )
1377
+ except Exception as e:
1378
+ return False, f"Failed to init table '{table}':\n{e}"
1379
+ return True, "Success"
@@ -51,7 +51,7 @@ def from_uri(
51
51
  if 'database' not in params:
52
52
  error("Unable to determine the database from the provided URI.")
53
53
 
54
- if flavor in ('sqlite', 'duckdb'):
54
+ if flavor in ('sqlite', 'duckdb', 'geopackage'):
55
55
  if params['database'] == ':memory:':
56
56
  params['label'] = label or f'memory_{flavor}'
57
57
  else:
@@ -74,7 +74,7 @@ def get_tables(
74
74
  cache_expired = refresh or (
75
75
  (
76
76
  _check_create_cache(conn, debug=debug)
77
- if conn.flavor != 'sqlite'
77
+ if conn.flavor not in ('sqlite', 'duckdb', 'geopackage')
78
78
  else True
79
79
  )
80
80
  if conn.type == 'sql'
@@ -264,7 +264,7 @@ def get_tables(
264
264
 
265
265
  _write_create_cache(mrsm.get_connector(str(mrsm_instance)), debug=debug)
266
266
 
267
- if conn.flavor != 'sqlite':
267
+ if conn.flavor not in ('sqlite', 'duckdb', 'geopackage'):
268
268
  with open(pickle_path, 'wb') as f:
269
269
  pickle.dump(conn.metadata, f)
270
270
 
@@ -155,7 +155,7 @@ def columns(self) -> Union[Dict[str, str], None]:
155
155
  cols = self.parameters.get('columns', {})
156
156
  if not isinstance(cols, dict):
157
157
  return {}
158
- return {col_ix: col for col_ix, col in cols.items() if col}
158
+ return {col_ix: col for col_ix, col in cols.items() if col and col_ix}
159
159
 
160
160
 
161
161
  @columns.setter
@@ -13,7 +13,7 @@ import functools
13
13
 
14
14
  import meerschaum as mrsm
15
15
  from meerschaum.utils.typing import Callable, Any, Union, Optional, Dict, List, Tuple
16
- from meerschaum.utils.threading import Lock, RLock
16
+ from meerschaum.utils.threading import RLock
17
17
  from meerschaum.core.Plugin import Plugin
18
18
 
19
19
  _api_plugins: Dict[str, List[Callable[['fastapi.App'], Any]]] = {}
@@ -760,7 +760,7 @@ def unload_custom_actions(plugins: Optional[List[str]] = None, debug: bool = Fal
760
760
  from meerschaum._internal.entry import _shell
761
761
  import meerschaum._internal.shell as shell_pkg
762
762
 
763
- plugins = plugins or list(_plugins_actions.keys())
763
+ plugins = plugins if plugins is not None else list(_plugins_actions)
764
764
 
765
765
  for plugin in plugins:
766
766
  action_names = _plugins_actions.get(plugin, [])
@@ -799,7 +799,7 @@ def unload_plugins(
799
799
  _synced_symlinks = False
800
800
 
801
801
  all_plugins = get_plugins_names()
802
- plugins = plugins or all_plugins
802
+ plugins = plugins if plugins is not None else all_plugins
803
803
  if debug:
804
804
  dprint(f"Unloading plugins: {plugins}")
805
805
 
@@ -38,6 +38,7 @@ def get_pipes(
38
38
  method: str = 'registered',
39
39
  workers: Optional[int] = None,
40
40
  debug: bool = False,
41
+ _cache_parameters: bool = True,
41
42
  **kw: Any
42
43
  ) -> Union[PipesDict, List[mrsm.Pipe], Dict[str, mrsm.Pipe]]:
43
44
  """
@@ -94,7 +95,6 @@ def get_pipes(
94
95
 
95
96
  **kw: Any:
96
97
  Keyword arguments to pass to the `meerschaum.Pipe` constructor.
97
-
98
98
 
99
99
  Returns
100
100
  -------
@@ -209,7 +209,7 @@ def get_pipes(
209
209
  pipe_tags_or_parameters
210
210
  if isinstance(pipe_tags_or_parameters, list)
211
211
  else (
212
- pipe_tags_or_parameters.get('tags', None)
212
+ pipe_tags_or_parameters.get('tags', [])
213
213
  if isinstance(pipe_tags_or_parameters, dict)
214
214
  else None
215
215
  )