dataall-cli 0.4.3__tar.gz → 0.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataall-cli
3
- Version: 0.4.3
3
+ Version: 0.5.0
4
4
  Summary: AWS data.all CLI
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -18,7 +18,7 @@ Requires-Dist: atomicfile (>=1.0.1)
18
18
  Requires-Dist: boto3 (>=1.40.55)
19
19
  Requires-Dist: botocore (>=1.40.55)
20
20
  Requires-Dist: click (>=8.1.6)
21
- Requires-Dist: dataall-core (>=0.4.3)
21
+ Requires-Dist: dataall-core (>=0.5.0,<0.6.0)
22
22
  Requires-Dist: packaging (>=24.2)
23
23
  Requires-Dist: setuptools ; python_version >= "3.12"
24
24
  Requires-Dist: typing-extensions (>=4.4.0)
@@ -2,10 +2,12 @@
2
2
 
3
3
  import json
4
4
  import logging
5
+ from pathlib import Path
5
6
  from typing import Any, Callable, Dict, Optional
6
7
 
7
8
  import click
8
9
  from dataall_core.dataall_client import DataallClient
10
+ from dataall_core.profile import get_profile
9
11
 
10
12
  logger = logging.getLogger(__name__)
11
13
 
@@ -45,10 +47,15 @@ def _bind_function(
45
47
 
46
48
  def func(**kwargs: Any) -> None:
47
49
  logger.debug("I am the '{}' command".format(name))
50
+ profile = kwargs.get("profile", "default")
51
+ if get_profile(profile=profile, config_path=Path(config_path)) is None:
52
+ raise click.ClickException(
53
+ f"Profile '{profile}' is not configured; run: dataall_cli configure --profile {profile}"
54
+ )
48
55
  da_client = DataallClient(
49
56
  schema_path=schema_path, schema_version=schema_version
50
57
  ).client(
51
- profile=kwargs.get("profile", "default"),
58
+ profile=profile,
52
59
  config_path=config_path,
53
60
  custom_headers=custom_headers,
54
61
  )
@@ -0,0 +1,288 @@
1
+ """CLI for data.all."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ import click
10
+ from dataall_core.dataall_client import DataallClient
11
+ from dataall_core.profile import CONFIG_PATH
12
+
13
+ from dataall_cli.bind_commands import bind
14
+ from dataall_cli.utils import discover_from_frontend, frontend_origin, save_config
15
+
16
+ DA_CONFIG_PATH = os.getenv("dataall_config_path", CONFIG_PATH)
17
+ CREDS_PATH = os.getenv("dataall_creds_path", None)
18
+ SCHEMA_PATH = os.getenv("dataall_schema_path", None)
19
+ SCHEMA_VERSION = os.getenv("dataall_schema_version", None)
20
+ DA_CUSTOM_HEADERS_JSON: str = os.getenv("dataall_custom_headers_json", "{}")
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ try:
25
+ custom_headers = json.loads(DA_CUSTOM_HEADERS_JSON)
26
+ except ValueError:
27
+ logger.info(
28
+ f"Invalid custom headers json string: {DA_CUSTOM_HEADERS_JSON}. Using default headers..."
29
+ )
30
+ custom_headers = {}
31
+
32
+ da = DataallClient(schema_path=SCHEMA_PATH, schema_version=SCHEMA_VERSION)
33
+ default_client = da.client(config_path=DA_CONFIG_PATH, custom_headers=custom_headers)
34
+ commands = da.op_dict
35
+
36
+
37
+ @click.group(name="dataall_cli", invoke_without_command=True)
38
+ def dataall_cli() -> None:
39
+ """data.all cli groups."""
40
+ click.echo("Executing dataall_cli.", err=True)
41
+ pass
42
+
43
+
44
+ bind(
45
+ dataall_cli=dataall_cli,
46
+ commands=commands,
47
+ config_path=DA_CONFIG_PATH,
48
+ schema_path=SCHEMA_PATH,
49
+ schema_version=SCHEMA_VERSION,
50
+ custom_headers=custom_headers,
51
+ )
52
+
53
+
54
+ AUTH_TYPES = ["CognitoAuth", "CustomAuth", "OidcBrowserAuth"]
55
+ DEFAULT_REDIRECT_URI = "http://localhost:8765/callback"
56
+ DEFAULT_FALLBACK_REDIRECT_URI = "http://localhost:8766/callback"
57
+ DEFAULT_SCOPES = "openid offline_access"
58
+ DISCOVERED = "dataall_discovered"
59
+ DISCOVERED_FOR = "dataall_discovered_for"
60
+
61
+ DOMAIN_PROMPT = "Enter data.all's domain URL (e.g. https://<DOMAIN>.com)"
62
+ IDP_PROMPT = "Enter data.all Identity Provider Domain (e.g. https://<IdP-DOMAIN>.com)"
63
+ ISSUER_PROMPT = (
64
+ "Enter OIDC issuer URL (e.g. https://<ORG>.okta.com/oauth2/<AUTH-SERVER-ID>)"
65
+ )
66
+ SECRET_PROMPT = "Enter IdP client secret (if applicable)"
67
+ AUTH_SERVER_PROMPT = "Enter IdP custom auth server (if applicable)"
68
+ FRONT_PAGE_PROMPT = "Enter data.all front page URL (leave empty to configure manually)"
69
+
70
+
71
+ class AuthScopedOption(click.Option):
72
+ """Option whose prompt depends on ``--auth_type``.
73
+
74
+ ``scoped`` maps an auth type to ``{"prompt": text, "default": value}``. A spec
75
+ without ``prompt`` uses its default silently; auth types not listed get ``None``;
76
+ without ``scoped`` the option prompts normally. Values discovered from
77
+ ``--dataall_url`` are used without prompting.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ *args: Any,
83
+ scoped: Optional[Dict[str, Dict[str, Any]]] = None,
84
+ **kwargs: Any,
85
+ ) -> None:
86
+ self.scoped = scoped
87
+ kwargs.setdefault("prompt", True)
88
+ super().__init__(*args, **kwargs)
89
+
90
+ def prompt_for_value(self, ctx: click.Context) -> Any:
91
+ """Return the value for the active auth type, prompting only when needed."""
92
+ discovered = ctx.meta.get(DISCOVERED, {})
93
+ if self.name in discovered:
94
+ return discovered[self.name]
95
+ if self.scoped is None:
96
+ return super().prompt_for_value(ctx)
97
+ spec = self.scoped.get(str(ctx.params.get("auth_type")))
98
+ if spec is None:
99
+ return None
100
+ if "prompt" not in spec:
101
+ return spec.get("default")
102
+ return click.prompt(
103
+ spec["prompt"],
104
+ default=spec.get("default"),
105
+ type=self.type,
106
+ value_proc=lambda x: self.process_value(ctx, x),
107
+ )
108
+
109
+
110
+ def _discover(
111
+ ctx: click.Context, _param: click.Parameter, value: Optional[str]
112
+ ) -> Optional[str]:
113
+ if not value or ctx.meta.get(DISCOVERED_FOR) == value:
114
+ return value
115
+ ctx.meta[DISCOVERED_FOR] = value
116
+ try:
117
+ found = discover_from_frontend(value)
118
+ except Exception as e:
119
+ click.echo(f"Could not read settings from {value}: {e}", err=True)
120
+ found = {"frontend_url": frontend_origin(value)}
121
+ chosen = ctx.params.get("auth_type")
122
+ detected = found.get("auth_type")
123
+ if chosen and detected and chosen != detected:
124
+ click.echo(
125
+ f"The front page uses {detected} but --auth_type {chosen} was given; "
126
+ "keeping only the page URL",
127
+ err=True,
128
+ )
129
+ found = {"frontend_url": found["frontend_url"]}
130
+ for key, item in found.items():
131
+ click.echo(f"Discovered {key}: {item}", err=True)
132
+ ctx.meta[DISCOVERED] = found
133
+ return value
134
+
135
+
136
+ def _for(auth_types: List[str], **spec: Any) -> Dict[str, Dict[str, Any]]:
137
+ return {auth_type: dict(spec) for auth_type in auth_types}
138
+
139
+
140
+ @dataall_cli.command()
141
+ @click.option(
142
+ "--dataall_url",
143
+ prompt=FRONT_PAGE_PROMPT,
144
+ default="",
145
+ show_default=False,
146
+ expose_value=False,
147
+ callback=_discover,
148
+ help="data.all front page URL; the auth type, IdP, client id and API endpoint are read from it",
149
+ )
150
+ @click.option(
151
+ "--auth_type",
152
+ cls=AuthScopedOption,
153
+ type=click.Choice(AUTH_TYPES),
154
+ default="CognitoAuth",
155
+ prompt="Select authentication type",
156
+ help="Authentication type: Cognito, Custom (username/password) or OIDC browser login",
157
+ )
158
+ @click.option(
159
+ "--client_id",
160
+ cls=AuthScopedOption,
161
+ required=True,
162
+ scoped=_for(AUTH_TYPES, prompt="Enter data.all app client id"),
163
+ help="data.all app client id",
164
+ )
165
+ @click.option(
166
+ "--api_endpoint_url",
167
+ cls=AuthScopedOption,
168
+ required=True,
169
+ scoped=_for(AUTH_TYPES, prompt="Enter data.all API endpoint url"),
170
+ help="data.all API endpoint url",
171
+ )
172
+ @click.option(
173
+ "--redirect_uri",
174
+ cls=AuthScopedOption,
175
+ required=True,
176
+ scoped={
177
+ **_for(["CognitoAuth", "CustomAuth"], prompt=DOMAIN_PROMPT),
178
+ "OidcBrowserAuth": {"default": DEFAULT_REDIRECT_URI},
179
+ },
180
+ help="OAuth redirect URI: the data.all domain URL, or the loopback URI registered for the CLI",
181
+ )
182
+ @click.option(
183
+ "--idp_domain_url",
184
+ cls=AuthScopedOption,
185
+ required=True,
186
+ scoped={
187
+ **_for(["CognitoAuth", "CustomAuth"], prompt=IDP_PROMPT),
188
+ "OidcBrowserAuth": {"prompt": ISSUER_PROMPT},
189
+ },
190
+ help="Identity provider domain, or the OIDC issuer URL for browser login",
191
+ )
192
+ @click.option(
193
+ "--client_secret",
194
+ cls=AuthScopedOption,
195
+ required=False,
196
+ scoped=_for(["CognitoAuth", "CustomAuth"], prompt=SECRET_PROMPT, default=""),
197
+ help="IdP client secret, if the app has one",
198
+ )
199
+ @click.option(
200
+ "--auth_server",
201
+ cls=AuthScopedOption,
202
+ required=False,
203
+ scoped=_for(
204
+ ["CognitoAuth", "CustomAuth"], prompt=AUTH_SERVER_PROMPT, default="default"
205
+ ),
206
+ help="identity provider's custom authorization server used to get well-known openid config",
207
+ )
208
+ @click.option(
209
+ "--scopes",
210
+ cls=AuthScopedOption,
211
+ required=False,
212
+ scoped={"OidcBrowserAuth": {"default": DEFAULT_SCOPES}},
213
+ help="OIDC scopes for browser login; use 'openid' if the IdP rejects offline_access",
214
+ )
215
+ @click.option(
216
+ "--frontend_url",
217
+ cls=AuthScopedOption,
218
+ required=False,
219
+ scoped={
220
+ "OidcBrowserAuth": {"prompt": "Enter data.all front page URL", "default": ""}
221
+ },
222
+ help="data.all UI URL sent as Origin header; some deployments only accept API calls carrying it",
223
+ )
224
+ @click.option(
225
+ "--fallback_redirect_uri",
226
+ default=DEFAULT_FALLBACK_REDIRECT_URI,
227
+ help="second loopback URI tried when the first port is busy (browser login)",
228
+ )
229
+ @click.option(
230
+ "--profile",
231
+ prompt="Enter data.all profile name",
232
+ default="default",
233
+ help="profile name for dataall_cli configured user",
234
+ )
235
+ def configure(
236
+ auth_type: str,
237
+ client_id: str,
238
+ api_endpoint_url: str,
239
+ redirect_uri: str,
240
+ idp_domain_url: str,
241
+ client_secret: Optional[str],
242
+ auth_server: Optional[str],
243
+ scopes: Optional[str],
244
+ frontend_url: Optional[str],
245
+ fallback_redirect_uri: str,
246
+ profile: str,
247
+ ) -> None:
248
+ """Configure data.all client for a given user, use profile to setup multiple user profiles."""
249
+ click.echo("Configuring data.all CLI...", err=True)
250
+
251
+ try:
252
+ profile_params_dict: Dict[str, Any] = {
253
+ "client_id": client_id,
254
+ "api_endpoint_url": api_endpoint_url,
255
+ "auth_type": auth_type,
256
+ "idp_domain_url": idp_domain_url,
257
+ "redirect_uri": redirect_uri,
258
+ "client_secret": client_secret,
259
+ }
260
+ if auth_type == "CustomAuth":
261
+ session_token_endpoint = click.prompt("Enter session token endpoint")
262
+ profile_params_dict.update(
263
+ {
264
+ "auth_server": auth_server,
265
+ "session_token_endpoint": session_token_endpoint,
266
+ }
267
+ )
268
+ if auth_type == "OidcBrowserAuth":
269
+ profile_params_dict.update(
270
+ {"scopes": scopes, "fallback_redirect_uri": fallback_redirect_uri}
271
+ )
272
+ if frontend_url:
273
+ profile_params_dict["frontend_url"] = frontend_url.rstrip("/")
274
+ if CREDS_PATH:
275
+ profile_params_dict.update(
276
+ {
277
+ "creds_path": str(CREDS_PATH),
278
+ }
279
+ )
280
+ save_config(
281
+ profile=profile,
282
+ auth_type=auth_type,
283
+ params_dict=profile_params_dict,
284
+ config_path=Path(DA_CONFIG_PATH),
285
+ )
286
+ click.echo("data.all CLI configured successfully.", err=True)
287
+ except Exception as e:
288
+ click.echo(f"An error occurred: {e}", err=True)
@@ -0,0 +1,6 @@
1
+ """data.all cli utils."""
2
+
3
+ from .config import load_config, save_config
4
+ from .discovery import discover_from_frontend, frontend_origin
5
+
6
+ __all__ = ["discover_from_frontend", "frontend_origin", "load_config", "save_config"]
@@ -0,0 +1,54 @@
1
+ """Discover CLI settings from a deployed data.all front page."""
2
+
3
+ import re
4
+ from typing import Dict
5
+ from urllib.parse import urljoin, urlparse
6
+
7
+ import httpx
8
+
9
+ BUNDLE_PATTERN = re.compile(r'src="([^"]*static/js/main\.[^"]+\.js)"')
10
+ VALUE_PATTERN = re.compile(r'(REACT_APP_[A-Z_]+):"([^"]*)"')
11
+ API_SUFFIX = "/graphql/api"
12
+
13
+
14
+ def frontend_origin(dataall_url: str) -> str:
15
+ """Return ``scheme://host`` of a data.all URL."""
16
+ parsed = urlparse(dataall_url)
17
+ return f"{parsed.scheme}://{parsed.netloc}"
18
+
19
+
20
+ def discover_from_frontend(dataall_url: str) -> Dict[str, str]:
21
+ """Read the auth type, identity provider, client id and API endpoint from the front page bundle.
22
+
23
+ Returns ``frontend_url`` plus whichever values were found.
24
+ """
25
+ found = {"frontend_url": frontend_origin(dataall_url)}
26
+ base = dataall_url.rstrip("/") + "/"
27
+ index = httpx.get(base, follow_redirects=True, timeout=30)
28
+ index.raise_for_status()
29
+ match = BUNDLE_PATTERN.search(index.text)
30
+ if not match:
31
+ return found
32
+ bundle = httpx.get(urljoin(base, match.group(1)), follow_redirects=True, timeout=60)
33
+ bundle.raise_for_status()
34
+ values = dict(VALUE_PATTERN.findall(bundle.text))
35
+
36
+ if values.get("REACT_APP_CUSTOM_AUTH") and values.get("REACT_APP_CUSTOM_AUTH_URL"):
37
+ found["auth_type"] = "OidcBrowserAuth"
38
+ found["idp_domain_url"] = values["REACT_APP_CUSTOM_AUTH_URL"]
39
+ if values.get("REACT_APP_CUSTOM_AUTH_CLIENT_ID"):
40
+ found["client_id"] = values["REACT_APP_CUSTOM_AUTH_CLIENT_ID"]
41
+ elif values.get("REACT_APP_COGNITO_APP_CLIENT_ID"):
42
+ found["auth_type"] = "CognitoAuth"
43
+ found["client_id"] = values["REACT_APP_COGNITO_APP_CLIENT_ID"]
44
+ domain = values.get("REACT_APP_COGNITO_DOMAIN", "")
45
+ if domain:
46
+ found["idp_domain_url"] = domain if "://" in domain else f"https://{domain}"
47
+ if values.get("REACT_APP_COGNITO_REDIRECT_SIGNIN"):
48
+ found["redirect_uri"] = values["REACT_APP_COGNITO_REDIRECT_SIGNIN"]
49
+ api = values.get("REACT_APP_GRAPHQL_API", "")
50
+ if api:
51
+ found["api_endpoint_url"] = (
52
+ api[: -len(API_SUFFIX)] if api.endswith(API_SUFFIX) else api
53
+ )
54
+ return found
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataall-cli"
3
- version = "0.4.3"
3
+ version = "0.5.0"
4
4
  description = "AWS data.all CLI"
5
5
  authors = [{ name = "Amazon Web Services" }]
6
6
  license = { text = "Apache License 2.0" }
@@ -22,7 +22,7 @@ dependencies = [
22
22
  "atomicfile>=1.0.1",
23
23
  "PyYAML>=6.0.1",
24
24
  "setuptools; python_version >= '3.12'",
25
- "dataall-core>=0.4.3",
25
+ "dataall-core>=0.5.0,<0.6.0",
26
26
  ]
27
27
 
28
28
  [project.scripts]
@@ -1,148 +0,0 @@
1
- """CLI for data.all."""
2
-
3
- import json
4
- import logging
5
- import os
6
- from pathlib import Path
7
-
8
- import click
9
- from dataall_core.dataall_client import DataallClient
10
- from dataall_core.profile import CONFIG_PATH
11
-
12
- from dataall_cli.bind_commands import bind
13
- from dataall_cli.utils import save_config
14
-
15
- DA_CONFIG_PATH = os.getenv("dataall_config_path", CONFIG_PATH)
16
- CREDS_PATH = os.getenv("dataall_creds_path", None)
17
- SCHEMA_PATH = os.getenv("dataall_schema_path", None)
18
- SCHEMA_VERSION = os.getenv("dataall_schema_version", None)
19
- DA_CUSTOM_HEADERS_JSON: str = os.getenv("dataall_custom_headers_json", "{}")
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
- try:
24
- custom_headers = json.loads(DA_CUSTOM_HEADERS_JSON)
25
- except ValueError:
26
- logger.info(
27
- f"Invalid custom headers json string: {DA_CUSTOM_HEADERS_JSON}. Using default headers..."
28
- )
29
- custom_headers = {}
30
-
31
- da = DataallClient(schema_path=SCHEMA_PATH, schema_version=SCHEMA_VERSION)
32
- default_client = da.client(config_path=DA_CONFIG_PATH, custom_headers=custom_headers)
33
- commands = da.op_dict
34
-
35
-
36
- @click.group(name="dataall_cli", invoke_without_command=True)
37
- def dataall_cli() -> None:
38
- """data.all cli groups."""
39
- click.echo("Executing dataall_cli.", err=True)
40
- pass
41
-
42
-
43
- bind(
44
- dataall_cli=dataall_cli,
45
- commands=commands,
46
- config_path=DA_CONFIG_PATH,
47
- schema_path=SCHEMA_PATH,
48
- schema_version=SCHEMA_VERSION,
49
- custom_headers=custom_headers,
50
- )
51
-
52
-
53
- @dataall_cli.command()
54
- @click.option(
55
- "--auth_type",
56
- type=click.Choice(["CognitoAuth", "CustomAuth"]),
57
- default="CognitoAuth",
58
- prompt="Select authentication type",
59
- help="Authentication type: Cognito or Custom",
60
- )
61
- @click.option(
62
- "--client_id",
63
- required=True,
64
- prompt="Enter data.all app client id",
65
- help="data.all app client id",
66
- )
67
- @click.option(
68
- "--api_endpoint_url",
69
- required=True,
70
- prompt="Enter data.all API endpoint url",
71
- help="data.all API endpoint url",
72
- )
73
- @click.option(
74
- "--redirect_uri",
75
- required=True,
76
- prompt="Enter data.all's domain URL (e.g. https://<DOMAIN>.com)",
77
- help="data.all domain URL",
78
- )
79
- @click.option(
80
- "--idp_domain_url",
81
- required=True,
82
- prompt="Enter data.all Identity Provider Domain (e.g. https://<IdP-DOMAIN>.com)",
83
- help="data.all IdP domain URL",
84
- )
85
- @click.option(
86
- "--client_secret",
87
- required=False,
88
- prompt="Enter IdP client secret (if applicable)",
89
- default="",
90
- help="profile name for dataall_cli configured user",
91
- )
92
- @click.option(
93
- "--auth_server",
94
- prompt="Enter IdP custom auth server (if applicable)",
95
- default="default",
96
- help="identity provider's custom authorization server used to get well-known openid config",
97
- )
98
- @click.option(
99
- "--profile",
100
- prompt="Enter data.all profile name",
101
- default="default",
102
- help="profile name for dataall_cli configured user",
103
- )
104
- def configure(
105
- client_id: str,
106
- api_endpoint_url: str,
107
- auth_type: str,
108
- redirect_uri: str,
109
- idp_domain_url: str,
110
- client_secret: str,
111
- auth_server: str,
112
- profile: str,
113
- ) -> None:
114
- """Configure data.all client for a given user, use profile to setup multiple user profiles."""
115
- click.echo("Configuring data.all CLI...", err=True)
116
-
117
- try:
118
- profile_params_dict = {
119
- "client_id": client_id,
120
- "api_endpoint_url": api_endpoint_url,
121
- "auth_type": auth_type,
122
- "idp_domain_url": idp_domain_url,
123
- "redirect_uri": redirect_uri,
124
- "client_secret": client_secret,
125
- }
126
- if auth_type == "CustomAuth":
127
- session_token_endpoint = click.prompt("Enter session token endpoint")
128
- profile_params_dict.update(
129
- {
130
- "auth_server": auth_server,
131
- "session_token_endpoint": session_token_endpoint,
132
- }
133
- )
134
- if CREDS_PATH:
135
- profile_params_dict.update(
136
- {
137
- "creds_path": str(CREDS_PATH),
138
- }
139
- )
140
- save_config(
141
- profile=profile,
142
- auth_type=auth_type,
143
- params_dict=profile_params_dict,
144
- config_path=Path(DA_CONFIG_PATH),
145
- )
146
- click.echo("data.all CLI configured successfully.", err=True)
147
- except Exception as e:
148
- click.echo(f"An error occurred: {e}", err=True)
@@ -1,5 +0,0 @@
1
- """data.all cli utils."""
2
-
3
- from .config import load_config, save_config
4
-
5
- __all__ = ["load_config", "save_config"]
File without changes
File without changes
File without changes
File without changes