Coverage for harbor_cli/utils/args.py: 95%
29 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3from typing import Any
4from typing import List
5from typing import Type
6from typing import TypeVar
8import typer
9from pydantic import BaseModel
11BaseModelType = TypeVar("BaseModelType", bound=BaseModel)
14def model_params_from_ctx(
15 ctx: typer.Context, model: Type[BaseModel], filter_none: bool = True
16) -> dict[str, Any]:
17 """Get the model parameters from a Typer context.
19 Given a command where the function parameter names match the
20 model field names, this function will return a dictionary of only the
21 parameters that are valid for the model.
23 If `filter_none` is True, then parameters that are None will be filtered out.
24 This is enabled by default, since most Harbor API model fields are optional,
25 and we want to signal to Pydantic that these fields should be treated
26 as "unset" rather than "set to None".
28 Parameters
29 ----------
30 ctx : typer.Context
31 The Typer context.
32 model : Type[BaseModel]
33 The model to get the parameters for.
34 filter_none : bool
35 Whether to filter out None values, by default True
37 Returns
38 -------
39 dict[str, Any]
40 The model parameters.
41 """
42 return {
43 key: value
44 for key, value in ctx.params.items()
45 if key in model.__fields__ and (not filter_none or value is not None)
46 }
49def create_updated_model(
50 existing: BaseModel,
51 new: Type[BaseModelType],
52 ctx: typer.Context,
53 extra: bool = False,
54 empty_ok: bool = False,
55) -> BaseModelType:
56 """Given a BaseModel and a new model type, create a new model
57 from the fields of the existing model combined with the arguments given
58 to the command in the Typer context.
60 Basically, when we call a PUT enpdoint, the API expects the full model definition,
61 but we want to allow the user to only specify the fields they want to update.
62 This function allows us to do that, by taking the existing model and updating
63 it with the new values from the Typer context (which derives its parameters
64 from the model used in send the PUT request.)
66 Examples
67 --------
68 >>> from pydantic import BaseModel
69 >>> class Foo(BaseModel):
70 ... a: Optional[int]
71 ... b: Optional[str]
72 ... c: Optional[bool]
73 >>> class FooUpdateReq(BaseModel):
74 ... a: Optional[int]
75 ... b: Optional[int]
76 ... c: Optional[bool]
77 ... insecure: bool = False
78 >>> foo = Foo(a=1, b="foo", c=True)
79 >>> # we get a ctx object from Typer inside the function of a command
80 >>> ctx = typer.Context(...) # --a 2 --b bar
81 >>> foo_update = create_updated_model(foo, FooUpdateReq, ctx)
82 >>> foo_update
83 FooUpdateReq(a=2, b='bar', c=True, insecure=False)
84 >>> # ^^^ ^^^^^^^
85 >>> # We created a FooUpdateReq with the new values from the context
87 Parameters
88 ----------
89 existing : BaseModel
90 The existing model to use as a base.
91 new : Type[BaseModelType]
92 The new model type to construct.
93 ctx : typer.Context
94 The Typer context to get the updated model parameters from.
95 extra : bool
96 Whether to include extra fields set on the existing model.
97 empty_ok: bool
98 Whether to allow the update to be empty. If False, an error will be raised
99 if no parameters are provided to update.
101 Returns
102 -------
103 BaseModelType
104 The updated model.
105 """
106 from ..output.console import exit_err
108 params = model_params_from_ctx(ctx, new)
109 if not params and not empty_ok: 109 ↛ 110line 109 didn't jump to line 110, because the condition on line 109 was never true
110 exit_err("No parameters provided to update")
112 # Cast existing model to dict, update it with the new values
113 d = existing.dict(include=None if extra else set(new.__fields__))
114 d.update(params)
116 return new.parse_obj(d)
119def parse_commalist(arg: List[str]) -> List[str]:
120 """Parses an argument that can be specified multiple times,
121 or as a comma-separated list, into a list of strings.
123 `harbor subcmd --arg foo --arg bar,baz`
124 will be parsed as: `["foo", "bar", "baz"]`
126 Examples
127 -------
128 >>> parse_commalist(["foo", "bar,baz"])
129 ["foo", "bar", "baz"]
130 """
131 return [item for arg_list in arg for item in arg_list.split(",")]
134def parse_key_value_args(arg: list[str]) -> dict[str, str]:
135 """Parses a list of key=value arguments.
137 Examples
138 -------
139 >>> parse_key_value_args(["foo=bar", "baz=qux"])
140 {'foo': 'bar', 'baz': 'qux'}
142 Parameters
143 ----------
144 arg
145 A list of key=value arguments.
147 Returns
148 -------
149 dict[str, str]
150 A dictionary mapping keys to values.
151 """
152 metadata = {}
153 for item in arg:
154 try:
155 key, value = item.split("=", maxsplit=1)
156 except ValueError:
157 raise typer.BadParameter(
158 f"Invalid metadata item {item!r}. Expected format: key=value"
159 )
160 metadata[key] = value
161 return metadata