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.
- meerschaum/__init__.py +5 -2
- meerschaum/_internal/__init__.py +1 -0
- meerschaum/_internal/arguments/_parse_arguments.py +4 -4
- meerschaum/_internal/arguments/_parser.py +19 -2
- meerschaum/_internal/docs/index.py +49 -2
- meerschaum/_internal/entry.py +6 -6
- meerschaum/_internal/shell/Shell.py +1 -1
- meerschaum/_internal/static.py +356 -0
- meerschaum/actions/api.py +12 -2
- meerschaum/actions/bootstrap.py +7 -7
- meerschaum/actions/edit.py +142 -18
- meerschaum/actions/register.py +137 -6
- meerschaum/actions/show.py +117 -29
- meerschaum/actions/stop.py +4 -1
- meerschaum/actions/sync.py +1 -1
- meerschaum/actions/tag.py +9 -8
- meerschaum/actions/verify.py +5 -8
- meerschaum/api/__init__.py +11 -3
- meerschaum/api/_events.py +39 -2
- meerschaum/api/_oauth2.py +118 -8
- meerschaum/api/_tokens.py +102 -0
- meerschaum/api/dash/__init__.py +0 -3
- meerschaum/api/dash/callbacks/custom.py +2 -2
- meerschaum/api/dash/callbacks/dashboard.py +103 -19
- meerschaum/api/dash/callbacks/plugins.py +0 -1
- meerschaum/api/dash/callbacks/register.py +1 -1
- meerschaum/api/dash/callbacks/settings/__init__.py +1 -0
- meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
- meerschaum/api/dash/callbacks/settings/tokens.py +388 -0
- meerschaum/api/dash/components.py +30 -8
- meerschaum/api/dash/keys.py +19 -93
- meerschaum/api/dash/pages/dashboard.py +1 -20
- meerschaum/api/dash/pages/settings/__init__.py +1 -0
- meerschaum/api/dash/pages/settings/password_reset.py +1 -1
- meerschaum/api/dash/pages/settings/tokens.py +55 -0
- meerschaum/api/dash/pipes.py +94 -59
- meerschaum/api/dash/sessions.py +12 -0
- meerschaum/api/dash/tokens.py +606 -0
- meerschaum/api/dash/websockets.py +1 -1
- meerschaum/api/dash/webterm.py +4 -0
- meerschaum/api/models/__init__.py +23 -3
- meerschaum/api/models/_actions.py +22 -0
- meerschaum/api/models/_pipes.py +85 -7
- meerschaum/api/models/_tokens.py +81 -0
- meerschaum/api/resources/templates/termpage.html +12 -0
- meerschaum/api/routes/__init__.py +1 -0
- meerschaum/api/routes/_actions.py +3 -4
- meerschaum/api/routes/_connectors.py +3 -7
- meerschaum/api/routes/_jobs.py +14 -35
- meerschaum/api/routes/_login.py +49 -12
- meerschaum/api/routes/_misc.py +5 -10
- meerschaum/api/routes/_pipes.py +173 -140
- meerschaum/api/routes/_plugins.py +38 -28
- meerschaum/api/routes/_tokens.py +236 -0
- meerschaum/api/routes/_users.py +47 -35
- meerschaum/api/routes/_version.py +3 -3
- meerschaum/config/__init__.py +43 -20
- meerschaum/config/_default.py +43 -6
- meerschaum/config/_edit.py +28 -24
- meerschaum/config/_environment.py +1 -1
- meerschaum/config/_patch.py +6 -6
- meerschaum/config/_paths.py +5 -1
- meerschaum/config/_read_config.py +65 -34
- meerschaum/config/_sync.py +6 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +31 -11
- meerschaum/config/static.py +18 -0
- meerschaum/connectors/_Connector.py +10 -4
- meerschaum/connectors/__init__.py +4 -20
- meerschaum/connectors/api/_APIConnector.py +34 -6
- meerschaum/connectors/api/_actions.py +2 -2
- meerschaum/connectors/api/_jobs.py +1 -1
- meerschaum/connectors/api/_login.py +33 -7
- meerschaum/connectors/api/_misc.py +2 -2
- meerschaum/connectors/api/_pipes.py +16 -31
- meerschaum/connectors/api/_plugins.py +2 -2
- meerschaum/connectors/api/_request.py +1 -1
- meerschaum/connectors/api/_tokens.py +146 -0
- meerschaum/connectors/api/_users.py +70 -58
- meerschaum/connectors/instance/_InstanceConnector.py +83 -0
- meerschaum/connectors/instance/__init__.py +10 -0
- meerschaum/connectors/instance/_pipes.py +442 -0
- meerschaum/connectors/instance/_plugins.py +151 -0
- meerschaum/connectors/instance/_tokens.py +296 -0
- meerschaum/connectors/instance/_users.py +181 -0
- meerschaum/connectors/parse.py +4 -1
- meerschaum/connectors/sql/_SQLConnector.py +8 -5
- meerschaum/connectors/sql/_cli.py +12 -11
- meerschaum/connectors/sql/_create_engine.py +9 -168
- meerschaum/connectors/sql/_fetch.py +2 -18
- meerschaum/connectors/sql/_pipes.py +156 -190
- meerschaum/connectors/sql/_plugins.py +29 -0
- meerschaum/connectors/sql/_sql.py +46 -21
- meerschaum/connectors/sql/_users.py +29 -2
- meerschaum/connectors/sql/tables/__init__.py +1 -1
- meerschaum/connectors/valkey/_ValkeyConnector.py +2 -4
- meerschaum/connectors/valkey/_pipes.py +53 -26
- meerschaum/connectors/valkey/_plugins.py +2 -26
- meerschaum/core/Pipe/__init__.py +59 -19
- meerschaum/core/Pipe/_attributes.py +412 -90
- meerschaum/core/Pipe/_bootstrap.py +54 -24
- meerschaum/core/Pipe/_data.py +96 -18
- meerschaum/core/Pipe/_dtypes.py +48 -18
- meerschaum/core/Pipe/_edit.py +14 -4
- meerschaum/core/Pipe/_fetch.py +1 -1
- meerschaum/core/Pipe/_show.py +5 -5
- meerschaum/core/Pipe/_sync.py +118 -193
- meerschaum/core/Pipe/_verify.py +4 -4
- meerschaum/{plugins → core/Plugin}/_Plugin.py +9 -11
- meerschaum/core/Plugin/__init__.py +1 -1
- meerschaum/core/Token/_Token.py +220 -0
- meerschaum/core/Token/__init__.py +12 -0
- meerschaum/core/User/_User.py +34 -8
- meerschaum/core/User/__init__.py +9 -1
- meerschaum/core/__init__.py +1 -0
- meerschaum/jobs/_Job.py +3 -2
- meerschaum/jobs/__init__.py +3 -2
- meerschaum/jobs/systemd.py +1 -1
- meerschaum/models/__init__.py +35 -0
- meerschaum/models/pipes.py +247 -0
- meerschaum/models/tokens.py +38 -0
- meerschaum/models/users.py +26 -0
- meerschaum/plugins/__init__.py +22 -7
- meerschaum/plugins/bootstrap.py +2 -1
- meerschaum/utils/_get_pipes.py +68 -27
- meerschaum/utils/daemon/Daemon.py +2 -1
- meerschaum/utils/daemon/__init__.py +30 -2
- meerschaum/utils/dataframe.py +473 -81
- meerschaum/utils/debug.py +15 -15
- meerschaum/utils/dtypes/__init__.py +473 -34
- meerschaum/utils/dtypes/sql.py +368 -28
- meerschaum/utils/formatting/__init__.py +1 -1
- meerschaum/utils/formatting/_pipes.py +5 -4
- meerschaum/utils/formatting/_shell.py +11 -9
- meerschaum/utils/misc.py +246 -148
- meerschaum/utils/packages/__init__.py +10 -27
- meerschaum/utils/packages/_packages.py +41 -34
- meerschaum/utils/pipes.py +181 -0
- meerschaum/utils/process.py +1 -1
- meerschaum/utils/prompt.py +3 -1
- meerschaum/utils/schedule.py +2 -1
- meerschaum/utils/sql.py +121 -44
- meerschaum/utils/typing.py +1 -4
- meerschaum/utils/venv/_Venv.py +2 -2
- meerschaum/utils/venv/__init__.py +5 -7
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/METADATA +92 -96
- meerschaum-3.0.0rc2.dist-info/RECORD +283 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/WHEEL +1 -1
- meerschaum-3.0.0rc2.dist-info/licenses/NOTICE +2 -0
- meerschaum/api/models/_interfaces.py +0 -15
- meerschaum/api/models/_locations.py +0 -15
- meerschaum/api/models/_metrics.py +0 -15
- meerschaum/config/static/__init__.py +0 -186
- meerschaum-2.9.5.dist-info/RECORD +0 -263
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/top_level.txt +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/zip-safe +0 -0
meerschaum/actions/show.py
CHANGED
@@ -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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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 =
|
808
|
-
|
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(
|
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
|
-
|
926
|
-
|
927
|
-
|
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
|
|
meerschaum/actions/stop.py
CHANGED
@@ -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,
|
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:
|
meerschaum/actions/sync.py
CHANGED
@@ -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.
|
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
|
11
|
+
from meerschaum.utils.typing import List, Any, SuccessTuple, Optional
|
12
12
|
|
13
13
|
def tag(
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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:
|
meerschaum/actions/verify.py
CHANGED
@@ -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,
|
61
|
-
|
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:
|
meerschaum/api/__init__.py
CHANGED
@@ -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.
|
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.
|
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
|
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
|
-
|
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
|
-
|
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() ->
|
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
|
meerschaum/api/dash/__init__.py
CHANGED
@@ -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
|
39
|
+
for page_group, pages_dicts in _plugin_endpoints_to_pages.items():
|
40
40
|
if debug:
|
41
|
-
dprint(f"Adding pages
|
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']:
|