Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.11-slim AS builder
FROM python:3.13-slim AS builder

ARG linode_cli_version

Expand All @@ -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
Expand Down
38 changes: 25 additions & 13 deletions linodecli/baked/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
28 changes: 25 additions & 3 deletions linodecli/baked/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 = ""
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand Down
50 changes: 16 additions & 34 deletions linodecli/configuration/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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]:
"""
Expand All @@ -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:
"""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions linodecli/configuration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
17 changes: 9 additions & 8 deletions wiki/Output.md
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down Expand Up @@ -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
```
13 changes: 13 additions & 0 deletions wiki/Uninstallation.md
Original file line number Diff line number Diff line change
@@ -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
```
3 changes: 2 additions & 1 deletion wiki/_Sidebar.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- [Installation](./Installation)
- [Uninstallation](./Uninstallation)
- [Configuration](./Configuration)
- [Usage](./Usage)
- [Output](./Output)
Expand All @@ -7,4 +8,4 @@
- [Overview](./Development%20-%20Overview)
- [Skeleton](./Development%20-%20Skeleton)
- [Setup](./Development%20-%20Setup)
- [Testing](./Development%20-%20Testing)
- [Testing](./Development%20-%20Testing)
Loading