From ce5750ae935f3a24bbd0880cee5c8e2978010749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20=22decko=22=20de=20Brito?= Date: Thu, 22 Aug 2024 09:41:59 -0300 Subject: [PATCH] Changes of code and docs. --- CHANGES/926.feature | 3 +- docs/user/reference/_SUMMARY.md | 1 + docs/user/reference/authentication.md | 15 +++ pulp-glue/pulp_glue/common/authentication.py | 10 +- pulp-glue/pulp_glue/common/openapi.py | 96 ++++++--------- pulpcore/cli/common/generic.py | 119 +++++++------------ 6 files changed, 105 insertions(+), 139 deletions(-) create mode 100644 docs/user/reference/authentication.md diff --git a/CHANGES/926.feature b/CHANGES/926.feature index 3559bcff4..d902a9fb9 100644 --- a/CHANGES/926.feature +++ b/CHANGES/926.feature @@ -1 +1,2 @@ -Added support to OAuth2 flow as authentication method. +Added support to OAuth2 ClientCredentials grant flow as authentication method. +This is tech preview and may change without previous warning. diff --git a/docs/user/reference/_SUMMARY.md b/docs/user/reference/_SUMMARY.md index b24c468fb..be5eb2f6c 100644 --- a/docs/user/reference/_SUMMARY.md +++ b/docs/user/reference/_SUMMARY.md @@ -1,2 +1,3 @@ * [Using the CLI](using_the_cli.md) * [Supported Workflows](supported_workflows.md) +* [Authentication Methods](authentication.md) diff --git a/docs/user/reference/authentication.md b/docs/user/reference/authentication.md new file mode 100644 index 000000000..97cd2a2ee --- /dev/null +++ b/docs/user/reference/authentication.md @@ -0,0 +1,15 @@ +# Supported Authentication Methods + +Pulp-CLI support some authentication methods to authenticate against a Pulp instance. +Some very simple and common, like HTTP Basic Auth, and some more complex like OAuth2. + +## OAuth2 ClientCredentials grant + +!!! warning + This is an experimental feature. The support of it could change without any major warning. + +More on https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 + +Using this method the pulp-cli can request a token from an Identity Provider using a pair of +credentials (client_id/client_secret). The token is ten sent through using the `Authorization` header. +The issuer URL and the scope of token must be specified by the Pulp server through the OpenAPI scheme definition. diff --git a/pulp-glue/pulp_glue/common/authentication.py b/pulp-glue/pulp_glue/common/authentication.py index 2548fc35d..fcd175e7b 100644 --- a/pulp-glue/pulp_glue/common/authentication.py +++ b/pulp-glue/pulp_glue/common/authentication.py @@ -59,7 +59,11 @@ def handle401( response.close() prepared_new_request = response.request.copy() - prepared_new_request.headers["Authorization"] = f"Bearer {self.access_token}" + prepared_new_request.headers["Authorization"] = ( + f"Bearer { + self.access_token + }" + ) # Avoid to enter into an infinity loop. # Call to untyped function "deregister_hook" in typed context @@ -68,9 +72,7 @@ def handle401( ) # "Response" has no attribute "connection" - new_response: requests.Response = response.connection.send( # type: ignore[attr-defined] - prepared_new_request, **kwargs - ) + new_response: requests.Response = response.connection.send(prepared_new_request, **kwargs) new_response.history.append(response) new_response.request = prepared_new_request diff --git a/pulp-glue/pulp_glue/common/openapi.py b/pulp-glue/pulp_glue/common/openapi.py index c8e772a68..091e5a402 100644 --- a/pulp-glue/pulp_glue/common/openapi.py +++ b/pulp-glue/pulp_glue/common/openapi.py @@ -50,21 +50,24 @@ class UnsafeCallError(OpenAPIError): class OpenAPISecurityScheme: - def __init__(self, security_scheme: SecurityScheme): + def __init__(self, security_scheme: t.Dict[str, t.Any]): self.security_scheme = security_scheme self.security_type = self.security_scheme["type"] self.description = self.security_scheme.get("description", "") if self.security_type == "oauth2": - self.flows: OAuth2Flows = self.security_scheme["flows"] - client_credentials: t.Optional[ClientCredentials] = self.flows.get( - "clientCredentials") - if client_credentials: - self.flow_type: t.Optional[str] = "clientCredentials" - self.token_url: str = client_credentials["tokenUrl"] - self.scopes: OAuth2FlowsScopes = list( - client_credentials.get("scopes").keys()) + try: + self.flows: t.Dict[str, t.Any] = self.security_scheme["flows"] + client_credentials: t.Optional[t.Dict[str, t.Any]] = self.flows.get( + "clientCredentials" + ) + if client_credentials: + self.flow_type: str = "clientCredentials" + self.token_url: str = client_credentials["tokenUrl"] + self.scopes: t.List[str] = list(client_credentials["scopes"].keys()) + except KeyError: + raise OpenAPIValidationError if self.security_type == "http": self.scheme = self.security_scheme["scheme"] @@ -104,12 +107,12 @@ def __call__( authorized_schemes_types.add(security_schemes[name]["type"]) if "oauth2" in authorized_schemes_types: - oauth_flow = [ - flow for flow in authorized_schemes - if flow["type"] == "oauth2" - ][0] - if "clientCredentials" in oauth_flow.get("flows"): - result = self.oauth2_client_credentials_auth(oauth_flow) + for flow in authorized_schemes: + if flow["type"] == "oauth2": + oauth2_flow = OpenAPISecurityScheme(flow) + + if oauth2_flow.flow_type == "client_credentials": + result = self.oauth2_client_credentials_auth(oauth2_flow) if result: return result elif "http" in authorized_schemes_types: @@ -170,8 +173,7 @@ def __init__( if not validate_certs: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - self.debug_callback: t.Callable[[ - int, str], t.Any] = debug_callback or (lambda i, x: None) + self.debug_callback: t.Callable[[int, str], t.Any] = debug_callback or (lambda i, x: None) self.base_url: str = base_url self.doc_path: str = doc_path self.safe_calls_only: bool = safe_calls_only @@ -217,8 +219,7 @@ def load_api(self, refresh_cache: bool = False) -> None: apidoc_cache: str = os.path.join( os.path.expanduser(xdg_cache_home), "squeezer", - (self.base_url + "_" + self.doc_path).replace(":", - "_").replace("/", "_") + "api.json", + (self.base_url + "_" + self.doc_path).replace(":", "_").replace("/", "_") + "api.json", ) try: if refresh_cache: @@ -250,8 +251,7 @@ def _parse_api(self, data: bytes) -> None: def _download_api(self) -> bytes: try: - response: requests.Response = self._session.get( - urljoin(self.base_url, self.doc_path)) + response: requests.Response = self._session.get(urljoin(self.base_url, self.doc_path)) except requests.RequestException as e: raise OpenAPIError(str(e)) response.raise_for_status() @@ -290,8 +290,7 @@ def param_spec( } ) if required: - param_spec = {k: v for k, v in param_spec.items() - if v.get("required", False)} + param_spec = {k: v for k, v in param_spec.items() if v.get("required", False)} return param_spec def extract_params( @@ -379,8 +378,7 @@ def validate_schema(self, schema: t.Any, name: str, value: t.Any) -> t.Any: break else: raise OpenAPIValidationError( - _("No schema in anyOf validated for {name}.").format( - name=name) + _("No schema in anyOf validated for {name}.").format(name=name) ) elif oneOf: old_value = value @@ -390,14 +388,12 @@ def validate_schema(self, schema: t.Any, name: str, value: t.Any) -> t.Any: value = self.validate_schema(sub_schema, name, old_value) if found_valid: raise OpenAPIValidationError( - _("Multiple schemas in oneOf validated for {name}.").format( - name=name) + _("Multiple schemas in oneOf validated for {name}.").format(name=name) ) found_valid = True if not found_valid: raise OpenAPIValidationError( - _("No schema in oneOf validated for {name}.").format( - name=name) + _("No schema in oneOf validated for {name}.").format(name=name) ) elif not_schema: try: @@ -406,8 +402,7 @@ def validate_schema(self, schema: t.Any, name: str, value: t.Any) -> t.Any: pass else: raise OpenAPIValidationError( - _("Forbidden schema for {name} validated.").format( - name=name) + _("Forbidden schema for {name} validated.").format(name=name) ) elif schema_type is None: # Schema type is not specified. @@ -441,8 +436,7 @@ def validate_schema(self, schema: t.Any, name: str, value: t.Any) -> t.Any: # TODO: Add more types here. else: raise OpenAPIError( - _("Type `{schema_type}` is not implemented yet.").format( - schema_type=schema_type) + _("Type `{schema_type}` is not implemented yet.").format(schema_type=schema_type) ) return value @@ -456,8 +450,7 @@ def validate_object(self, schema: t.Any, name: str, value: t.Any) -> t.Dict[str, if properties or additional_properties is not None: value = value.copy() for property_name, property_value in value.items(): - property_schema = properties.get( - property_name, additional_properties) + property_schema = properties.get(property_name, additional_properties) if not property_schema: raise OpenAPIValidationError( _("Unexpected property '{property_name}' for '{name}' provided.").format( @@ -479,8 +472,7 @@ def validate_object(self, schema: t.Any, name: str, value: t.Any) -> t.Dict[str, def validate_array(self, schema: t.Any, name: str, value: t.Any) -> t.List[t.Any]: if not isinstance(value, t.List): - raise OpenAPIValidationError( - _("'{name}' is expected to be a list.").format(name=name)) + raise OpenAPIValidationError(_("'{name}' is expected to be a list.").format(name=name)) item_schema = schema["items"] return [self.validate_schema(item_schema, name, item) for item in value] @@ -565,8 +557,7 @@ def render_request_body( request_body_spec = method_spec["requestBody"] except KeyError: if body is not None: - raise OpenAPIError( - _("This operation does not expect a request body.")) + raise OpenAPIError(_("This operation does not expect a request body.")) return None, None, None, None else: body_required = request_body_spec.get("required", False) @@ -579,8 +570,7 @@ def render_request_body( content_type: t.Optional[str] = None data: t.Optional[t.Dict[str, t.Any]] = None json: t.Optional[t.Dict[str, t.Any]] = None - files: t.Optional[t.List[t.Tuple[str, - t.Tuple[str, UploadType, str]]]] = None + files: t.Optional[t.List[t.Tuple[str, t.Tuple[str, UploadType, str]]]] = None candidate_content_types = [ "multipart/form-data", @@ -632,8 +622,7 @@ def render_request_body( else: data[key] = value if uploads: - files = [(key, upload_data) - for key, upload_data in uploads.items()] + files = [(key, upload_data) for key, upload_data in uploads.items()] break else: # No known content-type left @@ -660,8 +649,7 @@ def render_request( validate_body: bool = True, ) -> requests.PreparedRequest: method_spec = path_spec[method] - content_type, data, json, files = self.render_request_body( - method_spec, body, validate_body) + content_type, data, json, files = self.render_request_body(method_spec, body, validate_body) security: t.List[t.Dict[str, t.List[str]]] = method_spec.get( "security", self.api_spec.get("security", {}) ) @@ -670,8 +658,7 @@ def render_request( # Bad idea, but you wanted it that way. auth = None else: - auth = self.auth_provider( - security, self.api_spec["components"]["securitySchemes"]) + auth = self.auth_provider(security, self.api_spec["components"]["securitySchemes"]) else: # No auth required? Don't provide it. # No auth_provider available? Hope for the best (should do the trick for cert auth). @@ -703,8 +690,7 @@ def parse_response(self, method_spec: t.Dict[str, t.Any], response: requests.Res except KeyError: # Fallback 201 -> 200 try: - response_spec = method_spec["responses"][str( - 100 * int(response.status_code / 100))] + response_spec = method_spec["responses"][str(100 * int(response.status_code / 100))] except KeyError: raise OpenAPIError( _("Unexpected response '{code}' (expected '{expected}').").format( @@ -749,17 +735,14 @@ def call( parameters = parameters.copy() if any(self.extract_params("cookie", path_spec, method_spec, parameters)): - raise NotImplementedError( - _("Cookie parameters are not implemented.")) + raise NotImplementedError(_("Cookie parameters are not implemented.")) - headers = self.extract_params( - "header", path_spec, method_spec, parameters) + headers = self.extract_params("header", path_spec, method_spec, parameters) for name, value in self.extract_params("path", path_spec, method_spec, parameters).items(): path = path.replace("{" + name + "}", value) - query_params = self.extract_params( - "query", path_spec, method_spec, parameters) + query_params = self.extract_params("query", path_spec, method_spec, parameters) if any(parameters): raise OpenAPIError( @@ -798,8 +781,7 @@ def call( except requests.RequestException as e: raise OpenAPIError(str(e)) self.debug_callback( - 1, _("Response: {status_code}").format( - status_code=response.status_code) + 1, _("Response: {status_code}").format(status_code=response.status_code) ) for key, value in response.headers.items(): self.debug_callback(2, f" {key}: {value}") diff --git a/pulpcore/cli/common/generic.py b/pulpcore/cli/common/generic.py index 893fb3847..7351f83e8 100644 --- a/pulpcore/cli/common/generic.py +++ b/pulpcore/cli/common/generic.py @@ -89,11 +89,9 @@ def _none_formatter(result: t.Any) -> str: def _json_formatter(result: t.Any) -> str: isatty = sys.stdout.isatty() - output = json.dumps(result, cls=PulpJSONEncoder, - indent=(2 if isatty else None)) + output = json.dumps(result, cls=PulpJSONEncoder, indent=(2 if isatty else None)) if PYGMENTS and isatty: - output = highlight(output, JsonLexer(), - Terminal256Formatter(style=PYGMENTS_STYLE)) + output = highlight(output, JsonLexer(), Terminal256Formatter(style=PYGMENTS_STYLE)) return output @@ -101,8 +99,7 @@ def _yaml_formatter(result: t.Any) -> str: isatty = sys.stdout.isatty() output = yaml.dump(result) if PYGMENTS and isatty: - output = highlight(output, YamlLexer(), - Terminal256Formatter(style=PYGMENTS_STYLE)) + output = highlight(output, YamlLexer(), Terminal256Formatter(style=PYGMENTS_STYLE)) return output @@ -163,8 +160,7 @@ def output_result(self, result: t.Any) -> None: formatter = REGISTERED_OUTPUT_FORMATTERS[self.format] except KeyError: raise NotImplementedError( - _("Format '{format}' not implemented.").format( - format=self.format) + _("Format '{format}' not implemented.").format(format=self.format) ) click.echo(formatter(result)) @@ -188,15 +184,13 @@ def response_hook(self, response: requests.Response, **kwargs: t.Any) -> request assert isinstance(self.pulp_ctx.password, str) with closing(secretstorage.dbus_init()) as connection: - collection = secretstorage.get_default_collection( - connection) + collection = secretstorage.get_default_collection(connection) collection.create_item( "Pulp CLI", self.attr, self.pulp_ctx.password.encode(), replace=True ) elif response.status_code == 401 and self.password_in_manager: with closing(secretstorage.dbus_init()) as connection: - collection = secretstorage.get_default_collection( - connection) + collection = secretstorage.get_default_collection(connection) item = next(collection.search_items(self.attr), None) if item: if click.confirm(_("Remove failed password from password manager?")): @@ -212,11 +206,9 @@ def __call__(self, request: requests.PreparedRequest) -> requests.PreparedReques self.pulp_ctx.password = item.get_secret().decode() self.password_in_manager = True else: - self.pulp_ctx.password = str( - click.prompt("Password", hide_input=True)) + self.pulp_ctx.password = str(click.prompt("Password", hide_input=True)) self.password_in_manager = False - # type: ignore [no-untyped-call] - request.register_hook("response", self.response_hook) + request.register_hook("response", self.response_hook) # type: ignore [no-untyped-call] return requests.auth.HTTPBasicAuth( # type: ignore [no-any-return] self.pulp_ctx.username, self.pulp_ctx.password, @@ -237,11 +229,12 @@ def basic_auth(self) -> t.Optional[t.Union[t.Tuple[str, str], requests.auth.Auth # but we want to get a grip on the response_hook. return SecretStorageBasicAuth(self.pulp_ctx) else: - self.pulp_ctx.password = click.prompt( - "Password", hide_input=True) + self.pulp_ctx.password = click.prompt("Password", hide_input=True) return (self.pulp_ctx.username, self.pulp_ctx.password) - def oauth2_client_credentials_auth(self, flow: t.Any) -> t.Optional[requests.auth.AuthBase]: + def oauth2_client_credentials_auth( + self, oauth2_flow: t.Any + ) -> t.Optional[requests.auth.AuthBase]: if self.pulp_ctx.username is None: self.pulp_ctx.username = click.prompt("Username/ClientID") if self.pulp_ctx.password is None: @@ -250,8 +243,8 @@ def oauth2_client_credentials_auth(self, flow: t.Any) -> t.Optional[requests.aut return OAuth2ClientCredentialsAuth( client_id=self.pulp_ctx.username, client_secret=self.pulp_ctx.password, - token_url=flow["flows"]["clientCredentials"]["tokenUrl"], - scopes=flow["flows"]["clientCredentials"]["scopes"].keys() + token_url=oauth2_flow.token_url, + scopes=oauth2_flow.scopes, ) @@ -271,8 +264,7 @@ def oauth2_client_credentials_auth(self, flow: t.Any) -> t.Optional[requests.aut """Decorator to make the nearest content context available to a command.""" pass_repository_context = click.make_pass_decorator(PulpRepositoryContext) """Decorator to make the nearest repository context available to a command.""" -pass_repository_version_context = click.make_pass_decorator( - PulpRepositoryVersionContext) +pass_repository_version_context = click.make_pass_decorator(PulpRepositoryVersionContext) """Decorator to make the nearest repository version context available to a command.""" @@ -348,8 +340,7 @@ def format_help_text( if self.help is not None: entity_ctx = ctx.find_object(PulpEntityContext) if entity_ctx is not None: - self.help = self.help.format( - entity=entity_ctx.ENTITY, entities=entity_ctx.ENTITIES) + self.help = self.help.format(entity=entity_ctx.ENTITY, entities=entity_ctx.ENTITIES) super().format_help_text(ctx, formatter) def get_params(self, ctx: click.Context) -> t.List[click.Parameter]: @@ -377,8 +368,7 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> t.Optional[click.Com and not isinstance(ctx.obj, cmd.allowed_with_contexts) ): raise IncompatibleContext( - _("The subcommand '{name}' is not available in this context.").format( - name=cmd.name) + _("The subcommand '{name}' is not available in this context.").format(name=cmd.name) ) return cmd @@ -464,8 +454,7 @@ def get_help_record(self, ctx: click.Context) -> t.Optional[t.Tuple[str, str]]: synopsis, help_text = tmp entity_ctx = ctx.find_object(PulpEntityContext) if entity_ctx is not None: - help_text = help_text.format( - entity=entity_ctx.ENTITY, entities=entity_ctx.ENTITIES) + help_text = help_text.format(entity=entity_ctx.ENTITY, entities=entity_ctx.ENTITIES) return synopsis, help_text @@ -496,8 +485,7 @@ def handle_parse_result( self.prompt = None value = opts.get(self.name) if self.callback is not None and num_options != 0: - value = self.callback( - ctx, self, {o: opts[o] for o in options_present}) + value = self.callback(ctx, self, {o: opts[o] for o in options_present}) if self.expose_value: ctx.params[self.name] = value return value, args @@ -572,8 +560,7 @@ def _load_file_or_string_wrapper( the_content = fp.read() except OSError: raise click.ClickException( - _("Failed to load content from {file}").format( - file=the_file) + _("Failed to load content from {file}").format(file=the_file) ) else: the_content = value @@ -620,12 +607,10 @@ def load_labels_callback( value = load_json_callback(ctx, param, value) if isinstance(value, dict) and all( - (isinstance(key, str) and isinstance(val, str) - for key, val in value.items()) + (isinstance(key, str) and isinstance(val, str) for key, val in value.items()) ): return value - raise click.ClickException( - _("Labels must be provided as a dictionary of strings.")) + raise click.ClickException(_("Labels must be provided as a dictionary of strings.")) def create_content_json_callback( @@ -667,8 +652,7 @@ def parse_size_callback(ctx: click.Context, param: click.Parameter, value: str) size = value.strip().upper() match = re.match(r"^([0-9]+)\s*([KMGT]?B)?$", size) if not match: - raise click.ClickException( - "Please pass in a valid size of form: [0-9] [K/M/G/T]B") + raise click.ClickException("Please pass in a valid size of form: [0-9] [K/M/G/T]B") number, unit = match.groups(default="B") return int(float(number) * units[unit]) @@ -771,8 +755,7 @@ def resource_option(*args: t.Any, **kwargs: t.Any) -> t.Callable[[FC], FC]: default_plugin: t.Optional[str] = kwargs.pop("default_plugin", None) default_type: t.Optional[str] = kwargs.pop("default_type", None) lookup_key: str = kwargs.pop("lookup_key", "name") - context_table: t.Dict[str, t.Type[PulpEntityContext] - ] = kwargs.pop("context_table") + context_table: t.Dict[str, t.Type[PulpEntityContext]] = kwargs.pop("context_table") capabilities: t.Optional[t.List[str]] = kwargs.pop("capabilities", None) href_pattern: t.Optional[str] = kwargs.pop("href_pattern", None) parent_resource_lookup: t.Optional[t.Tuple[str, t.Type[PulpEntityContext]]] = kwargs.pop( @@ -849,8 +832,7 @@ def _option_callback( "is not valid for the {option_name} option." ).format(plugin=plugin, resource_type=resource_type, option_name=param.name) ) - entity_ctx: PulpEntityContext = context_class( - pulp_ctx, pulp_href=pulp_href, entity=entity) + entity_ctx: PulpEntityContext = context_class(pulp_ctx, pulp_href=pulp_href, entity=entity) if capabilities is not None: for capability in capabilities: @@ -890,22 +872,18 @@ def _multi_option_callback( "{plugin_default}{type_default}{multiple_note}" ).format( plugin_form=_("[:]") if default_plugin else _(":"), - type_form=_("[:]") if default_type else _( - ":"), + type_form=_("[:]") if default_type else _(":"), plugin_default=( - _("'' defaults to {plugin}. ").format( - plugin=default_plugin) + _("'' defaults to {plugin}. ").format(plugin=default_plugin) if default_plugin else "" ), type_default=( - _("'' defaults to {type}. ").format( - type=default_type) + _("'' defaults to {type}. ").format(type=default_type) if default_type else "" ), - multiple_note=(_("Can be specified multiple times.") - if kwargs.get("multiple") else ""), + multiple_note=(_("Can be specified multiple times.") if kwargs.get("multiple") else ""), ) return click.option(*args, **kwargs) @@ -939,8 +917,7 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: t.Optional if not args: args = ("-t", "--type", "entity_type") kwargs["callback"] = _type_callback - kwargs["type"] = click.types.Choice( - type_names, case_sensitive=case_sensitive) + kwargs["type"] = click.types.Choice(type_names, case_sensitive=case_sensitive) return click.option(*args, **kwargs) @@ -1009,8 +986,7 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: t.Optional name_icontains_option = pulp_option( "--name-icontains", "name__icontains", - help=_( - "Filter {entity} results where name contains value, case insensitive"), + help=_("Filter {entity} results where name contains value, case insensitive"), ) name_in_option = pulp_option( @@ -1171,8 +1147,7 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: t.Optional content_in_option, pulp_created_gte_option, pulp_created_lte_option, - pulp_option("--repository-version", - help=_("Search {entities} by repository version HREF")), + pulp_option("--repository-version", help=_("Search {entities} by repository version HREF")), ] @@ -1212,8 +1187,7 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: t.Optional "The password to authenticate to the proxy (can contain leading and trailing spaces)." ), ), - click.option("--rate-limit", type=int, - help=_("limit download rate in requests per second")), + click.option("--rate-limit", type=int, help=_("limit download rate in requests per second")), click.option("--sock-connect-timeout", type=float), click.option("--sock-read-timeout", type=float), click.option("--tls-validation", type=bool), @@ -1289,8 +1263,7 @@ def list_command(**kwargs: t.Any) -> click.Command: """A factory that creates a list command.""" kwargs.setdefault("name", "list") - kwargs.setdefault( - "help", _("Show the list of optionally filtered {entities}.")) + kwargs.setdefault("help", _("Show the list of optionally filtered {entities}.")) decorators = kwargs.pop("decorators", []) # This is a mypy bug getting confused with positional args @@ -1449,14 +1422,11 @@ def callback( ) -> None: ctx.obj = repository_ctx.get_version_context() - callback.add_command(list_command( - decorators=decorators + [content_in_option])) + callback.add_command(list_command(decorators=decorators + [content_in_option])) if not list_only: - callback.add_command(show_command( - decorators=decorators + [version_option])) - callback.add_command(destroy_command( - decorators=decorators + [version_option])) + callback.add_command(show_command(decorators=decorators + [version_option])) + callback.add_command(destroy_command(decorators=decorators + [version_option])) @callback.command() @repository_lookup_option @@ -1624,10 +1594,8 @@ def content_list( ) -> None: parameters = {k: v for k, v in params.items() if v is not None} parameters.update({"repository_version": version.pulp_href}) - content_ctx = PulpContentContext( - pulp_ctx) if all_types else content_ctx - result = content_ctx.list( - limit=limit, offset=offset, parameters=parameters) + content_ctx = PulpContentContext(pulp_ctx) if all_types else content_ctx + result = content_ctx.list(limit=limit, offset=offset, parameters=parameters) pulp_ctx.output_result(result) @pulp_command("add") @@ -1639,8 +1607,7 @@ def content_add( base_version: PulpRepositoryVersionContext, ) -> None: repo_ctx = base_version.repository_ctx - repo_ctx.modify(add_content=[content_ctx.pulp_href], - base_version=base_version.pulp_href) + repo_ctx.modify(add_content=[content_ctx.pulp_href], base_version=base_version.pulp_href) @pulp_command("remove") @click.option("--all", is_flag=True, help=_("Remove all content from repository version")) @@ -1654,8 +1621,7 @@ def content_remove( ) -> None: repo_ctx = base_version.repository_ctx remove_content = ["*" if all else content_ctx.pulp_href] - repo_ctx.modify(remove_content=remove_content, - base_version=base_version.pulp_href) + repo_ctx.modify(remove_content=remove_content, base_version=base_version.pulp_href) @pulp_command("modify") @repository_lookup_option @@ -1668,8 +1634,7 @@ def content_modify( repo_ctx = base_version.repository_ctx ac = [unit.pulp_href for unit in add_content] if add_content else None rc = [unit.pulp_href for unit in remove_content] if remove_content else None - repo_ctx.modify(add_content=ac, remove_content=rc, - base_version=base_version.pulp_href) + repo_ctx.modify(add_content=ac, remove_content=rc, base_version=base_version.pulp_href) command_decorators: t.Dict[click.Command, t.Optional[t.List[t.Callable[[FC], FC]]]] = { content_list: kwargs.pop("list_decorators", []),