sobe 0.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/__init__.py ADDED
File without changes
sobe/main.py ADDED
@@ -0,0 +1,153 @@
1
+ import argparse
2
+ import datetime
3
+ import functools
4
+ import json
5
+ import mimetypes
6
+ import pathlib
7
+ import sys
8
+ import time
9
+ import tomllib
10
+ import warnings
11
+
12
+ import boto3
13
+ import botocore.exceptions
14
+ import platformdirs
15
+ import urllib3.exceptions
16
+
17
+
18
+ def load_config():
19
+ path = platformdirs.PlatformDirs("sobe", "balbuena.ca").user_config_path / "config.toml"
20
+ if path.exists():
21
+ with path.open("rb") as f:
22
+ payload = tomllib.load(f)
23
+ if payload["bucket"] != "example-bucket":
24
+ return payload
25
+
26
+ defaults = """
27
+ # sobe configuration
28
+ bucket = "example-bucket"
29
+ url = "https://example.com/"
30
+ cloudfront = "E1111111111111"
31
+
32
+ [aws_session]
33
+ # If you already have AWS CLI set up, don't fill keys here.
34
+ # region_name = "..."
35
+ # profile_name = "..."
36
+ # aws_access_key_id = "..."
37
+ # aws_secret_access_key = "..."
38
+
39
+ [aws_client]
40
+ verify = true
41
+ """
42
+ defaults = "\n".join(line.strip() for line in defaults.lstrip().splitlines())
43
+ path.parent.mkdir(parents=True, exist_ok=True)
44
+ path.write_text(defaults)
45
+ print("Created config file at the path below. You must edit it before use.")
46
+ print(path)
47
+ sys.exit(1)
48
+
49
+
50
+ CONFIG = load_config()
51
+ write = functools.partial(print, flush=True, end="")
52
+ print = functools.partial(print, flush=True) # type: ignore
53
+ warnings.filterwarnings("ignore", category=urllib3.exceptions.InsecureRequestWarning)
54
+
55
+
56
+ def main() -> None:
57
+ args = parse_args()
58
+ session = boto3.Session(**CONFIG["aws_session"])
59
+ bucket = session.resource("s3", **CONFIG["aws_client"]).Bucket(CONFIG["bucket"])
60
+ for path, key in zip(args.paths, args.keys):
61
+ if args.delete:
62
+ delete(bucket, key)
63
+ else:
64
+ upload(bucket, path, key)
65
+ if args.invalidate:
66
+ invalidate(session)
67
+
68
+
69
+ def upload(bucket, path: pathlib.Path, remote_path: str) -> None:
70
+ write(f"{CONFIG['url']}{remote_path} ...")
71
+ type_guess, _ = mimetypes.guess_type(path)
72
+ extra_args = {"ContentType": type_guess or "application/octet-stream"}
73
+ bucket.upload_file(str(path), remote_path, ExtraArgs=extra_args)
74
+ print("ok.")
75
+
76
+
77
+ def delete(bucket, remote_path: str) -> None:
78
+ write(f"{CONFIG['url']}{remote_path} ...")
79
+ obj = bucket.Object(remote_path)
80
+ try:
81
+ obj.load()
82
+ obj.delete()
83
+ print("deleted.")
84
+ except botocore.exceptions.ClientError as e:
85
+ if e.response["Error"]["Code"] != "404":
86
+ raise
87
+ print("didn't exist.")
88
+
89
+
90
+ def invalidate(session: boto3.Session) -> None:
91
+ write("Clearing cache ...")
92
+ ref = datetime.datetime.now().astimezone().isoformat()
93
+ cloudfront = session.client("cloudfront", **CONFIG["aws_client"])
94
+ batch = {"Paths": {"Quantity": 1, "Items": ["/*"]}, "CallerReference": ref}
95
+ invalidation = cloudfront.create_invalidation(DistributionId=CONFIG["cloudfront"], InvalidationBatch=batch)
96
+ write("ok.")
97
+ invalidation_id = invalidation["Invalidation"]["Id"]
98
+ status = ""
99
+ while status != "Completed":
100
+ time.sleep(3)
101
+ write(".")
102
+ response = cloudfront.get_invalidation(DistributionId=CONFIG["cloudfront"], Id=invalidation_id)
103
+ status = response["Invalidation"]["Status"]
104
+ print("complete.")
105
+
106
+
107
+ def parse_args() -> argparse.Namespace:
108
+ parser = argparse.ArgumentParser(description="Upload files to your AWS drop box.")
109
+ parser.add_argument("-y", "--year", type=int, default=datetime.date.today().year, help="change year directory")
110
+ parser.add_argument("-i", "--invalidate", action="store_true", help="invalidate CloudFront cache")
111
+ parser.add_argument("-d", "--delete", action="store_true", help="delete instead of upload")
112
+ parser.add_argument("--policy", action="store_true", help="display IAM policy requirements and exit")
113
+ parser.add_argument("files", nargs="*", help="Source files.")
114
+ args = parser.parse_args()
115
+
116
+ if args.policy:
117
+ dump_policy()
118
+ sys.exit(0)
119
+
120
+ if not args.files and not args.invalidate:
121
+ parser.print_help()
122
+ sys.exit(0)
123
+
124
+ args.paths = [pathlib.Path(p) for p in args.files]
125
+ args.keys = [f"{args.year}/{p.name}" for p in args.paths]
126
+ if not args.delete:
127
+ missing = [p for p in args.paths if not p.exists()]
128
+ if missing:
129
+ print("The following files do not exist:")
130
+ for p in missing:
131
+ print(f" {p}")
132
+ sys.exit(1)
133
+
134
+ return args
135
+
136
+
137
+ def dump_policy() -> None:
138
+ session = boto3.Session(**CONFIG["aws_session"])
139
+ sts = session.client("sts", **CONFIG["aws_client"])
140
+ caller = sts.get_caller_identity()["Arn"]
141
+ account_id = caller.split(":")[4]
142
+ actions = """
143
+ s3:PutObject s3:GetObject s3:ListBucket s3:DeleteObject
144
+ cloudfront:CreateInvalidation cloudfront:GetInvalidation
145
+ """.split()
146
+ resources = [
147
+ f"arn:aws:s3:::{CONFIG['bucket']}",
148
+ f"arn:aws:s3:::{CONFIG['bucket']}/*",
149
+ f"arn:aws:cloudfront::{account_id}:distribution/{CONFIG['cloudfront']}",
150
+ ]
151
+ statement = {"Effect": "Allow", "Action": actions, "Resource": resources}
152
+ policy = {"Version": "2012-10-17", "Statement": [statement]}
153
+ print(json.dumps(policy, indent=2))
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.3
2
+ Name: sobe
3
+ Version: 0.1
4
+ Summary: AWS-based drop box uploader
5
+ Requires-Dist: boto3>=1.40.49
6
+ Requires-Dist: platformdirs>=4.5.0
7
+ Requires-Python: ==3.13.*
8
+ Description-Content-Type: text/markdown
9
+
10
+ # sobe
11
+
12
+ A simple command-line tool for uploading 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.
13
+
14
+ 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.
15
+
16
+ ## Installation
17
+
18
+ Use [uv](https://docs.astral.sh/uv/) to manage it.
19
+
20
+ ```bash
21
+ uv tool install https://github.com/Liz4v/sobe.git
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ 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:
27
+
28
+ ```toml
29
+ # sobe configuration
30
+ bucket = "your-bucket-name"
31
+ url = "https://your-public-url/"
32
+ cloudfront = "your-cloudfront-distribution-id"
33
+
34
+ [aws_session]
35
+ # If you already have AWS CLI set up, don't fill keys here.
36
+ # region_name = "..."
37
+ # profile_name = "..."
38
+ # aws_access_key_id = "..."
39
+ # aws_secret_access_key = "..."
40
+
41
+ [aws_client]
42
+ verify = true
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```bash
48
+ sobe [options] files...
49
+ ```
50
+
51
+ ### Options
52
+
53
+ - `-y`, `--year`: Change the target year directory (default: current year)
54
+ - `-i`, `--invalidate`: Invalidate CloudFront cache after upload
55
+ - `-d`, `--delete`: Delete files instead of uploading
56
+ - `-p`, `--policy`: Display required AWS IAM policy and exit
57
+
58
+ ### Examples
59
+
60
+ Upload files to current year directory:
61
+ ```bash
62
+ sobe file1.jpg file2.pdf
63
+ ```
64
+
65
+ Upload files to a specific year:
66
+ ```bash
67
+ sobe -y 2024 file1.jpg file2.pdf
68
+ ```
69
+
70
+ Upload and invalidate CloudFront cache:
71
+ ```bash
72
+ sobe -i file1.jpg
73
+ ```
74
+
75
+ Delete files:
76
+ ```bash
77
+ sobe -d file1.jpg
78
+ ```
79
+
80
+ Get required AWS IAM policy:
81
+ ```bash
82
+ sobe --policy
83
+ ```
84
+
85
+ ## AWS Permissions
86
+
87
+ Use `sobe --policy` to generate the exact IAM policy required for your configuration. The tool needs permissions for:
88
+ - S3: PutObject, GetObject, ListBucket, DeleteObject
89
+ - CloudFront: CreateInvalidation, GetInvalidation
90
+
91
+ ## License
92
+
93
+ See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,6 @@
1
+ sobe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sobe/main.py,sha256=3x3wsfo4l_rh7Nr-GEsL3jiqU8jTrYINkojzvKTB1eI,5232
3
+ sobe-0.1.dist-info/WHEEL,sha256=ZbtZh9LqsQoZs-WmwRO6z-tavdkb5LzNxvrOv2F_OXE,78
4
+ sobe-0.1.dist-info/entry_points.txt,sha256=a_cKExqUEzJ-t2MRWbxAHc8OavQIaL8a7JQ3obR2b-c,41
5
+ sobe-0.1.dist-info/METADATA,sha256=pd-fXFiH_aB6x8dr-tav4oUE4vtgE4FSlQ-_1FJ0ZSw,2219
6
+ sobe-0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ sobe = sobe.main:main
3
+