Skip to content

Commit d360bbf

Browse files
Merge pull request #491 from linode/openspec
Merge OpenAPI spec parser into `dev` branch
2 parents fedb0e8 + f34da16 commit d360bbf

35 files changed

+1736
-1088
lines changed

.pylintrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,8 @@ disable=raw-checker-failed,
422422
invalid-name,
423423
fixme,
424424
duplicate-code,
425+
too-few-public-methods,
426+
too-many-instance-attributes,
425427
use-symbolic-message-instead
426428

427429
# Enable the message, report, category or checker with the given id(s). You can

README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ This can be done using the following environment variables to override certain s
266266

267267
* ``LINODE_CLI_API_SCHEME`` - The request scheme to use (e.g. ``https``)
268268

269+
Alternatively, these values can be configured per-user using the ``linode-cli configure`` command.
270+
269271
Multiple Users
270272
^^^^^^^^^^^^^^
271273

linodecli/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@
2626
from .completion import bake_completions, get_completions
2727
from .configuration import ENV_TOKEN_NAME
2828
from .helpers import handle_url_overrides
29-
from .operation import CLIArg, CLIOperation, URLParam
3029
from .output import OutputMode
31-
from .response import ModelAttr, ResponseModel
3230

3331
# this might not be installed at the time of building
3432
try:

linodecli/api_request.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,47 @@
22
This module is responsible for handling HTTP requests to the Linode API.
33
"""
44

5+
import itertools
56
import json
67
import sys
78
from sys import version_info
8-
from typing import Optional
9+
from typing import Iterable, List, Optional
910

1011
import requests
1112
from packaging import version
1213
from requests import Response
1314

1415
from linodecli.helpers import API_CA_PATH
1516

17+
from .baked.operation import OpenAPIOperation
18+
from .helpers import handle_url_overrides
19+
20+
21+
def get_all_pages(ctx, operation: OpenAPIOperation, args: List[str]):
22+
"""
23+
Receive all pages of a resource from multiple
24+
API responses then merge into one page.
25+
26+
:param ctx: The main CLI object
27+
"""
28+
29+
ctx.page_size = 500
30+
ctx.page = 1
31+
result = do_request(ctx, operation, args).json()
32+
33+
total_pages = result.get("pages")
34+
35+
if total_pages and total_pages > 1:
36+
pages_needed = range(2, total_pages + 1)
37+
38+
result = _merge_results_data(
39+
itertools.chain(
40+
(result,),
41+
_generate_all_pages_results(ctx, operation, args, pages_needed),
42+
)
43+
)
44+
return result
45+
1646

1747
def do_request(
1848
ctx,
@@ -26,7 +56,10 @@ def do_request(
2656
"""
2757
Makes a request to an operation's URL and returns the resulting JSON, or
2858
prints and error if a non-200 comes back
59+
60+
:param ctx: The main CLI object
2961
"""
62+
# TODO: Revisit using pre-built calls from OpenAPI
3063
method = getattr(requests, operation.method)
3164
headers = {
3265
"Authorization": f"Bearer {ctx.config.get_token()}",
@@ -67,6 +100,39 @@ def do_request(
67100
return result
68101

69102

103+
def _merge_results_data(results: Iterable[dict]):
104+
"""Merge multiple json response into one"""
105+
106+
iterator = iter(results)
107+
merged_result = next(iterator, None)
108+
if not merged_result:
109+
return None
110+
111+
if "pages" in merged_result:
112+
merged_result["pages"] = 1
113+
if "page" in merged_result:
114+
merged_result["page"] = 1
115+
if "data" in merged_result:
116+
merged_result["data"] += list(
117+
itertools.chain.from_iterable(r["data"] for r in iterator)
118+
)
119+
return merged_result
120+
121+
122+
def _generate_all_pages_results(
123+
ctx,
124+
operation: OpenAPIOperation,
125+
args: List[str],
126+
pages_needed: Iterable[int],
127+
):
128+
"""
129+
:param ctx: The main CLI object
130+
"""
131+
for p in pages_needed:
132+
ctx.page = p
133+
yield do_request(ctx, operation, args).json()
134+
135+
70136
def _build_filter_header(
71137
operation, parsed_args, filter_header=None
72138
) -> Optional[str]:
@@ -102,7 +168,14 @@ def _build_filter_header(
102168

103169

104170
def _build_request_url(ctx, operation, parsed_args) -> str:
105-
result = operation.url.format(**vars(parsed_args))
171+
target_server = handle_url_overrides(
172+
operation.url_base,
173+
host=ctx.config.get_value("api_host"),
174+
version=ctx.config.get_value("api_version"),
175+
scheme=ctx.config.get_value("api_scheme"),
176+
)
177+
178+
result = f"{target_server}{operation.url_path}".format(**vars(parsed_args))
106179

107180
if operation.method == "get":
108181
result += f"?page={ctx.page}&page_size={ctx.page_size}"

linodecli/arg_helpers.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -341,35 +341,34 @@ def action_help(cli, command, action):
341341
return
342342
print(f"linode-cli {command} {action}", end="")
343343
for param in op.params:
344-
# clean up parameter names - we add an '_' at the end of them
345-
# during baking if it conflicts with the name of an argument.
346344
pname = param.name.upper()
347-
if pname[-1] == "_":
348-
pname = pname[:-1]
349345
print(f" [{pname}]", end="")
350346
print()
351347
print(op.summary)
352348
if op.docs_url:
353349
print(f"API Documentation: {op.docs_url}")
354350
print()
351+
if op.method == "get" and op.action == "list":
352+
filterable_attrs = [
353+
attr for attr in op.response_model.attrs if attr.filterable
354+
]
355+
356+
if filterable_attrs:
357+
print("You may filter results with:")
358+
for attr in filterable_attrs:
359+
print(f" --{attr.name}")
360+
return
355361
if op.args:
356362
print("Arguments:")
357363
for arg in sorted(op.args, key=lambda s: not s.required):
364+
if arg.read_only:
365+
continue
358366
is_required = (
359367
"(required) "
360368
if op.method in {"post", "put"} and arg.required
361369
else ""
362370
)
363371
print(f" --{arg.path}: {is_required}{arg.description}")
364-
elif op.method == "get" and op.action == "list":
365-
filterable_attrs = [
366-
attr for attr in op.response_model.attrs if attr.filterable
367-
]
368-
369-
if filterable_attrs:
370-
print("You may filter results with:")
371-
for attr in filterable_attrs:
372-
print(f" --{attr.name}")
373372

374373

375374
def bake_command(cli, spec_loc):

linodecli/baked/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""
2+
Collection of classes for handling the parsed OpenAPI Spec for the CLI
3+
"""
4+
from .operation import OpenAPIOperation

linodecli/baked/colors.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Applies shell color escapes for pretty printing
3+
"""
4+
import os
5+
import platform
6+
7+
DO_COLORS = True
8+
# !! Windows compatibility for ANSI color codes !!
9+
#
10+
# If we're running on windows, we need to run the "color" command to enable
11+
# ANSI color code support.
12+
if platform.system() == "Windows":
13+
ver = platform.version()
14+
15+
if "." in ver:
16+
ver = ver.split(".", 1)[0]
17+
18+
try:
19+
verNum = int(ver)
20+
except ValueError:
21+
DO_COLORS = False
22+
23+
# windows 10+ supports ANSI color codes after running the 'color' command to
24+
# properly set up the command prompt. Older versions of windows do not, and
25+
# we should not attempt to use them there.
26+
if verNum >= 10:
27+
os.system("color")
28+
else:
29+
DO_COLORS = False
30+
31+
32+
CLEAR_COLOR = "\x1b[0m"
33+
COLOR_CODE_MAP = {
34+
"red": "\x1b[31m",
35+
"green": "\x1b[32m",
36+
"yellow": "\x1b[33m",
37+
"black": "\x1b[30m",
38+
"white": "\x1b[40m",
39+
}
40+
41+
42+
def colorize_string(string, color):
43+
"""
44+
Returns the requested string, wrapped in ANSI color codes to colorize it as
45+
requested. On platforms where colors are not supported, this just returns
46+
the string passed into it.
47+
"""
48+
if not DO_COLORS:
49+
return string
50+
51+
col = COLOR_CODE_MAP.get(color, CLEAR_COLOR)
52+
53+
return f"{col}{string}{CLEAR_COLOR}"

0 commit comments

Comments
 (0)