Skip to content

Commit e61be12

Browse files
committed
feat: support x-cli-lookup attribute on request body properties
1 parent 310c9dc commit e61be12

12 files changed

Lines changed: 167 additions & 56 deletions

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]:

templates/endpoint_module/attributes.jinja

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
1-
{% macro add_primitive(property, argument, lookup_entity) %}
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) %}
216
add(PrimitiveAttribute(
317
"{{ property.python_name }}",
418
{{ property.get_type_string() }},
519
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 %}
623
{{ option_name(property if not argument else None) }}
7-
{% if not lookup_entity %}
824
{{ description_argument(property) }}
925
{% else %}
10-
metavar="{{ property.python_name.replace("_" + lookup_entity['id'], "") }}",
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) }}",
1137
{{ description_argument(property)
12-
.replace(" " + lookup_entity['id'].upper(), " " + lookup_entity['id'].upper() + " or " + lookup_entity['ref'])
13-
.replace(" " + lookup_entity['id'], " " + lookup_entity['id'] + " or " + lookup_entity['ref']) }}
14-
lookup = lookup_{{ property.python_name }},
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 )}}
1541
{% endif %}
1642
))
1743
{%- endmacro %}
@@ -45,3 +71,23 @@
4571
{% macro option_name(prop) %}
4672
option_name = {{ '"' + prop.name.replace('_','-') + '"' if prop else None }},
4773
{% 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 %}

templates/endpoint_module/create_mapping.jinja

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
{% import "endpoint_module/attributes.jinja" as attributes %}
1+
{% import "endpoint_module/attributes.jinja" as attributes with context %}
22
def create_mapping(self) -> Mapping:
33
mapping = Mapping(CommandRequest)
44
{% if (req_parameters + opt_parameters) %}
55

66
{% for property in endpoint.path_parameters.values() %}
7-
{# Check if parameter has a lookup command, and define handler if it does #}
8-
{% set lookup_endpoint = lookup_parameters.get(property.name) %}
9-
{% if lookup_endpoint %}
10-
{% set lookup_entity = lookup_endpoint.data['x-cli-entity'] %}
11-
def lookup_{{ property.python_name }}(ref: str) -> Union[None, {{ property.get_type_string() }}]:
12-
return api_{{ python_identifier(lookup_endpoint.name) }}.Command(self._context).lookup(ref)
13-
14-
{% endif %}
15-
mapping.{{ attributes.add_primitive(property, True, lookup_entity if lookup_endpoint else None) }}
7+
{{ attributes.add_lookup(property) }}
8+
{% endfor %}
9+
{% for property in endpoint.path_parameters.values() %}
10+
mapping.{{ attributes.add_primitive(property, True) }}
1611
{% endfor %}
1712

1813
{% if endpoint.json_body %}
@@ -34,6 +29,7 @@
3429
{{ add_object_to_parser(subparser, openapi.models_by_class[property.class_info]) }}
3530
{% else %}
3631

32+
{{ attributes.add_lookup(property) }}
3733
{{ parser }}.{{ attributes.add_primitive(property) }}
3834
{% endif %}
3935
{% endfor %}

0 commit comments

Comments
 (0)