Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various and sundry #332

Merged
merged 8 commits into from
Jan 11, 2024
4 changes: 4 additions & 0 deletions devel/generate-command-doc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ os.environ.update({

# Ensure we detect a browser for stable `nextstrain view` output.
"BROWSER": "/bin/true",

# Ensure HOST and PORT are stable for `nextstrain view` output.
"HOST": "127.0.0.1",
"PORT": "4000",
})

from nextstrain.cli import make_parser
Expand Down
27 changes: 2 additions & 25 deletions nextstrain/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from types import SimpleNamespace

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

# Maintain these manually for now while the list is very small. If we need
# to support pluggable commands or command discovery, we can switch to
# using the "entry points" system:
# https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
#
commands = [
build,
view,
deploy,
remote,
shell,
update,
setup,
check_setup,
login,
logout,
whoami,
version,
init_shell,
authorization,
debugger,
]

register_default_command(parser)
register_commands(parser, commands)
register_commands(parser, all_commands)
register_version_alias(parser)

return parser
Expand Down
2 changes: 1 addition & 1 deletion nextstrain/cli/aws/cognito/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def renew_tokens(self, *, refresh_token):
self.verify_tokens(
id_token = result.get("IdToken"),
access_token = result.get("AccessToken"),
refresh_token = refresh_token)
refresh_token = result.get("RefreshToken", refresh_token))


def verify_tokens(self, *, id_token, access_token, refresh_token):
Expand Down
67 changes: 67 additions & 0 deletions nextstrain/cli/browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Web browser interaction.

.. envvar:: BROWSER

A ``PATH``-like list of web browsers to try in preference order, before
falling back to a set of default browsers. May be program names, e.g.
``firefox``, or absolute paths to specific executables, e.g.
``/usr/bin/firefox``.

.. envvar:: NOBROWSER

If set to a truthy value (e.g. 1) then no web browser will be considered
available. This can be useful to prevent opening of a browser when there
are not other means of doing so.
"""
import webbrowser
from threading import Thread, ThreadError
from os import environ
from .util import warn


if environ.get("NOBROWSER"):
BROWSER = None
else:
# Avoid text-mode browsers
TERM = environ.pop("TERM", None)
try:
BROWSER = webbrowser.get()
except:
BROWSER = None
finally:
if TERM is not None:
environ["TERM"] = TERM


def open_browser(url: str, new_thread: bool = True):
"""
Opens *url* in a web browser.

Opens in a new tab, if possible, and raises the window to the top, if
possible.

Launches the browser from a separate thread by default so waiting on the
browser child process doesn't block the main (or calling) thread. Set
*new_thread* to False to launch from the same thread as the caller (e.g. if
you've already spawned a dedicated thread or process for the browser).
Note that some registered browsers launch in the background themselves, but
not all do, so this feature makes launch behaviour consistent across
browsers.

Prints a warning to stderr if a browser can't be found or can't be
launched, as automatically opening a browser is considered a
nice-but-not-necessary feature.
"""
if not BROWSER:
warn(f"Couldn't open <{url}> in browser: no browser found")
return

try:
if new_thread:
Thread(target = open_browser, args = (url, False), daemon = True).start()
else:
# new = 2 means new tab, if possible
BROWSER.open(url, new = 2, autoraise = True)
except (ThreadError, webbrowser.Error) as err:
warn(f"Couldn't open <{url}> in browser: {err!r}")
43 changes: 43 additions & 0 deletions nextstrain/cli/command/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from . import (
build,
view,
deploy,
remote,
shell,
update,
setup,
check_setup,
login,
logout,
whoami,
version,
init_shell,
authorization,
debugger,
)

# Maintain this list manually for now while its relatively static. If we need
# to support pluggable commands or command discovery, we can switch to using
# the "entry points" system:
# https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
#
# The order of this list is important and intentional: it determines the order
# in various user interfaces, e.g. `nextstrain --help`.
#
all_commands = [
build,
view,
deploy,
remote,
shell,
update,
setup,
check_setup,
login,
logout,
whoami,
version,
init_shell,
authorization,
debugger,
]
18 changes: 2 additions & 16 deletions nextstrain/cli/command/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
from multiprocessing import Process, ProcessError
import re
import requests
import webbrowser
from inspect import cleandoc
from os import environ
from pathlib import Path
Expand All @@ -57,6 +56,7 @@
from typing import Iterable, NamedTuple, Tuple, Union
from .. import runner
from ..argparse import add_extended_help_flags, SUPPRESS, SKIP_AUTO_DEFAULT_IN_HELP
from ..browser import BROWSER, open_browser as __open_browser
from ..runner import docker, ambient, conda, singularity
from ..util import colored, remove_suffix, warn
from ..volume import NamedVolume
Expand All @@ -67,16 +67,6 @@
PORT = environ.get("PORT") or "4000"


# Avoid text-mode browsers
TERM = environ.pop("TERM", None)
try:
BROWSER = webbrowser.get()
except:
BROWSER = None
finally:
if TERM is not None:
environ["TERM"] = TERM

OPEN_DEFAULT = bool(BROWSER)


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

try:
# new = 2 means new tab, if possible
BROWSER.open(url, new = 2, autoraise = True)
except webbrowser.Error as err:
warn(f"Couldn't open <{url}> in browser: {err!r}")
__open_browser(url, new_thread = False)
15 changes: 12 additions & 3 deletions nextstrain/cli/remote/nextstrain_dot_org.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,12 +464,14 @@ def delete(url: urllib.parse.ParseResult, recursively: bool = False, dry_run: bo
raise UserError(f"Path {path} does not seem to exist")

for resource in resources:
yield "nextstrain.org" + str(resource.path)
endpoint = api_endpoint(resource.path)

yield endpoint

if dry_run:
continue

response = http.delete(api_endpoint(resource.path))
response = http.delete(endpoint)

raise_for_status(response)

Expand Down Expand Up @@ -648,6 +650,10 @@ def __init__(self):
def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
if self.user and origin(request.url) == origin(NEXTSTRAIN_DOT_ORG):
request.headers["Authorization"] = self.user.http_authorization

# Used in error handling for more informative error messages
request._user = self.user # type: ignore

return request


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

elif status in {401, 403}:
user = current_user()
try:
user = response.request._user # type: ignore
except AttributeError:
user = None

if user:
challenge = authn_challenge(response) if status == 401 else None
Expand Down
5 changes: 1 addition & 4 deletions nextstrain/cli/remote/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
Nextstrain :term:`datasets <docs:dataset>` and :term:`narratives
<docs:narrative>` hosted on `Amazon S3 <https://aws.amazon.com/s3/>`_.
This functionality is primarily intended for use by the Nextstrain team and
operators of self-hosted :term:`docs:Auspice` instances. It is also used to
manage the contents of :doc:`Nextstrain Groups
<docs:learn/groups/index>` that have not migrated to using the
:doc:`/remotes/nextstrain.org`.
operators of self-hosted :term:`docs:Auspice` instances.


Remote paths
Expand Down