Repository: wandb/wandb

Issue:
[WB-6696] make api key prompting be more captive
https://wandb.atlassian.net/browse/WB-6696

Description
-----------

Currently if a user enters a malformed API key in the CLI, the response is somewhat hostile:

```
wandb: Paste an API key from your profile and hit enter: 
Traceback (most recent call last):
  File "/Users/jeff/.pyenv/versions/wb397/bin/wandb", line 33, in <module>
    sys.exit(load_entry_point('wandb', 'console_scripts', 'wandb')())
  File "/Users/jeff/.pyenv/versions/3.9.7/envs/wb397/lib/python3.9/site-packages/click/core.py", line 1137, in __call__
    return self.main(*args, **kwargs)
  File "/Users/jeff/.pyenv/versions/3.9.7/envs/wb397/lib/python3.9/site-packages/click/core.py", line 1062, in main
    rv = self.invoke(ctx)
  File "/Users/jeff/.pyenv/versions/3.9.7/envs/wb397/lib/python3.9/site-packages/click/core.py", line 1668, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/jeff/.pyenv/versions/3.9.7/envs/wb397/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/jeff/.pyenv/versions/3.9.7/envs/wb397/lib/python3.9/site-packages/click/core.py", line 763, in invoke
    return __callback(*args, **kwargs)
  File "/Users/jeff/.pyenv/versions/3.9.7/envs/wb397/lib/python3.9/site-packages/click/decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/Users/jeff/work/client/wandb/cli/cli.py", line 86, in wrapper
    return func(*args, **kwargs)
  File "/Users/jeff/work/client/wandb/cli/cli.py", line 1410, in local
    ctx.invoke(login, host=host)
  File "/Users/jeff/.pyenv/versions/3.9.7/envs/wb397/lib/python3.9/site-packages/click/core.py", line 763, in invoke
    return __callback(*args, **kwargs)
  File "/Users/jeff/work/client/wandb/cli/cli.py", line 86, in wrapper
    return func(*args, **kwargs)
  File "/Users/jeff/work/client/wandb/cli/cli.py", line 234, in login
    wandb.login(relogin=relogin, key=key, anonymous=anon_mode, host=host, force=True)
  File "/Users/jeff/work/client/wandb/sdk/wandb_login.py", line 65, in login
    configured = _login(**kwargs)
  File "/Users/jeff/work/client/wandb/sdk/wandb_login.py", line 268, in _login
    wlogin.prompt_api_key()
  File "/Users/jeff/work/client/wandb/sdk/wandb_login.py", line 196, in prompt_api_key
    key, status = self._prompt_api_key()
  File "/Users/jeff/work/client/wandb/sdk/wandb_login.py", line 180, in _prompt_api_key
    key = apikey.prompt_api_key(
  File "/Users/jeff/work/client/wandb/sdk/lib/apikey.py", line 121, in prompt_api_key
    write_key(settings, key, api=api)
  File "/Users/jeff/work/client/wandb/sdk/lib/apikey.py", line 206, in write_key
    raise ValueError("API key must be 40 characters long, yours was %s" % len(key))
ValueError: API key must be 40 characters long, yours was 18
```

This PR updates API key entry to be more captive. Now if an invalid API key is entered, we retry up to a specified number of retries (default 5). 

Now the flow looks like:

```
(wandb-3.7) danielgoldstein@Daniels-MBP ~ % WANDB_BASE_URL=http://localhost:8080 wandb login --relogin
wandb: You can find your API key in your browser here: http://localhost:8080/authorize
wandb: Paste an API key from your profile and hit enter:
wandb: ERROR API key must be 40 characters long, yours was 1
wandb: You can find your API key in your browser here: http://localhost:8080/authorize
wandb: Paste an API key from your profile and hit enter:
wandb: ERROR API key must be 40 characters long, yours was 1
wandb: You can find your API key in your browser here: http://localhost:8080/authorize
wandb: Paste an API key from your profile and hit enter:
wandb: ERROR API key must be 40 characters long, yours was 1
wandb: You can find your API key in your browser here: http://localhost:8080/authorize
wandb: Paste an API key from your profile and hit enter:
wandb: ERROR API key must be 40 characters long, yours was 1
wandb: You can find your API key in your browser here: http://localhost:8080/authorize
wandb: Paste an API key from your profile and hit enter:
wandb: ERROR API key must be 40 characters long, yours was 1
wandb: ERROR Failed to get API key after 5 attempts.
```

Testing
-------

How was this PR tested?

Manually

Release Notes
-------------

Below, please enter user-facing release notes as one or more bullet points.
If your change is not user-visible, write `NO RELEASE NOTES` instead, with no bullet points.

------------- BEGIN RELEASE NOTES ------------------
Makes API key entry more user friendly 
------------- END RELEASE NOTES --------------------



Discussion:
# [Codecov](https://codecov.io/gh/wandb/client/pull/2688?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb) Report
> Merging [#2688](https://codecov.io/gh/wandb/client/pull/2688?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb) (4e94d4b) into [master](https://codecov.io/gh/wandb/client/commit/043a5c43264c81cb0015c911993fd63666c11219?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb) (043a5c4) will **increase** coverage by `0.00%`.
> The diff coverage is `44.44%`.

[![Impacted file tree graph](https://codecov.io/gh/wandb/client/pull/2688/graphs/tree.svg?width=650&height=150&src=pr&token=41Iw2WzViQ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb)](https://codecov.io/gh/wandb/client/pull/2688?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb)

```diff
@@           Coverage Diff           @@
##           master    #2688   +/-   ##
=======================================
  Coverage   75.18%   75.19%           
=======================================
  Files         173      173           
  Lines       25498    25507    +9     
=======================================
+ Hits        19170    19179    +9     
  Misses       6328     6328           
```

| Flag | Coverage Δ | |
|---|---|---|
| functest | `45.75% <44.44%> (+0.03%)` | :arrow_up: |
| unittest | `72.83% <44.44%> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/wandb/client/pull/2688?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb) | Coverage Δ | |
|---|---|---|
| [wandb/sdk/wandb\_login.py](https://codecov.io/gh/wandb/client/pull/2688/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb#diff-d2FuZGIvc2RrL3dhbmRiX2xvZ2luLnB5) | `91.77% <44.44%> (-2.86%)` | :arrow_down: |
| [wandb/sdk/lib/git.py](https://codecov.io/gh/wandb/client/pull/2688/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb#diff-d2FuZGIvc2RrL2xpYi9naXQucHk=) | `75.86% <0.00%> (ø)` | |
| [wandb/sdk/internal/meta.py](https://codecov.io/gh/wandb/client/pull/2688/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wandb#diff-d2FuZGIvc2RrL2ludGVybmFsL21ldGEucHk=) | `90.12% <0.00%> (+3.08%)` | :arrow_up: |


Resolving patch:
diff --git a/wandb/sdk/wandb_login.py b/wandb/sdk/wandb_login.py
--- a/wandb/sdk/wandb_login.py
+++ b/wandb/sdk/wandb_login.py
@@ -21,9 +21,6 @@
 from ..apis import InternalApi
 
 
-MAX_API_KEY_PROMPTS = 5
-
-
 def _handle_host_wandb_setting(host: Optional[str], cloud: bool = False) -> None:
     """Write the host parameter from wandb.login or wandb login to
     the global settings file so that it is used automatically by
@@ -85,7 +82,6 @@ def __init__(self):
         self._wl = None
         self._key = None
         self._relogin = None
-        self._num_api_key_prompts = 0
 
     def setup(self, kwargs):
         self.kwargs = kwargs
@@ -179,8 +175,8 @@ def update_session(
             self._wl._update_user_settings()
 
     def _prompt_api_key(self) -> Tuple[Optional[str], ApiKeyStatus]:
+
         api = Api(self._settings)
-        self._num_api_key_prompts += 1
         try:
             key = apikey.prompt_api_key(
                 self._settings,
@@ -191,13 +187,7 @@ def _prompt_api_key(self) -> Tuple[Optional[str], ApiKeyStatus]:
         except ValueError as e:
             # invalid key provided, try again
             wandb.termerror(e.args[0])
-            if self._num_api_key_prompts < MAX_API_KEY_PROMPTS:
-                return self._prompt_api_key()
-            else:
-                wandb.termerror(
-                    f"Failed to get API key after {MAX_API_KEY_PROMPTS} attempts."
-                )
-                return None, ApiKeyStatus.DISABLED
+            return self._prompt_api_key()
 
         except TimeoutError:
             wandb.termlog("W&B disabled due to login timeout.")


Test patch: