Coverage for kryptic_cypher/app.py: 85%

93 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-20 18:52 +0000

1""" 

2Module that contains the click entrypoint for our cli interface. 

3 

4This is currently only for encoding and decoding data using the encode and decode commands. 

5""" 

6 

7import base64 

8from io import BytesIO 

9from logging import basicConfig 

10import os 

11import click 

12from kryptic_cypher.cypher.base import CypherResult 

13from .cypher import Cypher, CypherWithKey, register_all_cyphers, registered_cyphers 

14 

15 

16@click.group() 

17@click.pass_context 

18def main(ctx: click.Context): 

19 """ 

20 main group that represents the top-level: ***zombie-nomnom*** 

21 

22 This will be used to decorate sub-commands for zombie-nomnom. 

23 

24 ***Example Usage:*** 

25 ```python 

26 @main.command("sub-command") 

27 def sub_command(): 

28 # do actual meaningful work. 

29 pass 

30 ``` 

31 """ 

32 basicConfig(level=os.environ.get("LOG_LEVEL", "INFO").upper()) 

33 register_all_cyphers() 

34 

35 

36def resolve_cypher( 

37 cypher: str, 

38 text: str, 

39 input: str, 

40 key: str, 

41) -> Cypher | CypherWithKey: 

42 if not text and not input: 

43 raise click.ClickException("You must specify either -t or -i") 

44 

45 cypher_instance = registered_cyphers.get(cypher, None) 

46 

47 if not cypher_instance: 

48 raise click.ClickException( 

49 f"Invalid cypher: {cypher}, {', '.join(registered_cyphers.keys())}" 

50 ) 

51 

52 if isinstance(cypher_instance, CypherWithKey): 

53 if not key: 

54 raise click.ClickException("You must specify -k") 

55 result = cypher_instance.validate_key(key) 

56 if not result.success: 

57 raise click.ClickException("\n".join(result.messages)) 

58 

59 return cypher_instance 

60 

61 

62def process_output( 

63 output: str | None, 

64 result: CypherResult, 

65): 

66 if not result.success: 

67 raise click.ClickException(result.error) 

68 

69 if output: 

70 flags = "w" if isinstance(result.new_text, str) else "wb" 

71 with open(output, flags) as f: 

72 f.write(result.new_text) 

73 else: 

74 if isinstance(result.new_text, str): 

75 click.echo(result.new_text) 

76 else: 

77 encoded_text = BytesIO(result.new_text) 

78 full_value = encoded_text.read() 

79 binary_string = base64.b64encode(full_value).decode("utf-8") 

80 click.echo(binary_string) 

81 

82 

83def _run_cypher( 

84 action: str, 

85 cypher: str, 

86 text: str, 

87 input: str, 

88 key: str, 

89 output: str | None, 

90 binary: bool, 

91): 

92 """Shared runner for encode/decode click commands. 

93 

94 action must be either 'encode' or 'decode'. 

95 """ 

96 action = action.lower() 

97 if action not in ("encode", "decode"): 

98 raise click.ClickException("action must be 'encode' or 'decode'") 

99 

100 cypher_instance = resolve_cypher(cypher, text, input, key) 

101 

102 if input: 

103 with open(input, "rb" if binary else "r") as f: 

104 text = f.read() 

105 

106 # Resolve the bound method (`encode` or `decode`) and call it. 

107 method = getattr(cypher_instance, action) 

108 if isinstance(cypher_instance, CypherWithKey): 

109 result = method(text, key) 

110 else: 

111 result = method(text) 

112 

113 process_output(output, result) 

114 

115 

116@main.command("encode") 

117@click.option( 

118 "-c", 

119 "--cypher", 

120 help="The cypher to use", 

121 required=True, 

122) 

123@click.option("-t", "--text", help="The text to encode", required=False) 

124@click.option("-k", "--key", help="The input file to read text from", required=False) 

125@click.option("-i", "--input", help="The input file to read text from", required=False) 

126@click.option( 

127 "-o", 

128 "--output", 

129 help="The output file to write text to", 

130 required=False, 

131 type=click.Path(writable=True, dir_okay=False), 

132) 

133@click.option( 

134 "-b", 

135 "--binary", 

136 help="The output file to write text to", 

137 required=False, 

138 is_flag=True, 

139) 

140def encode( 

141 cypher: str, 

142 text: str, 

143 input: str, 

144 binary: bool, 

145 key: str, 

146 output: str | None, 

147): 

148 """ 

149 CLI command to encode text using a cypher in our system that will check to make sure the usage is valid i.e. input is given and key is valid if key is required. 

150 """ 

151 _run_cypher( 

152 "encode", 

153 cypher=cypher, 

154 text=text, 

155 input=input, 

156 key=key, 

157 output=output, 

158 binary=binary, 

159 ) 

160 

161 

162@main.command("decode") 

163@click.option( 

164 "-c", 

165 "--cypher", 

166 help="The cypher to use", 

167 required=True, 

168) 

169@click.option("-t", "--text", help="The text to encode", required=False) 

170@click.option("-i", "--input", help="The input file to read text from", required=False) 

171@click.option("-k", "--key", help="The input file to read text from", required=False) 

172@click.option( 

173 "-o", 

174 "--output", 

175 help="The output file to write text to", 

176 required=False, 

177 type=click.Path(writable=True, dir_okay=False), 

178) 

179@click.option( 

180 "-b", 

181 "--binary", 

182 help="The output file to write text to", 

183 required=False, 

184 is_flag=True, 

185) 

186def decode( 

187 cypher: str, 

188 text: str, 

189 input: str, 

190 key: str, 

191 output: str, 

192 binary: bool, 

193): 

194 """ 

195 CLI command to decode text using a cypher in our system that will check to make sure the usage is valid i.e. input is given and key is valid if key is required. 

196 """ 

197 _run_cypher( 

198 "decode", 

199 cypher=cypher, 

200 text=text, 

201 input=input, 

202 key=key, 

203 output=output, 

204 binary=binary, 

205 ) 

206 

207 

208@main.command("list") 

209def list_cyphers(): 

210 for cypher in sorted(registered_cyphers.keys()): 

211 click.echo( 

212 f"{cypher}: {'keyless' if isinstance(registered_cyphers[cypher], Cypher) else 'keyed'}" 

213 ) 

214 

215 

216@main.command("cypher") 

217@click.argument("cypher") 

218@click.argument("action", type=click.Choice(["encode", "decode"])) 

219@click.argument("text") 

220@click.option( 

221 "-k", 

222 "--key", 

223 help="The key to use for the cypher", 

224 required=False, 

225) 

226@click.option( 

227 "--binary", 

228 "-b", 

229 is_flag=True, 

230 help="Whether or not input is binary...", 

231) 

232@click.option( 

233 "-o", 

234 "--output", 

235 help="The output file to write text to", 

236 required=False, 

237 default=None, 

238) 

239def cypher( 

240 cypher: str, 

241 action: str, 

242 text: str, 

243 key: str, 

244 binary: bool, 

245 output: str | None, 

246): 

247 if os.path.exists(text): 

248 _run_cypher(action, cypher, None, text, key, output, binary) 

249 else: 

250 _run_cypher(action, cypher, text, None, key, output, binary) 

251 

252 

253try: 

254 from kryptic_cypher.bot import run 

255 

256 @main.command("bot") 

257 @click.option( 

258 "--env-file", 

259 type=click.Path( 

260 exists=True, 

261 file_okay=True, 

262 dir_okay=False, 

263 ), 

264 ) 

265 def run_bot(env_file: str = None): 

266 click.echo("Executing Discord Bot...") 

267 run(env_file=env_file) 

268 

269except ImportError: 

270 pass