From af23c4e7c10a21c674dc062f7b30cf8cfb8bede5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20=22decko=22=20de=20Brito?= Date: Tue, 20 Aug 2024 18:43:55 -0300 Subject: [PATCH] Revert to use the json payload instead of the SecurityScheme class --- pulp-glue/pulp_glue/common/openapi.py | 82 +++++++++++------ pulpcore/cli/common/generic.py | 123 +++++++++++++++++--------- 2 files changed, 135 insertions(+), 70 deletions(-) diff --git a/pulp-glue/pulp_glue/common/openapi.py b/pulp-glue/pulp_glue/common/openapi.py index 0030e4f80..c8e772a68 100644 --- a/pulp-glue/pulp_glue/common/openapi.py +++ b/pulp-glue/pulp_glue/common/openapi.py @@ -58,11 +58,13 @@ def __init__(self, security_scheme: SecurityScheme): if self.security_type == "oauth2": self.flows: OAuth2Flows = self.security_scheme["flows"] - client_credentials: t.Optional[ClientCredentials] = self.flows.get("clientCredentials") + 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()) + self.scopes: OAuth2FlowsScopes = list( + client_credentials.get("scopes").keys()) if self.security_type == "http": self.scheme = self.security_scheme["scheme"] @@ -81,7 +83,9 @@ def basic_auth(self) -> t.Optional[t.Union[t.Tuple[str, str], requests.auth.Auth """Implement this to provide means of http basic auth.""" return None - def oauth2_client_credentials_auth(self, flow: t.Any) -> t.Optional[t.Union[t.Tuple[str, str], requests.auth.AuthBase]]: + def oauth2_client_credentials_auth( + self, flow: t.Any + ) -> t.Optional[t.Union[t.Tuple[str, str], requests.auth.AuthBase]]: """Implement this to provide other authentication methods.""" return None @@ -100,10 +104,11 @@ def __call__( authorized_schemes_types.add(security_schemes[name]["type"]) if "oauth2" in authorized_schemes_types: - oauth_flow = OpenAPISecurityScheme( - [flow for flow in authorized_schemes if flow["type"] == "oauth2"][0] - ) - if oauth_flow.flow_type == "clientCredentials": + 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) if result: return result @@ -165,7 +170,8 @@ 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 @@ -211,7 +217,8 @@ 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: @@ -243,7 +250,8 @@ 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() @@ -282,7 +290,8 @@ 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( @@ -370,7 +379,8 @@ 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 @@ -380,12 +390,14 @@ 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: @@ -394,7 +406,8 @@ 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. @@ -428,7 +441,8 @@ 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 @@ -442,7 +456,8 @@ 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( @@ -464,7 +479,8 @@ 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] @@ -549,7 +565,8 @@ 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) @@ -562,7 +579,8 @@ 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", @@ -614,7 +632,8 @@ 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 @@ -641,7 +660,8 @@ 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", {}) ) @@ -650,7 +670,8 @@ 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). @@ -682,7 +703,8 @@ 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( @@ -727,14 +749,17 @@ 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( @@ -773,7 +798,8 @@ 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 be870f473..893fb3847 100644 --- a/pulpcore/cli/common/generic.py +++ b/pulpcore/cli/common/generic.py @@ -89,9 +89,11 @@ 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 @@ -99,7 +101,8 @@ 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 @@ -160,7 +163,8 @@ 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)) @@ -184,13 +188,15 @@ 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?")): @@ -206,9 +212,11 @@ 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 - request.register_hook("response", self.response_hook) # type: ignore [no-untyped-call] + # type: ignore [no-untyped-call] + request.register_hook("response", self.response_hook) return requests.auth.HTTPBasicAuth( # type: ignore [no-any-return] self.pulp_ctx.username, self.pulp_ctx.password, @@ -229,10 +237,11 @@ 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 auth(self, flow: t.Any) -> t.Optional[requests.auth.AuthBase]: + def oauth2_client_credentials_auth(self, 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: @@ -241,8 +250,8 @@ def auth(self, flow: t.Any) -> t.Optional[requests.auth.AuthBase]: return OAuth2ClientCredentialsAuth( client_id=self.pulp_ctx.username, client_secret=self.pulp_ctx.password, - token_url=flow.token_url, - scopes=flow.scopes, + token_url=flow["flows"]["clientCredentials"]["tokenUrl"], + scopes=flow["flows"]["clientCredentials"]["scopes"].keys() ) @@ -262,7 +271,8 @@ def auth(self, flow: t.Any) -> t.Optional[requests.auth.AuthBase]: """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.""" @@ -338,7 +348,8 @@ 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]: @@ -366,7 +377,8 @@ 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 @@ -452,7 +464,8 @@ 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 @@ -483,7 +496,8 @@ 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 @@ -558,7 +572,8 @@ 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 @@ -605,10 +620,12 @@ 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( @@ -650,7 +667,8 @@ 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]) @@ -716,7 +734,8 @@ def _option_callback( # The HREF of a resource was passed href_pattern = entity_ctx.HREF_PATTERN if pulp_ctx.domain_enabled: - pattern = rf"^{pulp_ctx._api_root}{domain_pattern}/api/v3/{href_pattern}" + pattern = rf"^{pulp_ctx._api_root}{ + domain_pattern}/api/v3/{href_pattern}" else: pattern = rf"^{pulp_ctx.api_path}{href_pattern}" match = re.match(pattern, value) @@ -752,7 +771,8 @@ 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( @@ -781,7 +801,8 @@ def _option_callback( ) ) if pulp_ctx.domain_enabled: - pattern = rf"^{pulp_ctx._api_root}{domain_pattern}/api/v3/{href_pattern}" + pattern = rf"^{pulp_ctx._api_root}{ + domain_pattern}/api/v3/{href_pattern}" else: pattern = rf"^{pulp_ctx.api_path}{href_pattern}" match = re.match(pattern, value) @@ -828,7 +849,8 @@ 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: @@ -868,18 +890,22 @@ 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) @@ -913,7 +939,8 @@ 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) @@ -982,7 +1009,8 @@ 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( @@ -1143,7 +1171,8 @@ 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")), ] @@ -1183,7 +1212,8 @@ 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), @@ -1259,7 +1289,8 @@ 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 @@ -1418,11 +1449,14 @@ 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 @@ -1590,8 +1624,10 @@ 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") @@ -1603,7 +1639,8 @@ 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")) @@ -1617,7 +1654,8 @@ 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 @@ -1630,7 +1668,8 @@ 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", []),