22
33import logging
44from pathlib import Path
5- from typing import Optional
65
76import classyclick
87import click
9- from platformdirs import user_config_dir
108
119from defectdojo_api_generated .client import DefectDojo
1210
1311from ... import __version__
1412
15- try :
16- # Python 3.11+
17- import tomllib
18- except ModuleNotFoundError :
19- # Python < 3.11
20- import tomli as tomllib # type: ignore
2113
22-
23- DEFAULT_PATH = Path (user_config_dir ('defectdojo-generated-api' )) / 'config.toml'
24-
25-
26- def ensure_config_file (config_path : Optional [Path ]) -> Path :
27- config_path = config_path or DEFAULT_PATH
28- if not config_path .exists ():
29- config_path .parent .mkdir (parents = True , exist_ok = True )
30- config_path .write_text ((Path (__file__ ).parent .parent / 'config.example.toml' ).read_text ())
31- print (f'Info: No configuration file found at { config_path } , a sample config has been placed there.' )
32-
33- return config_path
34-
35-
36- def load_config_data (config_path : Path ) -> dict :
37- with config_path .open ('rb' ) as f :
38- return tomllib .load (f )
39-
40-
41- class CLI (classyclick .Group ):
14+ class CLI (classyclick .helpers .ConfigFileMixin , classyclick .Group ):
4215 """DefectDojo CLI"""
4316
44- __config__ = classyclick .Group .Config (context_settings = dict (show_default = True ))
45-
46- config : Path = classyclick .Option (help = 'Path to the configuration file' , show_default = str (DEFAULT_PATH ))
47- env : str = classyclick .Option (
48- '-e' , help = 'Environment to use for the command (as many can be specified in config.toml)'
17+ __config__ = classyclick .Group .Config (
18+ context_settings = dict (show_default = True ),
19+ decorators = [click .version_option (version = __version__ , message = '%(version)s' )],
4920 )
21+ CONFIG_DEFAULT_NAME = 'defectdojo-generated-api'
22+ CONFIG_EXAMPLE_PATH = Path (__file__ ).parent .parent / 'config.example.toml'
23+
5024 host : str = classyclick .Option (
5125 '-h' ,
5226 envvar = 'DEFECTDOJO_HOST' ,
@@ -73,37 +47,9 @@ class CLI(classyclick.Group):
7347 )
7448 disable_tls : bool = classyclick .Option (help = 'Disable TLS verification in DefectDojo API requests' )
7549 debug_http : bool = classyclick .Option (help = 'Log HTTP requests and responses' )
76- ctx : classyclick .Context = classyclick .Context ()
7750
7851 def __call__ (self ):
79- self .config = ensure_config_file (self .config )
80- config_data = load_config_data (self .config )
81-
82- if self .env is None :
83- self .env = config_data .get ('default_env' )
84-
85- # allow empty string to choose root environment when "default_env" is set to something else
86- if self .env :
87- if self .env not in config_data .get ('env' , {}):
88- raise click .ClickException (f'Environment "{ self .env } " not found in { self .config } ' )
89- env_config = config_data ['env' ][self .env ]
90- config_data = merge_dicts (config_data , env_config )
91-
92- # to late to have these options from default_map, so process them manually
93- if self .host is None :
94- self .host = config_data .get ('host' )
95-
96- if self .token is None :
97- self .token = config_data .get ('token' )
98-
99- if self .user is None :
100- self .user = config_data .get ('user' )
101-
102- if self .password is None :
103- self .password = config_data .get ('password' )
104-
105- if self .disable_tls is None :
106- self .disable_tls = config_data .get ('disable_tls' , False )
52+ self .load_config ()
10753
10854 if self .debug_http :
10955 import http .client
@@ -120,32 +66,10 @@ def __call__(self):
12066 auth = None if self .token else (self .user , self .password ),
12167 verify_ssl = not self .disable_tls ,
12268 )
123- self .ctx .meta ['config_path' ] = self .config
124- self .ctx .meta ['config_data' ] = config_data
125- self .ctx .meta ['selected_env' ] = self .env
126-
127- self .ctx .default_map = config_data
128-
129-
130- def merge_dicts (base : dict , override : dict ) -> dict :
131- """
132- To merge two Python dictionaries where:
133- * nested dictionaries should merge recursively
134- * lists and other values should be replaced entirely
135-
136- by ChatGPT
137- """
138- result = base .copy ()
139- for key , value in override .items ():
140- if key in result :
141- if isinstance (result [key ], dict ) and isinstance (value , dict ):
142- result [key ] = merge_dicts (result [key ], value )
143- else :
144- result [key ] = value
145- else :
146- result [key ] = value
147- return result
148-
149-
150- # TODO: classyclick missing @click.version_option - https://github.com/fopina/classyclick/issues/48
151- CLI .click = click .version_option (version = __version__ , message = '%(version)s' )(CLI .click )
69+
70+
71+ class Config (classyclick .helpers .ConfigBaseCommand , CLI .Command ):
72+ pass
73+
74+
75+ classyclick .helpers .discover_commands (__package__ )
0 commit comments