Skip to content

Commit 140ff3c

Browse files
Refactor template data
1 parent 336b498 commit 140ff3c

10 files changed

Lines changed: 546 additions & 476 deletions

File tree

linodecli/documentation/template_data.py

Lines changed: 0 additions & 476 deletions
This file was deleted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
Contains all structures used to render static documentation templates.
3+
"""
4+
5+
from .action import *
6+
from .argument import *
7+
from .attribute import *
8+
from .field_section import *
9+
from .group import *
10+
from .param import *
11+
from .root import *
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""
2+
Contains the template data for Linode CLI actions.
3+
"""
4+
5+
from dataclasses import dataclass, field
6+
from io import StringIO
7+
from typing import List, Optional, Self, Set
8+
9+
from linodecli.baked import OpenAPIOperation
10+
from linodecli.baked.request import OpenAPIRequestArg
11+
from linodecli.documentation.template_data.argument import Argument
12+
from linodecli.documentation.template_data.attribute import ResponseAttribute
13+
from linodecli.documentation.template_data.field_section import FieldSection
14+
from linodecli.documentation.template_data.param import Param
15+
from linodecli.documentation.template_data.util import (
16+
_format_usage_text,
17+
_markdown_to_rst,
18+
_normalize_padding,
19+
)
20+
21+
22+
@dataclass
23+
class Action:
24+
"""
25+
Represents a single generated Linode CLI command/action.
26+
"""
27+
28+
command: str
29+
action: List[str]
30+
31+
usage: Optional[str] = None
32+
summary: Optional[str] = None
33+
description: Optional[str] = None
34+
api_documentation_url: Optional[str] = None
35+
deprecated: bool = False
36+
parameters: List[Param] = field(default_factory=lambda: [])
37+
samples: List[str] = field(default_factory=lambda: [])
38+
attributes: List[ResponseAttribute] = field(default_factory=lambda: [])
39+
filterable_attributes: List[ResponseAttribute] = field(
40+
default_factory=lambda: []
41+
)
42+
43+
argument_sections: List[FieldSection[Argument]] = field(
44+
default_factory=lambda: []
45+
)
46+
argument_sections_names: Set[str] = field(default_factory=lambda: {})
47+
48+
attribute_sections: List[FieldSection[ResponseAttribute]] = field(
49+
default_factory=lambda: []
50+
)
51+
attribute_sections_names: Set[str] = field(default_factory=lambda: {})
52+
53+
@classmethod
54+
def from_openapi(cls, operation: OpenAPIOperation) -> Self:
55+
"""
56+
Returns a new Action object initialized using values
57+
from the given operation.
58+
59+
:param operation: The operation to initialize the object with.
60+
61+
:returns: The initialized object.
62+
"""
63+
64+
result = cls(
65+
command=operation.command,
66+
action=[operation.action] + (operation.action_aliases or []),
67+
summary=_markdown_to_rst(operation.summary),
68+
description=(
69+
_markdown_to_rst(operation.description)
70+
if operation.description != ""
71+
else None
72+
),
73+
usage=cls._get_usage(operation),
74+
api_documentation_url=operation.docs_url,
75+
deprecated=operation.deprecated is not None
76+
and operation.deprecated,
77+
)
78+
79+
if operation.samples:
80+
result.samples = [
81+
_normalize_padding(sample["source"])
82+
for sample in operation.samples
83+
]
84+
85+
if operation.params:
86+
result.parameters = [
87+
Param.from_openapi(param) for param in operation.params
88+
]
89+
90+
if operation.method == "get" and operation.response_model.is_paginated:
91+
result.filterable_attributes = sorted(
92+
[
93+
ResponseAttribute.from_openapi(attr)
94+
for attr in operation.response_model.attrs
95+
if attr.filterable
96+
],
97+
key=lambda v: v.name,
98+
)
99+
100+
if operation.args:
101+
result.argument_sections = FieldSection.from_iter(
102+
iter(
103+
Argument.from_openapi(arg)
104+
for arg in operation.args
105+
if isinstance(arg, OpenAPIRequestArg) and not arg.read_only
106+
),
107+
get_parent=lambda arg: arg.parent if arg.is_child else None,
108+
sort_key=lambda arg: (
109+
not arg.required,
110+
"." in arg.path,
111+
arg.path,
112+
),
113+
)
114+
115+
result.argument_sections_names = {
116+
section.name for section in result.argument_sections
117+
}
118+
119+
if operation.response_model.attrs:
120+
result.attribute_sections = FieldSection.from_iter(
121+
iter(
122+
ResponseAttribute.from_openapi(attr)
123+
for attr in operation.response_model.attrs
124+
),
125+
get_parent=lambda attr: (
126+
attr.name.split(".", maxsplit=1)[0]
127+
if "." in attr.name
128+
else None
129+
),
130+
sort_key=lambda attr: attr.name,
131+
)
132+
133+
result.attribute_sections_names = {
134+
section.name for section in result.attribute_sections
135+
}
136+
137+
return result
138+
139+
@staticmethod
140+
def _get_usage(operation: OpenAPIOperation) -> str:
141+
"""
142+
Returns the formatted argparse usage string for the given operation.
143+
144+
:param: operation: The operation to get the usage string for.
145+
146+
:returns: The formatted usage string.
147+
"""
148+
149+
usage_io = StringIO()
150+
operation.build_parser()[0].print_usage(file=usage_io)
151+
152+
return _format_usage_text(usage_io.getvalue())
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
Contains the template data for Linode CLI arguments.
3+
"""
4+
5+
import json
6+
import sys
7+
from dataclasses import dataclass
8+
from typing import Any, Optional, Self
9+
10+
from linodecli.baked.request import OpenAPIRequestArg
11+
from linodecli.documentation.template_data.util import (
12+
_format_type,
13+
_markdown_to_rst,
14+
)
15+
16+
17+
@dataclass
18+
class Argument:
19+
"""
20+
Represents a single argument for a command/action.
21+
"""
22+
23+
path: str
24+
required: bool
25+
type: str
26+
27+
is_json: bool = False
28+
is_nullable: bool = False
29+
depth: int = 0
30+
description: Optional[str] = None
31+
example: Optional[Any] = None
32+
33+
is_parent: bool = False
34+
is_child: bool = False
35+
parent: Optional[str] = None
36+
37+
@staticmethod
38+
def _format_example(arg: OpenAPIRequestArg) -> Optional[str]:
39+
"""
40+
Returns a formatted example value for the given argument.
41+
42+
:param arg: The argument to get an example for.
43+
44+
:returns: The formatted example if it exists, else None.
45+
"""
46+
47+
example = arg.example
48+
49+
if not example:
50+
return None
51+
52+
if arg.datatype == "object":
53+
return json.dumps(arg.example)
54+
55+
if arg.datatype.startswith("array"):
56+
# We only want to show one entry for list arguments.
57+
if isinstance(example, list):
58+
if len(example) < 1:
59+
print(
60+
f"WARN: List example does not have any elements: {example}",
61+
file=sys.stderr,
62+
)
63+
return None
64+
65+
example = example[0]
66+
67+
if isinstance(example, bool):
68+
return "true" if example else "false"
69+
70+
return str(example)
71+
72+
@classmethod
73+
def from_openapi(cls, arg: OpenAPIRequestArg) -> Self:
74+
"""
75+
Returns a new Argument object initialized using values
76+
from the given OpenAPI request argument.
77+
78+
:param arg: The OpenAPI request argument to initialize the object with.
79+
80+
:returns: The initialized object.
81+
"""
82+
83+
return cls(
84+
path=arg.path,
85+
required=arg.required,
86+
type=_format_type(
87+
arg.datatype, item_type=arg.item_type, _format=arg.format
88+
),
89+
is_json=arg.format == "json",
90+
is_nullable=arg.nullable is not None,
91+
is_parent=arg.is_parent,
92+
parent=arg.parent,
93+
is_child=arg.is_child,
94+
depth=arg.depth,
95+
description=(
96+
_markdown_to_rst(arg.description)
97+
if arg.description != ""
98+
else None
99+
),
100+
example=cls._format_example(arg),
101+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
Contains the template data for Linode CLI response attributes.
3+
"""
4+
5+
import json
6+
from dataclasses import dataclass
7+
from typing import Any, Optional, Self
8+
9+
from linodecli.baked.response import OpenAPIResponseAttr
10+
from linodecli.documentation.template_data.util import (
11+
_format_type,
12+
_markdown_to_rst,
13+
)
14+
15+
16+
@dataclass
17+
class ResponseAttribute:
18+
"""
19+
Represents a single filterable attribute for a list command/action.
20+
"""
21+
22+
name: str
23+
type: str
24+
25+
description: Optional[str]
26+
example: Optional[Any]
27+
28+
@staticmethod
29+
def _format_example(attr: OpenAPIResponseAttr) -> Optional[str]:
30+
"""
31+
Returns a formatted example value for the given response attribute.
32+
33+
:param attr: The attribute to get an example for.
34+
35+
:returns: The formatted example if it exists, else None.
36+
"""
37+
38+
example = attr.example
39+
40+
if not example:
41+
return None
42+
43+
if attr.datatype in ["object", "array"]:
44+
return json.dumps(attr.example)
45+
46+
if isinstance(example, bool):
47+
return "true" if example else "false"
48+
49+
return str(example)
50+
51+
@classmethod
52+
def from_openapi(cls, attr: OpenAPIResponseAttr) -> Self:
53+
"""
54+
Returns a new FilterableAttribute object initialized using values
55+
from the given filterable OpenAPI response attribute.
56+
57+
:param attr: The OpenAPI response attribute to initialize the object with.
58+
59+
:returns: The initialized object.
60+
"""
61+
62+
return cls(
63+
name=attr.name,
64+
type=_format_type(attr.datatype, item_type=attr.item_type),
65+
description=(
66+
_markdown_to_rst(attr.description)
67+
if attr.description != ""
68+
else None
69+
),
70+
example=cls._format_example(attr),
71+
)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Contains the template data for field sections.
3+
"""
4+
5+
from collections import defaultdict
6+
from dataclasses import dataclass
7+
from typing import (
8+
Any,
9+
Callable,
10+
Generic,
11+
Iterable,
12+
List,
13+
Optional,
14+
Self,
15+
TypeVar,
16+
)
17+
18+
T = TypeVar("T")
19+
20+
21+
@dataclass
22+
class FieldSection(Generic[T]):
23+
"""
24+
Represents a single section of arguments.
25+
"""
26+
27+
name: str
28+
entries: List[T]
29+
30+
@classmethod
31+
def from_iter(
32+
cls,
33+
data: Iterable[T],
34+
get_parent: Callable[[T], Optional[str]],
35+
sort_key: Callable[[T], Any],
36+
) -> List[Self]:
37+
"""
38+
Builds a list of FieldSection created from the given data using the given functions.
39+
40+
:param data: The data to partition.
41+
:param get_parent: A function returning the parent of this entry.
42+
:param sort_key: A function passed into the `key` argument of the sort function.
43+
44+
:returns: The built list of sections.
45+
"""
46+
47+
sections = defaultdict(lambda: [])
48+
49+
for entry in data:
50+
parent = get_parent(entry)
51+
52+
sections[parent if parent is not None else ""].append(entry)
53+
54+
return sorted(
55+
[
56+
FieldSection(name=key, entries=sorted(section, key=sort_key))
57+
for key, section in sections.items()
58+
],
59+
key=lambda section: section.name,
60+
)

0 commit comments

Comments
 (0)