meerschaum 2.8.4__py3-none-any.whl → 2.9.0rc1__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.
Files changed (40) hide show
  1. meerschaum/api/_chunks.py +67 -0
  2. meerschaum/api/dash/callbacks/custom.py +23 -2
  3. meerschaum/api/dash/callbacks/dashboard.py +41 -3
  4. meerschaum/api/dash/components.py +27 -19
  5. meerschaum/api/dash/pages/dashboard.py +11 -9
  6. meerschaum/api/dash/pages/plugins.py +31 -27
  7. meerschaum/api/dash/webterm.py +6 -3
  8. meerschaum/api/resources/static/css/dash.css +1 -1
  9. meerschaum/api/resources/templates/termpage.html +4 -0
  10. meerschaum/api/routes/_pipes.py +191 -78
  11. meerschaum/config/_default.py +4 -0
  12. meerschaum/config/_version.py +1 -1
  13. meerschaum/connectors/api/_APIConnector.py +12 -1
  14. meerschaum/connectors/api/_pipes.py +27 -15
  15. meerschaum/connectors/api/_plugins.py +51 -45
  16. meerschaum/connectors/api/_request.py +1 -1
  17. meerschaum/connectors/parse.py +1 -2
  18. meerschaum/connectors/sql/_SQLConnector.py +3 -0
  19. meerschaum/connectors/sql/_cli.py +1 -0
  20. meerschaum/connectors/sql/_create_engine.py +51 -4
  21. meerschaum/connectors/sql/_pipes.py +13 -2
  22. meerschaum/connectors/sql/_sql.py +35 -4
  23. meerschaum/core/Pipe/_data.py +1 -2
  24. meerschaum/plugins/_Plugin.py +21 -5
  25. meerschaum/plugins/__init__.py +6 -4
  26. meerschaum/utils/dataframe.py +87 -2
  27. meerschaum/utils/dtypes/__init__.py +182 -1
  28. meerschaum/utils/dtypes/sql.py +114 -2
  29. meerschaum/utils/formatting/_shell.py +1 -4
  30. meerschaum/utils/packages/_packages.py +3 -0
  31. meerschaum/utils/sql.py +17 -5
  32. meerschaum/utils/venv/__init__.py +2 -0
  33. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0rc1.dist-info}/METADATA +10 -1
  34. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0rc1.dist-info}/RECORD +40 -39
  35. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0rc1.dist-info}/WHEEL +1 -1
  36. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0rc1.dist-info}/LICENSE +0 -0
  37. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0rc1.dist-info}/NOTICE +0 -0
  38. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0rc1.dist-info}/entry_points.txt +0 -0
  39. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0rc1.dist-info}/top_level.txt +0 -0
  40. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0rc1.dist-info}/zip-safe +0 -0
@@ -7,21 +7,25 @@ Manage plugins via the API connector
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- from meerschaum.utils.typing import Union, Any, Optional, SuccessTuple, Mapping, Sequence
10
+
11
+ import meerschaum as mrsm
12
+ from meerschaum.utils.typing import Union, Any, Optional, SuccessTuple, List, Dict
13
+
11
14
 
12
15
  def plugin_r_url(
13
- plugin: Union[meerschaum.core.Plugin, str]
14
- ) -> str:
16
+ plugin: Union[mrsm.core.Plugin, str],
17
+ ) -> str:
15
18
  """Generate a relative URL path from a Plugin."""
16
19
  from meerschaum.config.static import STATIC_CONFIG
17
20
  return f"{STATIC_CONFIG['api']['endpoints']['plugins']}/{plugin}"
18
21
 
22
+
19
23
  def register_plugin(
20
- self,
21
- plugin: meerschaum.core.Plugin,
22
- make_archive: bool = True,
23
- debug: bool = False,
24
- ) -> SuccessTuple:
24
+ self,
25
+ plugin: mrsm.core.Plugin,
26
+ make_archive: bool = True,
27
+ debug: bool = False,
28
+ ) -> SuccessTuple:
25
29
  """Register a plugin and upload its archive."""
26
30
  import json
27
31
  archive_path = plugin.make_tar(debug=debug) if make_archive else plugin.archive_path
@@ -34,27 +38,30 @@ def register_plugin(
34
38
  r_url = plugin_r_url(plugin)
35
39
  try:
36
40
  response = self.post(r_url, files=files, params=metadata, debug=debug)
37
- except Exception as e:
41
+ except Exception:
38
42
  return False, f"Failed to register plugin '{plugin}'."
39
43
  finally:
40
44
  file_pointer.close()
41
45
 
42
46
  try:
43
47
  success, msg = json.loads(response.text)
44
- except Exception as e:
48
+ except Exception:
45
49
  return False, response.text
46
50
 
47
51
  return success, msg
48
52
 
53
+
49
54
  def install_plugin(
50
- self,
51
- name: str,
52
- skip_deps: bool = False,
53
- force: bool = False,
54
- debug: bool = False
55
- ) -> SuccessTuple:
55
+ self,
56
+ name: str,
57
+ skip_deps: bool = False,
58
+ force: bool = False,
59
+ debug: bool = False
60
+ ) -> SuccessTuple:
56
61
  """Download and attempt to install a plugin from the API."""
57
- import os, pathlib, json
62
+ import os
63
+ import pathlib
64
+ import json
58
65
  from meerschaum.core import Plugin
59
66
  from meerschaum.config._paths import PLUGINS_TEMP_RESOURCES_PATH
60
67
  from meerschaum.utils.debug import dprint
@@ -75,41 +82,39 @@ def install_plugin(
75
82
  success, msg = tuple(j)
76
83
  elif isinstance(j, dict) and 'detail' in j:
77
84
  success, msg = False, fail_msg
78
- except Exception as e:
85
+ except Exception:
79
86
  success, msg = False, fail_msg
80
87
  return success, msg
81
88
  plugin = Plugin(name, archive_path=archive_path, repo_connector=self)
82
89
  return plugin.install(skip_deps=skip_deps, force=force, debug=debug)
83
90
 
91
+
84
92
  def get_plugins(
85
- self,
86
- user_id : Optional[int] = None,
87
- search_term : Optional[str] = None,
88
- debug : bool = False
89
- ) -> Sequence[str]:
93
+ self,
94
+ user_id: Optional[int] = None,
95
+ search_term: Optional[str] = None,
96
+ debug: bool = False
97
+ ) -> List[str]:
90
98
  """Return a list of registered plugin names.
91
99
 
92
100
  Parameters
93
101
  ----------
94
- user_id :
102
+ user_id: Optional[int], default None
95
103
  If specified, return all plugins from a certain user.
96
- user_id : Optional[int] :
97
- (Default value = None)
98
- search_term : Optional[str] :
99
- (Default value = None)
100
- debug : bool :
101
- (Default value = False)
104
+
105
+ search_term: Optional[str], default None
106
+ If specified, return plugins beginning with this string.
102
107
 
103
108
  Returns
104
109
  -------
105
-
110
+ A list of plugin names.
106
111
  """
107
112
  import json
108
- from meerschaum.utils.warnings import warn, error
113
+ from meerschaum.utils.warnings import error
109
114
  from meerschaum.config.static import STATIC_CONFIG
110
115
  response = self.get(
111
116
  STATIC_CONFIG['api']['endpoints']['plugins'],
112
- params = {'user_id' : user_id, 'search_term' : search_term},
117
+ params = {'user_id': user_id, 'search_term': search_term},
113
118
  use_token = True,
114
119
  debug = debug
115
120
  )
@@ -120,11 +125,12 @@ def get_plugins(
120
125
  error(response.text)
121
126
  return plugins
122
127
 
128
+
123
129
  def get_plugin_attributes(
124
- self,
125
- plugin: meerschaum.core.Plugin,
126
- debug: bool = False
127
- ) -> Mapping[str, Any]:
130
+ self,
131
+ plugin: mrsm.core.Plugin,
132
+ debug: bool = False
133
+ ) -> Dict[str, Any]:
128
134
  """
129
135
  Return a plugin's attributes.
130
136
  """
@@ -136,7 +142,7 @@ def get_plugin_attributes(
136
142
  if isinstance(attributes, str) and attributes and attributes[0] == '{':
137
143
  try:
138
144
  attributes = json.loads(attributes)
139
- except Exception as e:
145
+ except Exception:
140
146
  pass
141
147
  if not isinstance(attributes, dict):
142
148
  error(response.text)
@@ -145,23 +151,23 @@ def get_plugin_attributes(
145
151
  return {}
146
152
  return attributes
147
153
 
154
+
148
155
  def delete_plugin(
149
- self,
150
- plugin: meerschaum.core.Plugin,
151
- debug: bool = False
152
- ) -> SuccessTuple:
156
+ self,
157
+ plugin: mrsm.core.Plugin,
158
+ debug: bool = False
159
+ ) -> SuccessTuple:
153
160
  """Delete a plugin from an API repository."""
154
161
  import json
155
162
  r_url = plugin_r_url(plugin)
156
163
  try:
157
164
  response = self.delete(r_url, debug=debug)
158
- except Exception as e:
165
+ except Exception:
159
166
  return False, f"Failed to delete plugin '{plugin}'."
160
167
 
161
168
  try:
162
169
  success, msg = json.loads(response.text)
163
- except Exception as e:
170
+ except Exception:
164
171
  return False, response.text
165
172
 
166
173
  return success, msg
167
-
@@ -95,7 +95,7 @@ def make_request(
95
95
  return self.session.request(
96
96
  method.upper(),
97
97
  request_url,
98
- headers = headers,
98
+ headers=headers,
99
99
  **kwargs
100
100
  )
101
101
 
@@ -140,7 +140,6 @@ def is_valid_connector_keys(
140
140
  """
141
141
  try:
142
142
  success = parse_connector_keys(keys, construct=False) is not None
143
- except Exception as e:
143
+ except Exception:
144
144
  success = False
145
145
  return success
146
-
@@ -151,6 +151,9 @@ class SQLConnector(Connector):
151
151
  if uri.startswith('timescaledb://'):
152
152
  uri = uri.replace('timescaledb://', 'postgresql+psycopg://', 1)
153
153
  flavor = 'timescaledb'
154
+ if uri.startswith('postgis://'):
155
+ uri = uri.replace('postgis://', 'postgresql+psycopg://', 1)
156
+ flavor = 'postgis'
154
157
  kw['uri'] = uri
155
158
  from_uri_params = self.from_uri(kw['uri'], as_dict=True)
156
159
  label = label or from_uri_params.get('label', None)
@@ -15,6 +15,7 @@ from meerschaum.utils.typing import SuccessTuple
15
15
 
16
16
  flavor_clis = {
17
17
  'postgresql' : 'pgcli',
18
+ 'postgis' : 'pgcli',
18
19
  'timescaledb' : 'pgcli',
19
20
  'cockroachdb' : 'pgcli',
20
21
  'citus' : 'pgcli',
@@ -7,6 +7,7 @@ This module contains the logic that builds the sqlalchemy engine string.
7
7
  """
8
8
 
9
9
  import traceback
10
+ import meerschaum as mrsm
10
11
  from meerschaum.utils.debug import dprint
11
12
 
12
13
  ### determine driver and requirements from flavor
@@ -47,6 +48,16 @@ flavor_configs = {
47
48
  'port': 5432,
48
49
  },
49
50
  },
51
+ 'postgis': {
52
+ 'engine': 'postgresql+psycopg',
53
+ 'create_engine': default_create_engine_args,
54
+ 'omit_create_engine': {'method',},
55
+ 'to_sql': {},
56
+ 'requirements': default_requirements,
57
+ 'defaults': {
58
+ 'port': 5432,
59
+ },
60
+ },
50
61
  'citus': {
51
62
  'engine': 'postgresql+psycopg',
52
63
  'create_engine': default_create_engine_args,
@@ -162,6 +173,7 @@ install_flavor_drivers = {
162
173
  'mariadb': ['pymysql'],
163
174
  'timescaledb': ['psycopg'],
164
175
  'postgresql': ['psycopg'],
176
+ 'postgis': ['psycopg', 'geoalchemy'],
165
177
  'citus': ['psycopg'],
166
178
  'cockroachdb': ['psycopg', 'sqlalchemy_cockroachdb', 'sqlalchemy_cockroachdb.psycopg'],
167
179
  'mssql': ['pyodbc'],
@@ -198,8 +210,7 @@ def create_engine(
198
210
  warn=False,
199
211
  )
200
212
  if self.flavor == 'mssql':
201
- pyodbc = attempt_import('pyodbc', debug=debug, lazy=False, warn=False)
202
- pyodbc.pooling = False
213
+ _init_mssql_sqlalchemy()
203
214
  if self.flavor in require_patching_flavors:
204
215
  from meerschaum.utils.packages import determine_version, _monkey_patch_get_distribution
205
216
  import pathlib
@@ -257,8 +268,8 @@ def create_engine(
257
268
 
258
269
  ### Sometimes the timescaledb:// flavor can slip in.
259
270
  if _uri and self.flavor in _uri:
260
- if self.flavor == 'timescaledb':
261
- engine_str = engine_str.replace(f'{self.flavor}', 'postgresql', 1)
271
+ if self.flavor in ('timescaledb', 'postgis'):
272
+ engine_str = engine_str.replace(self.flavor, 'postgresql', 1)
262
273
  elif _uri.startswith('postgresql://'):
263
274
  engine_str = engine_str.replace('postgresql://', 'postgresql+psycopg2://')
264
275
 
@@ -313,3 +324,39 @@ def create_engine(
313
324
  if include_uri:
314
325
  return engine, engine_str
315
326
  return engine
327
+
328
+
329
+ def _init_mssql_sqlalchemy():
330
+ """
331
+ When first instantiating a SQLAlchemy connection to MSSQL,
332
+ monkey-patch `pyodbc` handling in SQLAlchemy.
333
+ """
334
+ pyodbc, sqlalchemy_dialects_mssql_pyodbc = mrsm.attempt_import(
335
+ 'pyodbc',
336
+ 'sqlalchemy.dialects.mssql.pyodbc',
337
+ lazy=False,
338
+ warn=False,
339
+ )
340
+ pyodbc.pooling = False
341
+
342
+ MSDialect_pyodbc = sqlalchemy_dialects_mssql_pyodbc.MSDialect_pyodbc
343
+
344
+ def _handle_geometry(val):
345
+ from binascii import hexlify
346
+ hex_str = f"0x{hexlify(val).decode().upper()}"
347
+ return hex_str
348
+
349
+ def custom_on_connect(self):
350
+ super_ = super(MSDialect_pyodbc, self).on_connect()
351
+
352
+ def _on_connect(conn):
353
+ if super_ is not None:
354
+ super_(conn)
355
+
356
+ self._setup_timestampoffset_type(conn)
357
+ conn.add_output_converter(-151, _handle_geometry)
358
+
359
+ return _on_connect
360
+
361
+ ### TODO: Parse proprietary MSSQL geometry bytes into WKB.
362
+ # MSDialect_pyodbc.on_connect = custom_on_connect
@@ -731,7 +731,7 @@ def get_create_index_queries(
731
731
  ) + f"{primary_key_name})"
732
732
  ),
733
733
  ])
734
- elif self.flavor in ('citus', 'postgresql', 'duckdb'):
734
+ elif self.flavor in ('citus', 'postgresql', 'duckdb', 'postgis'):
735
735
  primary_queries.extend([
736
736
  (
737
737
  f"ALTER TABLE {_pipe_name}\n"
@@ -1052,6 +1052,7 @@ def get_pipe_data(
1052
1052
  attempt_cast_to_numeric,
1053
1053
  attempt_cast_to_uuid,
1054
1054
  attempt_cast_to_bytes,
1055
+ attempt_cast_to_geometry,
1055
1056
  are_dtypes_equal,
1056
1057
  )
1057
1058
  from meerschaum.utils.dtypes.sql import get_pd_type_from_db_type
@@ -1138,6 +1139,11 @@ def get_pipe_data(
1138
1139
  for col, typ in pipe.dtypes.items()
1139
1140
  if typ == 'bytes' and col in dtypes
1140
1141
  ]
1142
+ geometry_columns = [
1143
+ col
1144
+ for col, typ in pipe.dtypes.items()
1145
+ if typ.startswith('geometry') and col in dtypes
1146
+ ]
1141
1147
 
1142
1148
  kw['coerce_float'] = kw.get('coerce_float', (len(numeric_columns) == 0))
1143
1149
 
@@ -1162,6 +1168,11 @@ def get_pipe_data(
1162
1168
  continue
1163
1169
  df[col] = df[col].apply(attempt_cast_to_bytes)
1164
1170
 
1171
+ for col in geometry_columns:
1172
+ if col not in df.columns:
1173
+ continue
1174
+ df[col] = df[col].apply(attempt_cast_to_geometry)
1175
+
1165
1176
  if self.flavor == 'sqlite':
1166
1177
  ignore_dt_cols = [
1167
1178
  col
@@ -1511,7 +1522,7 @@ def get_pipe_attributes(
1511
1522
  if isinstance(parameters, str) and parameters[0] == '{':
1512
1523
  parameters = json.loads(parameters)
1513
1524
  attributes['parameters'] = parameters
1514
- except Exception as e:
1525
+ except Exception:
1515
1526
  attributes['parameters'] = {}
1516
1527
 
1517
1528
  return attributes
@@ -17,7 +17,7 @@ from meerschaum.utils.debug import dprint
17
17
  from meerschaum.utils.warnings import warn
18
18
 
19
19
  ### database flavors that can use bulk insert
20
- _bulk_flavors = {'postgresql', 'timescaledb', 'citus', 'mssql'}
20
+ _bulk_flavors = {'postgresql', 'postgis', 'timescaledb', 'citus', 'mssql'}
21
21
  ### flavors that do not support chunks
22
22
  _disallow_chunks_flavors = ['duckdb']
23
23
  _max_chunks_flavors = {'sqlite': 1000}
@@ -798,6 +798,7 @@ def to_sql(
798
798
  get_numeric_cols,
799
799
  get_uuid_cols,
800
800
  get_bytes_cols,
801
+ get_geometry_cols,
801
802
  )
802
803
  from meerschaum.utils.dtypes import (
803
804
  are_dtypes_equal,
@@ -805,7 +806,9 @@ def to_sql(
805
806
  encode_bytes_for_bytea,
806
807
  serialize_bytes,
807
808
  serialize_decimal,
809
+ serialize_geometry,
808
810
  json_serialize_value,
811
+ get_geometry_type_srid,
809
812
  )
810
813
  from meerschaum.utils.dtypes.sql import (
811
814
  PD_TO_SQLALCHEMY_DTYPES_FLAVORS,
@@ -822,6 +825,7 @@ def to_sql(
822
825
 
823
826
  bytes_cols = get_bytes_cols(df)
824
827
  numeric_cols = get_numeric_cols(df)
828
+ geometry_cols = get_geometry_cols(df)
825
829
  ### NOTE: This excludes non-numeric serialized Decimals (e.g. SQLite).
826
830
  numeric_cols_dtypes = {
827
831
  col: typ
@@ -830,7 +834,6 @@ def to_sql(
830
834
  col in df.columns
831
835
  and 'numeric' in str(typ).lower()
832
836
  )
833
-
834
837
  }
835
838
  numeric_cols.extend([col for col in numeric_cols_dtypes if col not in numeric_cols])
836
839
  numeric_cols_precisions_scales = {
@@ -841,6 +844,22 @@ def to_sql(
841
844
  )
842
845
  for col, typ in numeric_cols_dtypes.items()
843
846
  }
847
+ geometry_cols_dtypes = {
848
+ col: typ
849
+ for col, typ in kw.get('dtype', {}).items()
850
+ if (
851
+ col in df.columns
852
+ and 'geometry' in str(typ).lower() or 'geography' in str(typ).lower()
853
+ )
854
+ }
855
+ geometry_cols.extend([col for col in geometry_cols_dtypes if col not in geometry_cols])
856
+ geometry_cols_types_srids = {
857
+ col: (typ.geometry_type, typ.srid)
858
+ if hasattr(typ, 'srid')
859
+ else get_geometry_type_srid()
860
+ for col, typ in geometry_cols_dtypes.items()
861
+ }
862
+
844
863
  cols_pd_types = {
845
864
  col: get_pd_type_from_db_type(str(typ))
846
865
  for col, typ in kw.get('dtype', {}).items()
@@ -856,8 +875,9 @@ def to_sql(
856
875
  }
857
876
 
858
877
  enable_bulk_insert = mrsm.get_config(
859
- 'system', 'connectors', 'sql', 'bulk_insert'
860
- ).get(self.flavor, False)
878
+ 'system', 'connectors', 'sql', 'bulk_insert', self.flavor,
879
+ warn=False,
880
+ ) or False
861
881
  stats = {'target': name}
862
882
  ### resort to defaults if None
863
883
  copied = False
@@ -901,6 +921,17 @@ def to_sql(
901
921
  )
902
922
  )
903
923
 
924
+ for col in geometry_cols:
925
+ geometry_type, srid = geometry_cols_types_srids.get(col, get_geometry_type_srid())
926
+ with warnings.catch_warnings():
927
+ warnings.simplefilter("ignore")
928
+ df[col] = df[col].apply(
929
+ functools.partial(
930
+ serialize_geometry,
931
+ as_wkt=(self.flavor == 'mssql')
932
+ )
933
+ )
934
+
904
935
  stats['method'] = method.__name__ if hasattr(method, '__name__') else str(method)
905
936
 
906
937
  default_chunksize = self._sys_config.get('chunksize', None)
@@ -88,9 +88,8 @@ def get_data(
88
88
  limit: Optional[int], default None
89
89
  If provided, cap the dataframe to this many rows.
90
90
 
91
- fresh: bool, default True
91
+ fresh: bool, default False
92
92
  If `True`, skip local cache and directly query the instance connector.
93
- Defaults to `True`.
94
93
 
95
94
  debug: bool, default False
96
95
  Verbosity toggle.
@@ -450,7 +450,7 @@ class Plugin:
450
450
  success, msg = False, (
451
451
  f"Failed to run post-install setup for plugin '{self}'." + '\n' +
452
452
  f"Check `setup()` in '{self.__file__}' for more information " +
453
- f"(no error message provided)."
453
+ "(no error message provided)."
454
454
  )
455
455
  else:
456
456
  success, msg = True, success_msg
@@ -458,7 +458,7 @@ class Plugin:
458
458
  success = True
459
459
  msg = (
460
460
  f"Post-install for plugin '{self}' returned None. " +
461
- f"Assuming plugin successfully installed."
461
+ "Assuming plugin successfully installed."
462
462
  )
463
463
  warn(msg)
464
464
  else:
@@ -469,7 +469,7 @@ class Plugin:
469
469
  )
470
470
 
471
471
  _ongoing_installations.remove(self.full_name)
472
- module = self.module
472
+ _ = self.module
473
473
  return success, msg
474
474
 
475
475
 
@@ -716,13 +716,14 @@ class Plugin:
716
716
  return required
717
717
 
718
718
 
719
- def get_required_plugins(self, debug: bool=False) -> List[meerschaum.plugins.Plugin]:
719
+ def get_required_plugins(self, debug: bool=False) -> List[mrsm.plugins.Plugin]:
720
720
  """
721
721
  Return a list of required Plugin objects.
722
722
  """
723
723
  from meerschaum.utils.warnings import warn
724
724
  from meerschaum.config import get_config
725
725
  from meerschaum.config.static import STATIC_CONFIG
726
+ from meerschaum.connectors.parse import is_valid_connector_keys
726
727
  plugins = []
727
728
  _deps = self.get_dependencies(debug=debug)
728
729
  sep = STATIC_CONFIG['plugins']['repo_separator']
@@ -731,11 +732,13 @@ class Plugin:
731
732
  if _d.startswith('plugin:') and len(_d) > len('plugin:')
732
733
  ]
733
734
  default_repo_keys = get_config('meerschaum', 'default_repository')
735
+ skipped_repo_keys = set()
736
+
734
737
  for _plugin_name in plugin_names:
735
738
  if sep in _plugin_name:
736
739
  try:
737
740
  _plugin_name, _repo_keys = _plugin_name.split(sep)
738
- except Exception as e:
741
+ except Exception:
739
742
  _repo_keys = default_repo_keys
740
743
  warn(
741
744
  f"Invalid repo keys for required plugin '{_plugin_name}'.\n "
@@ -744,7 +747,20 @@ class Plugin:
744
747
  )
745
748
  else:
746
749
  _repo_keys = default_repo_keys
750
+
751
+ if _repo_keys in skipped_repo_keys:
752
+ continue
753
+
754
+ if not is_valid_connector_keys(_repo_keys):
755
+ warn(
756
+ f"Invalid connector '{_repo_keys}'.\n"
757
+ f" Skipping required plugins from repository '{_repo_keys}'",
758
+ stack=False,
759
+ )
760
+ continue
761
+
747
762
  plugins.append(Plugin(_plugin_name, repo=_repo_keys))
763
+
748
764
  return plugins
749
765
 
750
766
 
@@ -166,10 +166,11 @@ def post_sync_hook(
166
166
 
167
167
  _plugin_endpoints_to_pages = {}
168
168
  def web_page(
169
- page: Union[str, None, Callable[[Any], Any]] = None,
170
- login_required: bool = True,
171
- **kwargs
172
- ) -> Any:
169
+ page: Union[str, None, Callable[[Any], Any]] = None,
170
+ login_required: bool = True,
171
+ skip_navbar: bool = False,
172
+ **kwargs
173
+ ) -> Any:
173
174
  """
174
175
  Quickly add pages to the dash application.
175
176
 
@@ -200,6 +201,7 @@ def web_page(
200
201
  _plugin_endpoints_to_pages[page_str] = {
201
202
  'function': _func,
202
203
  'login_required': login_required,
204
+ 'skip_navbar': skip_navbar,
203
205
  }
204
206
  return wrapper
205
207