openbucket-client 0.1.1__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.
- openbucket/__init__.py +58 -0
- openbucket/__main__.py +3 -0
- openbucket/cli.py +204 -0
- openbucket/client.py +727 -0
- openbucket/exceptions.py +56 -0
- openbucket/models.py +185 -0
- openbucket/py.typed +1 -0
- openbucket_client-0.1.1.dist-info/METADATA +176 -0
- openbucket_client-0.1.1.dist-info/RECORD +12 -0
- openbucket_client-0.1.1.dist-info/WHEEL +4 -0
- openbucket_client-0.1.1.dist-info/entry_points.txt +2 -0
- openbucket_client-0.1.1.dist-info/licenses/LICENSE +201 -0
openbucket/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Typed Python client for the OpenBucket management API."""
|
|
2
|
+
|
|
3
|
+
from .client import OpenBucketClient
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
OpenBucketConfigurationError,
|
|
6
|
+
OpenBucketConnectionError,
|
|
7
|
+
OpenBucketError,
|
|
8
|
+
OpenBucketHTTPError,
|
|
9
|
+
OpenBucketProtocolError,
|
|
10
|
+
)
|
|
11
|
+
from .models import (
|
|
12
|
+
AccessKey,
|
|
13
|
+
Analytics,
|
|
14
|
+
AnalyticsStorage,
|
|
15
|
+
Bucket,
|
|
16
|
+
ClientConfiguration,
|
|
17
|
+
DailyAnalytics,
|
|
18
|
+
DeleteResult,
|
|
19
|
+
Endpoints,
|
|
20
|
+
Health,
|
|
21
|
+
NodeInfo,
|
|
22
|
+
ObjectHead,
|
|
23
|
+
ObjectInfo,
|
|
24
|
+
RequestLog,
|
|
25
|
+
Share,
|
|
26
|
+
Status,
|
|
27
|
+
StopResult,
|
|
28
|
+
StorageInfo,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.1"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"AccessKey",
|
|
35
|
+
"Analytics",
|
|
36
|
+
"AnalyticsStorage",
|
|
37
|
+
"Bucket",
|
|
38
|
+
"ClientConfiguration",
|
|
39
|
+
"DailyAnalytics",
|
|
40
|
+
"DeleteResult",
|
|
41
|
+
"Endpoints",
|
|
42
|
+
"Health",
|
|
43
|
+
"NodeInfo",
|
|
44
|
+
"ObjectHead",
|
|
45
|
+
"ObjectInfo",
|
|
46
|
+
"OpenBucketClient",
|
|
47
|
+
"OpenBucketConfigurationError",
|
|
48
|
+
"OpenBucketConnectionError",
|
|
49
|
+
"OpenBucketError",
|
|
50
|
+
"OpenBucketHTTPError",
|
|
51
|
+
"OpenBucketProtocolError",
|
|
52
|
+
"RequestLog",
|
|
53
|
+
"Share",
|
|
54
|
+
"Status",
|
|
55
|
+
"StopResult",
|
|
56
|
+
"StorageInfo",
|
|
57
|
+
"__version__",
|
|
58
|
+
]
|
openbucket/__main__.py
ADDED
openbucket/cli.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Command-line interface for the Python management client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import mimetypes
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Mapping, Sequence
|
|
11
|
+
from dataclasses import asdict, is_dataclass
|
|
12
|
+
from datetime import date, datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from . import __version__
|
|
17
|
+
from .client import OpenBucketClient
|
|
18
|
+
from .exceptions import OpenBucketError, OpenBucketHTTPError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parser() -> argparse.ArgumentParser:
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
prog="openbucket-client",
|
|
24
|
+
description="Manage a running OpenBucket daemon through its HTTP API.",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--url",
|
|
28
|
+
default=os.environ.get("OPENBUCKET_API_URL", "http://127.0.0.1:7272"),
|
|
29
|
+
help="management API URL (default: OPENBUCKET_API_URL or http://127.0.0.1:7272)",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--token",
|
|
33
|
+
default=os.environ.get("OPENBUCKET_ADMIN_TOKEN"),
|
|
34
|
+
help="management bearer token (prefer OPENBUCKET_ADMIN_TOKEN to avoid process-list exposure)",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument("--timeout", type=float, default=30.0, help="request timeout in seconds")
|
|
37
|
+
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
38
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
39
|
+
|
|
40
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
41
|
+
commands.add_parser("health", help="check the unauthenticated daemon health endpoint")
|
|
42
|
+
commands.add_parser("status", help="show daemon and storage status")
|
|
43
|
+
commands.add_parser("config", help="show connection configuration")
|
|
44
|
+
commands.add_parser("analytics", help="show aggregate request and storage analytics")
|
|
45
|
+
|
|
46
|
+
logs = commands.add_parser("logs", help="show newest request logs")
|
|
47
|
+
logs.add_argument("--limit", type=int, default=100)
|
|
48
|
+
|
|
49
|
+
buckets = commands.add_parser("buckets", help="manage buckets").add_subparsers(dest="bucket_command", required=True)
|
|
50
|
+
buckets.add_parser("list", help="list buckets")
|
|
51
|
+
create_bucket = buckets.add_parser("create", help="create a bucket")
|
|
52
|
+
create_bucket.add_argument("name")
|
|
53
|
+
create_bucket.add_argument("--public", action="store_true")
|
|
54
|
+
update_bucket = buckets.add_parser("set-public", help="change anonymous-read visibility")
|
|
55
|
+
update_bucket.add_argument("name")
|
|
56
|
+
visibility = update_bucket.add_mutually_exclusive_group(required=True)
|
|
57
|
+
visibility.add_argument("--public", dest="public", action="store_true")
|
|
58
|
+
visibility.add_argument("--private", dest="public", action="store_false")
|
|
59
|
+
delete_bucket = buckets.add_parser("delete", help="delete a bucket")
|
|
60
|
+
delete_bucket.add_argument("name")
|
|
61
|
+
delete_bucket.add_argument("--force", action="store_true")
|
|
62
|
+
|
|
63
|
+
objects = commands.add_parser("objects", help="manage objects").add_subparsers(dest="object_command", required=True)
|
|
64
|
+
list_objects = objects.add_parser("list", help="list objects")
|
|
65
|
+
list_objects.add_argument("bucket")
|
|
66
|
+
list_objects.add_argument("--prefix", default="")
|
|
67
|
+
upload = objects.add_parser("upload", help="upload a local file")
|
|
68
|
+
upload.add_argument("bucket")
|
|
69
|
+
upload.add_argument("key")
|
|
70
|
+
upload.add_argument("file", type=Path)
|
|
71
|
+
upload.add_argument("--content-type")
|
|
72
|
+
download = objects.add_parser("download", help="download to a local file")
|
|
73
|
+
download.add_argument("bucket")
|
|
74
|
+
download.add_argument("key")
|
|
75
|
+
download.add_argument("output", type=Path)
|
|
76
|
+
download.add_argument("--force", action="store_true", help="replace an existing output file")
|
|
77
|
+
head = objects.add_parser("head", help="inspect object metadata")
|
|
78
|
+
head.add_argument("bucket")
|
|
79
|
+
head.add_argument("key")
|
|
80
|
+
delete_object = objects.add_parser("delete", help="delete an object")
|
|
81
|
+
delete_object.add_argument("bucket")
|
|
82
|
+
delete_object.add_argument("key")
|
|
83
|
+
|
|
84
|
+
keys = commands.add_parser("keys", help="manage S3 access keys").add_subparsers(dest="key_command", required=True)
|
|
85
|
+
keys.add_parser("list", help="list keys without secrets")
|
|
86
|
+
create_key = keys.add_parser("create", help="create a key; the secret is returned once")
|
|
87
|
+
create_key.add_argument("--name", default="access key")
|
|
88
|
+
create_key.add_argument("--read-only", action="store_true")
|
|
89
|
+
create_key.add_argument("--bucket")
|
|
90
|
+
revoke = keys.add_parser("revoke", help="revoke a key")
|
|
91
|
+
revoke.add_argument("id")
|
|
92
|
+
|
|
93
|
+
share = commands.add_parser("share", help="create an expiring object URL")
|
|
94
|
+
share.add_argument("bucket")
|
|
95
|
+
share.add_argument("key")
|
|
96
|
+
share.add_argument("--expires-in", type=int, default=3600)
|
|
97
|
+
|
|
98
|
+
return parser
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _jsonable(value: Any) -> Any:
|
|
102
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
103
|
+
return _jsonable(asdict(value))
|
|
104
|
+
if isinstance(value, datetime):
|
|
105
|
+
text = value.isoformat()
|
|
106
|
+
return text.replace("+00:00", "Z")
|
|
107
|
+
if isinstance(value, date):
|
|
108
|
+
return value.isoformat()
|
|
109
|
+
if isinstance(value, Mapping):
|
|
110
|
+
return {str(key): _jsonable(item) for key, item in value.items()}
|
|
111
|
+
if isinstance(value, (list, tuple)):
|
|
112
|
+
return [_jsonable(item) for item in value]
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _emit(value: Any, *, as_json: bool) -> None:
|
|
117
|
+
converted = _jsonable(value)
|
|
118
|
+
if as_json:
|
|
119
|
+
print(json.dumps(converted, ensure_ascii=False, separators=(",", ":")))
|
|
120
|
+
return
|
|
121
|
+
if isinstance(converted, list):
|
|
122
|
+
if not converted:
|
|
123
|
+
print("No results.")
|
|
124
|
+
return
|
|
125
|
+
for item in converted:
|
|
126
|
+
print(json.dumps(item, ensure_ascii=False, sort_keys=True))
|
|
127
|
+
return
|
|
128
|
+
if isinstance(converted, dict):
|
|
129
|
+
for key, item in converted.items():
|
|
130
|
+
if isinstance(item, (dict, list)):
|
|
131
|
+
print(f"{key}: {json.dumps(item, ensure_ascii=False, sort_keys=True)}")
|
|
132
|
+
else:
|
|
133
|
+
print(f"{key}: {item}")
|
|
134
|
+
return
|
|
135
|
+
print(converted)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _run(client: OpenBucketClient, args: argparse.Namespace) -> Any:
|
|
139
|
+
if args.command == "health":
|
|
140
|
+
return client.health()
|
|
141
|
+
if args.command == "status":
|
|
142
|
+
return client.status()
|
|
143
|
+
if args.command == "config":
|
|
144
|
+
return client.client_configuration()
|
|
145
|
+
if args.command == "analytics":
|
|
146
|
+
return client.analytics()
|
|
147
|
+
if args.command == "logs":
|
|
148
|
+
return client.logs(limit=args.limit)
|
|
149
|
+
if args.command == "buckets":
|
|
150
|
+
if args.bucket_command == "list":
|
|
151
|
+
return client.list_buckets()
|
|
152
|
+
if args.bucket_command == "create":
|
|
153
|
+
return client.create_bucket(args.name, public=args.public)
|
|
154
|
+
if args.bucket_command == "set-public":
|
|
155
|
+
return client.set_bucket_public(args.name, args.public)
|
|
156
|
+
if args.bucket_command == "delete":
|
|
157
|
+
return client.delete_bucket(args.name, force=args.force)
|
|
158
|
+
if args.command == "objects":
|
|
159
|
+
if args.object_command == "list":
|
|
160
|
+
return client.list_objects(args.bucket, prefix=args.prefix)
|
|
161
|
+
if args.object_command == "upload":
|
|
162
|
+
content_type = args.content_type or mimetypes.guess_type(args.file.name)[0] or "application/octet-stream"
|
|
163
|
+
return client.upload_file(args.bucket, args.key, args.file, content_type=content_type)
|
|
164
|
+
if args.object_command == "download":
|
|
165
|
+
head = client.download_to(args.bucket, args.key, args.output, overwrite=args.force)
|
|
166
|
+
return {"output": str(args.output), "object": head}
|
|
167
|
+
if args.object_command == "head":
|
|
168
|
+
return client.head_object(args.bucket, args.key)
|
|
169
|
+
if args.object_command == "delete":
|
|
170
|
+
return client.delete_object(args.bucket, args.key)
|
|
171
|
+
if args.command == "keys":
|
|
172
|
+
if args.key_command == "list":
|
|
173
|
+
return client.list_keys()
|
|
174
|
+
if args.key_command == "create":
|
|
175
|
+
return client.create_key(name=args.name, read_only=args.read_only, bucket=args.bucket)
|
|
176
|
+
if args.key_command == "revoke":
|
|
177
|
+
return client.revoke_key(args.id)
|
|
178
|
+
if args.command == "share":
|
|
179
|
+
return client.create_share(args.bucket, args.key, expires_in=args.expires_in)
|
|
180
|
+
raise RuntimeError("unreachable command")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
184
|
+
parser = _parser()
|
|
185
|
+
args = parser.parse_args(argv)
|
|
186
|
+
if not args.token:
|
|
187
|
+
parser.error("a management token is required; set OPENBUCKET_ADMIN_TOKEN or pass --token")
|
|
188
|
+
try:
|
|
189
|
+
client = OpenBucketClient(args.url, args.token, timeout=args.timeout)
|
|
190
|
+
result = _run(client, args)
|
|
191
|
+
_emit(result, as_json=args.json)
|
|
192
|
+
return 0
|
|
193
|
+
except OpenBucketHTTPError as error:
|
|
194
|
+
print(str(error), file=sys.stderr)
|
|
195
|
+
if error.details is not None:
|
|
196
|
+
print(f"details: {json.dumps(error.details, ensure_ascii=False)}", file=sys.stderr)
|
|
197
|
+
return 1
|
|
198
|
+
except (OpenBucketError, OSError, ValueError) as error:
|
|
199
|
+
print(f"openbucket-client: {error}", file=sys.stderr)
|
|
200
|
+
return 1
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
raise SystemExit(main())
|