imap-agent-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- imap_agent_cli/__init__.py +3 -0
- imap_agent_cli/cli.py +406 -0
- imap_agent_cli/config.py +310 -0
- imap_agent_cli/errors.py +18 -0
- imap_agent_cli/imap_client.py +303 -0
- imap_agent_cli/mime.py +319 -0
- imap_agent_cli/models.py +50 -0
- imap_agent_cli/render.py +39 -0
- imap_agent_cli/search.py +30 -0
- imap_agent_cli-0.1.0.dist-info/METADATA +164 -0
- imap_agent_cli-0.1.0.dist-info/RECORD +14 -0
- imap_agent_cli-0.1.0.dist-info/WHEEL +4 -0
- imap_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- imap_agent_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
imap_agent_cli/cli.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .config import add_profile, config_status, init_config, load_config, remove_profile, resolve_profile, set_default_profile
|
|
11
|
+
from .errors import AppError, ConfigError
|
|
12
|
+
from .imap_client import ImapSession
|
|
13
|
+
from .mime import create_draft_message, parse_message
|
|
14
|
+
from .render import write_error, write_json
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _read_json_arg(value: str) -> dict[str, Any]:
|
|
18
|
+
if value == "-":
|
|
19
|
+
text = sys.stdin.read()
|
|
20
|
+
else:
|
|
21
|
+
text = Path(value).read_text(encoding="utf-8")
|
|
22
|
+
try:
|
|
23
|
+
payload = json.loads(text)
|
|
24
|
+
except json.JSONDecodeError as exc:
|
|
25
|
+
raise AppError("invalid_request", f"invalid JSON: {exc}") from exc
|
|
26
|
+
if not isinstance(payload, dict):
|
|
27
|
+
raise AppError("invalid_request", "JSON input must be an object.")
|
|
28
|
+
return payload
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _read_password(args: argparse.Namespace) -> str | None:
|
|
32
|
+
if getattr(args, "password_stdin", False):
|
|
33
|
+
return sys.stdin.readline().rstrip("\r\n")
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _common_profile_args(parser: argparse.ArgumentParser) -> None:
|
|
38
|
+
parser.add_argument("--profile")
|
|
39
|
+
parser.add_argument("--host")
|
|
40
|
+
parser.add_argument("--port", type=int)
|
|
41
|
+
parser.add_argument("--username")
|
|
42
|
+
parser.add_argument("--password-stdin", action="store_true")
|
|
43
|
+
parser.add_argument("--tls", dest="tls", action="store_true", default=None)
|
|
44
|
+
parser.add_argument("--no-tls", dest="tls", action="store_false")
|
|
45
|
+
parser.add_argument("--ssl-mode", choices=["required", "preferred", "disabled"])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _session(args: argparse.Namespace) -> ImapSession:
|
|
49
|
+
config = load_config()
|
|
50
|
+
profile = resolve_profile(
|
|
51
|
+
config,
|
|
52
|
+
getattr(args, "profile", None),
|
|
53
|
+
host=getattr(args, "host", None),
|
|
54
|
+
port=getattr(args, "port", None),
|
|
55
|
+
username=getattr(args, "username", None),
|
|
56
|
+
password=_read_password(args),
|
|
57
|
+
tls=getattr(args, "tls", None),
|
|
58
|
+
ssl_mode=getattr(args, "ssl_mode", None),
|
|
59
|
+
)
|
|
60
|
+
return ImapSession(profile, config.defaults)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _max_body_chars(args: argparse.Namespace, payload: dict[str, Any] | None = None) -> int:
|
|
64
|
+
if getattr(args, "max_body_chars", None):
|
|
65
|
+
return int(args.max_body_chars)
|
|
66
|
+
if payload and payload.get("max_body_chars"):
|
|
67
|
+
return int(payload["max_body_chars"])
|
|
68
|
+
return load_config().defaults.max_body_chars
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _body_from_args(args: argparse.Namespace, payload: dict[str, Any]) -> str:
|
|
72
|
+
if getattr(args, "body", None) is not None:
|
|
73
|
+
return args.body
|
|
74
|
+
if getattr(args, "body_file", None):
|
|
75
|
+
return Path(args.body_file).read_text(encoding="utf-8")
|
|
76
|
+
if "body" in payload:
|
|
77
|
+
return str(payload["body"])
|
|
78
|
+
raise AppError("invalid_request", "draft body is required via --body, --body-file, or JSON body.")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _string_list(value: Any) -> list[str]:
|
|
82
|
+
if value is None:
|
|
83
|
+
return []
|
|
84
|
+
if isinstance(value, str):
|
|
85
|
+
return [value]
|
|
86
|
+
if isinstance(value, list):
|
|
87
|
+
items: list[str] = []
|
|
88
|
+
for item in value:
|
|
89
|
+
if isinstance(item, str):
|
|
90
|
+
items.append(item)
|
|
91
|
+
elif isinstance(item, dict) and item.get("email"):
|
|
92
|
+
email = str(item["email"])
|
|
93
|
+
name = str(item.get("name", "")).strip()
|
|
94
|
+
items.append(f"{name} <{email}>" if name else email)
|
|
95
|
+
return items
|
|
96
|
+
raise AppError("invalid_request", "address fields must be strings, lists of strings, or address objects.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse_repeated_addresses(values: list[str] | None) -> list[str]:
|
|
100
|
+
return values or []
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cmd_config(args: argparse.Namespace) -> int:
|
|
104
|
+
if args.config_command == "init":
|
|
105
|
+
path = init_config(from_env=args.from_env)
|
|
106
|
+
write_json({"created": True, "path": str(path)})
|
|
107
|
+
return 0
|
|
108
|
+
if args.config_command == "show":
|
|
109
|
+
write_json(config_status(load_config()))
|
|
110
|
+
return 0
|
|
111
|
+
if args.config_command == "set-default-profile":
|
|
112
|
+
path = set_default_profile(args.name)
|
|
113
|
+
write_json({"updated": True, "path": str(path), "default_profile": args.name})
|
|
114
|
+
return 0
|
|
115
|
+
if args.config_command == "add-profile":
|
|
116
|
+
path = add_profile(
|
|
117
|
+
args.name,
|
|
118
|
+
host=args.host,
|
|
119
|
+
port=args.port,
|
|
120
|
+
username=args.username,
|
|
121
|
+
password_env=args.password_env,
|
|
122
|
+
tls=args.tls,
|
|
123
|
+
ssl_mode=args.ssl_mode,
|
|
124
|
+
drafts_folder=args.drafts_folder or "",
|
|
125
|
+
)
|
|
126
|
+
write_json({"updated": True, "path": str(path), "profile": args.name})
|
|
127
|
+
return 0
|
|
128
|
+
if args.config_command == "remove-profile":
|
|
129
|
+
path = remove_profile(args.name)
|
|
130
|
+
write_json({"updated": True, "path": str(path), "removed_profile": args.name})
|
|
131
|
+
return 0
|
|
132
|
+
raise AppError("invalid_request", "unsupported config command.")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def cmd_profiles(args: argparse.Namespace) -> int:
|
|
136
|
+
config = load_config()
|
|
137
|
+
write_json({"profiles": [profile.name for profile in config.profiles.values()], "default": config.defaults.profile})
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cmd_folders(args: argparse.Namespace) -> int:
|
|
142
|
+
with _session(args) as session:
|
|
143
|
+
write_json(session.folders())
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cmd_search(args: argparse.Namespace) -> int:
|
|
148
|
+
payload: dict[str, Any] = _read_json_arg(args.json_input) if args.json_input else {}
|
|
149
|
+
config = load_config()
|
|
150
|
+
folder = args.folder or payload.get("folder") or config.defaults.default_folder
|
|
151
|
+
scope = payload.get("scope") or ("all" if args.all_folders else "recursive" if args.recursive else "folder")
|
|
152
|
+
max_results = int(args.max_results or payload.get("max_results") or config.defaults.max_results)
|
|
153
|
+
with _session(args) as session:
|
|
154
|
+
write_json(
|
|
155
|
+
session.search(
|
|
156
|
+
folder=folder,
|
|
157
|
+
scope=str(scope),
|
|
158
|
+
subject=args.subject or payload.get("subject"),
|
|
159
|
+
sender=args.sender or payload.get("from"),
|
|
160
|
+
since=args.since or payload.get("since"),
|
|
161
|
+
before=args.before or payload.get("before"),
|
|
162
|
+
max_results=max_results,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def cmd_read(args: argparse.Namespace) -> int:
|
|
169
|
+
payload: dict[str, Any] = _read_json_arg(args.json_input) if args.json_input else {}
|
|
170
|
+
folder = args.folder or payload.get("folder")
|
|
171
|
+
uid = args.uid or payload.get("uid")
|
|
172
|
+
if not folder or uid is None:
|
|
173
|
+
raise AppError("invalid_request", "read requires --folder and --uid.")
|
|
174
|
+
config = load_config()
|
|
175
|
+
body_format = args.body_format or payload.get("body_format") or config.defaults.body_format
|
|
176
|
+
max_body_chars = int(args.max_body_chars or payload.get("max_body_chars") or config.defaults.max_body_chars)
|
|
177
|
+
with _session(args) as session:
|
|
178
|
+
write_json(session.read(folder=str(folder), uid=int(uid), body_format=str(body_format), max_body_chars=max_body_chars))
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def cmd_attachments(args: argparse.Namespace) -> int:
|
|
183
|
+
if not args.folder or args.uid is None:
|
|
184
|
+
raise AppError("invalid_request", "attachments requires --folder and --uid.")
|
|
185
|
+
with _session(args) as session:
|
|
186
|
+
write_json(session.attachments(folder=args.folder, uid=args.uid))
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_attachments_download(args: argparse.Namespace) -> int:
|
|
191
|
+
if not args.all and not args.part_id:
|
|
192
|
+
raise AppError("invalid_request", "attachment download requires --part-id or --all.")
|
|
193
|
+
with _session(args) as session:
|
|
194
|
+
write_json(
|
|
195
|
+
session.download_attachments(
|
|
196
|
+
folder=args.folder,
|
|
197
|
+
uid=args.uid,
|
|
198
|
+
output_dir=Path(args.output_dir),
|
|
199
|
+
part_id=args.part_id,
|
|
200
|
+
all_parts=args.all,
|
|
201
|
+
include_inline=args.include_inline,
|
|
202
|
+
overwrite=args.overwrite,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
return 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def cmd_draft_create(args: argparse.Namespace) -> int:
|
|
209
|
+
payload: dict[str, Any] = _read_json_arg(args.json_input) if args.json_input else {}
|
|
210
|
+
body = _body_from_args(args, payload)
|
|
211
|
+
to = _parse_repeated_addresses(args.to) or _string_list(payload.get("to"))
|
|
212
|
+
if not to:
|
|
213
|
+
raise AppError("invalid_request", "draft create requires at least one recipient.")
|
|
214
|
+
config = load_config()
|
|
215
|
+
body_format = args.body_format or payload.get("body_format") or "plain"
|
|
216
|
+
attachments = [Path(path) for path in (args.attachment or payload.get("attachments") or [])]
|
|
217
|
+
with _session(args) as session:
|
|
218
|
+
message = create_draft_message(
|
|
219
|
+
sender=session.profile.username,
|
|
220
|
+
to=to,
|
|
221
|
+
cc=_parse_repeated_addresses(args.cc) or _string_list(payload.get("cc")),
|
|
222
|
+
bcc=_parse_repeated_addresses(args.bcc) or _string_list(payload.get("bcc")),
|
|
223
|
+
subject=args.subject or payload.get("subject") or "",
|
|
224
|
+
body=body,
|
|
225
|
+
body_format=str(body_format),
|
|
226
|
+
attachments=attachments,
|
|
227
|
+
)
|
|
228
|
+
write_json(session.append_draft(message, drafts_folder=args.drafts_folder or payload.get("drafts_folder")))
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _reply_recipients(source_message: Any) -> list[str]:
|
|
233
|
+
reply_to = source_message.get_all("reply-to", [])
|
|
234
|
+
if reply_to:
|
|
235
|
+
return _string_list([", ".join(reply_to)])
|
|
236
|
+
from_values = source_message.get_all("from", [])
|
|
237
|
+
return _string_list([", ".join(from_values)])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _reply_subject(source_subject: str) -> str:
|
|
241
|
+
value = source_subject.strip()
|
|
242
|
+
return value if value.lower().startswith("re:") else f"Re: {value}"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def cmd_draft_reply(args: argparse.Namespace) -> int:
|
|
246
|
+
payload: dict[str, Any] = _read_json_arg(args.json_input) if args.json_input else {}
|
|
247
|
+
source = payload.get("source") if isinstance(payload.get("source"), dict) else {}
|
|
248
|
+
folder = args.folder or source.get("folder")
|
|
249
|
+
uid = args.uid or source.get("uid")
|
|
250
|
+
if not folder or uid is None:
|
|
251
|
+
raise AppError("invalid_request", "draft reply requires source --folder and --uid.")
|
|
252
|
+
body = _body_from_args(args, payload)
|
|
253
|
+
body_format = args.body_format or payload.get("body_format") or "plain"
|
|
254
|
+
attachments = [Path(path) for path in (args.attachment or payload.get("attachments") or [])]
|
|
255
|
+
with _session(args) as session:
|
|
256
|
+
raw = session._fetch_raw(str(folder), int(uid))
|
|
257
|
+
source_message = parse_message(raw)
|
|
258
|
+
to = _parse_repeated_addresses(args.to) or _string_list(payload.get("to")) or _reply_recipients(source_message)
|
|
259
|
+
source_message_id = str(source_message.get("message-id", ""))
|
|
260
|
+
source_refs = str(source_message.get("references", "")).strip()
|
|
261
|
+
references = f"{source_refs} {source_message_id}".strip()
|
|
262
|
+
message = create_draft_message(
|
|
263
|
+
sender=session.profile.username,
|
|
264
|
+
to=to,
|
|
265
|
+
cc=_parse_repeated_addresses(args.cc) or _string_list(payload.get("cc")),
|
|
266
|
+
bcc=_parse_repeated_addresses(args.bcc) or _string_list(payload.get("bcc")),
|
|
267
|
+
subject=args.subject or payload.get("subject") or _reply_subject(str(source_message.get("subject", ""))),
|
|
268
|
+
body=body,
|
|
269
|
+
body_format=str(body_format),
|
|
270
|
+
attachments=attachments,
|
|
271
|
+
in_reply_to=source_message_id or None,
|
|
272
|
+
references=references or None,
|
|
273
|
+
)
|
|
274
|
+
write_json(session.append_draft(message, drafts_folder=args.drafts_folder or payload.get("drafts_folder")))
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
279
|
+
parser = argparse.ArgumentParser(prog="imap-agent-cli")
|
|
280
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
281
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
282
|
+
|
|
283
|
+
config = sub.add_parser("config")
|
|
284
|
+
config_sub = config.add_subparsers(dest="config_command", required=True)
|
|
285
|
+
config_init = config_sub.add_parser("init")
|
|
286
|
+
config_init.add_argument("--from-env", action="store_true")
|
|
287
|
+
config_init.set_defaults(func=cmd_config)
|
|
288
|
+
config_show = config_sub.add_parser("show")
|
|
289
|
+
config_show.set_defaults(func=cmd_config)
|
|
290
|
+
config_default = config_sub.add_parser("set-default-profile")
|
|
291
|
+
config_default.add_argument("name")
|
|
292
|
+
config_default.set_defaults(func=cmd_config)
|
|
293
|
+
config_add = config_sub.add_parser("add-profile")
|
|
294
|
+
config_add.add_argument("name")
|
|
295
|
+
config_add.add_argument("--host", required=True)
|
|
296
|
+
config_add.add_argument("--port", type=int, default=993)
|
|
297
|
+
config_add.add_argument("--username", required=True)
|
|
298
|
+
config_add.add_argument("--password-env", required=True)
|
|
299
|
+
config_add.add_argument("--tls", dest="tls", action="store_true", default=True)
|
|
300
|
+
config_add.add_argument("--no-tls", dest="tls", action="store_false")
|
|
301
|
+
config_add.add_argument("--ssl-mode", choices=["required", "preferred", "disabled"], default="required")
|
|
302
|
+
config_add.add_argument("--drafts-folder", default="")
|
|
303
|
+
config_add.set_defaults(func=cmd_config)
|
|
304
|
+
config_remove = config_sub.add_parser("remove-profile")
|
|
305
|
+
config_remove.add_argument("name")
|
|
306
|
+
config_remove.set_defaults(func=cmd_config)
|
|
307
|
+
|
|
308
|
+
profiles = sub.add_parser("profiles")
|
|
309
|
+
profiles.set_defaults(func=cmd_profiles)
|
|
310
|
+
|
|
311
|
+
folders = sub.add_parser("folders")
|
|
312
|
+
_common_profile_args(folders)
|
|
313
|
+
folders.set_defaults(func=cmd_folders)
|
|
314
|
+
|
|
315
|
+
search = sub.add_parser("search")
|
|
316
|
+
_common_profile_args(search)
|
|
317
|
+
search.add_argument("--json", dest="json_input")
|
|
318
|
+
search.add_argument("--folder")
|
|
319
|
+
search.add_argument("--recursive", action="store_true")
|
|
320
|
+
search.add_argument("--all-folders", action="store_true")
|
|
321
|
+
search.add_argument("--subject")
|
|
322
|
+
search.add_argument("--from", dest="sender")
|
|
323
|
+
search.add_argument("--since")
|
|
324
|
+
search.add_argument("--before")
|
|
325
|
+
search.add_argument("--max-results", type=int)
|
|
326
|
+
search.set_defaults(func=cmd_search)
|
|
327
|
+
|
|
328
|
+
read = sub.add_parser("read")
|
|
329
|
+
_common_profile_args(read)
|
|
330
|
+
read.add_argument("--json", dest="json_input")
|
|
331
|
+
read.add_argument("--folder")
|
|
332
|
+
read.add_argument("--uid", type=int)
|
|
333
|
+
read.add_argument("--body-format", choices=["html", "markdown", "plain", "raw-html", "metadata"])
|
|
334
|
+
read.add_argument("--max-body-chars", type=int)
|
|
335
|
+
read.set_defaults(func=cmd_read)
|
|
336
|
+
|
|
337
|
+
attachments = sub.add_parser("attachments")
|
|
338
|
+
_common_profile_args(attachments)
|
|
339
|
+
attachments.add_argument("--folder")
|
|
340
|
+
attachments.add_argument("--uid", type=int)
|
|
341
|
+
attachments.set_defaults(func=cmd_attachments)
|
|
342
|
+
attachments_sub = attachments.add_subparsers(dest="attachment_command")
|
|
343
|
+
download = attachments_sub.add_parser("download")
|
|
344
|
+
_common_profile_args(download)
|
|
345
|
+
download.add_argument("--folder", required=True)
|
|
346
|
+
download.add_argument("--uid", required=True, type=int)
|
|
347
|
+
download.add_argument("--part-id")
|
|
348
|
+
download.add_argument("--all", action="store_true")
|
|
349
|
+
download.add_argument("--output-dir", required=True)
|
|
350
|
+
download.add_argument("--include-inline", action="store_true")
|
|
351
|
+
download.add_argument("--overwrite", action="store_true")
|
|
352
|
+
download.set_defaults(func=cmd_attachments_download)
|
|
353
|
+
|
|
354
|
+
draft = sub.add_parser("draft")
|
|
355
|
+
draft_sub = draft.add_subparsers(dest="draft_command", required=True)
|
|
356
|
+
create = draft_sub.add_parser("create")
|
|
357
|
+
_common_profile_args(create)
|
|
358
|
+
create.add_argument("--json", dest="json_input")
|
|
359
|
+
create.add_argument("--to", action="append")
|
|
360
|
+
create.add_argument("--cc", action="append")
|
|
361
|
+
create.add_argument("--bcc", action="append")
|
|
362
|
+
create.add_argument("--subject")
|
|
363
|
+
create.add_argument("--body")
|
|
364
|
+
create.add_argument("--body-file")
|
|
365
|
+
create.add_argument("--body-format", choices=["html", "markdown", "plain"], default=None)
|
|
366
|
+
create.add_argument("--attachment", action="append")
|
|
367
|
+
create.add_argument("--drafts-folder")
|
|
368
|
+
create.set_defaults(func=cmd_draft_create)
|
|
369
|
+
|
|
370
|
+
reply = draft_sub.add_parser("reply")
|
|
371
|
+
_common_profile_args(reply)
|
|
372
|
+
reply.add_argument("--json", dest="json_input")
|
|
373
|
+
reply.add_argument("--folder")
|
|
374
|
+
reply.add_argument("--uid", type=int)
|
|
375
|
+
reply.add_argument("--to", action="append")
|
|
376
|
+
reply.add_argument("--cc", action="append")
|
|
377
|
+
reply.add_argument("--bcc", action="append")
|
|
378
|
+
reply.add_argument("--subject")
|
|
379
|
+
reply.add_argument("--body")
|
|
380
|
+
reply.add_argument("--body-file")
|
|
381
|
+
reply.add_argument("--body-format", choices=["html", "markdown", "plain"], default=None)
|
|
382
|
+
reply.add_argument("--attachment", action="append")
|
|
383
|
+
reply.add_argument("--drafts-folder")
|
|
384
|
+
reply.set_defaults(func=cmd_draft_reply)
|
|
385
|
+
|
|
386
|
+
return parser
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def main(argv: list[str] | None = None) -> int:
|
|
390
|
+
parser = build_parser()
|
|
391
|
+
args = parser.parse_args(argv)
|
|
392
|
+
try:
|
|
393
|
+
return int(args.func(args))
|
|
394
|
+
except AppError as exc:
|
|
395
|
+
write_error(exc)
|
|
396
|
+
return exc.exit_code
|
|
397
|
+
except ConfigError as exc:
|
|
398
|
+
write_error(exc)
|
|
399
|
+
return exc.exit_code
|
|
400
|
+
except KeyboardInterrupt:
|
|
401
|
+
write_error(AppError("interrupted", "interrupted by user", exit_code=130))
|
|
402
|
+
return 130
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
if __name__ == "__main__":
|
|
406
|
+
raise SystemExit(main())
|