sobe 0.2__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of sobe might be problematic. Click here for more details.
- sobe/aws.py +6 -4
- sobe/config.py +14 -10
- sobe/main.py +34 -14
- {sobe-0.2.dist-info → sobe-0.2.1.dist-info}/METADATA +24 -55
- sobe-0.2.1.dist-info/RECORD +8 -0
- {sobe-0.2.dist-info → sobe-0.2.1.dist-info}/WHEEL +1 -1
- sobe-0.2.dist-info/RECORD +0 -8
- {sobe-0.2.dist-info → sobe-0.2.1.dist-info}/entry_points.txt +0 -0
sobe/aws.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Everything related to AWS. In the future, we may support other cloud providers."""
|
|
2
|
+
|
|
1
3
|
import datetime
|
|
2
4
|
import json
|
|
3
5
|
import mimetypes
|
|
@@ -7,7 +9,7 @@ import time
|
|
|
7
9
|
import boto3
|
|
8
10
|
import botocore.exceptions
|
|
9
11
|
|
|
10
|
-
from .config import AWSConfig
|
|
12
|
+
from sobe.config import AWSConfig
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class AWS:
|
|
@@ -31,7 +33,7 @@ class AWS:
|
|
|
31
33
|
obj.load()
|
|
32
34
|
obj.delete()
|
|
33
35
|
return True
|
|
34
|
-
except botocore.exceptions.ClientError as e:
|
|
36
|
+
except botocore.exceptions.ClientError as e:
|
|
35
37
|
if e.response.get("Error", {}).get("Code") == "404":
|
|
36
38
|
return False
|
|
37
39
|
raise
|
|
@@ -44,7 +46,7 @@ class AWS:
|
|
|
44
46
|
response = self._cloudfront.create_invalidation(DistributionId=distribution, InvalidationBatch=batch)
|
|
45
47
|
invalidation = response["Invalidation"]["Id"]
|
|
46
48
|
status = "Created"
|
|
47
|
-
while status != "Completed":
|
|
49
|
+
while status != "Completed":
|
|
48
50
|
yield status
|
|
49
51
|
time.sleep(3)
|
|
50
52
|
response = self._cloudfront.get_invalidation(DistributionId=distribution, Id=invalidation)
|
|
@@ -55,7 +57,7 @@ class AWS:
|
|
|
55
57
|
try:
|
|
56
58
|
sts = self._session.client("sts", **self.config.service)
|
|
57
59
|
account_id = sts.get_caller_identity()["Account"]
|
|
58
|
-
except botocore.exceptions.ClientError:
|
|
60
|
+
except botocore.exceptions.ClientError:
|
|
59
61
|
account_id = "YOUR_ACCOUNT_ID"
|
|
60
62
|
|
|
61
63
|
actions = """
|
sobe/config.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
"""Everything related to user configuration file."""
|
|
2
|
+
|
|
1
3
|
import tomllib
|
|
4
|
+
from pathlib import Path
|
|
2
5
|
from typing import Any, NamedTuple, Self
|
|
3
6
|
|
|
4
7
|
from platformdirs import PlatformDirs
|
|
@@ -32,6 +35,13 @@ class Config(NamedTuple):
|
|
|
32
35
|
)
|
|
33
36
|
|
|
34
37
|
|
|
38
|
+
class MustEditConfig(Exception):
|
|
39
|
+
"""Config file must be edited before this tool can be used."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, path: Path):
|
|
42
|
+
self.path = path
|
|
43
|
+
|
|
44
|
+
|
|
35
45
|
DEFAULT_TEMPLATE = """
|
|
36
46
|
# sobe configuration
|
|
37
47
|
|
|
@@ -49,12 +59,12 @@ cloudfront = "E1111111111111"
|
|
|
49
59
|
# aws_secret_access_key = "..."
|
|
50
60
|
|
|
51
61
|
[aws.service]
|
|
52
|
-
verify = true
|
|
62
|
+
# verify = true
|
|
53
63
|
"""
|
|
54
64
|
|
|
55
65
|
|
|
56
66
|
def load_config() -> Config:
|
|
57
|
-
path = PlatformDirs("sobe"
|
|
67
|
+
path = PlatformDirs("sobe").user_config_path / "config.toml"
|
|
58
68
|
if path.exists():
|
|
59
69
|
with path.open("rb") as f:
|
|
60
70
|
payload = tomllib.load(f)
|
|
@@ -62,12 +72,6 @@ def load_config() -> Config:
|
|
|
62
72
|
return Config.from_dict(payload)
|
|
63
73
|
|
|
64
74
|
# create default file and exit for user to customize
|
|
65
|
-
defaults = "\n".join(line.strip() for line in DEFAULT_TEMPLATE.lstrip().splitlines())
|
|
66
75
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
-
path.write_text(
|
|
68
|
-
|
|
69
|
-
print(path)
|
|
70
|
-
raise SystemExit(1)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
CONFIG: Config = load_config()
|
|
76
|
+
path.write_text(DEFAULT_TEMPLATE.lstrip())
|
|
77
|
+
raise MustEditConfig(path)
|
sobe/main.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
"""Command-line interface entry point: input validation and output to user."""
|
|
2
|
+
|
|
1
3
|
import argparse
|
|
2
4
|
import datetime
|
|
3
5
|
import functools
|
|
4
6
|
import pathlib
|
|
5
|
-
import sys
|
|
6
7
|
import warnings
|
|
7
8
|
|
|
8
9
|
import urllib3.exceptions
|
|
9
10
|
|
|
10
|
-
from .aws import AWS
|
|
11
|
-
from .config import
|
|
11
|
+
from sobe.aws import AWS
|
|
12
|
+
from sobe.config import MustEditConfig, load_config
|
|
12
13
|
|
|
13
14
|
write = functools.partial(print, flush=True, end="")
|
|
14
15
|
print = functools.partial(print, flush=True) # type: ignore
|
|
@@ -17,10 +18,21 @@ warnings.filterwarnings("ignore", category=urllib3.exceptions.InsecureRequestWar
|
|
|
17
18
|
|
|
18
19
|
def main() -> None:
|
|
19
20
|
args = parse_args()
|
|
20
|
-
|
|
21
|
+
try:
|
|
22
|
+
config = load_config()
|
|
23
|
+
except MustEditConfig as err:
|
|
24
|
+
print("Created config file at the path below. You must edit it before use.")
|
|
25
|
+
print(err.path)
|
|
26
|
+
raise SystemExit(1) from err
|
|
27
|
+
|
|
28
|
+
aws = AWS(config.aws)
|
|
29
|
+
|
|
30
|
+
if args.policy:
|
|
31
|
+
print(aws.generate_needed_permissions())
|
|
32
|
+
return
|
|
21
33
|
|
|
22
34
|
for path in args.paths:
|
|
23
|
-
write(f"{
|
|
35
|
+
write(f"{config.url}{args.year}/{path.name} ...")
|
|
24
36
|
if args.delete:
|
|
25
37
|
existed = aws.delete(args.year, path.name)
|
|
26
38
|
print("deleted." if existed else "didn't exist.")
|
|
@@ -34,23 +46,31 @@ def main() -> None:
|
|
|
34
46
|
print("complete.")
|
|
35
47
|
|
|
36
48
|
|
|
37
|
-
def parse_args() -> argparse.Namespace:
|
|
49
|
+
def parse_args(argv=None) -> argparse.Namespace:
|
|
38
50
|
parser = argparse.ArgumentParser(description="Upload files to your AWS drop box.")
|
|
39
|
-
parser.add_argument("-y", "--year", type=
|
|
51
|
+
parser.add_argument("-y", "--year", type=str, help="change year directory")
|
|
40
52
|
parser.add_argument("-i", "--invalidate", action="store_true", help="invalidate CloudFront cache")
|
|
41
53
|
parser.add_argument("-d", "--delete", action="store_true", help="delete instead of upload")
|
|
42
|
-
parser.add_argument("--policy", action="store_true", help="generate IAM policy requirements and exit")
|
|
54
|
+
parser.add_argument("-p", "--policy", action="store_true", help="generate IAM policy requirements and exit")
|
|
43
55
|
parser.add_argument("files", nargs="*", help="Source files.")
|
|
44
|
-
args = parser.parse_args()
|
|
56
|
+
args = parser.parse_args(argv)
|
|
45
57
|
|
|
46
58
|
if args.policy:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
if args.year or args.delete or args.invalidate or args.files:
|
|
60
|
+
parser.error("--policy cannot be used with other arguments")
|
|
61
|
+
return args
|
|
62
|
+
|
|
63
|
+
if args.year is None:
|
|
64
|
+
args.year = datetime.date.today().year
|
|
65
|
+
elif not args.files:
|
|
66
|
+
parser.error("--year requires files to be specified")
|
|
67
|
+
|
|
68
|
+
if args.delete and not args.files:
|
|
69
|
+
parser.error("--delete requires files to be specified")
|
|
50
70
|
|
|
51
71
|
if not args.files and not args.invalidate:
|
|
52
72
|
parser.print_help()
|
|
53
|
-
|
|
73
|
+
raise SystemExit(0)
|
|
54
74
|
|
|
55
75
|
args.paths = [pathlib.Path(p) for p in args.files]
|
|
56
76
|
if not args.delete:
|
|
@@ -59,6 +79,6 @@ def parse_args() -> argparse.Namespace:
|
|
|
59
79
|
print("The following files do not exist:")
|
|
60
80
|
for p in missing:
|
|
61
81
|
print(f" {p}")
|
|
62
|
-
|
|
82
|
+
raise SystemExit(1)
|
|
63
83
|
|
|
64
84
|
return args
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sobe
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: AWS-based drop box uploader
|
|
5
5
|
Author: Liz Balbuena
|
|
6
6
|
License-Expression: MIT
|
|
@@ -16,20 +16,30 @@ Classifier: Topic :: Communications :: File Sharing
|
|
|
16
16
|
Classifier: Topic :: Utilities
|
|
17
17
|
Requires-Dist: boto3>=1.40.49
|
|
18
18
|
Requires-Dist: platformdirs>=4.5.0
|
|
19
|
+
Requires-Dist: furo>=2024.8.6 ; extra == 'docs'
|
|
20
|
+
Requires-Dist: sphinx>=7.0.0 ; extra == 'docs'
|
|
21
|
+
Requires-Dist: sphinx-autodoc-typehints>=2.0.0 ; extra == 'docs'
|
|
19
22
|
Requires-Python: >=3.11
|
|
20
23
|
Project-URL: Changelog, https://github.com/Liz4v/sobe/releases
|
|
21
24
|
Project-URL: Documentation, https://github.com/Liz4v/sobe/blob/main/README.md
|
|
22
25
|
Project-URL: Homepage, https://github.com/Liz4v/sobe
|
|
23
26
|
Project-URL: Issues, https://github.com/Liz4v/sobe/issues
|
|
24
27
|
Project-URL: Repository, https://github.com/Liz4v/sobe.git
|
|
28
|
+
Provides-Extra: docs
|
|
25
29
|
Description-Content-Type: text/markdown
|
|
26
30
|
|
|
27
31
|
# sobe
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
[](https://sobe.readthedocs.io/en/latest/)
|
|
34
|
+
|
|
35
|
+
A simple command-line tool to upload files to an AWS S3 bucket that is publicly available through a CloudFront distribution. This is the traditional "drop box" use case that existed long before the advent of modern file sharing services.
|
|
36
|
+
|
|
37
|
+
Full documentation: https://sobe.readthedocs.io/en/latest/
|
|
30
38
|
|
|
31
39
|
It will upload any files you give it to your bucket, in a current year subdirectory, because that's the only easy way to organize chaos.
|
|
32
40
|
|
|
41
|
+
"Sobe" is Portuguese for "take it up" (in the imperative), as in "upload".
|
|
42
|
+
|
|
33
43
|
## Installation
|
|
34
44
|
|
|
35
45
|
Use [uv](https://docs.astral.sh/uv/) to manage it.
|
|
@@ -46,70 +56,29 @@ pip install sobe
|
|
|
46
56
|
|
|
47
57
|
## Configuration
|
|
48
58
|
|
|
49
|
-
On first run, `sobe` will create its config file as appropriate to the platform. You'll need to edit this file with your AWS bucket and CloudFront details
|
|
59
|
+
On first run, `sobe` will create its config file as appropriate to the platform and tell you its location. You'll need to edit this file with your AWS bucket and CloudFront details.
|
|
60
|
+
|
|
61
|
+
Here's a minimal set up.
|
|
50
62
|
|
|
51
63
|
```toml
|
|
52
|
-
|
|
64
|
+
url = "https://example.com/"
|
|
65
|
+
[aws]
|
|
53
66
|
bucket = "your-bucket-name"
|
|
54
|
-
url = "https://your-public-url/"
|
|
55
67
|
cloudfront = "your-cloudfront-distribution-id"
|
|
56
|
-
|
|
57
|
-
[aws_session]
|
|
58
|
-
# If you already have AWS CLI set up, don't fill keys here.
|
|
59
|
-
# region_name = "..."
|
|
60
|
-
# profile_name = "..."
|
|
61
|
-
# aws_access_key_id = "..."
|
|
62
|
-
# aws_secret_access_key = "..."
|
|
63
|
-
|
|
64
|
-
[aws_client]
|
|
65
|
-
verify = true
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Usage
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
sobe [options] files...
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Options
|
|
75
|
-
|
|
76
|
-
- `-y`, `--year`: Change the target year directory (default: current year)
|
|
77
|
-
- `-i`, `--invalidate`: Invalidate CloudFront cache after upload
|
|
78
|
-
- `-d`, `--delete`: Delete files instead of uploading
|
|
79
|
-
- `-p`, `--policy`: Display required AWS IAM policy and exit
|
|
80
|
-
|
|
81
|
-
### Examples
|
|
82
|
-
|
|
83
|
-
Upload files to current year directory:
|
|
84
|
-
```bash
|
|
85
|
-
sobe file1.jpg file2.pdf
|
|
86
68
|
```
|
|
87
69
|
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
sobe -y 2024 file1.jpg file2.pdf
|
|
91
|
-
```
|
|
70
|
+
[More information in the docs.](https://sobe.readthedocs.io/en/latest/configuration.html)
|
|
92
71
|
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
sobe -i file1.jpg
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Delete files:
|
|
99
|
-
```bash
|
|
100
|
-
sobe -d file1.jpg
|
|
101
|
-
```
|
|
72
|
+
## Usage
|
|
102
73
|
|
|
103
|
-
|
|
74
|
+
The basic example is uploading files to current year directory:
|
|
104
75
|
```bash
|
|
105
|
-
sobe
|
|
76
|
+
$ sobe file1.jpg file2.pdf
|
|
77
|
+
https://example.com/2025/file1.jpg ...ok.
|
|
78
|
+
https://example.com/2025/file2.pdf ...ok.
|
|
106
79
|
```
|
|
107
80
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Use `sobe --policy` to generate the exact IAM policy required for your configuration. The tool needs permissions for:
|
|
111
|
-
- S3: PutObject, GetObject, ListBucket, DeleteObject
|
|
112
|
-
- CloudFront: CreateInvalidation, GetInvalidation
|
|
81
|
+
You can call it with `--help` for all available options. You can delete files, clear the CloudFront cache (cached objects stay for 1 day by default), tweak the upload year. [The documentation contains better examples.](https://sobe.readthedocs.io/en/latest/usage.html#command-line-interface)
|
|
113
82
|
|
|
114
83
|
## License
|
|
115
84
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
sobe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
sobe/aws.py,sha256=GcyN2A2NW74lYjNO0Lm8kecBTE89BwnCcLN6VX1JU5c,3151
|
|
3
|
+
sobe/config.py,sha256=A1T0rMHCZYF2yr_WygkURrv6vh1gOs2nH12OhNgehp4,1916
|
|
4
|
+
sobe/main.py,sha256=_p-FPPnDfM_iAmRuxnBqCOvP3mVDWldnFFepRwnqGVM,2811
|
|
5
|
+
sobe-0.2.1.dist-info/WHEEL,sha256=k57ZwB-NkeM_6AsPnuOHv5gI5KM5kPD6Vx85WmGEcI0,78
|
|
6
|
+
sobe-0.2.1.dist-info/entry_points.txt,sha256=a_cKExqUEzJ-t2MRWbxAHc8OavQIaL8a7JQ3obR2b-c,41
|
|
7
|
+
sobe-0.2.1.dist-info/METADATA,sha256=NVODkko2xKNg2EDNOWjP9fvDuDhEg1uhh4sSTYrmYME,3054
|
|
8
|
+
sobe-0.2.1.dist-info/RECORD,,
|
sobe-0.2.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
sobe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
sobe/aws.py,sha256=X5r0MTCfSQPhqRMHun0710ecZSV-6eOd_4E38oxBSsY,3172
|
|
3
|
-
sobe/config.py,sha256=a4lc7Rlw5Zs4QGdNRIhB8pscJxBn-ze2WHnzWV4d7ro,1882
|
|
4
|
-
sobe/main.py,sha256=M40CqclSwlCZeJvFi52kiyBIqcQ8Z1C9b7QsWk-tvt4,2079
|
|
5
|
-
sobe-0.2.dist-info/WHEEL,sha256=X16MKk8bp2DRsAuyteHJ-9qOjzmnY0x1aj0P1ftqqWA,78
|
|
6
|
-
sobe-0.2.dist-info/entry_points.txt,sha256=a_cKExqUEzJ-t2MRWbxAHc8OavQIaL8a7JQ3obR2b-c,41
|
|
7
|
-
sobe-0.2.dist-info/METADATA,sha256=vapxWrCS_zDIlXosohc-Px3k48Q_KG7KG-xswlWEqMM,3105
|
|
8
|
-
sobe-0.2.dist-info/RECORD,,
|
|
File without changes
|