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

1from __future__ import annotations 

2 

3from typing import Any 

4from typing import List 

5from typing import Type 

6from typing import TypeVar 

7 

8import typer 

9from pydantic import BaseModel 

10 

11BaseModelType = TypeVar("BaseModelType", bound=BaseModel) 

12 

13 

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. 

18 

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. 

22 

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". 

27 

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 

36 

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 } 

47 

48 

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. 

59 

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.) 

65 

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 

86 

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. 

100 

101 Returns 

102 ------- 

103 BaseModelType 

104 The updated model. 

105 """ 

106 from ..output.console import exit_err 

107 

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") 

111 

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) 

115 

116 return new.parse_obj(d) 

117 

118 

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. 

122 

123 `harbor subcmd --arg foo --arg bar,baz` 

124 will be parsed as: `["foo", "bar", "baz"]` 

125 

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(",")] 

132 

133 

134def parse_key_value_args(arg: list[str]) -> dict[str, str]: 

135 """Parses a list of key=value arguments. 

136 

137 Examples 

138 ------- 

139 >>> parse_key_value_args(["foo=bar", "baz=qux"]) 

140 {'foo': 'bar', 'baz': 'qux'} 

141 

142 Parameters 

143 ---------- 

144 arg 

145 A list of key=value arguments. 

146 

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