Skip to content

Commit

Permalink
Merge branch 'dev' into release/1.13.0
Browse files Browse the repository at this point in the history
# Conflicts:
#	binstar_client/plugins.py
#	tests/test_cli.py
  • Loading branch information
mattkram committed Sep 30, 2024
2 parents b3c43b2 + d734167 commit fd3ee21
Show file tree
Hide file tree
Showing 29 changed files with 2,116 additions and 286 deletions.
10 changes: 6 additions & 4 deletions .github/workflows/check-master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
- name: Export reports
if: ${{ always() && steps.conda_environment_information.outputs.exit_status == 'success' }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: report-lint-${{ matrix.python-version }}-${{ matrix.os }}
path: .artifacts/reports
Expand All @@ -101,10 +101,10 @@ jobs:
# unfortunately, as of June 2021 - GitHub doesn't support anchors for action scripts

- name: Checkout project
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Setup Miniconda
uses: conda-incubator/setup-miniconda@v2
uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}
Expand All @@ -127,14 +127,16 @@ jobs:
conda list --show-channel-urls
- name: Run tests
env:
_TYPER_FORCE_DISABLE_TERMINAL: 1
run: |
mkdir -p .artifacts/reports
python scripts/refresh_coveragerc.py
python -X utf8 -m pytest tests/ --cov-report html:.artifacts/reports/coverage --html=.artifacts/reports/pytest.html --self-contained-html
- name: Export reports
if: ${{ always() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: report-test-${{ matrix.python-version }}-${{ matrix.os }}
path: .artifacts/reports
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pip-log.txt
# Unit test / coverage reports
.coveragerc
.coverage
.coverage.*
.tox
nosetests.xml

Expand All @@ -48,3 +49,6 @@ __conda_*__.txt

# Additional files
/.artifacts/

# Local dev environment
env/
154 changes: 154 additions & 0 deletions binstar_client/commands/authorizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
import socket
import sys
import typing
from enum import Enum

import pytz
import typer
from dateutil.parser import parse as parse_date

from binstar_client import errors
Expand Down Expand Up @@ -271,3 +273,155 @@ def add_parser(subparsers):
action='store_true', help='Show information about the current authentication token')

parser.set_defaults(main=main)


def _exclusive_action(ctx: typer.Context, param: typer.CallbackParam, value: str) -> str:
"""Check for exclusivity of action options.
To do this, we attach a new special attribute onto the typer Context the first time
one of the options in the group is used.
"""
# pylint: disable=protected-access,invalid-name
if getattr(ctx, '_actions', None) is None:
ctx._actions = set() # type: ignore
if value:
if ctx._actions: # type: ignore
used_action, = ctx._actions # type: ignore
raise typer.BadParameter(f'mutually exclusive with {used_action}')
ctx._actions.add(' / '.join(f'\'{o}\'' for o in param.opts)) # type: ignore
return value


class TokenStrength(Enum):
"""Available options for strength when creating a token."""
STRONG = 'strong'
WEAK = 'weak'


def mount_subcommand(app: typer.Typer, name: str, hidden: bool, help_text: str, context_settings: dict) -> None:
@app.command(
name=name,
hidden=hidden,
help=help_text,
context_settings=context_settings,
# no_args_is_help=True,
)
def auth_subcommand(
ctx: typer.Context,
name_: str = typer.Option(
...,
'-n',
'--name',
default_factory=lambda: f'binstar_token:{socket.gethostname()}',
help='A unique name so you can identify this token later. View your tokens at anaconda.org/settings/access'
),
organization: typing.Optional[str] = typer.Option(
None,
'-o',
'--org',
'--organization',
help='Set the token owner (must be an organization)',
),
strength: TokenStrength = typer.Option(
default='strong',
help='Specify the strength of the token',
),
strong: typing.Optional[bool] = typer.Option(
None,
help='Create a longer token (default)',
),
weak: typing.Optional[bool] = typer.Option(
None,
'-w',
'--weak',
help='Create a shorter token',
),
url: str = typer.Option(
'http://anaconda.org',
help='The url of the application that will use this token',
),
max_age: typing.Optional[int] = typer.Option(
None,
help='The maximum age in seconds that this token will be valid for',
),
scopes: typing.Optional[typing.List[str]] = typer.Option(
[],
'-s',
'--scopes',
help=(
'Scopes for token. ' +
'For example if you want to limit this token to conda downloads only you would use ' +
'--scopes "repo conda:download". You can also provide multiple scopes by providing ' +
'this option multiple times, e.g. --scopes repo --scopes conda:download.'
),
),
out: typing.Optional[typer.FileTextWrite] = typer.Option(
sys.stdout,
),
list_scopes: typing.Optional[bool] = typer.Option(
False,
'-x',
'--list-scopes',
help='List all authentication scopes',
callback=_exclusive_action,
),
list_: typing.Optional[bool] = typer.Option(
False,
'-l',
'--list',
help='List all user authentication tokens',
callback=_exclusive_action,
),
create: typing.Optional[bool] = typer.Option(
False,
'-c',
'--create',
help='Create an authentication token',
callback=_exclusive_action,
),
info: typing.Optional[bool] = typer.Option(
False,
'-i',
'--info',
'--current-info',
help='Show information about the current authentication token',
callback=_exclusive_action,
),
remove: typing.List[str] = typer.Option(
[],
'-r',
'--remove',
help='Remove authentication tokens. Multiple token names can be provided',
callback=_exclusive_action,
),
) -> None:
# pylint: disable=too-many-arguments,too-many-locals,fixme
if not any([list_scopes, list_, create, info, remove]):
raise typer.BadParameter('one of --list-scopes, --list, --list, --info, or --remove must be provided')

if weak:
strength = TokenStrength.WEAK
if strong:
# Strong overrides everything
strength = TokenStrength.STRONG

args = argparse.Namespace(
token=ctx.obj.params.get('token'),
site=ctx.obj.params.get('site'),
name=name_,
organization=organization,
strength=strength.value,
list_scopes=list_scopes,
list=list_,
create=create,
info=info,
# TODO: The default of None here is to match existing argparse behavior
remove=remove or None,
url=url,
max_age=max_age,
scopes=scopes,
out=out,
)

main(args=args)
112 changes: 112 additions & 0 deletions binstar_client/commands/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import argparse
import functools
import logging
from typing import List, Optional, Tuple, Union

import typer

from binstar_client.utils import get_server_api

Expand Down Expand Up @@ -103,3 +106,112 @@ def _add_parser(subparsers, name, deprecated=False):
def add_parser(subparsers):
_add_parser(subparsers, name='label')
_add_parser(subparsers, name='channel', deprecated=True)


def _parse_optional_tuple(value: Optional[Tuple[str, str]]) -> Optional[List[str]]:
# pylint: disable=fixme
# Convert a sentinel tuple of empty strings to None, since it is not possible with typer parser or callback
if value == ('', '') or value is None:
return None
# TODO: We only return a list because argparse does. Should really be a tuple.
return list(value)


def _exclusive_action(
ctx: typer.Context,
param: typer.CallbackParam,
value: Union[str, Tuple[str, str]],
) -> Union[str, Tuple[str, str]]:
"""Check for exclusivity of action options.
To do this, we attach a new special attribute onto the typer Context the first time
one of the options in the group is used.
"""
# pylint: disable=protected-access,invalid-name
parsed_value: Union[List[str], str, None]
if isinstance(value, tuple):
# This is here so we can treat the empty tuple as falsy, but I don't like it
parsed_value = _parse_optional_tuple(value)
else:
parsed_value = value

if getattr(ctx, '_actions', None) is None:
ctx._actions = set() # type: ignore
if parsed_value:
if ctx._actions: # type: ignore
used_action, = ctx._actions # type: ignore
raise typer.BadParameter(f'mutually exclusive with {used_action}')
ctx._actions.add(' / '.join(f'\'{o}\'' for o in param.opts)) # type: ignore
return value


def mount_subcommand(app: typer.Typer, name: str, hidden: bool, help_text: str, context_settings: dict) -> None:

@app.command(
name=name,
hidden=hidden,
help=help_text,
context_settings=context_settings,
# no_args_is_help=True,
)
def channel(
ctx: typer.Context,
organization: Optional[str] = typer.Option(
None,
'-o',
'--organization',
help='Manage an organizations {}s'.format(name),
),
copy: Optional[Tuple[str, str]] = typer.Option(
('', ''),
help=f'Copy a package from one {name} to another',
show_default=False,
callback=_exclusive_action,
),
list_: Optional[bool] = typer.Option(
False,
'--list',
is_flag=True,
help=f'List all {name}s for a user',
callback=_exclusive_action,
),
show: Optional[str] = typer.Option(
None,
help=f'Show all of the files in a {name}',
callback=_exclusive_action,
),
lock: Optional[str] = typer.Option(
None,
help=f'Lock a {name}',
callback=_exclusive_action,
),
unlock: Optional[str] = typer.Option(
None,
help=f'Unlock a {name}',
callback=_exclusive_action,
),
remove: Optional[str] = typer.Option(
None,
help=f'Remove a {name}',
callback=_exclusive_action,
),
) -> None:
# pylint: disable=too-many-arguments
parsed_copy = _parse_optional_tuple(copy)
if not any([parsed_copy, list_, show, lock, unlock, remove]):
raise typer.BadParameter('one of --copy, --list, --show, --lock, --unlock, or --remove must be provided')

args = argparse.Namespace(
token=ctx.obj.params.get('token'),
site=ctx.obj.params.get('site'),
organization=organization,
copy=parsed_copy,
list=list_,
show=show,
lock=lock,
unlock=unlock,
remove=remove,
)

main(args=args, name='channel', deprecated=True)
Loading

0 comments on commit fd3ee21

Please sign in to comment.