meerschaum 2.9.5__py3-none-any.whl → 3.0.0rc2__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 (158) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/_internal/__init__.py +1 -0
  3. meerschaum/_internal/arguments/_parse_arguments.py +4 -4
  4. meerschaum/_internal/arguments/_parser.py +19 -2
  5. meerschaum/_internal/docs/index.py +49 -2
  6. meerschaum/_internal/entry.py +6 -6
  7. meerschaum/_internal/shell/Shell.py +1 -1
  8. meerschaum/_internal/static.py +356 -0
  9. meerschaum/actions/api.py +12 -2
  10. meerschaum/actions/bootstrap.py +7 -7
  11. meerschaum/actions/edit.py +142 -18
  12. meerschaum/actions/register.py +137 -6
  13. meerschaum/actions/show.py +117 -29
  14. meerschaum/actions/stop.py +4 -1
  15. meerschaum/actions/sync.py +1 -1
  16. meerschaum/actions/tag.py +9 -8
  17. meerschaum/actions/verify.py +5 -8
  18. meerschaum/api/__init__.py +11 -3
  19. meerschaum/api/_events.py +39 -2
  20. meerschaum/api/_oauth2.py +118 -8
  21. meerschaum/api/_tokens.py +102 -0
  22. meerschaum/api/dash/__init__.py +0 -3
  23. meerschaum/api/dash/callbacks/custom.py +2 -2
  24. meerschaum/api/dash/callbacks/dashboard.py +103 -19
  25. meerschaum/api/dash/callbacks/plugins.py +0 -1
  26. meerschaum/api/dash/callbacks/register.py +1 -1
  27. meerschaum/api/dash/callbacks/settings/__init__.py +1 -0
  28. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  29. meerschaum/api/dash/callbacks/settings/tokens.py +388 -0
  30. meerschaum/api/dash/components.py +30 -8
  31. meerschaum/api/dash/keys.py +19 -93
  32. meerschaum/api/dash/pages/dashboard.py +1 -20
  33. meerschaum/api/dash/pages/settings/__init__.py +1 -0
  34. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  35. meerschaum/api/dash/pages/settings/tokens.py +55 -0
  36. meerschaum/api/dash/pipes.py +94 -59
  37. meerschaum/api/dash/sessions.py +12 -0
  38. meerschaum/api/dash/tokens.py +606 -0
  39. meerschaum/api/dash/websockets.py +1 -1
  40. meerschaum/api/dash/webterm.py +4 -0
  41. meerschaum/api/models/__init__.py +23 -3
  42. meerschaum/api/models/_actions.py +22 -0
  43. meerschaum/api/models/_pipes.py +85 -7
  44. meerschaum/api/models/_tokens.py +81 -0
  45. meerschaum/api/resources/templates/termpage.html +12 -0
  46. meerschaum/api/routes/__init__.py +1 -0
  47. meerschaum/api/routes/_actions.py +3 -4
  48. meerschaum/api/routes/_connectors.py +3 -7
  49. meerschaum/api/routes/_jobs.py +14 -35
  50. meerschaum/api/routes/_login.py +49 -12
  51. meerschaum/api/routes/_misc.py +5 -10
  52. meerschaum/api/routes/_pipes.py +173 -140
  53. meerschaum/api/routes/_plugins.py +38 -28
  54. meerschaum/api/routes/_tokens.py +236 -0
  55. meerschaum/api/routes/_users.py +47 -35
  56. meerschaum/api/routes/_version.py +3 -3
  57. meerschaum/config/__init__.py +43 -20
  58. meerschaum/config/_default.py +43 -6
  59. meerschaum/config/_edit.py +28 -24
  60. meerschaum/config/_environment.py +1 -1
  61. meerschaum/config/_patch.py +6 -6
  62. meerschaum/config/_paths.py +5 -1
  63. meerschaum/config/_read_config.py +65 -34
  64. meerschaum/config/_sync.py +6 -3
  65. meerschaum/config/_version.py +1 -1
  66. meerschaum/config/stack/__init__.py +31 -11
  67. meerschaum/config/static.py +18 -0
  68. meerschaum/connectors/_Connector.py +10 -4
  69. meerschaum/connectors/__init__.py +4 -20
  70. meerschaum/connectors/api/_APIConnector.py +34 -6
  71. meerschaum/connectors/api/_actions.py +2 -2
  72. meerschaum/connectors/api/_jobs.py +1 -1
  73. meerschaum/connectors/api/_login.py +33 -7
  74. meerschaum/connectors/api/_misc.py +2 -2
  75. meerschaum/connectors/api/_pipes.py +16 -31
  76. meerschaum/connectors/api/_plugins.py +2 -2
  77. meerschaum/connectors/api/_request.py +1 -1
  78. meerschaum/connectors/api/_tokens.py +146 -0
  79. meerschaum/connectors/api/_users.py +70 -58
  80. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  81. meerschaum/connectors/instance/__init__.py +10 -0
  82. meerschaum/connectors/instance/_pipes.py +442 -0
  83. meerschaum/connectors/instance/_plugins.py +151 -0
  84. meerschaum/connectors/instance/_tokens.py +296 -0
  85. meerschaum/connectors/instance/_users.py +181 -0
  86. meerschaum/connectors/parse.py +4 -1
  87. meerschaum/connectors/sql/_SQLConnector.py +8 -5
  88. meerschaum/connectors/sql/_cli.py +12 -11
  89. meerschaum/connectors/sql/_create_engine.py +9 -168
  90. meerschaum/connectors/sql/_fetch.py +2 -18
  91. meerschaum/connectors/sql/_pipes.py +156 -190
  92. meerschaum/connectors/sql/_plugins.py +29 -0
  93. meerschaum/connectors/sql/_sql.py +46 -21
  94. meerschaum/connectors/sql/_users.py +29 -2
  95. meerschaum/connectors/sql/tables/__init__.py +1 -1
  96. meerschaum/connectors/valkey/_ValkeyConnector.py +2 -4
  97. meerschaum/connectors/valkey/_pipes.py +53 -26
  98. meerschaum/connectors/valkey/_plugins.py +2 -26
  99. meerschaum/core/Pipe/__init__.py +59 -19
  100. meerschaum/core/Pipe/_attributes.py +412 -90
  101. meerschaum/core/Pipe/_bootstrap.py +54 -24
  102. meerschaum/core/Pipe/_data.py +96 -18
  103. meerschaum/core/Pipe/_dtypes.py +48 -18
  104. meerschaum/core/Pipe/_edit.py +14 -4
  105. meerschaum/core/Pipe/_fetch.py +1 -1
  106. meerschaum/core/Pipe/_show.py +5 -5
  107. meerschaum/core/Pipe/_sync.py +118 -193
  108. meerschaum/core/Pipe/_verify.py +4 -4
  109. meerschaum/{plugins → core/Plugin}/_Plugin.py +9 -11
  110. meerschaum/core/Plugin/__init__.py +1 -1
  111. meerschaum/core/Token/_Token.py +220 -0
  112. meerschaum/core/Token/__init__.py +12 -0
  113. meerschaum/core/User/_User.py +34 -8
  114. meerschaum/core/User/__init__.py +9 -1
  115. meerschaum/core/__init__.py +1 -0
  116. meerschaum/jobs/_Job.py +3 -2
  117. meerschaum/jobs/__init__.py +3 -2
  118. meerschaum/jobs/systemd.py +1 -1
  119. meerschaum/models/__init__.py +35 -0
  120. meerschaum/models/pipes.py +247 -0
  121. meerschaum/models/tokens.py +38 -0
  122. meerschaum/models/users.py +26 -0
  123. meerschaum/plugins/__init__.py +22 -7
  124. meerschaum/plugins/bootstrap.py +2 -1
  125. meerschaum/utils/_get_pipes.py +68 -27
  126. meerschaum/utils/daemon/Daemon.py +2 -1
  127. meerschaum/utils/daemon/__init__.py +30 -2
  128. meerschaum/utils/dataframe.py +473 -81
  129. meerschaum/utils/debug.py +15 -15
  130. meerschaum/utils/dtypes/__init__.py +473 -34
  131. meerschaum/utils/dtypes/sql.py +368 -28
  132. meerschaum/utils/formatting/__init__.py +1 -1
  133. meerschaum/utils/formatting/_pipes.py +5 -4
  134. meerschaum/utils/formatting/_shell.py +11 -9
  135. meerschaum/utils/misc.py +246 -148
  136. meerschaum/utils/packages/__init__.py +10 -27
  137. meerschaum/utils/packages/_packages.py +41 -34
  138. meerschaum/utils/pipes.py +181 -0
  139. meerschaum/utils/process.py +1 -1
  140. meerschaum/utils/prompt.py +3 -1
  141. meerschaum/utils/schedule.py +2 -1
  142. meerschaum/utils/sql.py +121 -44
  143. meerschaum/utils/typing.py +1 -4
  144. meerschaum/utils/venv/_Venv.py +2 -2
  145. meerschaum/utils/venv/__init__.py +5 -7
  146. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/METADATA +92 -96
  147. meerschaum-3.0.0rc2.dist-info/RECORD +283 -0
  148. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/WHEEL +1 -1
  149. meerschaum-3.0.0rc2.dist-info/licenses/NOTICE +2 -0
  150. meerschaum/api/models/_interfaces.py +0 -15
  151. meerschaum/api/models/_locations.py +0 -15
  152. meerschaum/api/models/_metrics.py +0 -15
  153. meerschaum/config/static/__init__.py +0 -186
  154. meerschaum-2.9.5.dist-info/RECORD +0 -263
  155. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  156. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
  157. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/top_level.txt +0 -0
  158. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/zip-safe +0 -0
@@ -45,6 +45,7 @@ def show(
45
45
  'tags' : _show_tags,
46
46
  'schedules' : _show_schedules,
47
47
  'venvs' : _show_venvs,
48
+ 'tokens' : _show_tokens,
48
49
  }
49
50
  return choose_subaction(action, show_options, **kw)
50
51
 
@@ -113,11 +114,11 @@ def _show_help(**kw: Any) -> SuccessTuple:
113
114
 
114
115
 
115
116
  def _show_config(
116
- action: Optional[List[str]] = None,
117
- debug: bool = False,
118
- nopretty: bool = False,
119
- **kw: Any
120
- ) -> SuccessTuple:
117
+ action: Optional[List[str]] = None,
118
+ debug: bool = False,
119
+ nopretty: bool = False,
120
+ **kw: Any
121
+ ) -> SuccessTuple:
121
122
  """
122
123
  Show the configuration dictionary.
123
124
  Sub-actions defined in the action list are recursive indices in the config dictionary.
@@ -789,34 +790,22 @@ def _show_tags(
789
790
  from meerschaum.utils.formatting import pipe_repr, UNICODE, ANSI
790
791
  from meerschaum.utils.pool import get_pool
791
792
  from meerschaum.config import get_config
793
+ from meerschaum.connectors.parse import parse_instance_keys
792
794
  rich_tree, rich_panel, rich_text, rich_console, rich_columns = (
793
795
  mrsm.attempt_import('rich.tree', 'rich.panel', 'rich.text', 'rich.console', 'rich.columns')
794
796
  )
795
- panel = rich_panel.Panel.fit('Tags')
796
- tree = rich_tree.Tree(panel)
797
797
  action = action or []
798
798
  tags = action + (tags or [])
799
- pipes = mrsm.get_pipes(as_list=True, tags=tags, **kwargs)
800
- if not pipes:
801
- return False, f"No pipes were found with the given tags."
802
799
 
803
- pool = get_pool(workers=workers)
804
800
  tag_prefix = get_config('formatting', 'pipes', 'unicode', 'icons', 'tag') if UNICODE else ''
805
801
  tag_style = get_config('formatting', 'pipes', 'ansi', 'styles', 'tags') if ANSI else None
806
802
 
807
- tags_pipes = defaultdict(lambda: [])
808
- gather_pipe_tags = lambda pipe: (pipe, (pipe.tags or []))
809
-
810
- pipes_tags = dict(pool.map(gather_pipe_tags, pipes))
811
-
812
- for pipe, tags in pipes_tags.items():
813
- for tag in tags:
814
- if action and tag not in action:
815
- continue
816
- tags_pipes[tag].append(pipe)
803
+ tags_pipes = mrsm.get_pipes(as_tags_dict=True, tags=tags, **kwargs)
804
+ if action:
805
+ tags_pipes = {tag: pipes for tag, pipes in tags_pipes.items() if tag in action}
817
806
 
818
807
  columns = []
819
- sorted_tags = sorted([tag for tag in tags_pipes])
808
+ sorted_tags = sorted(list(tags_pipes))
820
809
  for tag in sorted_tags:
821
810
  _pipes = tags_pipes[tag]
822
811
  tag_text = (
@@ -904,9 +893,7 @@ def _show_schedules(
904
893
  return True, "Success"
905
894
 
906
895
 
907
- def _show_venvs(
908
- **kwargs: Any
909
- ):
896
+ def _show_venvs(**kwargs: Any) -> SuccessTuple:
910
897
  """
911
898
  Print the available virtual environments in the current MRSM_ROOT_DIR.
912
899
  """
@@ -921,12 +908,113 @@ def _show_venvs(
921
908
  for _venv in os.listdir(VIRTENV_RESOURCES_PATH)
922
909
  if venv_exists(_venv)
923
910
  ]
924
- print_options(
925
- venvs,
926
- name = 'Venvs:',
927
- **kwargs
911
+ print_options(venvs, name='Virtual Environments:', **kwargs)
912
+ return True, "Success"
913
+
914
+
915
+ def _show_tokens(
916
+ action: Optional[List[str]] = None,
917
+ mrsm_instance: Optional[str] = None,
918
+ nopretty: bool = False,
919
+ debug: bool = False,
920
+ **kwargs: Any
921
+ ) -> SuccessTuple:
922
+ """
923
+ Print a table of the registered tokens on the instance.
924
+ """
925
+ import json
926
+ import uuid
927
+ from meerschaum.connectors.parse import parse_instance_keys
928
+ from meerschaum.utils.dtypes import value_is_null, json_serialize_value
929
+ from meerschaum.utils.misc import is_uuid
930
+ from meerschaum.utils.packages import import_rich
931
+ from meerschaum.utils.formatting import UNICODE, get_console
932
+ rich = import_rich()
933
+ rich_table, rich_json = mrsm.attempt_import('rich.table', 'rich.json')
934
+
935
+ conn = parse_instance_keys(mrsm_instance)
936
+
937
+ labels = [
938
+ label
939
+ for label in (action or [])
940
+ if not is_uuid(label)
941
+ ]
942
+ potential_token_ids = [
943
+ uuid.UUID(potential_id)
944
+ for potential_id in (action or [])
945
+ if is_uuid(potential_id)
946
+ ]
947
+
948
+ tokens = conn.get_tokens(
949
+ labels=(labels or None),
950
+ ids=(potential_token_ids or None),
951
+ debug=debug,
952
+ )
953
+
954
+ if nopretty:
955
+ for token in tokens:
956
+ print(json.dumps({
957
+ 'id': str(token.id),
958
+ 'label': token.label,
959
+ 'scopes': token.scopes,
960
+ 'expiration': (
961
+ token.expiration.isoformat()
962
+ if not value_is_null(token.expiration)
963
+ else None
964
+ ),
965
+ 'creation': (token.creation.isoformat() if not value_is_null(token.creation) else None),
966
+ 'user': (token.user.username if token.user is not None else None),
967
+ 'is_valid': token.is_valid,
968
+ }))
969
+ return True, "Success"
970
+
971
+ if len(tokens) == 1:
972
+ token = tokens[0]
973
+ tokens_json = json.dumps({
974
+ 'id': str(token.id),
975
+ 'label': token.label,
976
+ 'scopes': token.scopes,
977
+ 'creation': (token.creation.isoformat() if not value_is_null(token.creation) else None),
978
+ 'expiration': (token.expiration.isoformat() if not value_is_null(token.expiration) else None),
979
+ 'user': (token.user.username if token.user is not None else None),
980
+ 'is_valid': token.is_valid,
981
+ }, default=json_serialize_value, indent=4)
982
+ get_console().print(rich_json.JSON(tokens_json))
983
+
984
+ return True, "Success"
985
+
986
+ is_valid_true = (
987
+ "🟢"
988
+ if UNICODE
989
+ else "[+]"
928
990
  )
991
+ is_valid_false = (
992
+ "🔴"
993
+ if UNICODE
994
+ else "[-]"
995
+ )
996
+
997
+ table = rich_table.Table(title=f"Registered Tokens on instance '{conn}'")
998
+ table.add_column("ID")
999
+ table.add_column("Label")
1000
+ table.add_column("User")
1001
+ table.add_column("Expiration")
1002
+ table.add_column("Valid")
1003
+
1004
+ for token in tokens:
1005
+ table.add_row(
1006
+ str(token.id),
1007
+ token.label,
1008
+ (token.user.username if token.user is not None else ""),
1009
+ (
1010
+ token.expiration.isoformat()
1011
+ if not value_is_null(token.expiration)
1012
+ else 'Does not expire'
1013
+ ),
1014
+ (is_valid_true if token.is_valid else is_valid_false),
1015
+ )
929
1016
 
1017
+ mrsm.pprint(table)
930
1018
  return True, "Success"
931
1019
 
932
1020
 
@@ -7,7 +7,8 @@ Stop running jobs that were started with `-d` or `start job`.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- from meerschaum.utils.typing import Optional, List, Dict, SuccessTuple, Any
10
+ from meerschaum.utils.typing import Optional, List, SuccessTuple, Any
11
+
11
12
 
12
13
  def stop(action: Optional[List[str]] = None, **kw) -> SuccessTuple:
13
14
  """
@@ -111,6 +112,8 @@ def _stop_jobs(
111
112
  )
112
113
 
113
114
  if not jobs_to_stop:
115
+ if jobs:
116
+ return True, "The selected jobs are currently running."
114
117
  return False, "No running, paused or restarting jobs to stop."
115
118
 
116
119
  if not action:
@@ -287,7 +287,7 @@ def _sync_pipes(
287
287
  from meerschaum.utils.formatting._shell import progress
288
288
  from meerschaum.utils.formatting._shell import clear_screen
289
289
  from meerschaum.utils.formatting import print_pipes_results
290
- from meerschaum.config.static import STATIC_CONFIG
290
+ from meerschaum._internal.static import STATIC_CONFIG
291
291
  from meerschaum.utils.misc import interval_str
292
292
 
293
293
  noninteractive_val = os.environ.get(STATIC_CONFIG['environment']['noninteractive'], None)
meerschaum/actions/tag.py CHANGED
@@ -8,12 +8,12 @@ Functions for editing elements belong here.
8
8
 
9
9
  from __future__ import annotations
10
10
  import meerschaum as mrsm
11
- from meerschaum.utils.typing import List, Any, SuccessTuple, Optional, Dict
11
+ from meerschaum.utils.typing import List, Any, SuccessTuple, Optional
12
12
 
13
13
  def tag(
14
- action: Optional[List[str]] = None,
15
- **kwargs: Any
16
- ) -> SuccessTuple:
14
+ action: Optional[List[str]] = None,
15
+ **kwargs: Any
16
+ ) -> SuccessTuple:
17
17
  """
18
18
  Edit an existing element.
19
19
  """
@@ -25,10 +25,10 @@ def tag(
25
25
 
26
26
 
27
27
  def _tag_pipes(
28
- action: Optional[List[str]] = None,
29
- debug: bool = False,
30
- **kwargs: Any
31
- ) -> SuccessTuple:
28
+ action: Optional[List[str]] = None,
29
+ debug: bool = False,
30
+ **kwargs: Any
31
+ ) -> SuccessTuple:
32
32
  """
33
33
  Add or remove tags to registered pipes.
34
34
  Prefix a tag with a leading underscore to remove it.
@@ -68,6 +68,7 @@ def _tag_pipes(
68
68
  pipe_was_edited = True
69
69
 
70
70
  if pipe_was_edited:
71
+ pipe.tags = pipe_tags
71
72
  edited_pipes.append(pipe)
72
73
 
73
74
  if not edited_pipes:
@@ -57,8 +57,11 @@ def _verify_packages(
57
57
  Verify the versions of packages.
58
58
  """
59
59
  from meerschaum.utils.packages import (
60
- attempt_import, all_packages, is_installed, venv_contains_package,
61
- _monkey_patch_get_distribution, manually_import_module,
60
+ attempt_import,
61
+ all_packages,
62
+ is_installed,
63
+ venv_contains_package,
64
+ manually_import_module,
62
65
  )
63
66
 
64
67
  venv_packages, base_packages, miss_packages = [], [], []
@@ -78,12 +81,6 @@ def _verify_packages(
78
81
  )
79
82
  _where_list.append(import_name)
80
83
 
81
- if 'flask_compress' in venv_packages or 'dash' in venv_packages:
82
- flask_compress = attempt_import('flask_compress', lazy=False, debug=debug)
83
- _monkey_patch_get_distribution('flask-compress', flask_compress.__version__)
84
- if 'flask_compress' in venv_packages:
85
- venv_packages.remove('flask_compress')
86
-
87
84
  for import_name in base_packages:
88
85
  manually_import_module(import_name, debug=debug, venv=None)
89
86
  for import_name in venv_packages:
@@ -14,7 +14,7 @@ from fnmatch import fnmatch
14
14
  import meerschaum as mrsm
15
15
  from meerschaum.utils.typing import Dict, Any, Optional, PipesDict
16
16
  from meerschaum.config import get_config
17
- from meerschaum.config.static import STATIC_CONFIG, SERVER_ID
17
+ from meerschaum._internal.static import STATIC_CONFIG, SERVER_ID
18
18
  from meerschaum.utils.packages import attempt_import
19
19
  from meerschaum.utils import get_pipes as _get_pipes
20
20
  from meerschaum.config._paths import API_UVICORN_CONFIG_PATH, API_UVICORN_RESOURCES_PATH
@@ -95,10 +95,12 @@ def get_uvicorn_config() -> Dict[str, Any]:
95
95
 
96
96
  debug = get_uvicorn_config().get('debug', False)
97
97
  no_dash = get_uvicorn_config().get('no_dash', False)
98
+ no_webterm = get_uvicorn_config().get('no_webterm', False)
98
99
  no_auth = get_uvicorn_config().get('no_auth', False)
99
100
  private = get_uvicorn_config().get('private', False)
100
101
  production = get_uvicorn_config().get('production', False)
101
102
  _include_dash = (not no_dash)
103
+ _include_webterm = (not no_webterm) and _include_dash
102
104
  docs_enabled = not production or sys_config.get('endpoints', {}).get('docs_in_production', True)
103
105
 
104
106
  default_instance_keys = None
@@ -210,8 +212,14 @@ def get_pipe(
210
212
  if location_key in ('[None]', 'None', 'null'):
211
213
  location_key = None
212
214
  instance_keys = str(get_api_connector(instance_keys))
215
+ if connector_keys == 'mrsm':
216
+ raise fastapi.HTTPException(
217
+ status_code=403,
218
+ detail="Unable to serve any pipes with connector keys `mrsm` over the API.",
219
+ )
220
+
213
221
  pipe = mrsm.Pipe(connector_keys, metric_key, location_key, mrsm_instance=instance_keys)
214
- if is_pipe_registered(pipe, pipes(instance_keys)):
222
+ if is_pipe_registered(pipe, pipes(instance_keys, refresh=False)):
215
223
  return pipes(instance_keys, refresh=refresh)[connector_keys][metric_key][location_key]
216
224
  return pipe
217
225
 
@@ -291,7 +299,7 @@ def __getattr__(name: str):
291
299
  raise AttributeError(f"Could not import '{name}'.")
292
300
 
293
301
  ### Import everything else within the API.
294
- from meerschaum.api._oauth2 import manager
302
+ from meerschaum.api._oauth2 import manager, ScopedAuth
295
303
  import meerschaum.api.routes as routes
296
304
  import meerschaum.api._events
297
305
  import meerschaum.api._websockets
meerschaum/api/_events.py CHANGED
@@ -16,6 +16,8 @@ from meerschaum.api import (
16
16
  get_uvicorn_config,
17
17
  debug,
18
18
  no_dash,
19
+ _include_dash,
20
+ _include_webterm,
19
21
  )
20
22
  from meerschaum.utils.debug import dprint
21
23
  from meerschaum.connectors.poll import retry_connect
@@ -25,7 +27,7 @@ from meerschaum.jobs import (
25
27
  start_check_jobs_thread,
26
28
  stop_check_jobs_thread,
27
29
  )
28
- from meerschaum.config.static import STATIC_CONFIG
30
+ from meerschaum._internal.static import STATIC_CONFIG
29
31
 
30
32
  TEMP_PREFIX: str = STATIC_CONFIG['api']['jobs']['temp_prefix']
31
33
 
@@ -35,8 +37,43 @@ async def startup():
35
37
  """
36
38
  Connect to the instance database and begin monitoring jobs.
37
39
  """
40
+ app.openapi_schema = app.openapi()
41
+
42
+ ### Remove the implicitly added HTTPBearer scheme if it exists.
43
+ if 'BearerAuth' in app.openapi_schema['components']['securitySchemes']:
44
+ del app.openapi_schema['components']['securitySchemes']['BearerAuth']
45
+ elif 'HTTPBearer' in app.openapi_schema['components']['securitySchemes']:
46
+ del app.openapi_schema['components']['securitySchemes']['HTTPBearer']
47
+ if 'LoginManager' in app.openapi_schema['components']['securitySchemes']:
48
+ del app.openapi_schema['components']['securitySchemes']['LoginManager']
49
+
50
+ scopes = STATIC_CONFIG['tokens']['scopes']
51
+ app.openapi_schema['components']['securitySchemes']['OAuth2PasswordBearer'] = {
52
+ 'type': 'oauth2',
53
+ 'flows': {
54
+ 'password': {
55
+ 'tokenUrl': STATIC_CONFIG['api']['endpoints']['login'],
56
+ 'scopes': scopes,
57
+ },
58
+ },
59
+ }
60
+ app.openapi_schema['components']['securitySchemes']['APIKey'] = {
61
+ 'type': 'http',
62
+ 'scheme': 'bearer',
63
+ 'bearerFormat': 'mrsm-key:{client_id}:{client_secret}',
64
+ 'description': 'Authentication using a Meerschaum API Key.',
65
+ }
66
+ app.openapi_schema['security'] = [
67
+ {
68
+ 'OAuth2PasswordBearer': [],
69
+ },
70
+ {
71
+ 'APIKey': [],
72
+ }
73
+ ]
74
+
38
75
  try:
39
- if not no_dash:
76
+ if _include_webterm:
40
77
  from meerschaum.api.dash.webterm import start_webterm
41
78
  start_webterm()
42
79
 
meerschaum/api/_oauth2.py CHANGED
@@ -7,21 +7,34 @@ Define JWT authorization here.
7
7
  """
8
8
 
9
9
  import os
10
- from meerschaum.api import app, endpoints, CHECK_UPDATE
10
+ import base64
11
+ import functools
12
+ import inspect
13
+ from typing import List, Optional, Union
14
+
15
+ from meerschaum.api import endpoints, CHECK_UPDATE, no_auth, debug
16
+ from meerschaum.api._tokens import optional_token, get_token_from_authorization
17
+ from meerschaum._internal.static import STATIC_CONFIG
11
18
  from meerschaum.utils.packages import attempt_import
12
- fastapi = attempt_import('fastapi', lazy=False, check_update=CHECK_UPDATE)
19
+ from meerschaum.core import User, Token
20
+
21
+ fastapi, starlette = attempt_import('fastapi', 'starlette', lazy=False, check_update=CHECK_UPDATE)
13
22
  fastapi_responses = attempt_import('fastapi.responses', lazy=False, check_update=CHECK_UPDATE)
14
23
  fastapi_login = attempt_import('fastapi_login', check_update=CHECK_UPDATE)
24
+ from fastapi import Depends, HTTPException, Request
25
+ from starlette import status
26
+
15
27
 
16
28
  class CustomOAuth2PasswordRequestForm:
17
29
  def __init__(
18
30
  self,
19
31
  grant_type: str = fastapi.Form(None, regex="password|client_credentials"),
20
- username: str = fastapi.Form(...),
21
- password: str = fastapi.Form(...),
22
- scope: str = fastapi.Form(""),
23
- client_id: str = fastapi.Form(None),
24
- client_secret: str = fastapi.Form(None),
32
+ username: Optional[str] = fastapi.Form(None),
33
+ password: Optional[str] = fastapi.Form(None),
34
+ scope: str = fastapi.Form(" ".join(STATIC_CONFIG['tokens']['scopes'])),
35
+ client_id: Optional[str] = fastapi.Form(None),
36
+ client_secret: Optional[str] = fastapi.Form(None),
37
+ authorization: Optional[str] = fastapi.Header(None),
25
38
  ):
26
39
  self.grant_type = grant_type
27
40
  self.username = username
@@ -30,9 +43,106 @@ class CustomOAuth2PasswordRequestForm:
30
43
  self.client_id = client_id
31
44
  self.client_secret = client_secret
32
45
 
46
+ if (
47
+ not username
48
+ and not password
49
+ and not self.client_id
50
+ and not self.client_secret
51
+ and authorization
52
+ ):
53
+ try:
54
+ scheme, credentials = authorization.split()
55
+ if credentials.startswith('mrsm-key:'):
56
+ credentials = credentials[len('mrsm-key:'):]
57
+ if scheme.lower() in ('basic', 'bearer'):
58
+ decoded_credentials = base64.b64decode(credentials).decode('utf-8')
59
+ _client_id, _client_secret = decoded_credentials.split(':', 1)
60
+ self.client_id = _client_id
61
+ self.client_secret = _client_secret
62
+ self.grant_type = 'client_credentials'
63
+ except ValueError:
64
+ pass
65
+
66
+
67
+ async def optional_user(request: Request) -> Optional[User]:
68
+ """
69
+ FastAPI dependency that returns a User if logged in, otherwise None.
70
+ """
71
+ if no_auth:
72
+ return None
73
+ return await manager(request)
74
+ try:
75
+ return await manager(request)
76
+ except HTTPException:
77
+ return None
78
+
79
+
80
+ async def load_user_or_token(
81
+ request: Request,
82
+ users: bool = True,
83
+ tokens: bool = True,
84
+ ) -> Union[User, Token, None]:
85
+ """
86
+ Load the current user or token.
87
+ """
88
+ authorization = request.headers.get('authorization', request.headers.get('Authorization', None))
89
+ if not authorization:
90
+ raise HTTPException(
91
+ status_code=status.HTTP_401_UNAUTHORIZED,
92
+ detail="Not authenticated.",
93
+ )
94
+ authorization = authorization.replace('Basic ', '').replace('Bearer ', '')
95
+ if not authorization.startswith('mrsm-key:'):
96
+ if not users:
97
+ raise HTTPException(
98
+ status=status.HTTP_401_UNAUTHORIZED,
99
+ detail="Users not authenticated for this endpoint.",
100
+ )
101
+ return await manager(request)
102
+ if not tokens:
103
+ raise HTTPException(
104
+ status_code=status.HTTP_401_UNAUTHORIZED,
105
+ detail="Tokens not authenticated for this endpoint.",
106
+ )
107
+ return get_token_from_authorization(authorization)
108
+
109
+
110
+ def ScopedAuth(scopes: List[str]):
111
+ """
112
+ Dependency factory for authenticating with either a user session or a scoped token.
113
+ """
114
+ async def _authenticate(
115
+ user_or_token: Union[User, Token, None] = Depends(
116
+ load_user_or_token,
117
+ ),
118
+ ) -> Union[User, Token, None]:
119
+ if no_auth:
120
+ return None
121
+
122
+ if not user_or_token:
123
+ raise HTTPException(
124
+ status_code=status.HTTP_401_UNAUTHORIZED,
125
+ detail="Not authenticated.",
126
+ headers={"WWW-Authenticate": "Basic"},
127
+ )
128
+
129
+ fresh_scopes = user_or_token.get_scopes(refresh=True, debug=debug)
130
+ if '*' in fresh_scopes:
131
+ return user_or_token
132
+
133
+ for scope in scopes:
134
+ if scope not in fresh_scopes:
135
+ raise HTTPException(
136
+ status_code=status.HTTP_403_FORBIDDEN,
137
+ detail=f"Missing required scope: '{scope}'",
138
+ )
139
+
140
+ return user_or_token
141
+ return _authenticate
142
+
33
143
 
34
144
  LoginManager = fastapi_login.LoginManager
35
- def generate_secret_key() -> str:
145
+ def generate_secret_key() -> bytes:
36
146
  """
37
147
  Read or generate the secret keyfile.
38
148
  """
@@ -0,0 +1,102 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define the authentication logic for Meerschaum Tokens.
6
+ """
7
+
8
+ import base64
9
+ import uuid
10
+ from typing import Optional, Union, List
11
+ from datetime import datetime, timezone
12
+
13
+ from fastapi import Depends, HTTPException, Request
14
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
15
+ from starlette import status
16
+
17
+ import meerschaum as mrsm
18
+ from meerschaum.api import (
19
+ get_api_connector,
20
+ debug,
21
+ )
22
+ from meerschaum.core import Token, User
23
+ from meerschaum.core.User import verify_password
24
+
25
+
26
+ http_bearer = HTTPBearer(auto_error=False, scheme_name="APIKey")
27
+
28
+
29
+ def get_token_from_authorization(authorization: str) -> Token:
30
+ """
31
+ Helper function to decode and verify a token from credentials.
32
+ Raises HTTPException on failure.
33
+ """
34
+ if authorization.startswith('mrsm-key:'):
35
+ authorization = authorization[len('mrsm-key:'):]
36
+ try:
37
+ credential_string = base64.b64decode(authorization).decode('utf-8')
38
+ token_id_str, secret = credential_string.split(':', 1)
39
+ token_id = uuid.UUID(token_id_str)
40
+ except Exception:
41
+ raise HTTPException(
42
+ status_code=status.HTTP_401_UNAUTHORIZED,
43
+ detail="Invalid token format. Expected Base64-encoded 'token_id:secret'.",
44
+ headers={"WWW-Authenticate": "Bearer"},
45
+ )
46
+
47
+ conn = get_api_connector()
48
+ token = conn.get_token(token_id)
49
+
50
+ if not token or not token.is_valid:
51
+ raise HTTPException(
52
+ status_code=status.HTTP_401_UNAUTHORIZED,
53
+ detail="Invalid or revoked token.",
54
+ headers={"WWW-Authenticate": "Bearer"},
55
+ )
56
+
57
+ if token.get_expiration_status(debug=debug):
58
+ raise HTTPException(
59
+ status_code=status.HTTP_401_UNAUTHORIZED,
60
+ detail="Token has expired.",
61
+ headers={"WWW-Authenticate": "Bearer"},
62
+ )
63
+
64
+ if not verify_password(secret, token.secret_hash):
65
+ raise HTTPException(
66
+ status_code=status.HTTP_401_UNAUTHORIZED,
67
+ detail="Invalid secret.",
68
+ headers={"WWW-Authenticate": "Bearer"},
69
+ )
70
+
71
+ return token
72
+
73
+
74
+ def get_current_token(
75
+ auth_creds: Optional[HTTPAuthorizationCredentials] = Depends(http_bearer),
76
+ ) -> Token:
77
+ """
78
+ FastAPI dependency to authenticate a request with a Meerschaum Token.
79
+ This dependency will fail if no token is provided.
80
+ """
81
+ if auth_creds is None:
82
+ raise HTTPException(
83
+ status_code=status.HTTP_401_UNAUTHORIZED,
84
+ detail="Not authenticated.",
85
+ headers={"WWW-Authenticate": "Bearer"},
86
+ )
87
+ return get_token_from_authorization(auth_creds.credentials)
88
+
89
+
90
+ async def optional_token(
91
+ auth_creds: Optional[HTTPAuthorizationCredentials] = Depends(http_bearer),
92
+ ) -> Optional[Token]:
93
+ """
94
+ FastAPI dependency that returns a Token if provided, otherwise None.
95
+ """
96
+ if not auth_creds:
97
+ return None
98
+
99
+ try:
100
+ return get_token_from_authorization(auth_creds.credentials)
101
+ except HTTPException as e:
102
+ return None
@@ -12,14 +12,11 @@ from meerschaum.utils.packages import (
12
12
  attempt_import,
13
13
  import_dcc,
14
14
  import_html,
15
- _monkey_patch_get_distribution,
16
15
  )
17
16
  flask_compress = attempt_import('flask_compress', lazy=False)
18
- _monkey_patch_get_distribution('flask-compress', flask_compress.__version__)
19
17
  dash = attempt_import('dash', lazy=False)
20
18
 
21
19
  from meerschaum.utils.typing import List, Optional
22
- from meerschaum.config.static import _static_config
23
20
  from meerschaum.api import (
24
21
  app as fastapi_app,
25
22
  debug,
@@ -36,9 +36,9 @@ def add_plugin_pages(debug: bool = False):
36
36
  """
37
37
  Allow users to add pages via the `@web_page` decorator.
38
38
  """
39
- for plugin_name, pages_dicts in _plugin_endpoints_to_pages.items():
39
+ for page_group, pages_dicts in _plugin_endpoints_to_pages.items():
40
40
  if debug:
41
- dprint(f"Adding pages from plugin '{plugin_name}'...")
41
+ dprint(f"Adding pages for group '{page_group}'...")
42
42
  for _endpoint, _page_dict in pages_dicts.items():
43
43
  page_layout = _page_dict['function']()
44
44
  if not _page_dict['skip_navbar']: