diff --git a/Dockerfile b/Dockerfile index 0397591fe..cc9374f17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim AS builder +FROM python:3.13-slim AS builder ARG linode_cli_version @@ -15,11 +15,11 @@ RUN make requirements RUN LINODE_CLI_VERSION=$linode_cli_version GITHUB_TOKEN=$github_token make build -FROM python:3.11-slim +FROM python:3.13-slim COPY --from=builder /src/dist /dist -RUN pip3 install /dist/*.whl boto3 +RUN pip3 install --no-cache-dir /dist/*.whl boto3 RUN useradd -ms /bin/bash cli USER cli:cli diff --git a/linodecli/baked/request.py b/linodecli/baked/request.py index 4023d7434..e429895ac 100644 --- a/linodecli/baked/request.py +++ b/linodecli/baked/request.py @@ -2,9 +2,13 @@ Request details for a CLI Operation """ +from typing import List, Optional + +from openapi3.paths import MediaType from openapi3.schemas import Schema from linodecli.baked.parsing import simplify_description +from linodecli.baked.response import OpenAPIResponse from linodecli.baked.util import _aggregate_schema_properties @@ -13,16 +17,16 @@ class OpenAPIRequestArg: A single argument to a request as defined by a Schema in the OpenAPI spec """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, - name, - schema, - required, - prefix=None, - is_parent=False, - parent=None, - depth=0, - ): # pylint: disable=too-many-arguments + name: str, + schema: Schema, + required: bool, + prefix: Optional[str] = None, + is_parent: bool = False, + parent: Optional[str] = None, + depth: int = 0, + ) -> None: """ Parses a single Schema node into a argument the CLI can use when making requests. @@ -120,9 +124,14 @@ def __init__( ) -def _parse_request_model(schema, prefix=None, parent=None, depth=0): +def _parse_request_model( + schema: Schema, + prefix: Optional[str] = None, + parent: Optional[str] = None, + depth: int = 0, +) -> List[OpenAPIRequestArg]: """ - Parses a schema into a list of OpenAPIRequest objects + Parses an OpenAPI schema into a list of OpenAPIRequest objects :param schema: The schema to parse as a request model :type schema: openapi3.Schema :param prefix: The prefix to add to all keys in this schema, as a json path @@ -143,6 +152,7 @@ def _parse_request_model(schema, prefix=None, parent=None, depth=0): return args for k, v in properties.items(): + # Handle nested objects which aren't read-only and have properties if ( v.type == "object" and not v.readOnly @@ -159,6 +169,8 @@ def _parse_request_model(schema, prefix=None, parent=None, depth=0): # parent arguments. depth=depth, ) + + # Handle arrays of objects that not marked as JSON elif ( v.type == "array" and v.items @@ -209,7 +221,7 @@ class OpenAPIRequest: on the MediaType object of a requestBody portion of an OpenAPI Operation """ - def __init__(self, request): + def __init__(self, request: MediaType) -> None: """ :param request: The request's MediaType object in the OpenAPI spec, corresponding to the application/json data the endpoint @@ -256,7 +268,7 @@ class OpenAPIFilteringRequest: endpoints where filters are accepted. """ - def __init__(self, response_model): + def __init__(self, response_model: OpenAPIResponse) -> None: """ :param response_model: The parsed response model whose properties may be filterable. diff --git a/linodecli/baked/response.py b/linodecli/baked/response.py index 9d08a7a46..9e64d0be2 100644 --- a/linodecli/baked/response.py +++ b/linodecli/baked/response.py @@ -2,7 +2,10 @@ Converting the processed OpenAPI Responses into something the CLI can work with """ +from typing import Optional + from openapi3.paths import MediaType +from openapi3.schemas import Schema from linodecli.baked.util import _aggregate_schema_properties @@ -30,7 +33,13 @@ class OpenAPIResponseAttr: from it. """ - def __init__(self, name, schema, prefix=None, nested_list_depth=0): + def __init__( + self, + name: str, + schema: Schema, + prefix: Optional[str] = None, + nested_list_depth: int = 0, + ) -> None: """ :param name: The key that held this schema in the properties list, representing its name in a response. @@ -84,10 +93,13 @@ def __init__(self, name, schema, prefix=None, nested_list_depth=0): self.item_type = schema.items.type @property - def path(self): + def path(self) -> str: """ This is a helper for filterable fields to return the json path to this element in a response. + + :returns: The json path to the element in a response. + :rtype: str """ return self.name @@ -129,6 +141,7 @@ def render_value(self, model, colorize=True): value = str(value) color = self.color_map.get(value) or self.color_map["default_"] value = f"[{color}]{value}[/]" + # Convert None value to an empty string for better display if value is None: # Prints the word None if you don't change it value = "" @@ -194,12 +207,14 @@ def _parse_response_model(schema, prefix=None, nested_list_depth=0): elif v.type == "object": attrs += _parse_response_model(v, prefix=pref) elif v.type == "array" and v.items.type == "object": + # Parse arrays for objects recursively and increase the nesting depth attrs += _parse_response_model( v.items, prefix=pref, nested_list_depth=nested_list_depth + 1, ) else: + # Handle any other simple types attrs.append( OpenAPIResponseAttr( k, v, prefix=prefix, nested_list_depth=nested_list_depth @@ -215,7 +230,7 @@ class OpenAPIResponse: responses section of an OpenAPI Operation """ - def __init__(self, response: MediaType): + def __init__(self, response: MediaType) -> None: """ :param response: The response's MediaType object in the OpenAPI spec, corresponding to the application/json response type @@ -287,15 +302,22 @@ def _fix_nested_list(self, json): nested_lists = [c.strip() for c in self.nested_list.split(",")] result = [] + for nested_list in nested_lists: path_parts = nested_list.split(".") + if not isinstance(json, list): json = [json] + for cur in json: + # Get the nested list using the path nlist_path = cur for p in path_parts: nlist_path = nlist_path.get(p) nlist = nlist_path + + # For each item in the nested list, + # combine the parent properties with the nested item for item in nlist: cobj = {k: v for k, v in cur.items() if k != path_parts[0]} cobj["_split"] = path_parts[-1] diff --git a/linodecli/configuration/config.py b/linodecli/configuration/config.py index de2dca86f..2532ed13a 100644 --- a/linodecli/configuration/config.py +++ b/linodecli/configuration/config.py @@ -82,10 +82,7 @@ def default_username(self) -> str: :returns: The `default-user` username or an empty string. :rtype: str """ - if self.config.has_option("DEFAULT", "default-user"): - return self.config.get("DEFAULT", "default-user") - - return "" + return self.config.get("DEFAULT", "default-user", fallback="") def set_user(self, username: str): """ @@ -153,15 +150,11 @@ def get_token(self) -> str: :rtype: str """ if self.used_env_token: - return os.environ.get(ENV_TOKEN_NAME, None) + return os.getenv(ENV_TOKEN_NAME, None) - if self.config.has_option( - self.username or self.default_username(), "token" - ): - return self.config.get( - self.username or self.default_username(), "token" - ) - return "" + return self.config.get( + self.username or self.default_username(), "token", fallback="" + ) def get_value(self, key: str) -> Optional[Any]: """ @@ -180,12 +173,9 @@ def get_value(self, key: str) -> Optional[Any]: current user. :rtype: any """ - username = self.username or self.default_username() - - if not self.config.has_option(username, key): - return None - - return self.config.get(username, key) + return self.config.get( + self.username or self.default_username(), key, fallback=None + ) def get_bool(self, key: str) -> bool: """ @@ -204,12 +194,10 @@ def get_bool(self, key: str) -> bool: current user. :rtype: any """ - username = self.username or self.default_username() - - if not self.config.has_option(username, key): - return False - return self.config.getboolean(username, key) + return self.config.getboolean( + self.username or self.default_username(), key, fallback=False + ) # plugin methods - these are intended for plugins to utilize to store their # own persistent config information @@ -255,13 +243,10 @@ def plugin_get_value(self, key: str) -> Optional[Any]: "No running plugin to retrieve configuration for!" ) - username = self.username or self.default_username() + username = self.username or self.default_username() or "DEFAULT" full_key = f"plugin-{self.running_plugin}-{key}" - if not self.config.has_option(username, full_key): - return None - - return self.config.get(username, full_key) + return self.config.get(username, full_key, fallback=None) # TODO: this is more of an argparsing function than it is a config function # might be better to move this to argparsing during refactor and just have @@ -308,11 +293,8 @@ def update( # these don't get included in the updated namespace if key.startswith("plugin-"): continue - value = None - if self.config.has_option(username, key): - value = self.config.get(username, key) - else: - value = ns_dict[key] + + value = self.config.get(username, key, fallback=ns_dict.get(key)) if not value: continue @@ -553,7 +535,7 @@ def _handle_no_default_user(self): # pylint: disable=too-many-branches if len(users) == 0: # config is new or _really_ old - token = self.config.get("DEFAULT", "token") + token = self.config.get("DEFAULT", "token", fallback=None) if token is not None: # there's a token in the config - configure that user diff --git a/linodecli/configuration/helpers.py b/linodecli/configuration/helpers.py index cb0d5e9ea..a0398e2b3 100644 --- a/linodecli/configuration/helpers.py +++ b/linodecli/configuration/helpers.py @@ -299,6 +299,4 @@ def _config_get_with_default( :returns: The value pulled from the config or the default value. :rtype: Any """ - return ( - config.get(user, field) if config.has_option(user, field) else default - ) + return config.get(user, field, fallback=default) diff --git a/wiki/Output.md b/wiki/Output.md index 52db17fc7..b83cbf776 100644 --- a/wiki/Output.md +++ b/wiki/Output.md @@ -5,12 +5,12 @@ By default, the CLI displays on some pre-selected fields for a given type of response. If you want to see everything, just ask:: ```bash -linode-cli linodes list --all +linode-cli linodes list --all-columns ``` -Using `--all` will cause the CLI to display all returned columns of output. -Note that this will probably be hard to read on normal-sized screens for most -actions. +Using `--all-columns` will cause the CLI to display all returned columns of +output. Note that this will probably be hard to read on normal-sized screens +for most actions. If you want even finer control over your output, you can request specific columns be displayed:: @@ -48,11 +48,12 @@ linode-cli linodes list --no-headers --text To get JSON output from the CLI, simple request it:: ```bash -linode-cli linodes list --json --all +linode-cli linodes list --json --all-columns ``` -While the `--all` is optional, you probably want to see all output fields in -your JSON output. If you want your JSON pretty-printed, we can do that too:: +While the `--all-columns` is optional, you probably want to see all output +fields in your JSON output. If you want your JSON pretty-printed, we can do +that too:: ```bash -linode-cli linodes list --json --pretty --all +linode-cli linodes list --json --pretty --all-columns ``` diff --git a/wiki/Uninstallation.md b/wiki/Uninstallation.md new file mode 100644 index 000000000..22fb9d402 --- /dev/null +++ b/wiki/Uninstallation.md @@ -0,0 +1,13 @@ +# Uninstallation + +## PyPi + +```bash +pip3 uninstall linode-cli +``` + +If you would like to remove the config file (easy to re-create) you must do so manually. + +```bash +rm $HOME/.config/linode-cli +``` diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index a22828883..26921b0fd 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -1,4 +1,5 @@ - [Installation](./Installation) +- [Uninstallation](./Uninstallation) - [Configuration](./Configuration) - [Usage](./Usage) - [Output](./Output) @@ -7,4 +8,4 @@ - [Overview](./Development%20-%20Overview) - [Skeleton](./Development%20-%20Skeleton) - [Setup](./Development%20-%20Setup) - - [Testing](./Development%20-%20Testing) \ No newline at end of file + - [Testing](./Development%20-%20Testing)