cloudinary-cli 1.15.0__tar.gz → 1.16.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.
- {cloudinary_cli-1.15.0/cloudinary_cli.egg-info → cloudinary_cli-1.16.0}/PKG-INFO +25 -2
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/README.md +23 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/auth/__init__.py +3 -31
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/auth/session.py +6 -7
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/cli_group.py +3 -1
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/core/__init__.py +2 -0
- cloudinary_cli-1.16.0/cloudinary_cli/core/agent.py +203 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/core/auth.py +3 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/core/config.py +8 -5
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/defaults.py +35 -1
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/utils/api_utils.py +14 -2
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/utils/config_listing.py +14 -6
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/utils/config_resolver.py +42 -20
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/utils/config_utils.py +90 -1
- cloudinary_cli-1.16.0/cloudinary_cli/utils/url_utils.py +29 -0
- cloudinary_cli-1.16.0/cloudinary_cli/version.py +1 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0/cloudinary_cli.egg-info}/PKG-INFO +25 -2
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli.egg-info/SOURCES.txt +3 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli.egg-info/requires.txt +1 -1
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/requirements.txt +1 -1
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_auth_session.py +2 -2
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_cli.py +1 -0
- cloudinary_cli-1.16.0/test/test_cli_agent.py +402 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_cli_config.py +9 -5
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_cli_config_oauth.py +37 -34
- cloudinary_cli-1.16.0/test/test_default_config.py +213 -0
- cloudinary_cli-1.15.0/cloudinary_cli/version.py +0 -1
- cloudinary_cli-1.15.0/test/test_default_config.py +0 -57
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/LICENSE +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/MANIFEST.in +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/__init__.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/auth/callback_page.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/auth/flow.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/auth/loopback_server.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/auth/oauth_config.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/auth/refresh.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/cli.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/core/admin.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/core/overrides.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/core/provisioning.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/core/search.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/core/uploader.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/core/utils.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/modules/__init__.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/modules/clone.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/modules/make.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/modules/migrate.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/modules/regen_derived.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/modules/sync.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/modules/upload_dir.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/samples/__init__.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/html/media_library_widget +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/html/product_gallery +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/html/upload_widget +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/html/video_player +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/node/upload +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/python/base +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/python/explicit +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/python/find_all_empty_folders +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/python/upload +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/templates/ruby/upload +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/utils/__init__.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/utils/file_utils.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/utils/json_utils.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/utils/search_utils.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli/utils/utils.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli.egg-info/dependency_links.txt +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli.egg-info/entry_points.txt +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli.egg-info/not-zip-safe +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/cloudinary_cli.egg-info/top_level.txt +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/setup.cfg +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/setup.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/__init__.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/conftest.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/helper_test.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/oauth_helpers.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_auth_flow.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_auth_loopback.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_auth_region.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_cli_api.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_cli_samples.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_cli_search_api.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_cli_url.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_cli_utils.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_config_cache.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_config_concurrency.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_config_permissions.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_file_utils.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_json_utils.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_modules/__init__.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_modules/test_cli_clone.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_modules/test_cli_make.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_modules/test_cli_sync.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_modules/test_cli_upload_dir.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_oauth_multiprocess.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_oauth_retry.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_oauth_token_seam.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_search_utils.py +0 -0
- {cloudinary_cli-1.15.0 → cloudinary_cli-1.16.0}/test/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloudinary-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
4
4
|
Summary: A command line interface for Cloudinary with full API support
|
|
5
5
|
Home-page: https://github.com/cloudinary/cloudinary-cli
|
|
6
6
|
Author: Cloudinary, Brian Luk
|
|
@@ -13,7 +13,7 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.8.0
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: cloudinary>=1.
|
|
16
|
+
Requires-Dist: cloudinary>=1.45.0
|
|
17
17
|
Requires-Dist: pygments
|
|
18
18
|
Requires-Dist: jinja2
|
|
19
19
|
Requires-Dist: click
|
|
@@ -168,6 +168,7 @@ cld logout # Revokes and removes a saved OAuth login.
|
|
|
168
168
|
cld search --help # Shows usage for the Search API.
|
|
169
169
|
cld admin # Lists Admin API methods.
|
|
170
170
|
cld uploader # Lists Upload API methods.
|
|
171
|
+
cld agent signup # For AI agents: creates a Cloudinary account on behalf of a human.
|
|
171
172
|
```
|
|
172
173
|
|
|
173
174
|
## Docker Usage
|
|
@@ -341,6 +342,28 @@ cld [cli options] migrate [command options] upload_mapping file
|
|
|
341
342
|
|
|
342
343
|
For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#migrate).
|
|
343
344
|
|
|
345
|
+
### `agent signup`
|
|
346
|
+
|
|
347
|
+
**For AI agents only.** Creates a Free-plan Cloudinary account on behalf of a human. No existing configuration is required to run it. A verification email is sent to the address, and the returned credentials are **inert until the human completes that verification**. The new product environment is saved as a named configuration (named after the cloud) so it is ready to use once activated.
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
cld agent signup [command options] <email> <agent_framework> <agent_llm_model> <agent_goal>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
cld agent signup you@example.com claude-code claude-fable-5 "test the agent account flow"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Options:
|
|
360
|
+
|
|
361
|
+
* `--name <name>` — name for the saved configuration (default: the returned cloud name).
|
|
362
|
+
* `--set-default` — set the saved configuration as the default.
|
|
363
|
+
* `--no-save` — show the credentials but do not save them as a configuration.
|
|
364
|
+
* `--sdk-framework <name>` — the Cloudinary SDK framework the agent intends to use.
|
|
365
|
+
* `--json` — output the full raw JSON response (the agent contract) instead of the human-readable summary.
|
|
366
|
+
|
|
344
367
|
## Additional configurations
|
|
345
368
|
|
|
346
369
|
A configuration is a reference to a specified Cloudinary account or cloud name via its environment variable. You set the default configuration during setup and installation. Using different configurations allows you to access different Cloudinary cloud names, such as sub-accounts of your main Cloudinary account, or any additional Cloudinary accounts you may have.
|
|
@@ -142,6 +142,7 @@ cld logout # Revokes and removes a saved OAuth login.
|
|
|
142
142
|
cld search --help # Shows usage for the Search API.
|
|
143
143
|
cld admin # Lists Admin API methods.
|
|
144
144
|
cld uploader # Lists Upload API methods.
|
|
145
|
+
cld agent signup # For AI agents: creates a Cloudinary account on behalf of a human.
|
|
145
146
|
```
|
|
146
147
|
|
|
147
148
|
## Docker Usage
|
|
@@ -315,6 +316,28 @@ cld [cli options] migrate [command options] upload_mapping file
|
|
|
315
316
|
|
|
316
317
|
For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#migrate).
|
|
317
318
|
|
|
319
|
+
### `agent signup`
|
|
320
|
+
|
|
321
|
+
**For AI agents only.** Creates a Free-plan Cloudinary account on behalf of a human. No existing configuration is required to run it. A verification email is sent to the address, and the returned credentials are **inert until the human completes that verification**. The new product environment is saved as a named configuration (named after the cloud) so it is ready to use once activated.
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
cld agent signup [command options] <email> <agent_framework> <agent_llm_model> <agent_goal>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
cld agent signup you@example.com claude-code claude-fable-5 "test the agent account flow"
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Options:
|
|
334
|
+
|
|
335
|
+
* `--name <name>` — name for the saved configuration (default: the returned cloud name).
|
|
336
|
+
* `--set-default` — set the saved configuration as the default.
|
|
337
|
+
* `--no-save` — show the credentials but do not save them as a configuration.
|
|
338
|
+
* `--sdk-framework <name>` — the Cloudinary SDK framework the agent intends to use.
|
|
339
|
+
* `--json` — output the full raw JSON response (the agent contract) instead of the human-readable summary.
|
|
340
|
+
|
|
318
341
|
## Additional configurations
|
|
319
342
|
|
|
320
343
|
A configuration is a reference to a specified Cloudinary account or cloud name via its environment variable. You set the default configuration during setup and installation. Using different configurations allows you to access different Cloudinary cloud names, such as sub-accounts of your main Cloudinary account, or any additional Cloudinary accounts you may have.
|
|
@@ -23,13 +23,9 @@ from cloudinary_cli.auth.refresh import (
|
|
|
23
23
|
from cloudinary_cli.defaults import logger, normalize_region, DEFAULT_REGION, CLOUDINARY_REGION
|
|
24
24
|
from cloudinary_cli.utils.config_utils import (
|
|
25
25
|
load_config,
|
|
26
|
-
update_config,
|
|
27
26
|
remove_config_keys,
|
|
28
|
-
|
|
29
|
-
get_default_config_name,
|
|
30
|
-
set_default_config,
|
|
27
|
+
save_named_config,
|
|
31
28
|
is_reserved_config_name,
|
|
32
|
-
is_env_configured,
|
|
33
29
|
)
|
|
34
30
|
from cloudinary_cli.utils.utils import log_exception, is_interactive
|
|
35
31
|
|
|
@@ -62,32 +58,8 @@ def login(region=None, name=None, set_default=False):
|
|
|
62
58
|
raise RuntimeError("Login token did not include a cloud name; cannot save this login.")
|
|
63
59
|
config_name = name or _derive_config_name(session.cloud_name, region)
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if was_default:
|
|
69
|
-
return config_name, "already"
|
|
70
|
-
if set_default or _should_auto_default(config_name):
|
|
71
|
-
set_default_config(config_name)
|
|
72
|
-
return config_name, "made"
|
|
73
|
-
return config_name, "no"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _should_auto_default(name):
|
|
77
|
-
"""
|
|
78
|
-
True when the just-saved login should become the default without an explicit flag: it is the
|
|
79
|
-
only saved config, the environment configures nothing, and no default is already stored.
|
|
80
|
-
|
|
81
|
-
A stored default outranks the environment, so auto-defaulting is suppressed when an env config
|
|
82
|
-
is present: a single `cld login` must not silently override a user's CLOUDINARY_URL. They can
|
|
83
|
-
still opt in with `--set-default`.
|
|
84
|
-
"""
|
|
85
|
-
cfg = load_config()
|
|
86
|
-
return (
|
|
87
|
-
user_config_names(cfg) == [name]
|
|
88
|
-
and not is_env_configured()
|
|
89
|
-
and not get_default_config_name()
|
|
90
|
-
)
|
|
61
|
+
default_status = save_named_config(config_name, to_cloudinary_url(session), set_default=set_default)
|
|
62
|
+
return config_name, default_status
|
|
91
63
|
|
|
92
64
|
|
|
93
65
|
def logout(name):
|
|
@@ -4,13 +4,14 @@ dataclass is the in-memory form, `to_cloudinary_url`/`from_cloudinary_url` the p
|
|
|
4
4
|
import base64
|
|
5
5
|
import json
|
|
6
6
|
import time
|
|
7
|
-
import urllib.parse
|
|
8
7
|
from dataclasses import dataclass
|
|
9
8
|
|
|
10
9
|
from cloudinary_cli.defaults import (
|
|
11
10
|
OAUTH_EXPIRY_SKEW_SECONDS,
|
|
12
11
|
api_host_for_region,
|
|
13
12
|
)
|
|
13
|
+
from cloudinary_cli.utils.url_utils import url_params, url_host
|
|
14
|
+
from cloudinary_cli.utils.config_utils import build_config_url
|
|
14
15
|
|
|
15
16
|
# Query-string keys that carry the OAuth session inside a cloudinary:// URL.
|
|
16
17
|
_OAUTH_MARKER = "oauth_token"
|
|
@@ -71,15 +72,14 @@ def to_cloudinary_url(session):
|
|
|
71
72
|
"issuer": session.issuer or "",
|
|
72
73
|
"upload_prefix": api_host_for_region(session.region),
|
|
73
74
|
}
|
|
74
|
-
return
|
|
75
|
+
return build_config_url(session.cloud_name, params)
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
def from_cloudinary_url(url):
|
|
78
79
|
"""Parse an OAuth cloudinary:// URL back into a Session."""
|
|
79
|
-
|
|
80
|
-
q = {k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()}
|
|
80
|
+
q = url_params(url)
|
|
81
81
|
return Session(
|
|
82
|
-
cloud_name=
|
|
82
|
+
cloud_name=url_host(url),
|
|
83
83
|
access_token=q.get("oauth_token"),
|
|
84
84
|
refresh_token=q.get("refresh_token") or None,
|
|
85
85
|
issued_at=int(q.get("issued_at", 0) or 0),
|
|
@@ -92,8 +92,7 @@ def from_cloudinary_url(url):
|
|
|
92
92
|
def is_oauth_url(url):
|
|
93
93
|
if not isinstance(url, str):
|
|
94
94
|
return False
|
|
95
|
-
|
|
96
|
-
return _OAUTH_MARKER in urllib.parse.parse_qs(query)
|
|
95
|
+
return _OAUTH_MARKER in url_params(url)
|
|
97
96
|
|
|
98
97
|
|
|
99
98
|
def _decode_jwt_payload(access_token):
|
|
@@ -28,7 +28,9 @@ CONTEXT_SETTINGS = dict(max_content_width=shutil.get_terminal_size()[0], termina
|
|
|
28
28
|
@click_log.simple_verbosity_option(logger)
|
|
29
29
|
@click.pass_context
|
|
30
30
|
def cli(ctx, config, config_saved):
|
|
31
|
-
|
|
31
|
+
subcommand = cli.get_command(ctx, ctx.invoked_subcommand) if ctx.invoked_subcommand else None
|
|
32
|
+
warn_if_unconfigured = not getattr(subcommand, "config_optional", False)
|
|
33
|
+
resolve_cli_config(config, config_saved, warn_if_unconfigured=warn_if_unconfigured)
|
|
32
34
|
|
|
33
35
|
if ctx.invoked_subcommand is None:
|
|
34
36
|
click.echo(ctx.get_help())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
3
|
from cloudinary_cli.core.admin import admin
|
|
4
|
+
from cloudinary_cli.core.agent import agent_group
|
|
4
5
|
from cloudinary_cli.core.auth import login, logout
|
|
5
6
|
from cloudinary_cli.core.config import config_command
|
|
6
7
|
from cloudinary_cli.core.search import search, search_folders
|
|
@@ -13,6 +14,7 @@ setattr(click.Group, "resolve_command", resolve_command)
|
|
|
13
14
|
|
|
14
15
|
commands = [
|
|
15
16
|
config_command,
|
|
17
|
+
agent_group,
|
|
16
18
|
login,
|
|
17
19
|
logout,
|
|
18
20
|
search,
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
import cloudinary.provisioning
|
|
5
|
+
from click import group, argument, option, echo, style, BadParameter, ClickException
|
|
6
|
+
from cloudinary.exceptions import Error as CloudinaryError, RateLimited
|
|
7
|
+
|
|
8
|
+
from cloudinary_cli.defaults import logger, ACCOUNT_EMAIL_PARAM
|
|
9
|
+
from cloudinary_cli.utils.api_utils import call_api
|
|
10
|
+
from cloudinary_cli.utils.json_utils import print_json
|
|
11
|
+
from cloudinary_cli.utils.config_utils import (
|
|
12
|
+
save_named_config,
|
|
13
|
+
is_reserved_config_name,
|
|
14
|
+
config_name_for_email,
|
|
15
|
+
build_config_url,
|
|
16
|
+
user_config_names,
|
|
17
|
+
config_optional,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@config_optional
|
|
22
|
+
@group("agent", help="Commands for AI agents acting on behalf of a human.")
|
|
23
|
+
def agent_group():
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@agent_group.command("signup",
|
|
28
|
+
short_help="Create a Cloudinary account on behalf of a human (for AI agents only).",
|
|
29
|
+
help="""\b
|
|
30
|
+
For AI agents only: create a Free-plan Cloudinary account on behalf of a human.
|
|
31
|
+
A verification email is sent to the address; the credentials are inert until the human verifies it.
|
|
32
|
+
If you are a human, or the human prefers to sign up themselves, use https://cloudinary.com/users/register_free instead.
|
|
33
|
+
The returned product environment is saved as a named configuration (use --no-save to skip).
|
|
34
|
+
Format: cld agent signup <email> <agent_framework> <agent_llm_model> <agent_goal>
|
|
35
|
+
\te.g. cld agent signup you@example.com claude-code claude-fable-5 "test the agent account flow"
|
|
36
|
+
""")
|
|
37
|
+
@argument("email")
|
|
38
|
+
@argument("agent_framework")
|
|
39
|
+
@argument("agent_llm_model")
|
|
40
|
+
@argument("agent_goal")
|
|
41
|
+
@option("--sdk-framework", "sdk_framework", help="The Cloudinary SDK framework the agent intends to use.")
|
|
42
|
+
@option("--name", help="Name for the saved configuration (default: the returned cloud name).")
|
|
43
|
+
@option("--set-default", "set_default", is_flag=True, help="Set the saved configuration as the default.")
|
|
44
|
+
@option("--no-save", "no_save", is_flag=True, help="Do not save the returned credentials as a configuration.")
|
|
45
|
+
@option("--json", "as_json", is_flag=True,
|
|
46
|
+
help="Output the full raw JSON response (agent contract) instead of the human summary.")
|
|
47
|
+
def signup(email, agent_framework, agent_llm_model, agent_goal, sdk_framework, name, set_default, no_save,
|
|
48
|
+
as_json):
|
|
49
|
+
if name and is_reserved_config_name(name):
|
|
50
|
+
raise BadParameter(f"'{name}' is a reserved configuration name.")
|
|
51
|
+
if not email or not email.strip():
|
|
52
|
+
raise BadParameter("email must not be empty.")
|
|
53
|
+
|
|
54
|
+
existing = config_name_for_email(email)
|
|
55
|
+
if existing:
|
|
56
|
+
raise ClickException(_already_have_config_message(email, existing))
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
result = call_api(cloudinary.provisioning.create_agent_account, email, agent_framework,
|
|
60
|
+
agent_llm_model, agent_goal, sdk_framework=sdk_framework)
|
|
61
|
+
except RateLimited as e:
|
|
62
|
+
raise ClickException(
|
|
63
|
+
f"Rate limited while creating the account: {e}. This endpoint is limited per IP address; "
|
|
64
|
+
f"wait a bit and try again.")
|
|
65
|
+
except CloudinaryError as e:
|
|
66
|
+
raise ClickException(_signup_error_message(email, e))
|
|
67
|
+
|
|
68
|
+
# Show the freshly-minted credentials BEFORE saving, so a save failure can never lose them.
|
|
69
|
+
if as_json:
|
|
70
|
+
print_json(result)
|
|
71
|
+
else:
|
|
72
|
+
_print_signup_summary(result)
|
|
73
|
+
|
|
74
|
+
if not no_save:
|
|
75
|
+
save_agent_config(result, email, name=name, set_default=set_default)
|
|
76
|
+
|
|
77
|
+
logger.info("Note: the account's credentials are inert until the emailed verification is completed.")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _already_have_config_message(email, name):
|
|
81
|
+
return (f"You already signed up with {email} (saved as '{name}'). "
|
|
82
|
+
f"Use it with `cld -C {name} <command>`. "
|
|
83
|
+
f"If it's not activated yet, complete the verification email; or run `cld login` if you use OAuth.")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _account_exists_message(email):
|
|
87
|
+
return (f"An account already exists for {email}, but no configuration is saved on this machine. "
|
|
88
|
+
f"If it's already verified, add its CLOUDINARY_URL with `cld config -n <name> <url>` "
|
|
89
|
+
f"(or `cld login` if you use OAuth). If you just created it, complete the verification email first.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _signup_error_message(email, error):
|
|
93
|
+
text = str(error)
|
|
94
|
+
if "has already been taken" in text or "409" in text:
|
|
95
|
+
return _account_exists_message(email)
|
|
96
|
+
|
|
97
|
+
detail = _parse_error_detail(text)
|
|
98
|
+
return f"Signup failed: {detail}." if detail else f"Signup failed: {text}."
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _parse_error_detail(text):
|
|
102
|
+
"""Best-effort human message from a provisioning error string like
|
|
103
|
+
'Error 400 - {"email":["is invalid"]}' or '... {"error":{"message":"boom"}}'. Returns None on any
|
|
104
|
+
failure so the caller falls back to the raw text."""
|
|
105
|
+
match = re.search(r"\{.*\}", text)
|
|
106
|
+
if not match:
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
payload = json.loads(match.group(0))
|
|
110
|
+
except ValueError:
|
|
111
|
+
return None
|
|
112
|
+
if not isinstance(payload, dict) or not payload:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
error = payload.get("error")
|
|
116
|
+
if isinstance(error, dict) and error.get("message"):
|
|
117
|
+
return str(error["message"])
|
|
118
|
+
|
|
119
|
+
parts = []
|
|
120
|
+
for field, msgs in payload.items():
|
|
121
|
+
msgs = msgs if isinstance(msgs, list) else [msgs]
|
|
122
|
+
parts.append(f"{field} {', '.join(str(m) for m in msgs)}")
|
|
123
|
+
return "; ".join(parts) or None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Top-level and product-environment response keys the summary renders explicitly (or deliberately
|
|
127
|
+
# omits, e.g. secrets folded into CLOUDINARY_URL). Any key NOT listed here is surfaced generically
|
|
128
|
+
# by _extra_rows so future fields the server adds are never silently dropped.
|
|
129
|
+
_KNOWN_TOP_LEVEL_KEYS = {"email", "plan_name", "product_environments", "guidance"}
|
|
130
|
+
_KNOWN_ENV_KEYS = {"cloud_name", "api_key", "api_secret", "api_environment_variable", "external_id"}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _print_signup_summary(result):
|
|
134
|
+
environment = (result.get("product_environments") or [{}])[0]
|
|
135
|
+
rows = [
|
|
136
|
+
("Email", result.get("email", "")),
|
|
137
|
+
("Plan", result.get("plan_name", "")),
|
|
138
|
+
("Cloud name", environment.get("cloud_name", "")),
|
|
139
|
+
("API key", environment.get("api_key", "")),
|
|
140
|
+
("CLOUDINARY_URL", _config_url_from_environment(environment)),
|
|
141
|
+
]
|
|
142
|
+
rows += _extra_rows(result, _KNOWN_TOP_LEVEL_KEYS)
|
|
143
|
+
rows += _extra_rows(environment, _KNOWN_ENV_KEYS)
|
|
144
|
+
rows = [(label, value) for label, value in rows if value]
|
|
145
|
+
|
|
146
|
+
echo(style("Cloudinary account created.", fg="green"))
|
|
147
|
+
if rows:
|
|
148
|
+
width = max(len(label) for label, _ in rows) + 1
|
|
149
|
+
template = "{0:" + str(width) + "} {1}"
|
|
150
|
+
echo("\n".join(template.format(f"{label}:", value) for label, value in rows))
|
|
151
|
+
|
|
152
|
+
guidance = result.get("guidance")
|
|
153
|
+
if guidance:
|
|
154
|
+
echo(f"\n{guidance}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _extra_rows(data, known_keys):
|
|
158
|
+
"""(label, value) rows for scalar keys not already rendered, so response fields the server adds
|
|
159
|
+
in the future surface instead of being dropped. Skips nested dict/list values (shown elsewhere
|
|
160
|
+
or via --json) and empties."""
|
|
161
|
+
rows = []
|
|
162
|
+
for key, value in data.items():
|
|
163
|
+
if key in known_keys or isinstance(value, (dict, list)) or value in (None, ""):
|
|
164
|
+
continue
|
|
165
|
+
rows.append((key.replace("_", " ").capitalize(), value))
|
|
166
|
+
return rows
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def save_agent_config(result, email, name=None, set_default=False):
|
|
170
|
+
environment = (result.get("product_environments") or [{}])[0]
|
|
171
|
+
config_name = name or environment.get("cloud_name")
|
|
172
|
+
stored_url = _config_url_from_environment(environment, email=email)
|
|
173
|
+
if not stored_url or not config_name:
|
|
174
|
+
logger.warning("Could not save the configuration automatically (missing credentials in the response). "
|
|
175
|
+
"Add it manually with `cld config -n <name> <CLOUDINARY_URL>`.")
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
if name and name in user_config_names():
|
|
179
|
+
logger.warning(f"Overwriting existing config '{name}'.")
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
default_status = save_named_config(config_name, stored_url, set_default=set_default)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning(f"Could not save the configuration '{config_name}': {e}. "
|
|
185
|
+
f"Add it manually with `cld config -n {config_name} {_config_url_from_environment(environment)}`.")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
logger.info(f"Config '{config_name}' saved!")
|
|
189
|
+
logger.info(f"Example usage: cld -C {config_name} <command>")
|
|
190
|
+
if default_status == "made":
|
|
191
|
+
logger.info(f"Default set to '{config_name}'. Run `cld <command>` to use it, "
|
|
192
|
+
f"or `cld -C {config_name} <command>` to select it explicitly.")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _config_url_from_environment(environment, email=None):
|
|
196
|
+
"""Build a validated cloudinary:// config URL from a product-environment's credential fields,
|
|
197
|
+
optionally carrying the account email. Returns "" when the response lacks the credentials."""
|
|
198
|
+
params = {ACCOUNT_EMAIL_PARAM: email.strip().lower()} if email and email.strip() else None
|
|
199
|
+
try:
|
|
200
|
+
return build_config_url(environment["cloud_name"], params=params,
|
|
201
|
+
api_key=environment["api_key"], api_secret=environment["api_secret"])
|
|
202
|
+
except (KeyError, ValueError):
|
|
203
|
+
return ""
|
|
@@ -2,9 +2,11 @@ from click import command, argument, option, echo
|
|
|
2
2
|
|
|
3
3
|
from cloudinary_cli.auth import login as run_login, logout as run_logout, list_oauth_login_names
|
|
4
4
|
from cloudinary_cli.defaults import logger
|
|
5
|
+
from cloudinary_cli.utils.config_utils import config_optional
|
|
5
6
|
from cloudinary_cli.utils.utils import log_exception, prompt_user
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
@config_optional
|
|
8
10
|
@command("login", help="Log in to Cloudinary via OAuth (opens a browser). The session is saved "
|
|
9
11
|
"as a named configuration you can select with `-C`.")
|
|
10
12
|
@argument("name", required=False)
|
|
@@ -34,6 +36,7 @@ def login(name, region, set_default):
|
|
|
34
36
|
return True
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
@config_optional
|
|
37
40
|
@command("logout", help="Log out: revoke a saved OAuth login's token and remove its configuration. "
|
|
38
41
|
"Run without a name to choose from the saved logins.")
|
|
39
42
|
@argument("name", required=False)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import cloudinary
|
|
2
2
|
from click import command, option, echo, BadParameter, UsageError
|
|
3
3
|
|
|
4
|
-
from cloudinary_cli.defaults import logger, DEFAULT_CONFIG_KEY
|
|
4
|
+
from cloudinary_cli.defaults import logger, DEFAULT_CONFIG_KEY, NO_CONFIG_MESSAGE
|
|
5
5
|
from cloudinary_cli.utils.config_utils import (
|
|
6
6
|
load_config,
|
|
7
7
|
verify_cloudinary_url,
|
|
8
|
-
|
|
8
|
+
save_named_config,
|
|
9
9
|
remove_config_keys,
|
|
10
10
|
show_cloudinary_config,
|
|
11
11
|
is_valid_cloudinary_config,
|
|
@@ -15,6 +15,7 @@ from cloudinary_cli.utils.config_utils import (
|
|
|
15
15
|
clear_default_config,
|
|
16
16
|
is_reserved_config_name,
|
|
17
17
|
config_type,
|
|
18
|
+
config_optional,
|
|
18
19
|
)
|
|
19
20
|
from cloudinary_cli.utils.utils import ConfigurationError
|
|
20
21
|
from cloudinary_cli.utils.json_utils import print_json
|
|
@@ -30,6 +31,7 @@ from cloudinary_cli.utils.config_listing import (
|
|
|
30
31
|
)
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
@config_optional
|
|
33
35
|
@command("config", help="Display the current configuration, and manage additional configurations.")
|
|
34
36
|
@option("-n", "--new", help="""\b Create and name a configuration from a Cloudinary account environment variable.
|
|
35
37
|
e.g. cld config -n <NAME> <CLOUDINARY_URL>""", nargs=2)
|
|
@@ -80,13 +82,12 @@ def config_command(new, ls, as_json, show, rm, from_url, default, set_default, u
|
|
|
80
82
|
|
|
81
83
|
config_name = config_name or cloudinary.config().cloud_name
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
default_status = save_named_config(config_name, cloudinary_url, set_default=set_default)
|
|
84
86
|
|
|
85
87
|
logger.info("Config '{}' saved!".format(config_name))
|
|
86
88
|
logger.info("Example usage: cld -C {} <command>".format(config_name))
|
|
87
89
|
|
|
88
|
-
if
|
|
89
|
-
set_default_config(config_name)
|
|
90
|
+
if default_status == "made":
|
|
90
91
|
logger.info(f"Default set to '{config_name}'. Run `cld <command>` to use it, "
|
|
91
92
|
f"or `cld -C {config_name} <command>` to select it explicitly.")
|
|
92
93
|
elif default:
|
|
@@ -110,6 +111,8 @@ def config_command(new, ls, as_json, show, rm, from_url, default, set_default, u
|
|
|
110
111
|
rows = list_configs()
|
|
111
112
|
if as_json:
|
|
112
113
|
print_json(rows)
|
|
114
|
+
elif not rows:
|
|
115
|
+
echo(NO_CONFIG_MESSAGE)
|
|
113
116
|
else:
|
|
114
117
|
echo(render_config_table(rows))
|
|
115
118
|
elif show:
|
|
@@ -28,6 +28,40 @@ CLOUDINARY_CLI_CONFIG_FILE = abspath(path_join(CLOUDINARY_HOME, 'config.json'))
|
|
|
28
28
|
# names are rejected as user config names, so this can't collide with a saved config.
|
|
29
29
|
DEFAULT_CONFIG_KEY = "__default__"
|
|
30
30
|
|
|
31
|
+
# Query param carried inside a saved cloudinary:// URL recording the email the account was created
|
|
32
|
+
# for (via `cld agent signup`). Stripped before display and before reaching the SDK.
|
|
33
|
+
ACCOUNT_EMAIL_PARAM = "account_email"
|
|
34
|
+
|
|
35
|
+
# Guidance shown when no configuration is available (the group callback for account-consuming
|
|
36
|
+
# commands, and the empty `config -ls`). Printed verbatim to stderr, without the logger's
|
|
37
|
+
# "warning:" prefix, so the copy-pasteable command lines stay clean.
|
|
38
|
+
NO_CONFIG_MESSAGE = (
|
|
39
|
+
"No Cloudinary configuration found.\n"
|
|
40
|
+
" - Log in with OAuth: cld login\n"
|
|
41
|
+
" - Add an API-key config: cld config -n <name> "
|
|
42
|
+
"cloudinary://<api_key>:<api_secret>@<cloud_name> --set-default\n"
|
|
43
|
+
" - Set an existing config\n"
|
|
44
|
+
" as the default: cld config -d <name>\n"
|
|
45
|
+
" - AI agents only, create\n"
|
|
46
|
+
" an account for a human: cld agent signup <email> <framework> <model> <goal>"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Shown when saved configs exist but none is active (no default set, no environment config, and no
|
|
50
|
+
# -c/-C on the command line). The account is there; the CLI just doesn't know which one to use.
|
|
51
|
+
NO_DEFAULT_CONFIG_MESSAGE = (
|
|
52
|
+
"No default Cloudinary configuration is set. Select one per command with `-C <name>`, "
|
|
53
|
+
"or set a default with `cld config -d <name>`.\n"
|
|
54
|
+
"List your saved configurations with `cld config -ls`."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Shown when an explicitly selected config (-c URL or -C saved name) has a cloud name but no
|
|
58
|
+
# credentials (api_key/api_secret or an OAuth token). The user picked a config on purpose, so the
|
|
59
|
+
# generic "no config found" guidance would be misleading; the config is just incomplete.
|
|
60
|
+
INCOMPLETE_CONFIG_MESSAGE = (
|
|
61
|
+
"The selected configuration is incomplete: it has a cloud name but no credentials "
|
|
62
|
+
"(api_key/api_secret or an OAuth token). Operations that need authentication will fail."
|
|
63
|
+
)
|
|
64
|
+
|
|
31
65
|
# OAuth configuration for `cld login`. The region string derives both the API and
|
|
32
66
|
# OAuth hosts; an unknown region simply fails to resolve.
|
|
33
67
|
DEFAULT_REGION = 'api'
|
|
@@ -82,7 +116,7 @@ OAUTH_REDIRECT_PORT = int(os.environ.get('CLOUDINARY_OAUTH_REDIRECT_PORT', OAUTH
|
|
|
82
116
|
OAUTH_CALLBACK_PATH = '/callback'
|
|
83
117
|
|
|
84
118
|
OAUTH_CALLBACK_TIMEOUT_SECONDS = 300
|
|
85
|
-
OAUTH_EXPIRY_SKEW_SECONDS =
|
|
119
|
+
OAUTH_EXPIRY_SKEW_SECONDS = 30
|
|
86
120
|
OAUTH_HTTP_TIMEOUT_SECONDS = 30
|
|
87
121
|
|
|
88
122
|
TEMPLATE_FOLDER_NAME = 'templates'
|
|
@@ -9,7 +9,7 @@ from cloudinary.exceptions import AuthorizationRequired
|
|
|
9
9
|
from cloudinary.utils import cloudinary_url
|
|
10
10
|
|
|
11
11
|
from cloudinary_cli.defaults import logger
|
|
12
|
-
from cloudinary_cli.utils.config_utils import is_valid_cloudinary_config
|
|
12
|
+
from cloudinary_cli.utils.config_utils import is_valid_cloudinary_config, user_config_names
|
|
13
13
|
from cloudinary_cli.utils.file_utils import (normalize_file_extension, posix_rel_path, get_destination_folder,
|
|
14
14
|
populate_duplicate_name)
|
|
15
15
|
from cloudinary_cli.utils.json_utils import print_json, write_json_to_file
|
|
@@ -35,6 +35,15 @@ DESTRUCTIVE_BULK_API_METHODS = {
|
|
|
35
35
|
# (NConfig.max_resource_count_for_delete). Used purely for clearer prompt wording.
|
|
36
36
|
MAX_DESTRUCTIVE_BULK_PER_CALL = 1000
|
|
37
37
|
|
|
38
|
+
# Public, unauthenticated API methods that must run without a Cloudinary configuration.
|
|
39
|
+
PUBLIC_API_METHODS = {
|
|
40
|
+
"provisioning": {"create_agent_account"},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_public_api_method(api_name, method_name):
|
|
45
|
+
return method_name in PUBLIC_API_METHODS.get(api_name, set())
|
|
46
|
+
|
|
38
47
|
|
|
39
48
|
def is_destructive_bulk_api_method(method_name):
|
|
40
49
|
return method_name in DESTRUCTIVE_BULK_API_METHODS
|
|
@@ -368,7 +377,10 @@ def handle_api_command(
|
|
|
368
377
|
if not confirm_destructive_bulk_api_method(api_name, func.__name__, force):
|
|
369
378
|
return False
|
|
370
379
|
|
|
371
|
-
if not is_valid_cloudinary_config():
|
|
380
|
+
if not is_public_api_method(api_name, func.__name__) and not is_valid_cloudinary_config():
|
|
381
|
+
if user_config_names():
|
|
382
|
+
raise ConfigurationError("No default Cloudinary configuration is set. "
|
|
383
|
+
"Select one with `-C <name>` or set a default with `cld config -d <name>`.")
|
|
372
384
|
raise ConfigurationError("No Cloudinary configuration found.")
|
|
373
385
|
|
|
374
386
|
try:
|
|
@@ -12,6 +12,7 @@ from cloudinary_cli.utils.config_utils import (
|
|
|
12
12
|
config_type,
|
|
13
13
|
cloudinary_config_details,
|
|
14
14
|
is_env_configured,
|
|
15
|
+
email_from_url,
|
|
15
16
|
)
|
|
16
17
|
from cloudinary_cli.utils.config_resolver import (
|
|
17
18
|
active_config_name,
|
|
@@ -31,6 +32,8 @@ def config_type_label(config_obj):
|
|
|
31
32
|
|
|
32
33
|
_TABLE_COLUMNS = [("name", "NAME"), ("cloud_name", "CLOUD"), ("type", "TYPE"),
|
|
33
34
|
("default", "DEFAULT"), ("active", "ACTIVE")]
|
|
35
|
+
# EMAIL is appended dynamically (see render_config_table) only when at least one row carries one.
|
|
36
|
+
_EMAIL_COLUMN = ("email", "EMAIL")
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
def list_configs():
|
|
@@ -45,8 +48,8 @@ def list_configs():
|
|
|
45
48
|
rows.append(_url_row()) # an inline -c URL: not a saved config, but it is what's active now
|
|
46
49
|
if is_env_configured():
|
|
47
50
|
rows.append(_env_row(env_active=active_config_is_env()))
|
|
48
|
-
|
|
49
|
-
{
|
|
51
|
+
for name in user_config_names(cfg):
|
|
52
|
+
row = {
|
|
50
53
|
"name": name,
|
|
51
54
|
"cloud_name": cloud_name_from_url(cfg[name]),
|
|
52
55
|
"type": config_type(cfg[name]),
|
|
@@ -54,8 +57,10 @@ def list_configs():
|
|
|
54
57
|
"default": name == default,
|
|
55
58
|
"active": name == active_name,
|
|
56
59
|
}
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
email = email_from_url(cfg[name])
|
|
61
|
+
if email: # only surfaced when the config records an account email (e.g. from `agent signup`)
|
|
62
|
+
row["email"] = email
|
|
63
|
+
rows.append(row)
|
|
59
64
|
return rows
|
|
60
65
|
|
|
61
66
|
|
|
@@ -88,8 +93,11 @@ def active_config_meta(config_obj):
|
|
|
88
93
|
|
|
89
94
|
|
|
90
95
|
def render_config_table(rows):
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
columns = list(_TABLE_COLUMNS)
|
|
97
|
+
if any(row.get("email") for row in rows): # add EMAIL only when some config records one
|
|
98
|
+
columns.append(_EMAIL_COLUMN)
|
|
99
|
+
headers = [title for _, title in columns]
|
|
100
|
+
cells = [[_cell(row, key) for key, _ in columns] for row in rows]
|
|
93
101
|
widths = [max(len(headers[i]), *(len(r[i]) for r in cells)) if cells else len(headers[i])
|
|
94
102
|
for i in range(len(headers))]
|
|
95
103
|
line = lambda values: " ".join(v.ljust(widths[i]) for i, v in enumerate(values)).rstrip()
|