Skip to content

Commit 8d144c2

Browse files
authored
feat: support x-cli-lookup attribute on request body properties (#68)
This PR adds support for using x-cli-lookup attribute on request body properties, which currently affects following commands: - load-balancer create - load-balancer update - load-balancer server create - load-balancer server delete If the generated CLI option name ends with x-cli-entity.id or its plural ("_id" or "_ids") that is removed, resulting in the four commands above now using `--servers` instead of `--server-ids`. The former `--server-ids` is still accepted to provide compatibility with previous releases.
1 parent 7c63057 commit 8d144c2

12 files changed

+212
-93
lines changed

src/binarylane/console/commands/api/delete_v2_load_balancers_load_balancer_id_servers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
if TYPE_CHECKING:
1212
from binarylane.client import Client
1313

14+
import binarylane.console.commands.api.get_v2_servers as api_get_v2_servers
1415
from binarylane.console.parser import Mapping, PrimitiveAttribute
1516
from binarylane.console.runners.command import CommandRunner
1617

@@ -44,13 +45,18 @@ def create_mapping(self) -> Mapping:
4445

4546
json_body = mapping.add_json_body(ServerIdsRequest)
4647

48+
def lookup_server_id(ref: str) -> Union[None, int]:
49+
return api_get_v2_servers.Command(self._context).lookup(ref)
50+
4751
json_body.add(
4852
PrimitiveAttribute(
4953
"server_ids",
5054
List[int],
5155
required=True,
52-
option_name="server-ids",
53-
description="""A list of server IDs.""",
56+
option_name=("servers", "server-ids"),
57+
metavar="servers",
58+
description="""A list of server ID or names.""",
59+
lookup=lookup_server_id,
5460
)
5561
)
5662

src/binarylane/console/commands/api/post_v2_load_balancers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
if TYPE_CHECKING:
1717
from binarylane.client import Client
1818

19+
import binarylane.console.commands.api.get_v2_servers as api_get_v2_servers
1920
from binarylane.console.parser import ListAttribute, Mapping, ObjectAttribute, PrimitiveAttribute
2021
from binarylane.console.runners.actionlink import ActionLinkRunner
2122

@@ -112,13 +113,18 @@ def create_mapping(self) -> Mapping:
112113
)
113114
)
114115

116+
def lookup_server_id(ref: str) -> Union[None, int]:
117+
return api_get_v2_servers.Command(self._context).lookup(ref)
118+
115119
json_body.add(
116120
PrimitiveAttribute(
117121
"server_ids",
118122
Union[Unset, None, List[int]],
119123
required=False,
120-
option_name="server-ids",
121-
description="""A list of server IDs to assign to this load balancer.""",
124+
option_name=("servers", "server-ids"),
125+
metavar="servers",
126+
description="""A list of server ID or names to assign to this load balancer.""",
127+
lookup=lookup_server_id,
122128
)
123129
)
124130

src/binarylane/console/commands/api/post_v2_load_balancers_load_balancer_id_servers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
if TYPE_CHECKING:
1212
from binarylane.client import Client
1313

14+
import binarylane.console.commands.api.get_v2_servers as api_get_v2_servers
1415
from binarylane.console.parser import Mapping, PrimitiveAttribute
1516
from binarylane.console.runners.command import CommandRunner
1617

@@ -44,13 +45,18 @@ def create_mapping(self) -> Mapping:
4445

4546
json_body = mapping.add_json_body(ServerIdsRequest)
4647

48+
def lookup_server_id(ref: str) -> Union[None, int]:
49+
return api_get_v2_servers.Command(self._context).lookup(ref)
50+
4751
json_body.add(
4852
PrimitiveAttribute(
4953
"server_ids",
5054
List[int],
5155
required=True,
52-
option_name="server-ids",
53-
description="""A list of server IDs.""",
56+
option_name=("servers", "server-ids"),
57+
metavar="servers",
58+
description="""A list of server ID or names.""",
59+
lookup=lookup_server_id,
5460
)
5561
)
5662

src/binarylane/console/commands/api/put_v2_load_balancers_load_balancer_id.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
if TYPE_CHECKING:
1818
from binarylane.client import Client
1919

20+
import binarylane.console.commands.api.get_v2_servers as api_get_v2_servers
2021
from binarylane.console.parser import ListAttribute, Mapping, ObjectAttribute, PrimitiveAttribute
2122
from binarylane.console.runners.command import CommandRunner
2223

@@ -125,13 +126,18 @@ def create_mapping(self) -> Mapping:
125126
)
126127
)
127128

129+
def lookup_server_id(ref: str) -> Union[None, int]:
130+
return api_get_v2_servers.Command(self._context).lookup(ref)
131+
128132
json_body.add(
129133
PrimitiveAttribute(
130134
"server_ids",
131135
Union[Unset, None, List[int]],
132136
required=False,
133-
option_name="server-ids",
134-
description="""A list of server IDs to assign to this load balancer.""",
137+
option_name=("servers", "server-ids"),
138+
metavar="servers",
139+
description="""A list of server ID or names to assign to this load balancer.""",
140+
lookup=lookup_server_id,
135141
)
136142
)
137143

src/binarylane/console/parser/attribute.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44
from abc import ABC, abstractmethod
5-
from typing import TYPE_CHECKING, ClassVar, List, Optional
5+
from typing import TYPE_CHECKING, ClassVar, List, Optional, Sequence, Union
66

77
logger = logging.getLogger(__name__)
88

@@ -23,7 +23,7 @@ class Attribute(ABC):
2323
init: bool
2424
# if required is True, this attribute is mandatory in the command-line parsing sense
2525
required: bool
26-
option_name: Optional[str]
26+
option_names: List[str]
2727
description: Optional[str]
2828

2929
def __init__(
@@ -32,14 +32,14 @@ def __init__(
3232
attribute_type: type,
3333
*,
3434
required: bool,
35-
option_name: Optional[str],
35+
option_name: Union[str, Sequence[str], None],
3636
description: Optional[str],
3737
) -> None:
3838
self.attribute_name = attribute_name
3939
self.attribute_type = attribute_type
4040
self.init = required
4141
self.required = required
42-
self.option_name = option_name
42+
self.option_names = [option_name] if isinstance(option_name, str) else list(option_name) if option_name else []
4343
self.description = description
4444

4545
@property
@@ -51,8 +51,14 @@ def usage(self) -> Optional[str]:
5151
return None
5252

5353
@property
54-
def name_or_flag(self) -> str:
55-
return f"--{self.option_name}" if self.option_name else self.attribute_name
54+
def option_name(self) -> Optional[str]:
55+
return self.option_names[0] if self.option_names else None
56+
57+
@property
58+
def name_or_flag(self) -> Sequence[str]:
59+
if not self.option_names:
60+
return [self.attribute_name]
61+
return [f"--{opt}" for opt in self.option_names]
5662

5763
@property
5864
def attributes(self) -> List[Attribute]:

src/binarylane/console/parser/object_attribute.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,10 @@ def configure(self, parser: Parser) -> None:
7272
existing_arguments = parser.argument_names
7373

7474
# If any argument names for this class conflict with existing names, prefix all the argument names
75-
if any(arg for arg in self.attributes if arg.name_or_flag in existing_arguments):
75+
if any(arg for arg in self.attributes if any(opt for opt in arg.name_or_flag if opt in existing_arguments)):
7676
self._unsupported("Prefixing option names", False)
7777
for arg in self.attributes:
78-
if arg.option_name:
79-
arg.option_name = f"{self.attribute_name.replace('_', '-')}-{arg.option_name}"
78+
arg.option_names = [f"{self.attribute_name.replace('_', '-')}-{opt}" for opt in arg.option_names]
8079

8180
group = self.group_name
8281
if group:
@@ -97,7 +96,9 @@ def construct(self, parser: Parser, parsed: argparse.Namespace) -> object:
9796
# If there are required attributes for the class constructor
9897
if init_kwargs:
9998
# See if any were not provided a value
100-
missing = [attr.name_or_flag for attr in self.init_attributes if init_kwargs[attr.attribute_name] is UNSET]
99+
missing = [
100+
attr.name_or_flag[0] for attr in self.init_attributes if init_kwargs[attr.attribute_name] is UNSET
101+
]
101102

102103
# If one or more required attributes did not receive a value:
103104
if missing:

src/binarylane/console/parser/parser.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ def remove_group(self, group_name: str) -> None:
133133
del self._groups[group_name]
134134
self._action_groups = [g for g in self._action_groups if g.title != group_name]
135135

136-
def add_to_group(self, group_name: Union[str, bool], name_or_flag: str, type_: type, **kwargs: Any) -> None:
136+
def add_to_group(self, group_name: Union[str, bool], names: Sequence[str], type_: type, **kwargs: Any) -> None:
137+
name_or_flag, *aliases = names
137138
self._argument_names.append(name_or_flag)
138139

139140
if isinstance(group_name, bool):
@@ -150,4 +151,8 @@ def add_to_group(self, group_name: Union[str, bool], name_or_flag: str, type_: t
150151
del kwargs["required"]
151152

152153
logger.debug("add_argument %s (%s) - %s", name_or_flag, type_, repr(kwargs))
153-
group.add_argument(name_or_flag, type=type_, **kwargs)
154+
155+
action = group.add_argument(*names, type=type_, **kwargs)
156+
# Do not show option aliases in help output
157+
if aliases:
158+
action.option_strings = [name_or_flag]

src/binarylane/console/parser/primitive_attribute.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
from datetime import datetime
66
from enum import Enum
7-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
7+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Type, Union
88
from binarylane.pycompat import actions, typing
99

1010
from binarylane.types import UNSET, Unset
@@ -47,7 +47,7 @@ def __init__(
4747
attribute_name: str,
4848
attribute_type_hint: object,
4949
*,
50-
option_name: Optional[str],
50+
option_name: Union[Sequence[str], str, None],
5151
required: bool,
5252
description: Optional[str] = None,
5353
metavar: Optional[str] = None,
@@ -72,7 +72,7 @@ def __init__(
7272
self._dest = attribute_name
7373
self._action = action
7474
self._lookup = lookup
75-
self._metavar = (metavar or option_name or attribute_name).replace("-", "_").upper()
75+
self._metavar = (metavar or self.option_name or attribute_name).replace("-", "_").upper()
7676

7777
@property
7878
def usage(self) -> Optional[str]:
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{% macro add_lookup(property) %}
2+
{% set lookup_endpoint = lookup_parameters.get(property.name) %}
3+
{% set lookup_entity = lookup_endpoint.data["x-cli-entity"] if lookup_endpoint %}
4+
{% set lookup_type = property.get_instance_type_string() %}
5+
{% if lookup_type == 'list' %}
6+
{% set lookup_type = property.inner_property.get_instance_type_string() %}
7+
{% endif %}
8+
{% if lookup_endpoint -%}
9+
def {{ lookup_function(property, lookup_entity.id) }}(ref: str) -> Union[None, {{ lookup_type }}]:
10+
return api_{{ python_identifier(lookup_endpoint.name) }}.Command(self._context).lookup(ref)
11+
12+
{% endif %}
13+
{% endmacro %}
14+
15+
{% macro add_primitive(property, argument) %}
16+
add(PrimitiveAttribute(
17+
"{{ property.python_name }}",
18+
{{ property.get_type_string() }},
19+
required = {{ property.required if not argument else True }},
20+
{% set lookup_endpoint = lookup_parameters.get(property.name) %}
21+
{% set lookup_entity = lookup_endpoint.data["x-cli-entity"] if lookup_endpoint %}
22+
{% if not lookup_endpoint %}
23+
{{ option_name(property if not argument else None) }}
24+
{{ description_argument(property) }}
25+
{% else %}
26+
{% set option_name = property.name.replace('_','-') %}
27+
{% set entity_id = lookup_entity['id'] %}
28+
{% if argument %}
29+
option_name = None,
30+
{% elif option_name.endswith("-" + entity_id) or option_name.endswith("-" + entity_id + "s") %}
31+
option_name = ("{{ remove_suffix(option_name, "-" + entity_id) }}", "{{ option_name }}"),
32+
{% else %}
33+
option_name = "{{ option_name }}",
34+
{% endif %}
35+
{% set entity_ref = lookup_entity['ref'] %}
36+
metavar="{{ remove_suffix(property.python_name, "_" + entity_id) }}",
37+
{{ description_argument(property)
38+
.replace(" " + entity_id.upper(), " " + entity_id.upper() + " or " + entity_ref)
39+
.replace(" " + entity_id, " " + entity_id + " or " + entity_ref) }}
40+
lookup = {{ lookup_function(property, entity_id )}}
41+
{% endif %}
42+
))
43+
{%- endmacro %}
44+
45+
{% macro add_list(property) %}
46+
add(ListAttribute(
47+
"{{ property.python_name }}",
48+
{{ property.inner_property.get_type_string(no_optional=True) }},
49+
required={{ property.required }},
50+
{{ option_name(property) }}
51+
{{ description_argument(property) }}
52+
))
53+
{% endmacro %}
54+
55+
{% macro add_object(property, description_property) %}
56+
add(ObjectAttribute(
57+
"{{ property.python_name }}",
58+
{{ property.get_type_string(no_optional=True) }},
59+
{{ option_name(property) }}
60+
required = {{ property.required }},
61+
{{ description_argument(description_property) }}
62+
))
63+
{% endmacro %}
64+
65+
{% macro description_argument(prop) %}
66+
{% if prop.description %}
67+
description="""{{ prop.description.replace(" ","") }}""",
68+
{% endif %}
69+
{% endmacro %}
70+
71+
{% macro option_name(prop) %}
72+
option_name = {{ '"' + prop.name.replace('_','-') + '"' if prop else None }},
73+
{% endmacro %}
74+
75+
{% macro lookup_function(property, entity_id) %}
76+
{% filter trim %}
77+
{% set suffix = "_" + entity_id %}
78+
{# Normalize property.python_name of "server"/"server_id"/"server_ids" to "server_id" #}
79+
lookup_{{ property.python_name.removesuffix(suffix+"s").removesuffix(suffix) }}{{ suffix }}
80+
{% endfilter %}
81+
{% endmacro %}
82+
83+
{% macro remove_suffix(value, suffix) %}
84+
{% filter trim %}
85+
{% if value.endswith(suffix + "s") %}
86+
{{ value.removesuffix(suffix + "s") }}s
87+
{% elif value.endswith(suffix) %}
88+
{{ value.removesuffix(suffix) }}
89+
{% else %}
90+
{{ value }}
91+
{% endif %}
92+
{% endfilter %}
93+
{% endmacro %}

0 commit comments

Comments
 (0)