cloudinary-cli 1.16.0__tar.gz → 1.16.1__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.16.0/cloudinary_cli.egg-info → cloudinary_cli-1.16.1}/PKG-INFO +1 -1
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/auth/__init__.py +2 -2
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/auth.py +16 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/config.py +4 -7
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/defaults.py +4 -7
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/config_utils.py +40 -6
- cloudinary_cli-1.16.1/cloudinary_cli/version.py +1 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1/cloudinary_cli.egg-info}/PKG-INFO +1 -1
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_cli_config_oauth.py +60 -16
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_default_config.py +86 -1
- cloudinary_cli-1.16.0/cloudinary_cli/version.py +0 -1
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/LICENSE +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/MANIFEST.in +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/README.md +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/__init__.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/auth/callback_page.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/auth/flow.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/auth/loopback_server.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/auth/oauth_config.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/auth/refresh.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/auth/session.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/cli.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/cli_group.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/__init__.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/admin.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/agent.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/overrides.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/provisioning.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/search.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/uploader.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/core/utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/modules/__init__.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/modules/clone.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/modules/make.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/modules/migrate.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/modules/regen_derived.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/modules/sync.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/modules/upload_dir.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/samples/__init__.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/html/media_library_widget +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/html/product_gallery +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/html/upload_widget +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/html/video_player +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/node/upload +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/python/base +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/python/explicit +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/python/find_all_empty_folders +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/python/upload +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/ruby/upload +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/__init__.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/api_utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/config_listing.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/config_resolver.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/file_utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/json_utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/search_utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/url_utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/utils/utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli.egg-info/SOURCES.txt +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli.egg-info/dependency_links.txt +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli.egg-info/entry_points.txt +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli.egg-info/not-zip-safe +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli.egg-info/requires.txt +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli.egg-info/top_level.txt +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/requirements.txt +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/setup.cfg +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/setup.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/__init__.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/conftest.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/helper_test.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/oauth_helpers.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_auth_flow.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_auth_loopback.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_auth_region.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_auth_session.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_cli.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_cli_agent.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_cli_api.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_cli_config.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_cli_samples.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_cli_search_api.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_cli_url.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_cli_utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_config_cache.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_config_concurrency.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_config_permissions.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_file_utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_json_utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_modules/__init__.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_modules/test_cli_clone.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_modules/test_cli_make.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_modules/test_cli_sync.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_modules/test_cli_upload_dir.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_oauth_multiprocess.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_oauth_retry.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_oauth_token_seam.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_search_utils.py +0 -0
- {cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/test/test_utils.py +0 -0
|
@@ -23,7 +23,7 @@ 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
|
-
|
|
26
|
+
remove_named_config,
|
|
27
27
|
save_named_config,
|
|
28
28
|
is_reserved_config_name,
|
|
29
29
|
)
|
|
@@ -78,7 +78,7 @@ def logout(name):
|
|
|
78
78
|
return "not_oauth"
|
|
79
79
|
|
|
80
80
|
revoked = _revoke_login(name, saved[name])
|
|
81
|
-
|
|
81
|
+
remove_named_config(name)
|
|
82
82
|
return "removed" if revoked else "revoke_failed"
|
|
83
83
|
|
|
84
84
|
|
|
@@ -74,6 +74,9 @@ def _select_oauth_login():
|
|
|
74
74
|
logger.info("No saved OAuth logins to log out of.")
|
|
75
75
|
return "none", None
|
|
76
76
|
|
|
77
|
+
if len(names) == 1:
|
|
78
|
+
return _confirm_sole_login(names[0])
|
|
79
|
+
|
|
77
80
|
echo("Saved OAuth logins:")
|
|
78
81
|
for i, name in enumerate(names, start=1):
|
|
79
82
|
echo(f" {i}) {name}")
|
|
@@ -92,3 +95,16 @@ def _select_oauth_login():
|
|
|
92
95
|
logger.error(f"Invalid selection '{choice}'. Expected a number between 1 and {len(names)}.")
|
|
93
96
|
return "invalid", None
|
|
94
97
|
return "selected", names[int(choice) - 1]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _confirm_sole_login(name):
|
|
101
|
+
"""With a single saved login there is nothing to choose, so confirm that one rather than
|
|
102
|
+
presenting a one-item menu. Logging out still revokes a token, so it is not done unasked."""
|
|
103
|
+
choice = prompt_user(
|
|
104
|
+
f"Log out of '{name}'? [y/N]: ",
|
|
105
|
+
noninteractive_hint="Pass the configuration name directly: `cld logout <name>`.")
|
|
106
|
+
if choice is None:
|
|
107
|
+
return "invalid", None
|
|
108
|
+
if choice.strip().lower() not in ("y", "yes"):
|
|
109
|
+
return "cancelled", None
|
|
110
|
+
return "selected", name
|
|
@@ -6,11 +6,10 @@ from cloudinary_cli.utils.config_utils import (
|
|
|
6
6
|
load_config,
|
|
7
7
|
verify_cloudinary_url,
|
|
8
8
|
save_named_config,
|
|
9
|
-
|
|
9
|
+
remove_named_config,
|
|
10
10
|
show_cloudinary_config,
|
|
11
11
|
is_valid_cloudinary_config,
|
|
12
12
|
user_config_names,
|
|
13
|
-
get_default_config_name,
|
|
14
13
|
set_default_config,
|
|
15
14
|
clear_default_config,
|
|
16
15
|
is_reserved_config_name,
|
|
@@ -101,12 +100,10 @@ def config_command(new, ls, as_json, show, rm, from_url, default, set_default, u
|
|
|
101
100
|
clear_default_config()
|
|
102
101
|
logger.info("Default configuration cleared.")
|
|
103
102
|
elif rm:
|
|
104
|
-
if
|
|
105
|
-
logger.warning(f"Configuration '{rm}' not found.")
|
|
106
|
-
else:
|
|
107
|
-
if get_default_config_name() == rm:
|
|
108
|
-
clear_default_config()
|
|
103
|
+
if remove_named_config(rm):
|
|
109
104
|
logger.info(f"Configuration '{rm}' deleted.")
|
|
105
|
+
else:
|
|
106
|
+
logger.warning(f"Configuration '{rm}' not found.")
|
|
110
107
|
elif ls:
|
|
111
108
|
rows = list_configs()
|
|
112
109
|
if as_json:
|
|
@@ -37,13 +37,10 @@ ACCOUNT_EMAIL_PARAM = "account_email"
|
|
|
37
37
|
# "warning:" prefix, so the copy-pasteable command lines stay clean.
|
|
38
38
|
NO_CONFIG_MESSAGE = (
|
|
39
39
|
"No Cloudinary configuration found.\n"
|
|
40
|
-
" - Log in with OAuth:
|
|
41
|
-
" - Add an API-key config:
|
|
42
|
-
"
|
|
43
|
-
" -
|
|
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>"
|
|
40
|
+
" - Log in with OAuth: cld login\n"
|
|
41
|
+
" - Add an API-key config: cld config -n <name> cloudinary://<api_key>:<api_secret>@<cloud_name> --set-default\n"
|
|
42
|
+
" - Set an existing config as the default: cld config -d <name>\n"
|
|
43
|
+
" - AI agents - provision an environment to be claimed by a human: cld agent signup <email> <framework> <model> <goal>"
|
|
47
44
|
)
|
|
48
45
|
|
|
49
46
|
# Shown when saved configs exist but none is active (no default set, no environment config, and no
|
|
@@ -72,10 +72,21 @@ def load_config():
|
|
|
72
72
|
if stat is not None and stat == _config_cache_stat and _config_cache is not None:
|
|
73
73
|
return dict(_config_cache) # copy: callers mutate the result in place (e.g. cfg.update(...))
|
|
74
74
|
cfg = read_json_from_file(CLOUDINARY_CLI_CONFIG_FILE, does_not_exist_ok=True)
|
|
75
|
+
_rectify_config(cfg)
|
|
75
76
|
_config_cache, _config_cache_stat = cfg, stat
|
|
76
77
|
return dict(cfg)
|
|
77
78
|
|
|
78
79
|
|
|
80
|
+
def _rectify_config(cfg):
|
|
81
|
+
"""Correct invalid state in a freshly-read config, in place, so no caller has to guard against
|
|
82
|
+
it. The repair reaches disk on the next write, since every mutation rebuilds the file from a
|
|
83
|
+
load_config result."""
|
|
84
|
+
default = cfg.get(DEFAULT_CONFIG_KEY)
|
|
85
|
+
if default is not None and default not in [k for k in cfg if k != DEFAULT_CONFIG_KEY]:
|
|
86
|
+
del cfg[DEFAULT_CONFIG_KEY]
|
|
87
|
+
logger.debug(f"Ignoring stored default '{default}': no such saved configuration.")
|
|
88
|
+
|
|
89
|
+
|
|
79
90
|
def save_config(config):
|
|
80
91
|
# 0600 from the start: the config file holds secrets (api_secret, account_url, OAuth tokens),
|
|
81
92
|
# and writing the temp file 0600 before the atomic replace means it is never momentarily
|
|
@@ -140,13 +151,36 @@ def save_named_config(name, cloudinary_url, set_default=False):
|
|
|
140
151
|
return "no"
|
|
141
152
|
|
|
142
153
|
|
|
154
|
+
def remove_named_config(name):
|
|
155
|
+
"""
|
|
156
|
+
Delete a named configuration, and re-point the stored default when it named that config: a lone
|
|
157
|
+
surviving config is promoted, otherwise the default is cleared. The counterpart to
|
|
158
|
+
save_named_config: the single way to remove a saved config. Returns True if the configuration
|
|
159
|
+
existed and was removed.
|
|
160
|
+
"""
|
|
161
|
+
with config_lock():
|
|
162
|
+
cfg = load_config()
|
|
163
|
+
if name not in user_config_names(cfg):
|
|
164
|
+
return False
|
|
165
|
+
del cfg[name]
|
|
166
|
+
if cfg.get(DEFAULT_CONFIG_KEY) == name:
|
|
167
|
+
del cfg[DEFAULT_CONFIG_KEY]
|
|
168
|
+
remaining = user_config_names(cfg)
|
|
169
|
+
if remaining and _is_sole_usable_config(remaining[0], cfg):
|
|
170
|
+
cfg[DEFAULT_CONFIG_KEY] = remaining[0]
|
|
171
|
+
save_config(cfg)
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
|
|
143
175
|
def _should_auto_default(name):
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
176
|
+
return _is_sole_usable_config(name) and not get_default_config_name()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _is_sole_usable_config(name, cfg=None):
|
|
180
|
+
"""Whether name is the only config a bare `cld <command>` could use: the only saved config, with
|
|
181
|
+
nothing configured in the environment. A stored default outranks the environment, so defaulting
|
|
182
|
+
to a saved config while CLOUDINARY_URL is set would silently override the user's choice."""
|
|
183
|
+
return user_config_names(cfg) == [name] and not is_env_configured()
|
|
150
184
|
|
|
151
185
|
|
|
152
186
|
def user_config_names(cfg=None):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.16.1"
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
4
|
import unittest
|
|
5
|
+
from contextlib import contextmanager
|
|
5
6
|
from unittest.mock import patch
|
|
6
7
|
|
|
7
8
|
import cloudinary
|
|
@@ -11,6 +12,7 @@ from cloudinary_cli.auth.session import Session, to_cloudinary_url
|
|
|
11
12
|
from test.oauth_helpers import jwt_access_token
|
|
12
13
|
from cloudinary_cli.cli import cli
|
|
13
14
|
from cloudinary_cli.utils.config_resolver import config_to_api_kwargs, get_cloudinary_config
|
|
15
|
+
from cloudinary_cli.utils import config_utils
|
|
14
16
|
from cloudinary_cli.utils.config_utils import config_to_dict, show_cloudinary_config
|
|
15
17
|
|
|
16
18
|
|
|
@@ -20,6 +22,27 @@ def _oauth_url(cloud="eu-cloud", region="api-eu"):
|
|
|
20
22
|
expires_at=int(time.time()) + 300, region=region, issuer="https://oauth.cloudinary.com/"))
|
|
21
23
|
|
|
22
24
|
|
|
25
|
+
@contextmanager
|
|
26
|
+
def _patched_config_store(initial=None):
|
|
27
|
+
"""Back load_config/save_config with an in-memory dict (no real config.json or lock)."""
|
|
28
|
+
store = {"cfg": dict(initial or {})}
|
|
29
|
+
|
|
30
|
+
@contextmanager
|
|
31
|
+
def _noop_lock():
|
|
32
|
+
yield
|
|
33
|
+
|
|
34
|
+
def _load():
|
|
35
|
+
cfg = dict(store["cfg"])
|
|
36
|
+
config_utils._rectify_config(cfg) # the real load_config rectifies; mirror it here
|
|
37
|
+
return cfg
|
|
38
|
+
|
|
39
|
+
with patch("cloudinary_cli.utils.config_utils.load_config", side_effect=_load), \
|
|
40
|
+
patch("cloudinary_cli.utils.config_utils.save_config",
|
|
41
|
+
side_effect=lambda cfg: store.__setitem__("cfg", dict(cfg))), \
|
|
42
|
+
patch("cloudinary_cli.utils.config_utils.config_lock", _noop_lock):
|
|
43
|
+
yield store
|
|
44
|
+
|
|
45
|
+
|
|
23
46
|
class _RestoresSdkConfig(unittest.TestCase):
|
|
24
47
|
def setUp(self):
|
|
25
48
|
self._env_snapshot = dict(os.environ)
|
|
@@ -43,7 +66,7 @@ class TestLogoutScope(unittest.TestCase):
|
|
|
43
66
|
from cloudinary_cli.auth import logout
|
|
44
67
|
saved = {"eu-cloud": _oauth_url()}
|
|
45
68
|
with patch("cloudinary_cli.auth.load_config", return_value=saved), \
|
|
46
|
-
patch("cloudinary_cli.auth.
|
|
69
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove, \
|
|
47
70
|
patch("cloudinary_cli.auth.flow.revoke") as revoke:
|
|
48
71
|
self.assertEqual("removed", logout("eu-cloud"))
|
|
49
72
|
remove.assert_called_once_with("eu-cloud")
|
|
@@ -54,7 +77,7 @@ class TestLogoutScope(unittest.TestCase):
|
|
|
54
77
|
from cloudinary_cli.auth import logout
|
|
55
78
|
saved = {"eu-cloud": _oauth_url()}
|
|
56
79
|
with patch("cloudinary_cli.auth.load_config", return_value=saved), \
|
|
57
|
-
patch("cloudinary_cli.auth.
|
|
80
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove, \
|
|
58
81
|
patch("cloudinary_cli.auth.flow.revoke", side_effect=requests.ConnectionError()):
|
|
59
82
|
self.assertEqual("revoke_failed", logout("eu-cloud"))
|
|
60
83
|
remove.assert_called_once_with("eu-cloud") # local entry removed despite revoke failure
|
|
@@ -63,14 +86,14 @@ class TestLogoutScope(unittest.TestCase):
|
|
|
63
86
|
from cloudinary_cli.auth import logout
|
|
64
87
|
saved = {"mykey": "cloudinary://key:secret@cloud"}
|
|
65
88
|
with patch("cloudinary_cli.auth.load_config", return_value=saved), \
|
|
66
|
-
patch("cloudinary_cli.auth.
|
|
89
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove:
|
|
67
90
|
self.assertEqual("not_oauth", logout("mykey"))
|
|
68
91
|
remove.assert_not_called()
|
|
69
92
|
|
|
70
93
|
def test_missing_name(self):
|
|
71
94
|
from cloudinary_cli.auth import logout
|
|
72
95
|
with patch("cloudinary_cli.auth.load_config", return_value={}), \
|
|
73
|
-
patch("cloudinary_cli.auth.
|
|
96
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove:
|
|
74
97
|
self.assertEqual("not_found", logout("nope"))
|
|
75
98
|
remove.assert_not_called()
|
|
76
99
|
|
|
@@ -79,13 +102,15 @@ class TestLogoutInteractiveSelect(unittest.TestCase):
|
|
|
79
102
|
"""`cld logout` with no name lists OAuth logins and removes the chosen one."""
|
|
80
103
|
|
|
81
104
|
runner = CliRunner()
|
|
105
|
+
# The numbered menu only appears with more than one login, so menu tests need two.
|
|
106
|
+
_TWO_LOGINS = {"cloud-a": _oauth_url("cloud-a"), "cloud-b": _oauth_url("cloud-b")}
|
|
82
107
|
|
|
83
108
|
def test_lists_only_oauth_and_removes_selected(self):
|
|
84
109
|
saved = {"mykey": "cloudinary://key:secret@cloud",
|
|
85
110
|
"cloud-a": _oauth_url("cloud-a"), "cloud-b": _oauth_url("cloud-b")}
|
|
86
111
|
with patch("cloudinary_cli.auth.load_config", return_value=saved), \
|
|
87
112
|
patch("cloudinary_cli.auth.refresh.load_config", return_value=saved), \
|
|
88
|
-
patch("cloudinary_cli.auth.
|
|
113
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove, \
|
|
89
114
|
patch("cloudinary_cli.auth.flow.revoke"):
|
|
90
115
|
result = self.runner.invoke(cli, ["logout"], input="2\n")
|
|
91
116
|
self.assertIn("cloud-a", result.output)
|
|
@@ -96,40 +121,60 @@ class TestLogoutInteractiveSelect(unittest.TestCase):
|
|
|
96
121
|
def test_no_oauth_logins(self):
|
|
97
122
|
with patch("cloudinary_cli.auth.refresh.load_config",
|
|
98
123
|
return_value={"mykey": "cloudinary://key:secret@cloud"}), \
|
|
99
|
-
patch("cloudinary_cli.auth.
|
|
124
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove:
|
|
100
125
|
result = self.runner.invoke(cli, ["logout"], input="\n")
|
|
101
126
|
self.assertIn("No saved OAuth logins", result.output)
|
|
102
127
|
remove.assert_not_called()
|
|
103
128
|
|
|
104
129
|
def test_cancel_on_empty_input(self):
|
|
105
130
|
with patch("cloudinary_cli.auth.refresh.load_config", return_value={"cloud-a": _oauth_url("cloud-a")}), \
|
|
106
|
-
patch("cloudinary_cli.auth.
|
|
131
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove:
|
|
107
132
|
result = self.runner.invoke(cli, ["logout"], input="\n")
|
|
108
133
|
remove.assert_not_called()
|
|
109
134
|
self.assertEqual(0, result.exit_code)
|
|
110
135
|
|
|
111
136
|
def test_invalid_non_numeric_errors(self):
|
|
112
|
-
with patch("cloudinary_cli.auth.refresh.load_config", return_value=
|
|
113
|
-
patch("cloudinary_cli.auth.
|
|
137
|
+
with patch("cloudinary_cli.auth.refresh.load_config", return_value=self._TWO_LOGINS), \
|
|
138
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove:
|
|
114
139
|
result = self.runner.invoke(cli, ["logout"], input="sdfdsf\n", standalone_mode=False)
|
|
115
140
|
self.assertIn("Invalid selection", result.output)
|
|
116
141
|
self.assertFalse(result.return_value) # main() maps falsy -> exit 1
|
|
117
142
|
remove.assert_not_called()
|
|
118
143
|
|
|
119
144
|
def test_out_of_range_errors(self):
|
|
120
|
-
with patch("cloudinary_cli.auth.refresh.load_config", return_value=
|
|
121
|
-
patch("cloudinary_cli.auth.
|
|
145
|
+
with patch("cloudinary_cli.auth.refresh.load_config", return_value=self._TWO_LOGINS), \
|
|
146
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove:
|
|
122
147
|
result = self.runner.invoke(cli, ["logout"], input="5\n", standalone_mode=False)
|
|
123
148
|
self.assertIn("Invalid selection", result.output)
|
|
124
149
|
self.assertFalse(result.return_value)
|
|
125
150
|
remove.assert_not_called()
|
|
126
151
|
|
|
152
|
+
def test_sole_login_confirms_instead_of_listing(self):
|
|
153
|
+
saved = {"cloud-a": _oauth_url("cloud-a")}
|
|
154
|
+
with patch("cloudinary_cli.auth.load_config", return_value=saved), \
|
|
155
|
+
patch("cloudinary_cli.auth.refresh.load_config", return_value=saved), \
|
|
156
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove, \
|
|
157
|
+
patch("cloudinary_cli.auth.flow.revoke"):
|
|
158
|
+
result = self.runner.invoke(cli, ["logout"], input="y\n")
|
|
159
|
+
self.assertNotIn("Saved OAuth logins:", result.output) # no one-item menu
|
|
160
|
+
self.assertIn("cloud-a", result.output)
|
|
161
|
+
remove.assert_called_once_with("cloud-a")
|
|
162
|
+
|
|
163
|
+
def test_sole_login_declined_removes_nothing(self):
|
|
164
|
+
saved = {"cloud-a": _oauth_url("cloud-a")}
|
|
165
|
+
with patch("cloudinary_cli.auth.load_config", return_value=saved), \
|
|
166
|
+
patch("cloudinary_cli.auth.refresh.load_config", return_value=saved), \
|
|
167
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove:
|
|
168
|
+
result = self.runner.invoke(cli, ["logout"], input="n\n")
|
|
169
|
+
remove.assert_not_called()
|
|
170
|
+
self.assertEqual(0, result.exit_code)
|
|
171
|
+
|
|
127
172
|
def test_noninteractive_stdin_errors_with_hint(self):
|
|
128
173
|
# Closed stdin (no input at all): the selection cannot be made, so error with the
|
|
129
174
|
# non-interactive form (`cld logout <name>`) and exit non-zero, not a silent no-op.
|
|
130
175
|
import builtins
|
|
131
176
|
with patch("cloudinary_cli.auth.refresh.load_config", return_value={"cloud-a": _oauth_url("cloud-a")}), \
|
|
132
|
-
patch("cloudinary_cli.auth.
|
|
177
|
+
patch("cloudinary_cli.auth.remove_named_config") as remove, \
|
|
133
178
|
patch.object(builtins, "input", side_effect=EOFError()):
|
|
134
179
|
result = self.runner.invoke(cli, ["logout"], standalone_mode=False)
|
|
135
180
|
self.assertIn("cld logout <name>", result.output)
|
|
@@ -811,12 +856,11 @@ class TestConfigDefaultCommands(_RestoresSdkConfig):
|
|
|
811
856
|
self.assertIn("(environment)", by_name)
|
|
812
857
|
|
|
813
858
|
def test_rm_of_default_clears_it(self):
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
patch("cloudinary_cli.core.config.clear_default_config") as clear:
|
|
859
|
+
stored = {"prod": "cloudinary://k:s@prod", "__default__": "prod"}
|
|
860
|
+
with _patched_config_store(stored) as store:
|
|
817
861
|
result = self.runner.invoke(cli, ['config', '-rm', 'prod'])
|
|
818
862
|
self.assertEqual(0, result.exit_code, result.output)
|
|
819
|
-
|
|
863
|
+
self.assertEqual({}, store["cfg"]) # the name and the default it held both gone
|
|
820
864
|
|
|
821
865
|
def test_reserved_name_rejected_on_new(self):
|
|
822
866
|
result = self.runner.invoke(
|
|
@@ -12,7 +12,9 @@ def _in_memory_config(initial=None):
|
|
|
12
12
|
store = {"cfg": dict(initial or {})}
|
|
13
13
|
|
|
14
14
|
def _load():
|
|
15
|
-
|
|
15
|
+
cfg = dict(store["cfg"])
|
|
16
|
+
config_utils._rectify_config(cfg) # the real load_config rectifies; mirror it here
|
|
17
|
+
return cfg
|
|
16
18
|
|
|
17
19
|
def _save(cfg):
|
|
18
20
|
store["cfg"] = dict(cfg)
|
|
@@ -143,6 +145,89 @@ class TestAccountEmailInUrl(unittest.TestCase):
|
|
|
143
145
|
self.assertEqual("you@example.com", config_utils.config_to_dict(cfg).get("account_email"))
|
|
144
146
|
|
|
145
147
|
|
|
148
|
+
class TestDanglingDefault(unittest.TestCase):
|
|
149
|
+
"""A __default__ naming no saved config is invalid state; reads must not observe it."""
|
|
150
|
+
|
|
151
|
+
def test_dangling_default_is_ignored(self):
|
|
152
|
+
with _in_memory_config({"__default__": "gone"}):
|
|
153
|
+
self.assertIsNone(config_utils.get_default_config_name())
|
|
154
|
+
|
|
155
|
+
def test_dangling_default_does_not_block_auto_default(self):
|
|
156
|
+
with _in_memory_config({"__default__": "gone"}), \
|
|
157
|
+
patch("cloudinary_cli.utils.config_utils.is_env_configured", return_value=False):
|
|
158
|
+
status = config_utils.save_named_config("prod", "cloudinary://k:s@prod")
|
|
159
|
+
self.assertEqual("made", status)
|
|
160
|
+
self.assertEqual("prod", config_utils.get_default_config_name())
|
|
161
|
+
|
|
162
|
+
def test_saving_under_dangling_default_name_is_not_already(self):
|
|
163
|
+
with _in_memory_config({"__default__": "gone"}), \
|
|
164
|
+
patch("cloudinary_cli.utils.config_utils.is_env_configured", return_value=False):
|
|
165
|
+
status = config_utils.save_named_config("gone", "cloudinary://k:s@gone")
|
|
166
|
+
self.assertEqual("made", status)
|
|
167
|
+
self.assertEqual("gone", config_utils.get_default_config_name())
|
|
168
|
+
|
|
169
|
+
def test_dangling_default_dropped_from_file_on_next_write(self):
|
|
170
|
+
with _in_memory_config({"prod": "cloudinary://k:s@prod", "__default__": "gone"}) as store:
|
|
171
|
+
config_utils.update_config({"staging": "cloudinary://k:s@staging"})
|
|
172
|
+
self.assertNotIn("__default__", store["cfg"])
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestRemoveNamedConfig(unittest.TestCase):
|
|
176
|
+
def test_keeps_default_pointing_at_another_config(self):
|
|
177
|
+
with _in_memory_config({"prod": "cloudinary://k:s@prod",
|
|
178
|
+
"staging": "cloudinary://k:s@staging",
|
|
179
|
+
"__default__": "prod"}) as store, \
|
|
180
|
+
patch("cloudinary_cli.utils.config_utils.is_env_configured", return_value=False):
|
|
181
|
+
self.assertTrue(config_utils.remove_named_config("staging"))
|
|
182
|
+
self.assertEqual("prod", store["cfg"]["__default__"])
|
|
183
|
+
|
|
184
|
+
def test_promotes_lone_survivor_to_default(self):
|
|
185
|
+
with _in_memory_config({"prod": "cloudinary://k:s@prod",
|
|
186
|
+
"staging": "cloudinary://k:s@staging",
|
|
187
|
+
"__default__": "prod"}) as store, \
|
|
188
|
+
patch("cloudinary_cli.utils.config_utils.is_env_configured", return_value=False):
|
|
189
|
+
self.assertTrue(config_utils.remove_named_config("prod"))
|
|
190
|
+
self.assertEqual("staging", store["cfg"]["__default__"])
|
|
191
|
+
|
|
192
|
+
def test_no_promotion_when_env_configured(self):
|
|
193
|
+
with _in_memory_config({"prod": "cloudinary://k:s@prod",
|
|
194
|
+
"staging": "cloudinary://k:s@staging",
|
|
195
|
+
"__default__": "prod"}) as store, \
|
|
196
|
+
patch("cloudinary_cli.utils.config_utils.is_env_configured", return_value=True):
|
|
197
|
+
self.assertTrue(config_utils.remove_named_config("prod"))
|
|
198
|
+
self.assertNotIn("__default__", store["cfg"]) # env keeps precedence
|
|
199
|
+
|
|
200
|
+
def test_no_promotion_when_several_survive(self):
|
|
201
|
+
with _in_memory_config({"a": "cloudinary://k:s@a", "b": "cloudinary://k:s@b",
|
|
202
|
+
"c": "cloudinary://k:s@c", "__default__": "a"}) as store, \
|
|
203
|
+
patch("cloudinary_cli.utils.config_utils.is_env_configured", return_value=False):
|
|
204
|
+
self.assertTrue(config_utils.remove_named_config("a"))
|
|
205
|
+
self.assertNotIn("__default__", store["cfg"]) # ambiguous, so the user picks
|
|
206
|
+
|
|
207
|
+
def test_no_promotion_when_removed_was_not_default(self):
|
|
208
|
+
with _in_memory_config({"prod": "cloudinary://k:s@prod",
|
|
209
|
+
"staging": "cloudinary://k:s@staging"}) as store, \
|
|
210
|
+
patch("cloudinary_cli.utils.config_utils.is_env_configured", return_value=False):
|
|
211
|
+
self.assertTrue(config_utils.remove_named_config("staging"))
|
|
212
|
+
self.assertNotIn("__default__", store["cfg"]) # there was no default to re-point
|
|
213
|
+
|
|
214
|
+
def test_removing_last_config_leaves_no_default(self):
|
|
215
|
+
with _in_memory_config({"prod": "cloudinary://k:s@prod", "__default__": "prod"}) as store, \
|
|
216
|
+
patch("cloudinary_cli.utils.config_utils.is_env_configured", return_value=False):
|
|
217
|
+
self.assertTrue(config_utils.remove_named_config("prod"))
|
|
218
|
+
self.assertEqual({}, store["cfg"])
|
|
219
|
+
|
|
220
|
+
def test_missing_name_reports_false(self):
|
|
221
|
+
with _in_memory_config({"prod": "cloudinary://k:s@prod"}) as store:
|
|
222
|
+
self.assertFalse(config_utils.remove_named_config("nope"))
|
|
223
|
+
self.assertEqual({"prod": "cloudinary://k:s@prod"}, store["cfg"])
|
|
224
|
+
|
|
225
|
+
def test_refuses_reserved_key(self):
|
|
226
|
+
with _in_memory_config({"prod": "cloudinary://k:s@prod", "__default__": "prod"}) as store:
|
|
227
|
+
self.assertFalse(config_utils.remove_named_config("__default__"))
|
|
228
|
+
self.assertEqual("prod", store["cfg"]["__default__"])
|
|
229
|
+
|
|
230
|
+
|
|
146
231
|
class TestBuildConfigUrl(unittest.TestCase):
|
|
147
232
|
def _parsed(self, url):
|
|
148
233
|
import cloudinary
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.16.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/html/media_library_widget
RENAMED
|
File without changes
|
{cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli/templates/html/product_gallery
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudinary_cli-1.16.0 → cloudinary_cli-1.16.1}/cloudinary_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|