meerschaum 2.9.0rc3__py3-none-any.whl → 2.9.2__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/_internal/shell/Shell.py +79 -30
  2. meerschaum/api/dash/callbacks/__init__.py +5 -2
  3. meerschaum/api/dash/callbacks/custom.py +17 -25
  4. meerschaum/api/dash/callbacks/dashboard.py +5 -21
  5. meerschaum/api/dash/callbacks/settings/__init__.py +8 -0
  6. meerschaum/api/dash/callbacks/settings/password_reset.py +76 -0
  7. meerschaum/api/dash/components.py +110 -7
  8. meerschaum/api/dash/pages/__init__.py +1 -0
  9. meerschaum/api/dash/pages/pipes.py +9 -6
  10. meerschaum/api/dash/pages/settings/__init__.py +8 -0
  11. meerschaum/api/dash/pages/settings/password_reset.py +63 -0
  12. meerschaum/api/resources/static/css/dash.css +7 -0
  13. meerschaum/api/resources/templates/termpage.html +2 -0
  14. meerschaum/api/routes/_pipes.py +52 -37
  15. meerschaum/config/_version.py +1 -1
  16. meerschaum/connectors/__init__.py +1 -0
  17. meerschaum/connectors/api/_pipes.py +79 -30
  18. meerschaum/connectors/sql/_pipes.py +38 -5
  19. meerschaum/connectors/valkey/_pipes.py +1 -0
  20. meerschaum/core/Pipe/_data.py +10 -1
  21. meerschaum/core/Pipe/_verify.py +1 -0
  22. meerschaum/plugins/__init__.py +26 -4
  23. meerschaum/utils/dataframe.py +8 -1
  24. meerschaum/utils/dtypes/__init__.py +14 -13
  25. meerschaum/utils/misc.py +34 -1
  26. meerschaum/utils/packages/_packages.py +0 -1
  27. meerschaum/utils/sql.py +42 -4
  28. {meerschaum-2.9.0rc3.dist-info → meerschaum-2.9.2.dist-info}/METADATA +3 -4
  29. {meerschaum-2.9.0rc3.dist-info → meerschaum-2.9.2.dist-info}/RECORD +35 -36
  30. {meerschaum-2.9.0rc3.dist-info → meerschaum-2.9.2.dist-info}/WHEEL +1 -1
  31. meerschaum/_internal/gui/__init__.py +0 -43
  32. meerschaum/_internal/gui/app/__init__.py +0 -50
  33. meerschaum/_internal/gui/app/_windows.py +0 -74
  34. meerschaum/_internal/gui/app/actions.py +0 -30
  35. meerschaum/_internal/gui/app/pipes.py +0 -47
  36. {meerschaum-2.9.0rc3.dist-info → meerschaum-2.9.2.dist-info}/entry_points.txt +0 -0
  37. {meerschaum-2.9.0rc3.dist-info → meerschaum-2.9.2.dist-info/licenses}/LICENSE +0 -0
  38. {meerschaum-2.9.0rc3.dist-info → meerschaum-2.9.2.dist-info/licenses}/NOTICE +0 -0
  39. {meerschaum-2.9.0rc3.dist-info → meerschaum-2.9.2.dist-info}/top_level.txt +0 -0
  40. {meerschaum-2.9.0rc3.dist-info → meerschaum-2.9.2.dist-info}/zip-safe +0 -0
@@ -55,6 +55,8 @@ window.addEventListener(
55
55
  if (typeof lk === "string"){
56
56
  quote_str = lk.includes(" ") ? "'" : "";
57
57
  location_keys_str += " " + quote_str + lk + quote_str;
58
+ } else if (lk === null) {
59
+ location_keys_str += " None";
58
60
  }
59
61
  }
60
62
  if (location_keys_str === " -l"){
@@ -387,8 +387,6 @@ def get_pipe_data(
387
387
  date_unit: str = 'us',
388
388
  double_precision: int = 15,
389
389
  geometry_format: str = 'wkb_hex',
390
- as_chunks: bool = False,
391
- chunk_interval: Optional[int] = None,
392
390
  curr_user = (
393
391
  fastapi.Depends(manager) if not no_auth else None
394
392
  ),
@@ -414,14 +412,11 @@ def get_pipe_data(
414
412
  `'ms'` (milliseconds), `'us'` (microseconds), and `'ns'`.
415
413
 
416
414
  double_precision: int, default 15
417
- The number of decimal places to use when encoding floating point values (maximum 15).
415
+ The number of decimal places to use when encoding floating point values (maximum `15`).
418
416
 
419
417
  geometry_format: str, default 'wkb_hex'
420
418
  The serialization format for geometry data.
421
419
  Accepted values are `geojson`, `wkb_hex`, and `wkt`.
422
-
423
- as_chunks: bool, default False
424
- If `True`, return a chunk token to be consumed by the `/chunks` endpoint.
425
420
  """
426
421
  if limit > MAX_RESPONSE_ROW_LIMIT:
427
422
  raise fastapi.HTTPException(
@@ -488,25 +483,6 @@ def get_pipe_data(
488
483
  detail=f"Cannot retrieve data from protected table '{pipe.target}'.",
489
484
  )
490
485
 
491
- if as_chunks:
492
- chunks_cursor_token = generate_chunks_cursor_token(
493
- pipe,
494
- select_columns=_select_columns,
495
- omit_columns=_omit_columns,
496
- begin=begin,
497
- end=end,
498
- params=_params,
499
- limit=limit,
500
- order=order,
501
- debug=debug,
502
- )
503
- return fastapi.Response(
504
- json.dumps({
505
- 'chunks_cursor': chunks_cursor_token,
506
- }),
507
- media_type='application/json',
508
- )
509
-
510
486
  df = pipe.get_data(
511
487
  select_columns=_select_columns,
512
488
  omit_columns=_omit_columns,
@@ -579,37 +555,70 @@ def get_pipe_chunk_bounds(
579
555
  )
580
556
 
581
557
 
582
- @app.get(
583
- pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/chunk/{chunk_token}',
558
+ @app.delete(
559
+ pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/drop',
584
560
  tags=['Pipes: Data'],
585
561
  )
586
- def get_pipe_chunk(
562
+ def drop_pipe(
587
563
  connector_keys: str,
588
564
  metric_key: str,
589
565
  location_key: str,
590
- chunk_token: str
591
- ) -> Dict[str, Any]:
566
+ instance_keys: Optional[str] = None,
567
+ curr_user = (
568
+ fastapi.Depends(manager) if not no_auth else None
569
+ ),
570
+ ):
592
571
  """
593
- Consume a chunk token, returning the dataframe.
572
+ Drop a pipe's target table.
594
573
  """
574
+ allow_actions = mrsm.get_config('system', 'api', 'permissions', 'actions', 'non_admin')
575
+ if not allow_actions:
576
+ return False, (
577
+ "The administrator for this server has not allowed actions.\n\n"
578
+ "Please contact the system administrator, or if you are running this server, "
579
+ "open the configuration file with `edit config system` and search for 'permissions'."
580
+ " Under the keys `api:permissions:actions`, " +
581
+ "you can toggle non-admin actions."
582
+ )
583
+ pipe_object = get_pipe(connector_keys, metric_key, location_key, instance_keys)
584
+ results = get_api_connector(instance_keys=instance_keys).drop_pipe(pipe_object, debug=debug)
585
+ pipes(instance_keys, refresh=True)
586
+ return results
595
587
 
596
588
 
597
589
  @app.delete(
598
- pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/drop',
590
+ pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/clear',
599
591
  tags=['Pipes: Data'],
600
592
  )
601
- def drop_pipe(
593
+ def clear_pipe(
602
594
  connector_keys: str,
603
595
  metric_key: str,
604
596
  location_key: str,
605
597
  instance_keys: Optional[str] = None,
598
+ begin: Union[str, int, None] = None,
599
+ end: Union[str, int, None] = None,
600
+ params: Optional[str] = None,
606
601
  curr_user = (
607
602
  fastapi.Depends(manager) if not no_auth else None
608
603
  ),
609
604
  ):
610
605
  """
611
- Drop a pipes' underlying target table.
606
+ Delete rows from a pipe's target table.
612
607
  """
608
+ _params = {}
609
+ if params == 'null':
610
+ params = None
611
+ if params is not None:
612
+ try:
613
+ _params = json.loads(params)
614
+ except Exception:
615
+ _params = None
616
+ if not isinstance(_params, dict):
617
+ raise fastapi.HTTPException(
618
+ status_code=409,
619
+ detail="Params must be a valid JSON-encoded dictionary.",
620
+ )
621
+
613
622
  allow_actions = mrsm.get_config('system', 'api', 'permissions', 'actions', 'non_admin')
614
623
  if not allow_actions:
615
624
  return False, (
@@ -619,13 +628,19 @@ def drop_pipe(
619
628
  " Under the keys `api:permissions:actions`, " +
620
629
  "you can toggle non-admin actions."
621
630
  )
622
- pipe_object = get_pipe(connector_keys, metric_key, location_key, instance_keys)
623
- results = get_api_connector(instance_keys=instance_keys).drop_pipe(pipe_object, debug=debug)
631
+ pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
632
+ begin, end = pipe.parse_date_bounds(begin, end)
633
+ results = get_api_connector(instance_keys=instance_keys).clear_pipe(
634
+ pipe,
635
+ begin=begin,
636
+ end=end,
637
+ params=_params,
638
+ debug=debug,
639
+ )
624
640
  pipes(instance_keys, refresh=True)
625
641
  return results
626
642
 
627
643
 
628
-
629
644
  @app.get(
630
645
  pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/csv',
631
646
  tags=['Pipes: Data'],
@@ -2,4 +2,4 @@
2
2
  Specify the Meerschaum release version.
3
3
  """
4
4
 
5
- __version__ = "2.9.0rc3"
5
+ __version__ = "2.9.2"
@@ -34,6 +34,7 @@ __all__ = (
34
34
  "api",
35
35
  "sql",
36
36
  "valkey",
37
+ "parse",
37
38
  )
38
39
 
39
40
  ### store connectors partitioned by
@@ -414,19 +414,24 @@ def get_pipe_id(
414
414
  self,
415
415
  pipe: mrsm.Pipe,
416
416
  debug: bool = False,
417
- ) -> int:
417
+ ) -> Union[int, str, None]:
418
418
  """Get a Pipe's ID from the API."""
419
419
  from meerschaum.utils.misc import is_int
420
420
  r_url = pipe_r_url(pipe)
421
421
  response = self.get(
422
422
  r_url + '/id',
423
- debug = debug
423
+ params={
424
+ 'instance': self.get_pipe_instance_keys(pipe),
425
+ },
426
+ debug=debug,
424
427
  )
425
428
  if debug:
426
429
  dprint(f"Got pipe ID: {response.text}")
427
430
  try:
428
431
  if is_int(response.text):
429
432
  return int(response.text)
433
+ if response.text and response.text[0] != '{':
434
+ return response.text
430
435
  except Exception as e:
431
436
  warn(f"Failed to get the ID for {pipe}:\n{e}")
432
437
  return None
@@ -450,7 +455,13 @@ def get_pipe_attributes(
450
455
  If the pipe does not exist, return an empty dictionary.
451
456
  """
452
457
  r_url = pipe_r_url(pipe)
453
- response = self.get(r_url + '/attributes', debug=debug)
458
+ response = self.get(
459
+ r_url + '/attributes',
460
+ params={
461
+ 'instance': self.get_pipe_instance_keys(pipe),
462
+ },
463
+ debug=debug
464
+ )
454
465
  try:
455
466
  return json.loads(response.text)
456
467
  except Exception as e:
@@ -489,9 +500,13 @@ def get_sync_time(
489
500
  r_url = pipe_r_url(pipe)
490
501
  response = self.get(
491
502
  r_url + '/sync_time',
492
- json = params,
493
- params = {'newest': newest, 'debug': debug},
494
- debug = debug,
503
+ json=params,
504
+ params={
505
+ 'instance': self.get_pipe_instance_keys(pipe),
506
+ 'newest': newest,
507
+ 'debug': debug,
508
+ },
509
+ debug=debug,
495
510
  )
496
511
  if not response:
497
512
  warn(f"Failed to get the sync time for {pipe}:\n" + response.text)
@@ -532,7 +547,13 @@ def pipe_exists(
532
547
  from meerschaum.utils.debug import dprint
533
548
  from meerschaum.utils.warnings import warn
534
549
  r_url = pipe_r_url(pipe)
535
- response = self.get(r_url + '/exists', debug=debug)
550
+ response = self.get(
551
+ r_url + '/exists',
552
+ params={
553
+ 'instance': self.get_pipe_instance_keys(pipe),
554
+ },
555
+ debug=debug,
556
+ )
536
557
  if not response:
537
558
  warn(f"Failed to check if {pipe} exists:\n{response.text}")
538
559
  return False
@@ -570,8 +591,8 @@ def create_metadata(
570
591
  def get_pipe_rowcount(
571
592
  self,
572
593
  pipe: mrsm.Pipe,
573
- begin: Optional[datetime] = None,
574
- end: Optional[datetime] = None,
594
+ begin: Union[str, datetime, int, None] = None,
595
+ end: Union[str, datetime, int, None] = None,
575
596
  params: Optional[Dict[str, Any]] = None,
576
597
  remote: bool = False,
577
598
  debug: bool = False,
@@ -583,10 +604,10 @@ def get_pipe_rowcount(
583
604
  pipe: 'meerschaum.Pipe':
584
605
  The pipe whose row count we are counting.
585
606
 
586
- begin: Optional[datetime], default None
607
+ begin: Union[str, datetime, int, None], default None
587
608
  If provided, bound the count by this datetime.
588
609
 
589
- end: Optional[datetime]
610
+ end: Union[str, datetime, int, None], default None
590
611
  If provided, bound the count by this datetime.
591
612
 
592
613
  params: Optional[Dict[str, Any]], default None
@@ -608,6 +629,7 @@ def get_pipe_rowcount(
608
629
  'begin': begin,
609
630
  'end': end,
610
631
  'remote': remote,
632
+ 'instance': self.get_pipe_instance_keys(pipe),
611
633
  },
612
634
  debug = debug
613
635
  )
@@ -645,7 +667,10 @@ def drop_pipe(
645
667
  r_url = pipe_r_url(pipe)
646
668
  response = self.delete(
647
669
  r_url + '/drop',
648
- debug = debug,
670
+ params={
671
+ 'instance': self.get_pipe_instance_keys(pipe),
672
+ },
673
+ debug=debug,
649
674
  )
650
675
  if debug:
651
676
  dprint(response.text)
@@ -668,6 +693,9 @@ def drop_pipe(
668
693
  def clear_pipe(
669
694
  self,
670
695
  pipe: mrsm.Pipe,
696
+ begin: Union[str, datetime, int, None] = None,
697
+ end: Union[str, datetime, int, None] = None,
698
+ params: Optional[Dict[str, Any]] = None,
671
699
  debug: bool = False,
672
700
  **kw
673
701
  ) -> SuccessTuple:
@@ -683,20 +711,33 @@ def clear_pipe(
683
711
  -------
684
712
  A success tuple.
685
713
  """
686
- kw.pop('metric_keys', None)
687
- kw.pop('connector_keys', None)
688
- kw.pop('location_keys', None)
689
- kw.pop('action', None)
690
- kw.pop('force', None)
691
- return self.do_action_legacy(
692
- ['clear', 'pipes'],
693
- connector_keys=pipe.connector_keys,
694
- metric_keys=pipe.metric_key,
695
- location_keys=pipe.location_key,
696
- force=True,
714
+ r_url = pipe_r_url(pipe)
715
+ response = self.delete(
716
+ r_url + '/clear',
717
+ params={
718
+ 'begin': begin,
719
+ 'end': end,
720
+ 'params': json.dumps(params),
721
+ 'instance': self.get_pipe_instance_keys(pipe),
722
+ },
697
723
  debug=debug,
698
- **kw
699
724
  )
725
+ if debug:
726
+ dprint(response.text)
727
+
728
+ try:
729
+ data = response.json()
730
+ except Exception as e:
731
+ return False, f"Failed to clear {pipe} with constraints {begin=}, {end=}, {params=}."
732
+
733
+ if isinstance(data, list):
734
+ response_tuple = data[0], data[1]
735
+ elif 'detail' in response.json():
736
+ response_tuple = response.__bool__(), data['detail']
737
+ else:
738
+ response_tuple = response.__bool__(), response.text
739
+
740
+ return response_tuple
700
741
 
701
742
 
702
743
  def get_pipe_columns_types(
@@ -728,7 +769,10 @@ def get_pipe_columns_types(
728
769
  r_url = pipe_r_url(pipe) + '/columns/types'
729
770
  response = self.get(
730
771
  r_url,
731
- debug=debug
772
+ params={
773
+ 'instance': self.get_pipe_instance_keys(pipe),
774
+ },
775
+ debug=debug,
732
776
  )
733
777
  j = response.json()
734
778
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
@@ -760,7 +804,10 @@ def get_pipe_columns_indices(
760
804
  r_url = pipe_r_url(pipe) + '/columns/indices'
761
805
  response = self.get(
762
806
  r_url,
763
- debug=debug
807
+ params={
808
+ 'instance': self.get_pipe_instance_keys(pipe),
809
+ },
810
+ debug=debug,
764
811
  )
765
812
  j = response.json()
766
813
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
@@ -779,14 +826,16 @@ def get_pipe_index_names(self, pipe: mrsm.Pipe, debug: bool = False) -> Dict[str
779
826
  r_url = pipe_r_url(pipe) + '/indices/names'
780
827
  response = self.get(
781
828
  r_url,
782
- debug=debug
829
+ params={
830
+ 'instance': self.get_pipe_instance_keys(pipe),
831
+ },
832
+ debug=debug,
783
833
  )
784
834
  j = response.json()
785
835
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
786
836
  warn(j['detail'])
787
- return None
837
+ return {}
788
838
  if not isinstance(j, dict):
789
839
  warn(response.text)
790
- return None
840
+ return {}
791
841
  return j
792
-
@@ -499,6 +499,7 @@ def get_create_index_queries(
499
499
  get_rename_table_queries,
500
500
  COALESCE_UNIQUE_INDEX_FLAVORS,
501
501
  )
502
+ from meerschaum.utils.dtypes import are_dtypes_equal
502
503
  from meerschaum.utils.dtypes.sql import (
503
504
  get_db_type_from_pd_type,
504
505
  get_pd_type_from_db_type,
@@ -793,8 +794,19 @@ def get_create_index_queries(
793
794
  cols_names = [sql_item_name(col, self.flavor, None) for col in cols if col]
794
795
  if not cols_names:
795
796
  continue
797
+
796
798
  cols_names_str = ", ".join(cols_names)
797
- index_queries[ix_key] = [f"CREATE INDEX {ix_name} ON {_pipe_name} ({cols_names_str})"]
799
+ index_query_params_clause = f" ({cols_names_str})"
800
+ if self.flavor == 'postgis':
801
+ for col in cols:
802
+ col_typ = existing_cols_pd_types.get(cols[0], 'object')
803
+ if col_typ != 'object' and are_dtypes_equal(col_typ, 'geometry'):
804
+ index_query_params_clause = f" USING GIST ({cols_names_str})"
805
+ break
806
+
807
+ index_queries[ix_key] = [
808
+ f"CREATE INDEX {ix_name} ON {_pipe_name}{index_query_params_clause}"
809
+ ]
798
810
 
799
811
  indices_cols_str = ', '.join(
800
812
  list({
@@ -1544,7 +1556,11 @@ def create_pipe_table_from_df(
1544
1556
  get_datetime_cols,
1545
1557
  get_bytes_cols,
1546
1558
  )
1547
- from meerschaum.utils.sql import get_create_table_queries, sql_item_name
1559
+ from meerschaum.utils.sql import (
1560
+ get_create_table_queries,
1561
+ sql_item_name,
1562
+ get_create_schema_if_not_exists_queries,
1563
+ )
1548
1564
  from meerschaum.utils.dtypes.sql import get_db_type_from_pd_type
1549
1565
  primary_key = pipe.columns.get('primary', None)
1550
1566
  primary_key_typ = (
@@ -1601,15 +1617,21 @@ def create_pipe_table_from_df(
1601
1617
  if autoincrement:
1602
1618
  _ = new_dtypes.pop(primary_key, None)
1603
1619
 
1620
+ schema = self.get_pipe_schema(pipe)
1604
1621
  create_table_queries = get_create_table_queries(
1605
1622
  new_dtypes,
1606
1623
  pipe.target,
1607
1624
  self.flavor,
1608
- schema=self.get_pipe_schema(pipe),
1625
+ schema=schema,
1609
1626
  primary_key=primary_key,
1610
1627
  primary_key_db_type=primary_key_db_type,
1611
1628
  datetime_column=dt_col,
1612
1629
  )
1630
+ if schema:
1631
+ create_table_queries = (
1632
+ get_create_schema_if_not_exists_queries(schema, self.flavor)
1633
+ + create_table_queries
1634
+ )
1613
1635
  success = all(
1614
1636
  self.exec_queries(create_table_queries, break_on_error=True, rollback=True, debug=debug)
1615
1637
  )
@@ -2085,6 +2107,7 @@ def sync_pipe_inplace(
2085
2107
  get_update_queries,
2086
2108
  get_null_replacement,
2087
2109
  get_create_table_queries,
2110
+ get_create_schema_if_not_exists_queries,
2088
2111
  get_table_cols_types,
2089
2112
  session_execute,
2090
2113
  dateadd_str,
@@ -2164,18 +2187,28 @@ def sync_pipe_inplace(
2164
2187
  warn(drop_stale_msg)
2165
2188
  return drop_stale_success, drop_stale_msg
2166
2189
 
2167
- sqlalchemy, sqlalchemy_orm = mrsm.attempt_import('sqlalchemy', 'sqlalchemy.orm')
2190
+ sqlalchemy, sqlalchemy_orm = mrsm.attempt_import(
2191
+ 'sqlalchemy',
2192
+ 'sqlalchemy.orm',
2193
+ )
2168
2194
  if not pipe.exists(debug=debug):
2195
+ schema = self.get_pipe_schema(pipe)
2169
2196
  create_pipe_queries = get_create_table_queries(
2170
2197
  metadef,
2171
2198
  pipe.target,
2172
2199
  self.flavor,
2173
- schema=self.get_pipe_schema(pipe),
2200
+ schema=schema,
2174
2201
  primary_key=primary_key,
2175
2202
  primary_key_db_type=primary_key_db_type,
2176
2203
  autoincrement=autoincrement,
2177
2204
  datetime_column=dt_col,
2178
2205
  )
2206
+ if schema:
2207
+ create_pipe_queries = (
2208
+ get_create_schema_if_not_exists_queries(schema, self.flavor)
2209
+ + create_pipe_queries
2210
+ )
2211
+
2179
2212
  results = self.exec_queries(create_pipe_queries, debug=debug)
2180
2213
  if not all(results):
2181
2214
  _ = clean_up_temp_tables()
@@ -448,6 +448,7 @@ def get_pipe_data(
448
448
  debug=debug,
449
449
  )
450
450
  ]
451
+ print(f"{ix_docs=}")
451
452
  try:
452
453
  docs_strings = [
453
454
  self.get(
@@ -679,12 +679,15 @@ def get_chunk_bounds(
679
679
  include_greater_than_end = not bounded and end is None
680
680
  if begin is None:
681
681
  begin = self.get_sync_time(newest=False, debug=debug)
682
+ consolidate_end_chunk = False
682
683
  if end is None:
683
684
  end = self.get_sync_time(newest=True, debug=debug)
684
685
  if end is not None and hasattr(end, 'tzinfo'):
685
686
  end += timedelta(minutes=1)
687
+ consolidate_end_chunk = True
686
688
  elif are_dtypes_equal(str(type(end)), 'int'):
687
689
  end += 1
690
+ consolidate_end_chunk = True
688
691
  if begin is None and end is None:
689
692
  return [(None, None)]
690
693
 
@@ -707,9 +710,15 @@ def get_chunk_bounds(
707
710
  num_chunks += 1
708
711
  if num_chunks >= max_chunks:
709
712
  raise ValueError(
710
- f"Too many chunks of size '{interval_str(chunk_interval)}' between '{begin}' and '{end}'."
713
+ f"Too many chunks of size '{interval_str(chunk_interval)}' "
714
+ f"between '{begin}' and '{end}'."
711
715
  )
712
716
 
717
+ if num_chunks > 1 and consolidate_end_chunk:
718
+ last_bounds, second_last_bounds = chunk_bounds[-1], chunk_bounds[-2]
719
+ chunk_bounds = chunk_bounds[:-2]
720
+ chunk_bounds.append((second_last_bounds[0], last_bounds[1]))
721
+
713
722
  ### The chunk interval might be too large.
714
723
  if not chunk_bounds and end >= begin:
715
724
  chunk_bounds = [(begin, end)]
@@ -282,6 +282,7 @@ def verify(
282
282
  )
283
283
  bounds_success_tuples[first_chunk_bounds] = (first_success, first_msg)
284
284
  info(f"Completed first chunk for {self}:\n {first_label}\n")
285
+ chunk_bounds = chunk_bounds[1:]
285
286
 
286
287
  pool = get_pool(workers=workers)
287
288
  batches = self.get_chunk_bounds_batches(chunk_bounds, batchsize=batchsize, workers=workers)
@@ -91,8 +91,8 @@ def make_action(
91
91
 
92
92
 
93
93
  def pre_sync_hook(
94
- function: Callable[[Any], Any],
95
- ) -> Callable[[Any], Any]:
94
+ function: Callable[[Any], Any],
95
+ ) -> Callable[[Any], Any]:
96
96
  """
97
97
  Register a function as a sync hook to be executed right before sync.
98
98
 
@@ -169,6 +169,7 @@ def web_page(
169
169
  page: Union[str, None, Callable[[Any], Any]] = None,
170
170
  login_required: bool = True,
171
171
  skip_navbar: bool = False,
172
+ page_group: Optional[str] = None,
172
173
  **kwargs
173
174
  ) -> Any:
174
175
  """
@@ -188,7 +189,7 @@ def web_page(
188
189
  page_str = None
189
190
 
190
191
  def _decorator(_func: Callable[[Any], Any]) -> Callable[[Any], Any]:
191
- nonlocal page_str
192
+ nonlocal page_str, page_group
192
193
 
193
194
  @functools.wraps(_func)
194
195
  def wrapper(*_args, **_kwargs):
@@ -198,10 +199,31 @@ def web_page(
198
199
  page_str = _func.__name__
199
200
 
200
201
  page_str = page_str.lstrip('/').rstrip('/').strip()
201
- _plugin_endpoints_to_pages[page_str] = {
202
+ page_key = (
203
+ ' '.join(
204
+ [
205
+ word.capitalize()
206
+ for word in (
207
+ page_str.replace('/dash', '').lstrip('/').rstrip('/').strip()
208
+ .replace('-', ' ').replace('_', ' ').split(' ')
209
+ )
210
+ ]
211
+ )
212
+ )
213
+
214
+ package_name = _func.__globals__['__name__']
215
+ plugin_name = (
216
+ package_name.split('.')[1]
217
+ if package_name.startswith('plugins.') else None
218
+ )
219
+ page_group = page_group or plugin_name
220
+ if page_group not in _plugin_endpoints_to_pages:
221
+ _plugin_endpoints_to_pages[page_group] = {}
222
+ _plugin_endpoints_to_pages[page_group][page_str] = {
202
223
  'function': _func,
203
224
  'login_required': login_required,
204
225
  'skip_navbar': skip_navbar,
226
+ 'page_key': page_key,
205
227
  }
206
228
  return wrapper
207
229
 
@@ -922,7 +922,12 @@ def get_geometry_cols(
922
922
  for col in geo_cols:
923
923
  try:
924
924
  sample_geo_series = gpd.GeoSeries(df[col], crs=None)
925
- geometry_types = {geom.geom_type for geom in sample_geo_series}
925
+ geometry_types = {
926
+ geom.geom_type
927
+ for geom in sample_geo_series
928
+ if hasattr(geom, 'geom_type')
929
+ }
930
+ geometry_has_z = any(getattr(geom, 'has_z', False) for geom in sample_geo_series)
926
931
  srid = (
927
932
  (
928
933
  sample_geo_series.crs.sub_crs_list[0].to_epsg()
@@ -933,6 +938,8 @@ def get_geometry_cols(
933
938
  else 0
934
939
  )
935
940
  geometry_type = list(geometry_types)[0] if len(geometry_types) == 1 else 'geometry'
941
+ if geometry_type != 'geometry' and geometry_has_z:
942
+ geometry_type = geometry_type + 'Z'
936
943
  except Exception:
937
944
  srid = 0
938
945
  geometry_type = 'geometry'