-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli_app.py
More file actions
188 lines (160 loc) · 6.04 KB
/
cli_app.py
File metadata and controls
188 lines (160 loc) · 6.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
"""
cli_app.py
Command-line entrypoint for the S3 file upload application.
Usage:
python -m cli_app --source <local-file> --bucket <bucket>
[--key <object-key>] [--key-prefix <prefix>]
[--region <aws-region>] [--profile <aws-profile>]
[--content-type <mime>]
"""
import argparse
import sys
from typing import NoReturn
import local_file_helper
import s3_gateway
import transfer_models
import transfer_service
__all__ = ["main"]
# ---------------------------------------------------------------------------
# Private exit-code constants
# ---------------------------------------------------------------------------
_EXIT_SUCCESS = 0
_EXIT_RUNTIME_ERROR = 1
_EXIT_USAGE_ERROR = 2
# ---------------------------------------------------------------------------
# Private exception for CLI usage errors
# ---------------------------------------------------------------------------
class _CliUsageError(Exception):
"""Raised when the user provides invalid or missing CLI arguments."""
# ---------------------------------------------------------------------------
# Private ArgumentParser subclass that raises instead of calling sys.exit
# ---------------------------------------------------------------------------
class _ArgumentParser(argparse.ArgumentParser):
"""Custom parser that raises _CliUsageError instead of calling sys.exit."""
def error(self, message: str) -> NoReturn:
"""Override to raise _CliUsageError instead of terminating the process."""
raise _CliUsageError(message)
# ---------------------------------------------------------------------------
# Private helper: build the argument parser
# ---------------------------------------------------------------------------
def _build_parser() -> argparse.ArgumentParser:
"""Create and return the configured CLI argument parser."""
parser = _ArgumentParser(
prog="python -m cli_app",
description="Upload a local file to an Amazon S3 bucket.",
)
parser.add_argument(
"--source",
required=True,
metavar="LOCAL_FILE",
help="Path to the local file to upload.",
)
parser.add_argument(
"--bucket",
required=True,
metavar="BUCKET",
help="Name of the target S3 bucket.",
)
parser.add_argument(
"--key",
required=False,
default=None,
metavar="OBJECT_KEY",
help="Explicit S3 object key. If omitted, the filename is used.",
)
parser.add_argument(
"--key-prefix",
required=False,
default=None,
metavar="PREFIX",
dest="key_prefix",
help="Key prefix to prepend to the resolved object key.",
)
parser.add_argument(
"--region",
required=False,
default=None,
metavar="AWS_REGION",
help="AWS region for the S3 client (e.g. us-east-1).",
)
parser.add_argument(
"--profile",
required=False,
default=None,
metavar="AWS_PROFILE",
help="AWS CLI/boto3 named profile to use for credentials.",
)
parser.add_argument(
"--content-type",
required=False,
default=None,
metavar="MIME_TYPE",
dest="content_type",
help="MIME content type for the uploaded object (e.g. text/plain).",
)
return parser
# ---------------------------------------------------------------------------
# Private helper: convert parsed namespace to UploadRequest
# ---------------------------------------------------------------------------
def _namespace_to_request(args: argparse.Namespace) -> transfer_models.UploadRequest:
"""Convert parsed CLI arguments into an UploadRequest data object."""
return transfer_models.UploadRequest(
source_path=args.source,
bucket=args.bucket,
key=args.key,
key_prefix=args.key_prefix,
region=args.region,
profile=args.profile,
content_type=args.content_type,
)
# ---------------------------------------------------------------------------
# Public entrypoint
# ---------------------------------------------------------------------------
def main(argv: list[str] | None = None) -> int:
"""
Parse CLI arguments, wire dependencies, execute the S3 upload, and
print the result.
Parameters
----------
argv:
Argument vector to parse. When ``None``, ``sys.argv[1:]`` is used.
Returns
-------
int
Exit code: 0 on success, 1 on runtime/transfer error, 2 on usage error.
"""
effective_argv: list[str] = sys.argv[1:] if argv is None else argv
parser = _build_parser()
# --- Parse arguments ---------------------------------------------------
try:
args = parser.parse_args(effective_argv)
except _CliUsageError as exc:
print(f"Error: {exc}", file=sys.stderr)
return _EXIT_USAGE_ERROR
except SystemExit as exc:
# argparse raises SystemExit(0) after printing --help
code = exc.code
if code is None or code == 0:
return _EXIT_SUCCESS
return int(code)
# --- Build request and execute transfer --------------------------------
try:
request = _namespace_to_request(args)
inspector = local_file_helper.LocalFileInspector()
uploader = s3_gateway.S3Uploader(region=request.region, profile=request.profile)
service = transfer_service.FileTransferService(
file_inspector=inspector,
s3_uploader=uploader,
)
result: transfer_models.UploadResult = service.transfer(request)
except Exception as exc: # noqa: BLE001
print(f"Error: {exc}", file=sys.stderr)
return _EXIT_RUNTIME_ERROR
# --- Report success ----------------------------------------------------
print(f"s3://{result.bucket}/{result.key}")
return _EXIT_SUCCESS
# ---------------------------------------------------------------------------
# Module execution hook
# ---------------------------------------------------------------------------
if __name__ == "__main__":
raise SystemExit(main())