Skip to content

Commit a2a88d4

Browse files
committed
Merge branch 'trs/various-and-sundry'
2 parents c628033 + 32f06fe commit a2a88d4

File tree

8 files changed

+132
-49
lines changed

8 files changed

+132
-49
lines changed

devel/generate-command-doc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ os.environ.update({
4646

4747
# Ensure we detect a browser for stable `nextstrain view` output.
4848
"BROWSER": "/bin/true",
49+
50+
# Ensure HOST and PORT are stable for `nextstrain view` output.
51+
"HOST": "127.0.0.1",
52+
"PORT": "4000",
4953
})
5054

5155
from nextstrain.cli import make_parser

nextstrain/cli/__init__.py

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from types import SimpleNamespace
1818

1919
from .argparse import HelpFormatter, register_commands, register_default_command
20-
from .command import build, view, deploy, remote, shell, update, setup, check_setup, login, logout, whoami, version, init_shell, authorization, debugger
20+
from .command import all_commands, version
2121
from .debug import DEBUGGING
2222
from .errors import NextstrainCliError, UsageError
2323
from .util import warn
@@ -69,31 +69,8 @@ def make_parser():
6969
formatter_class = HelpFormatter,
7070
)
7171

72-
# Maintain these manually for now while the list is very small. If we need
73-
# to support pluggable commands or command discovery, we can switch to
74-
# using the "entry points" system:
75-
# https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
76-
#
77-
commands = [
78-
build,
79-
view,
80-
deploy,
81-
remote,
82-
shell,
83-
update,
84-
setup,
85-
check_setup,
86-
login,
87-
logout,
88-
whoami,
89-
version,
90-
init_shell,
91-
authorization,
92-
debugger,
93-
]
94-
9572
register_default_command(parser)
96-
register_commands(parser, commands)
73+
register_commands(parser, all_commands)
9774
register_version_alias(parser)
9875

9976
return parser

nextstrain/cli/aws/cognito/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def renew_tokens(self, *, refresh_token):
194194
self.verify_tokens(
195195
id_token = result.get("IdToken"),
196196
access_token = result.get("AccessToken"),
197-
refresh_token = refresh_token)
197+
refresh_token = result.get("RefreshToken", refresh_token))
198198

199199

200200
def verify_tokens(self, *, id_token, access_token, refresh_token):

nextstrain/cli/browser.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Web browser interaction.
3+
4+
.. envvar:: BROWSER
5+
6+
A ``PATH``-like list of web browsers to try in preference order, before
7+
falling back to a set of default browsers. May be program names, e.g.
8+
``firefox``, or absolute paths to specific executables, e.g.
9+
``/usr/bin/firefox``.
10+
11+
.. envvar:: NOBROWSER
12+
13+
If set to a truthy value (e.g. 1) then no web browser will be considered
14+
available. This can be useful to prevent opening of a browser when there
15+
are not other means of doing so.
16+
"""
17+
import webbrowser
18+
from threading import Thread, ThreadError
19+
from os import environ
20+
from .util import warn
21+
22+
23+
if environ.get("NOBROWSER"):
24+
BROWSER = None
25+
else:
26+
# Avoid text-mode browsers
27+
TERM = environ.pop("TERM", None)
28+
try:
29+
BROWSER = webbrowser.get()
30+
except:
31+
BROWSER = None
32+
finally:
33+
if TERM is not None:
34+
environ["TERM"] = TERM
35+
36+
37+
def open_browser(url: str, new_thread: bool = True):
38+
"""
39+
Opens *url* in a web browser.
40+
41+
Opens in a new tab, if possible, and raises the window to the top, if
42+
possible.
43+
44+
Launches the browser from a separate thread by default so waiting on the
45+
browser child process doesn't block the main (or calling) thread. Set
46+
*new_thread* to False to launch from the same thread as the caller (e.g. if
47+
you've already spawned a dedicated thread or process for the browser).
48+
Note that some registered browsers launch in the background themselves, but
49+
not all do, so this feature makes launch behaviour consistent across
50+
browsers.
51+
52+
Prints a warning to stderr if a browser can't be found or can't be
53+
launched, as automatically opening a browser is considered a
54+
nice-but-not-necessary feature.
55+
"""
56+
if not BROWSER:
57+
warn(f"Couldn't open <{url}> in browser: no browser found")
58+
return
59+
60+
try:
61+
if new_thread:
62+
Thread(target = open_browser, args = (url, False), daemon = True).start()
63+
else:
64+
# new = 2 means new tab, if possible
65+
BROWSER.open(url, new = 2, autoraise = True)
66+
except (ThreadError, webbrowser.Error) as err:
67+
warn(f"Couldn't open <{url}> in browser: {err!r}")

nextstrain/cli/command/__init__.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from . import (
2+
build,
3+
view,
4+
deploy,
5+
remote,
6+
shell,
7+
update,
8+
setup,
9+
check_setup,
10+
login,
11+
logout,
12+
whoami,
13+
version,
14+
init_shell,
15+
authorization,
16+
debugger,
17+
)
18+
19+
# Maintain this list manually for now while its relatively static. If we need
20+
# to support pluggable commands or command discovery, we can switch to using
21+
# the "entry points" system:
22+
# https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
23+
#
24+
# The order of this list is important and intentional: it determines the order
25+
# in various user interfaces, e.g. `nextstrain --help`.
26+
#
27+
all_commands = [
28+
build,
29+
view,
30+
deploy,
31+
remote,
32+
shell,
33+
update,
34+
setup,
35+
check_setup,
36+
login,
37+
logout,
38+
whoami,
39+
version,
40+
init_shell,
41+
authorization,
42+
debugger,
43+
]

nextstrain/cli/command/view.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
from multiprocessing import Process, ProcessError
4949
import re
5050
import requests
51-
import webbrowser
5251
from inspect import cleandoc
5352
from os import environ
5453
from pathlib import Path
@@ -57,6 +56,7 @@
5756
from typing import Iterable, NamedTuple, Tuple, Union
5857
from .. import runner
5958
from ..argparse import add_extended_help_flags, SUPPRESS, SKIP_AUTO_DEFAULT_IN_HELP
59+
from ..browser import BROWSER, open_browser as __open_browser
6060
from ..runner import docker, ambient, conda, singularity
6161
from ..util import colored, remove_suffix, warn
6262
from ..volume import NamedVolume
@@ -67,16 +67,6 @@
6767
PORT = environ.get("PORT") or "4000"
6868

6969

70-
# Avoid text-mode browsers
71-
TERM = environ.pop("TERM", None)
72-
try:
73-
BROWSER = webbrowser.get()
74-
except:
75-
BROWSER = None
76-
finally:
77-
if TERM is not None:
78-
environ["TERM"] = TERM
79-
8070
OPEN_DEFAULT = bool(BROWSER)
8171

8272

@@ -454,8 +444,4 @@ def _open_browser(url: str):
454444
warn(f"Couldn't open <{url}> in browser: Auspice never started listening")
455445
return
456446

457-
try:
458-
# new = 2 means new tab, if possible
459-
BROWSER.open(url, new = 2, autoraise = True)
460-
except webbrowser.Error as err:
461-
warn(f"Couldn't open <{url}> in browser: {err!r}")
447+
__open_browser(url, new_thread = False)

nextstrain/cli/remote/nextstrain_dot_org.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,12 +464,14 @@ def delete(url: urllib.parse.ParseResult, recursively: bool = False, dry_run: bo
464464
raise UserError(f"Path {path} does not seem to exist")
465465

466466
for resource in resources:
467-
yield "nextstrain.org" + str(resource.path)
467+
endpoint = api_endpoint(resource.path)
468+
469+
yield endpoint
468470

469471
if dry_run:
470472
continue
471473

472-
response = http.delete(api_endpoint(resource.path))
474+
response = http.delete(endpoint)
473475

474476
raise_for_status(response)
475477

@@ -648,6 +650,10 @@ def __init__(self):
648650
def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
649651
if self.user and origin(request.url) == origin(NEXTSTRAIN_DOT_ORG):
650652
request.headers["Authorization"] = self.user.http_authorization
653+
654+
# Used in error handling for more informative error messages
655+
request._user = self.user # type: ignore
656+
651657
return request
652658

653659

@@ -708,7 +714,10 @@ def raise_for_status(response: requests.Response) -> None:
708714
""", msg = indent("\n".join(wrap(msg)), " ")) from err
709715

710716
elif status in {401, 403}:
711-
user = current_user()
717+
try:
718+
user = response.request._user # type: ignore
719+
except AttributeError:
720+
user = None
712721

713722
if user:
714723
challenge = authn_challenge(response) if status == 401 else None

nextstrain/cli/remote/s3.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
Nextstrain :term:`datasets <docs:dataset>` and :term:`narratives
88
<docs:narrative>` hosted on `Amazon S3 <https://aws.amazon.com/s3/>`_.
99
This functionality is primarily intended for use by the Nextstrain team and
10-
operators of self-hosted :term:`docs:Auspice` instances. It is also used to
11-
manage the contents of :doc:`Nextstrain Groups
12-
<docs:learn/groups/index>` that have not migrated to using the
13-
:doc:`/remotes/nextstrain.org`.
10+
operators of self-hosted :term:`docs:Auspice` instances.
1411
1512
1613
Remote paths

0 commit comments

Comments
 (0)