meerschaum 2.9.4__py3-none-any.whl → 3.0.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 (154) 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 +17 -1
  5. meerschaum/_internal/entry.py +6 -6
  6. meerschaum/_internal/shell/Shell.py +1 -1
  7. meerschaum/_internal/static.py +372 -0
  8. meerschaum/actions/api.py +12 -2
  9. meerschaum/actions/bootstrap.py +7 -7
  10. meerschaum/actions/edit.py +142 -18
  11. meerschaum/actions/register.py +137 -6
  12. meerschaum/actions/show.py +117 -29
  13. meerschaum/actions/stop.py +4 -1
  14. meerschaum/actions/sync.py +1 -1
  15. meerschaum/actions/tag.py +9 -8
  16. meerschaum/api/__init__.py +9 -2
  17. meerschaum/api/_events.py +39 -2
  18. meerschaum/api/_oauth2.py +118 -8
  19. meerschaum/api/_tokens.py +102 -0
  20. meerschaum/api/dash/__init__.py +0 -1
  21. meerschaum/api/dash/callbacks/custom.py +2 -2
  22. meerschaum/api/dash/callbacks/dashboard.py +133 -18
  23. meerschaum/api/dash/callbacks/plugins.py +0 -1
  24. meerschaum/api/dash/callbacks/register.py +1 -1
  25. meerschaum/api/dash/callbacks/settings/__init__.py +1 -0
  26. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  27. meerschaum/api/dash/callbacks/settings/tokens.py +388 -0
  28. meerschaum/api/dash/components.py +30 -8
  29. meerschaum/api/dash/keys.py +19 -93
  30. meerschaum/api/dash/pages/dashboard.py +1 -20
  31. meerschaum/api/dash/pages/settings/__init__.py +1 -0
  32. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  33. meerschaum/api/dash/pages/settings/tokens.py +55 -0
  34. meerschaum/api/dash/pipes.py +156 -58
  35. meerschaum/api/dash/sessions.py +12 -0
  36. meerschaum/api/dash/tokens.py +606 -0
  37. meerschaum/api/dash/websockets.py +1 -1
  38. meerschaum/api/dash/webterm.py +4 -0
  39. meerschaum/api/models/__init__.py +23 -3
  40. meerschaum/api/models/_actions.py +22 -0
  41. meerschaum/api/models/_pipes.py +85 -7
  42. meerschaum/api/models/_tokens.py +81 -0
  43. meerschaum/api/resources/static/css/dash.css +16 -0
  44. meerschaum/api/resources/templates/termpage.html +12 -0
  45. meerschaum/api/routes/__init__.py +1 -0
  46. meerschaum/api/routes/_actions.py +3 -4
  47. meerschaum/api/routes/_connectors.py +3 -7
  48. meerschaum/api/routes/_jobs.py +14 -35
  49. meerschaum/api/routes/_login.py +49 -12
  50. meerschaum/api/routes/_misc.py +5 -10
  51. meerschaum/api/routes/_pipes.py +134 -111
  52. meerschaum/api/routes/_plugins.py +38 -28
  53. meerschaum/api/routes/_tokens.py +236 -0
  54. meerschaum/api/routes/_users.py +47 -35
  55. meerschaum/api/routes/_version.py +3 -3
  56. meerschaum/config/__init__.py +43 -20
  57. meerschaum/config/_default.py +32 -5
  58. meerschaum/config/_edit.py +28 -24
  59. meerschaum/config/_environment.py +1 -1
  60. meerschaum/config/_patch.py +6 -6
  61. meerschaum/config/_paths.py +5 -1
  62. meerschaum/config/_read_config.py +65 -34
  63. meerschaum/config/_sync.py +6 -3
  64. meerschaum/config/_version.py +1 -1
  65. meerschaum/config/stack/__init__.py +24 -5
  66. meerschaum/config/static.py +18 -0
  67. meerschaum/connectors/_Connector.py +10 -4
  68. meerschaum/connectors/__init__.py +4 -20
  69. meerschaum/connectors/api/_APIConnector.py +34 -6
  70. meerschaum/connectors/api/_actions.py +2 -2
  71. meerschaum/connectors/api/_jobs.py +1 -1
  72. meerschaum/connectors/api/_login.py +33 -7
  73. meerschaum/connectors/api/_misc.py +2 -2
  74. meerschaum/connectors/api/_pipes.py +15 -14
  75. meerschaum/connectors/api/_plugins.py +2 -2
  76. meerschaum/connectors/api/_request.py +1 -1
  77. meerschaum/connectors/api/_tokens.py +146 -0
  78. meerschaum/connectors/api/_users.py +70 -58
  79. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  80. meerschaum/connectors/instance/__init__.py +10 -0
  81. meerschaum/connectors/instance/_pipes.py +442 -0
  82. meerschaum/connectors/instance/_plugins.py +151 -0
  83. meerschaum/connectors/instance/_tokens.py +296 -0
  84. meerschaum/connectors/instance/_users.py +181 -0
  85. meerschaum/connectors/parse.py +4 -1
  86. meerschaum/connectors/sql/_SQLConnector.py +8 -5
  87. meerschaum/connectors/sql/_cli.py +12 -11
  88. meerschaum/connectors/sql/_create_engine.py +6 -154
  89. meerschaum/connectors/sql/_fetch.py +2 -18
  90. meerschaum/connectors/sql/_pipes.py +42 -31
  91. meerschaum/connectors/sql/_plugins.py +29 -0
  92. meerschaum/connectors/sql/_sql.py +9 -2
  93. meerschaum/connectors/sql/_users.py +29 -2
  94. meerschaum/connectors/sql/tables/__init__.py +1 -1
  95. meerschaum/connectors/valkey/_ValkeyConnector.py +2 -4
  96. meerschaum/connectors/valkey/_pipes.py +9 -10
  97. meerschaum/connectors/valkey/_plugins.py +2 -26
  98. meerschaum/core/Pipe/__init__.py +31 -14
  99. meerschaum/core/Pipe/_attributes.py +156 -58
  100. meerschaum/core/Pipe/_bootstrap.py +54 -24
  101. meerschaum/core/Pipe/_data.py +41 -1
  102. meerschaum/core/Pipe/_dtypes.py +29 -14
  103. meerschaum/core/Pipe/_edit.py +12 -4
  104. meerschaum/core/Pipe/_show.py +5 -5
  105. meerschaum/core/Pipe/_sync.py +48 -53
  106. meerschaum/core/Pipe/_verify.py +1 -1
  107. meerschaum/{plugins → core/Plugin}/_Plugin.py +9 -11
  108. meerschaum/core/Plugin/__init__.py +1 -1
  109. meerschaum/core/Token/_Token.py +221 -0
  110. meerschaum/core/Token/__init__.py +12 -0
  111. meerschaum/core/User/_User.py +34 -8
  112. meerschaum/core/User/__init__.py +9 -1
  113. meerschaum/core/__init__.py +1 -0
  114. meerschaum/jobs/_Job.py +3 -2
  115. meerschaum/jobs/__init__.py +3 -2
  116. meerschaum/jobs/systemd.py +1 -1
  117. meerschaum/models/__init__.py +35 -0
  118. meerschaum/models/pipes.py +247 -0
  119. meerschaum/models/tokens.py +38 -0
  120. meerschaum/models/users.py +26 -0
  121. meerschaum/plugins/__init__.py +22 -7
  122. meerschaum/plugins/bootstrap.py +2 -1
  123. meerschaum/utils/_get_pipes.py +68 -27
  124. meerschaum/utils/daemon/Daemon.py +2 -1
  125. meerschaum/utils/daemon/__init__.py +30 -2
  126. meerschaum/utils/dataframe.py +96 -15
  127. meerschaum/utils/dtypes/__init__.py +93 -21
  128. meerschaum/utils/dtypes/sql.py +44 -0
  129. meerschaum/utils/formatting/__init__.py +1 -1
  130. meerschaum/utils/formatting/_pipes.py +5 -4
  131. meerschaum/utils/formatting/_shell.py +11 -9
  132. meerschaum/utils/misc.py +237 -80
  133. meerschaum/utils/packages/__init__.py +3 -6
  134. meerschaum/utils/packages/_packages.py +34 -32
  135. meerschaum/utils/pipes.py +181 -0
  136. meerschaum/utils/process.py +1 -1
  137. meerschaum/utils/prompt.py +3 -1
  138. meerschaum/utils/schedule.py +1 -0
  139. meerschaum/utils/sql.py +115 -39
  140. meerschaum/utils/typing.py +1 -4
  141. meerschaum/utils/venv/_Venv.py +2 -2
  142. meerschaum/utils/venv/__init__.py +5 -7
  143. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0rc1.dist-info}/METADATA +88 -80
  144. meerschaum-3.0.0rc1.dist-info/RECORD +282 -0
  145. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0rc1.dist-info}/WHEEL +1 -1
  146. meerschaum/api/models/_interfaces.py +0 -15
  147. meerschaum/api/models/_locations.py +0 -15
  148. meerschaum/api/models/_metrics.py +0 -15
  149. meerschaum/config/static/__init__.py +0 -186
  150. meerschaum-2.9.4.dist-info/RECORD +0 -263
  151. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0rc1.dist-info}/entry_points.txt +0 -0
  152. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0rc1.dist-info}/licenses/LICENSE +0 -0
  153. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0rc1.dist-info}/top_level.txt +0 -0
  154. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0rc1.dist-info}/zip-safe +0 -0
@@ -8,14 +8,92 @@ Pydantic model for a pipe's keys.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ from typing import Optional, List, Tuple, Dict, Union, Any
12
+
11
13
  import meerschaum as mrsm
12
- from meerschaum.utils.typing import Optional
13
14
 
14
- pydantic = mrsm.attempt_import('pydantic', warn=False, lazy=False)
15
+ from meerschaum.models.pipes import (
16
+ PipeModel as BasePipeModel,
17
+ ConnectorKeysModel,
18
+ MetricKeyModel,
19
+ LocationKeyModel,
20
+ )
21
+
22
+ pydantic = mrsm.attempt_import('pydantic', lazy=False)
23
+ from pydantic import (
24
+ BaseModel,
25
+ RootModel,
26
+ field_validator,
27
+ ValidationInfo,
28
+ ConfigDict,
29
+ )
30
+
31
+
32
+ class PipeModel(BasePipeModel):
33
+ """
34
+ A `Pipe`'s model to be used in API responses.
35
+ """
36
+ parameters: Optional[dict] = None
37
+ model_config = ConfigDict(
38
+ json_schema_extra={
39
+ 'example': {
40
+ 'connector_keys': 'sql:main',
41
+ 'metric_key': 'weather',
42
+ 'location_key': 'us.co.denver',
43
+ 'instance_keys': 'sql:main',
44
+ 'parameters': {
45
+ 'columns': {
46
+ 'datetime': 'dt',
47
+ 'id': 'id',
48
+ 'value': 'val',
49
+ },
50
+ },
51
+ },
52
+ },
53
+ )
54
+
55
+
56
+ class FetchPipesKeysResponseModel(
57
+ RootModel[List[Tuple[ConnectorKeysModel, MetricKeyModel, LocationKeyModel]]]
58
+ ):
59
+ """
60
+ A list of tuples containing connector, metric, and location keys.
61
+ """
62
+ model_config = ConfigDict(
63
+ json_schema_extra={
64
+ 'example': [
65
+ ['sql:main', 'weather', 'greenville'],
66
+ ['plugin:noaa', 'weather', 'greenville'],
67
+ ],
68
+ },
69
+ )
15
70
 
16
71
 
17
- class MetaPipe(pydantic.BaseModel):
18
- connector_keys: str
19
- metric_key: str
20
- location_key: Optional[str] = None
21
- instance_keys: Optional[str] = None
72
+ class SyncPipeRequestModel(
73
+ RootModel[
74
+ Union[
75
+ List[Dict[str, Any]],
76
+ Dict[str, List[Any]],
77
+ str
78
+ ]
79
+ ]
80
+ ):
81
+ """
82
+ The accepted formats of dataframes to be synced.
83
+ """
84
+ model_config = ConfigDict(
85
+ json_schema_extra={
86
+ 'example': [
87
+ {
88
+ 'timestamp': '2026-01-01',
89
+ 'id': 1,
90
+ 'value': 100.1,
91
+ },
92
+ {
93
+ 'timestamp': '2026-01-02',
94
+ 'id': 1,
95
+ 'value': 200.2,
96
+ }
97
+ ],
98
+ }
99
+ )
@@ -0,0 +1,81 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Response models for tokens.
6
+ """
7
+
8
+ import uuid
9
+ from datetime import datetime
10
+ from typing import Optional, List, Union
11
+
12
+ import meerschaum as mrsm
13
+ from meerschaum._internal.static import STATIC_CONFIG
14
+
15
+ from pydantic import BaseModel, RootModel, Field, ConfigDict
16
+
17
+
18
+ class RegisterTokenRequestModel(BaseModel):
19
+ label: Optional[str] = None
20
+ expiration: Optional[datetime] = None
21
+ scopes: List[str] = Field(default_factory=lambda: list(STATIC_CONFIG['tokens']['scopes']))
22
+ model_config = ConfigDict(
23
+ json_schema_extra = {
24
+ 'examples': [
25
+ {
26
+ 'label': 'my-iot-device',
27
+ 'expiration': '2026-01-01T00:00:00Z',
28
+ 'scopes': list(STATIC_CONFIG['tokens']['scopes']),
29
+ }
30
+ ]
31
+ }
32
+ )
33
+
34
+
35
+ class RegisterTokenResponseModel(BaseModel):
36
+ label: str
37
+ secret: str
38
+ id: uuid.UUID
39
+ api_key: str
40
+ expiration: Optional[datetime]
41
+ model_config = ConfigDict(
42
+ json_schema_extra = {
43
+ 'examples': [
44
+ {
45
+ 'label': 'my-iot-device',
46
+ 'secret': 'a_very_long_secret_string_that_is_only_shown_once',
47
+ 'id': '1540c2f6-a99d-463c-bfab-47d361200123',
48
+ 'expiration': '2026-01-01T00:00:00Z',
49
+ 'api_key': 'mrsm-key:MTU0MGMyZjYtYTk5ZC00NjNjLWJmYWItNDdkMzYxMjAwMTIzOmFfdmVyeV9sb25nX3NlY3JldF9zdHJpbmdfdGhhdF9pc19vbmx5X3Nob3duX29uY2U=',
50
+ }
51
+ ]
52
+ }
53
+ )
54
+
55
+
56
+ class GetTokenResponseModel(BaseModel):
57
+ id: Optional[uuid.UUID] = Field(default=None)
58
+ creation: datetime = Field()
59
+ expiration: Optional[datetime] = Field()
60
+ label: str = Field()
61
+ user_id: Optional[Union[int, str, uuid.UUID]] = Field(default=None)
62
+ scopes: List[str] = Field(default=list(STATIC_CONFIG['tokens']['scopes']))
63
+ is_valid: bool = Field(default=True)
64
+
65
+
66
+ class GetTokensResponseModel(RootModel[List[GetTokenResponseModel]]):
67
+ model_config = ConfigDict(
68
+ json_schema_extra={
69
+ 'example': [
70
+ {
71
+ 'label': 'my-iot-device',
72
+ 'id': '1540c2f6-a99d-463c-bfab-47d361200123',
73
+ 'user_id': 1,
74
+ 'scopes': ['pipes:write'],
75
+ 'creation': '2025-07-01T00:00:00Z',
76
+ 'expiration': '2026-01-01T00:00:00Z',
77
+ 'is_valid': True,
78
+ },
79
+ ],
80
+ }
81
+ )
@@ -80,3 +80,19 @@ a {
80
80
  .pages-offcanvas-accordion div {
81
81
  padding: 0 !important;
82
82
  }
83
+
84
+ /* restyle radio items */
85
+ .radio-group .form-check {
86
+ padding-left: 0;
87
+ }
88
+
89
+ .radio-group .btn-group > .form-check:not(:last-child) > .btn {
90
+ border-top-right-radius: 0;
91
+ border-bottom-right-radius: 0;
92
+ }
93
+
94
+ .radio-group .btn-group > .form-check:not(:first-child) > .btn {
95
+ border-top-left-radius: 0;
96
+ border-bottom-left-radius: 0;
97
+ margin-left: -1px;
98
+ }
@@ -30,6 +30,7 @@ window.addEventListener(
30
30
  let connector_keys = event.data['connector_keys'] ? event.data['connector_keys'] : [];
31
31
  let metric_keys = event.data['metric_keys'] ? event.data['metric_keys'] : [];
32
32
  let location_keys = event.data['location_keys'] ? event.data['location_keys'] : [];
33
+ let tags = event.data['tags'] ? event.data['tags'] : [];
33
34
  let connector_keys_str = " -c";
34
35
  for (let ck of connector_keys){
35
36
  if (typeof ck === "string"){
@@ -62,6 +63,16 @@ window.addEventListener(
62
63
  if (location_keys_str === " -l"){
63
64
  location_keys_str = "";
64
65
  }
66
+ let tags_str = " -t";
67
+ for (tag of tags){
68
+ if (typeof tag === "string"){
69
+ quote_str = tag.includes(" ") ? "'" : "";
70
+ tags_str += " " + quote_str + tag + quote_str;
71
+ }
72
+ }
73
+ if (tags_str === " -t"){
74
+ tags_str = "";
75
+ }
65
76
 
66
77
  let instance = event.data['instance'] ? event.data['instance'] : '';
67
78
  let flags_str = "";
@@ -105,6 +116,7 @@ window.addEventListener(
105
116
  + connector_keys_str
106
117
  + metric_keys_str
107
118
  + location_keys_str
119
+ + tags_str
108
120
  + flags_str
109
121
  + '\r'
110
122
  );
@@ -15,6 +15,7 @@ import meerschaum.api.routes._misc
15
15
  import meerschaum.api.routes._pipes
16
16
  import meerschaum.api.routes._plugins
17
17
  import meerschaum.api.routes._users
18
+ import meerschaum.api.routes._tokens
18
19
  import meerschaum.api.routes._version
19
20
 
20
21
  from meerschaum.api import _include_dash
@@ -18,6 +18,7 @@ from meerschaum.api import (
18
18
  manager,
19
19
  private,
20
20
  no_auth,
21
+ ScopedAuth,
21
22
  )
22
23
  from meerschaum.actions import actions
23
24
  from meerschaum.core.User import is_user_allowed_to_execute
@@ -28,7 +29,7 @@ actions_endpoint = endpoints['actions']
28
29
  @app.get(actions_endpoint, tags=['Actions'])
29
30
  def get_actions(
30
31
  curr_user = (
31
- fastapi.Depends(manager) if private else None
32
+ fastapi.Depends(ScopedAuth(['actions:execute'])) if private else None
32
33
  ),
33
34
  ) -> List[str]:
34
35
  """
@@ -41,9 +42,7 @@ def get_actions(
41
42
  def do_action_legacy(
42
43
  action: str,
43
44
  keywords: Dict[str, Any] = fastapi.Body(...),
44
- curr_user = (
45
- fastapi.Depends(manager) if not no_auth else None
46
- ),
45
+ curr_user = fastapi.Depends(ScopedAuth(['actions:execute'])),
47
46
  ) -> SuccessTuple:
48
47
  """
49
48
  Perform a Meerschaum action (if permissions allow).
@@ -10,7 +10,7 @@ import fastapi
10
10
  from fastapi import HTTPException
11
11
 
12
12
  import meerschaum as mrsm
13
- from meerschaum.api import app, endpoints, no_auth, manager
13
+ from meerschaum.api import app, endpoints, ScopedAuth
14
14
  from meerschaum.utils.typing import Optional, Dict, List, Union
15
15
 
16
16
  endpoint = endpoints['connectors']
@@ -19,9 +19,7 @@ endpoint = endpoints['connectors']
19
19
  @app.get(endpoint, tags=['Connectors'])
20
20
  def get_connectors(
21
21
  type: Optional[str] = None,
22
- curr_user = (
23
- fastapi.Depends(manager) if not no_auth else None
24
- ),
22
+ curr_user=fastapi.Depends(ScopedAuth(['connectors:read'])),
25
23
  ) -> Union[Dict[str, List[str]], List[str]]:
26
24
  """
27
25
  Return the keys of the registered connectors.
@@ -54,9 +52,7 @@ def get_connectors(
54
52
  @app.get(endpoint + "/{type}", tags=['Connectors'])
55
53
  def get_connectors_by_type(
56
54
  type: str,
57
- curr_user = (
58
- fastapi.Depends(manager) if not no_auth else None
59
- ),
55
+ curr_user=fastapi.Depends(ScopedAuth(['connectors:read']))
60
56
  ):
61
57
  """
62
58
  Convenience method for `get_connectors()`.
@@ -28,11 +28,12 @@ from meerschaum.api import (
28
28
  fastapi,
29
29
  app,
30
30
  endpoints,
31
- manager,
31
+ ScopedAuth,
32
32
  no_auth,
33
+ manager,
33
34
  debug,
34
35
  )
35
- from meerschaum.config.static import STATIC_CONFIG
36
+ from meerschaum._internal.static import STATIC_CONFIG
36
37
  from meerschaum.core.User import is_user_allowed_to_execute
37
38
 
38
39
 
@@ -53,9 +54,7 @@ def _get_job(name: str, sysargs: Union[str, List[str], None] = None):
53
54
 
54
55
  @app.get(endpoints['jobs'], tags=['Jobs'])
55
56
  def get_jobs(
56
- curr_user=(
57
- fastapi.Depends(manager) if not no_auth else None
58
- ),
57
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:read'])),
59
58
  ) -> Dict[str, Dict[str, Any]]:
60
59
  """
61
60
  Return metadata about the current jobs.
@@ -84,9 +83,7 @@ def get_jobs(
84
83
  @app.get(endpoints['jobs'] + '/{name}', tags=['Jobs'])
85
84
  def get_job(
86
85
  name: str,
87
- curr_user=(
88
- fastapi.Depends(manager) if not no_auth else None
89
- ),
86
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:read'])),
90
87
  ) -> Dict[str, Any]:
91
88
  """
92
89
  Return metadata for a single job.
@@ -137,9 +134,7 @@ def clean_sysargs(sysargs: List[str]) -> List[str]:
137
134
  def create_job(
138
135
  name: str,
139
136
  metadata: Union[List[str], Dict[str, Any]],
140
- curr_user=(
141
- fastapi.Depends(manager) if not no_auth else None
142
- ),
137
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:execute', 'jobs:write'])),
143
138
  ) -> SuccessTuple:
144
139
  """
145
140
  Create and start a new job.
@@ -172,9 +167,7 @@ def create_job(
172
167
  @app.delete(endpoints['jobs'] + '/{name}', tags=['Jobs'])
173
168
  def delete_job(
174
169
  name: str,
175
- curr_user=(
176
- fastapi.Depends(manager) if not no_auth else None
177
- ),
170
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:delete'])),
178
171
  ) -> SuccessTuple:
179
172
  """
180
173
  Delete a job.
@@ -189,9 +182,7 @@ def delete_job(
189
182
  @app.get(endpoints['jobs'] + '/{name}/exists', tags=['Jobs'])
190
183
  def get_job_exists(
191
184
  name: str,
192
- curr_user=(
193
- fastapi.Depends(manager) if not no_auth else None
194
- ),
185
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:read'])),
195
186
  ) -> bool:
196
187
  """
197
188
  Return whether a job exists.
@@ -203,9 +194,7 @@ def get_job_exists(
203
194
  @app.get(endpoints['logs'] + '/{name}', tags=['Jobs'])
204
195
  def get_logs(
205
196
  name: str,
206
- curr_user=(
207
- fastapi.Depends(manager) if not no_auth else None
208
- ),
197
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:read', 'logs:read'])),
209
198
  ) -> Union[str, None]:
210
199
  """
211
200
  Return a job's log text.
@@ -224,9 +213,7 @@ def get_logs(
224
213
  @app.post(endpoints['jobs'] + '/{name}/start', tags=['Jobs'])
225
214
  def start_job(
226
215
  name: str,
227
- curr_user=(
228
- fastapi.Depends(manager) if not no_auth else None
229
- ),
216
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:execute'])),
230
217
  ) -> SuccessTuple:
231
218
  """
232
219
  Start a job if stopped.
@@ -247,9 +234,7 @@ def start_job(
247
234
  @app.post(endpoints['jobs'] + '/{name}/stop', tags=['Jobs'])
248
235
  def stop_job(
249
236
  name: str,
250
- curr_user=(
251
- fastapi.Depends(manager) if not no_auth else None
252
- ),
237
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:execute', 'josb:stop'])),
253
238
  ) -> SuccessTuple:
254
239
  """
255
240
  Stop a job if running.
@@ -270,9 +255,7 @@ def stop_job(
270
255
  @app.post(endpoints['jobs'] + '/{name}/pause', tags=['Jobs'])
271
256
  def pause_job(
272
257
  name: str,
273
- curr_user=(
274
- fastapi.Depends(manager) if not no_auth else None
275
- ),
258
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:execute', 'jobs:pause'])),
276
259
  ) -> SuccessTuple:
277
260
  """
278
261
  Pause a job if running.
@@ -293,9 +276,7 @@ def pause_job(
293
276
  @app.get(endpoints['jobs'] + '/{name}/stop_time', tags=['Jobs'])
294
277
  def get_stop_time(
295
278
  name: str,
296
- curr_user=(
297
- fastapi.Depends(manager) if not no_auth else None
298
- ),
279
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:read'])),
299
280
  ) -> Union[datetime, None]:
300
281
  """
301
282
  Get the timestamp when the job was manually stopped.
@@ -307,9 +288,7 @@ def get_stop_time(
307
288
  @app.get(endpoints['jobs'] + '/{name}/is_blocking_on_stdin', tags=['Jobs'])
308
289
  def get_is_blocking_on_stdin(
309
290
  name: str,
310
- curr_user=(
311
- fastapi.Depends(manager) if not no_auth else None
312
- ),
291
+ curr_user=fastapi.Depends(ScopedAuth(['jobs:read'])),
313
292
  ) -> bool:
314
293
  """
315
294
  Return whether a job is blocking on stdin.
@@ -6,19 +6,22 @@
6
6
  Manage access and refresh tokens.
7
7
  """
8
8
 
9
+ import uuid
9
10
  from datetime import datetime, timedelta, timezone
11
+ from typing import Union
10
12
 
11
13
  import fastapi
12
- from fastapi import Request, status
14
+ from fastapi import Request, status, Response
13
15
  from fastapi_login.exceptions import InvalidCredentialsException
14
16
  from fastapi.exceptions import RequestValidationError
15
17
  from starlette.responses import JSONResponse
16
18
 
17
19
  from meerschaum.api import endpoints, get_api_connector, app, debug, manager, no_auth
18
20
  from meerschaum.core import User
19
- from meerschaum.config.static import STATIC_CONFIG
21
+ from meerschaum._internal.static import STATIC_CONFIG
20
22
  from meerschaum.utils.typing import Dict, Any
21
- from meerschaum.core.User._User import verify_password
23
+ from meerschaum.utils.misc import is_uuid
24
+ from meerschaum.core.User import verify_password
22
25
  from meerschaum.utils.warnings import warn
23
26
  from meerschaum.api._oauth2 import CustomOAuth2PasswordRequestForm
24
27
 
@@ -39,26 +42,60 @@ def login(
39
42
  Login and set the session token.
40
43
  """
41
44
  username, password = (
42
- (data['username'], data['password'])
45
+ (data.get('username', None), data.get('password', None))
43
46
  if isinstance(data, dict)
44
47
  else (data.username, data.password)
45
- ) if not no_auth else ('no-auth', 'no-auth')
46
-
47
- user = User(username, password)
48
- correct_password = no_auth or verify_password(
49
- password,
50
- get_api_connector().get_user_password_hash(user, debug=debug)
51
48
  )
52
- if not correct_password:
49
+ client_id, client_secret = (
50
+ (data.get('client_id', None), data.get('client_secret', None))
51
+ if isinstance(data, dict)
52
+ else (data.client_id, data.client_secret)
53
+ )
54
+ grant_type = (
55
+ data.get('grant_type', None)
56
+ if isinstance(data, dict)
57
+ else data.grant_type
58
+ )
59
+ if not grant_type:
60
+ grant_type = (
61
+ 'password'
62
+ if username and password
63
+ else 'client_credentials'
64
+ )
65
+
66
+ expires_dt: Union[datetime, None] = None
67
+ if grant_type == 'password':
68
+ user = User(str(username), str(password), instance=get_api_connector())
69
+ correct_password = no_auth or verify_password(
70
+ str(password),
71
+ get_api_connector().get_user_password_hash(user, debug=debug)
72
+ )
73
+ if not correct_password:
74
+ raise InvalidCredentialsException
75
+
76
+ elif grant_type == 'client_credentials':
77
+ if not is_uuid(str(client_id)):
78
+ raise InvalidCredentialsException
79
+ token_id = uuid.UUID(client_id)
80
+ correct_password = no_auth or verify_password(
81
+ str(client_secret),
82
+ str(get_api_connector().get_token_secret_hash(token_id, debug=debug))
83
+ )
84
+ if not correct_password:
85
+ raise InvalidCredentialsException
86
+ else:
53
87
  raise InvalidCredentialsException
54
88
 
55
89
  expires_minutes = STATIC_CONFIG['api']['oauth']['token_expires_minutes']
56
90
  expires_delta = timedelta(minutes=expires_minutes)
57
91
  expires_dt = datetime.now(timezone.utc).replace(tzinfo=None) + expires_delta
58
92
  access_token = manager.create_access_token(
59
- data={'sub': username},
93
+ data={
94
+ 'sub': (username if grant_type == 'password' else client_id),
95
+ },
60
96
  expires=expires_delta
61
97
  )
98
+
62
99
  return {
63
100
  'access_token': access_token,
64
101
  'token_type': 'bearer',
@@ -19,7 +19,8 @@ from meerschaum.api import (
19
19
  debug,
20
20
  get_api_connector,
21
21
  private,
22
- manager
22
+ manager,
23
+ ScopedAuth,
23
24
  )
24
25
  from meerschaum.config.paths import API_STATIC_PATH
25
26
  from meerschaum import __version__ as version
@@ -38,9 +39,7 @@ def get_favicon() -> Any:
38
39
 
39
40
  @app.get(endpoints['chaining'], tags=['Misc'])
40
41
  def get_chaining_status(
41
- curr_user = (
42
- fastapi.Depends(manager) if private else None
43
- ),
42
+ curr_user = fastapi.Depends(ScopedAuth(['instance:read'])) if private else None,
44
43
  ) -> bool:
45
44
  """
46
45
  Return whether this API instance may be chained.
@@ -50,9 +49,7 @@ def get_chaining_status(
50
49
 
51
50
  @app.get(endpoints['info'], tags=['Misc'])
52
51
  def get_instance_info(
53
- curr_user = (
54
- fastapi.Depends(manager) if private else None
55
- ),
52
+ curr_user = fastapi.Depends(ScopedAuth(['instance:read'])) if private else None,
56
53
  instance_keys: Optional[str] = None,
57
54
  ) -> Dict[str, Union[str, int]]:
58
55
  """
@@ -87,9 +84,7 @@ def get_healtheck(instance_keys: Optional[str] = None) -> Dict[str, Any]:
87
84
  if debug:
88
85
  @app.get('/id', tags=['Misc'])
89
86
  def get_ids(
90
- curr_user = (
91
- fastapi.Depends(manager) if private else None
92
- ),
87
+ curr_user = fastapi.Depends(ScopedAuth(['instance:read'])) if private else None,
93
88
  ) -> Dict[str, Union[int, str]]:
94
89
  return {
95
90
  'server': SERVER_ID,