sobe 0.1__tar.gz → 0.2__tar.gz
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.
- {sobe-0.1 → sobe-0.2}/PKG-INFO +27 -4
- {sobe-0.1 → sobe-0.2}/README.md +7 -1
- sobe-0.2/pyproject.toml +48 -0
- sobe-0.2/src/sobe/aws.py +72 -0
- sobe-0.2/src/sobe/config.py +73 -0
- sobe-0.2/src/sobe/main.py +64 -0
- sobe-0.1/pyproject.toml +0 -25
- sobe-0.1/src/sobe/main.py +0 -153
- {sobe-0.1 → sobe-0.2}/src/sobe/__init__.py +0 -0
{sobe-0.1 → sobe-0.2}/PKG-INFO
RENAMED
|
@@ -1,10 +1,27 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sobe
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2
|
|
4
4
|
Summary: AWS-based drop box uploader
|
|
5
|
+
Author: Liz Balbuena
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Communications :: File Sharing
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
5
17
|
Requires-Dist: boto3>=1.40.49
|
|
6
18
|
Requires-Dist: platformdirs>=4.5.0
|
|
7
|
-
Requires-Python:
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Project-URL: Changelog, https://github.com/Liz4v/sobe/releases
|
|
21
|
+
Project-URL: Documentation, https://github.com/Liz4v/sobe/blob/main/README.md
|
|
22
|
+
Project-URL: Homepage, https://github.com/Liz4v/sobe
|
|
23
|
+
Project-URL: Issues, https://github.com/Liz4v/sobe/issues
|
|
24
|
+
Project-URL: Repository, https://github.com/Liz4v/sobe.git
|
|
8
25
|
Description-Content-Type: text/markdown
|
|
9
26
|
|
|
10
27
|
# sobe
|
|
@@ -18,7 +35,13 @@ It will upload any files you give it to your bucket, in a current year subdirect
|
|
|
18
35
|
Use [uv](https://docs.astral.sh/uv/) to manage it.
|
|
19
36
|
|
|
20
37
|
```bash
|
|
21
|
-
uv tool install
|
|
38
|
+
uv tool install sobe
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If you have Python ≥ 3.11, you can also install it via pip:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install sobe
|
|
22
45
|
```
|
|
23
46
|
|
|
24
47
|
## Configuration
|
{sobe-0.1 → sobe-0.2}/README.md
RENAMED
|
@@ -9,7 +9,13 @@ It will upload any files you give it to your bucket, in a current year subdirect
|
|
|
9
9
|
Use [uv](https://docs.astral.sh/uv/) to manage it.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
uv tool install
|
|
12
|
+
uv tool install sobe
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
If you have Python ≥ 3.11, you can also install it via pip:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install sobe
|
|
13
19
|
```
|
|
14
20
|
|
|
15
21
|
## Configuration
|
sobe-0.2/pyproject.toml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sobe"
|
|
3
|
+
version = "0.2"
|
|
4
|
+
description = "AWS-based drop box uploader"
|
|
5
|
+
classifiers = [
|
|
6
|
+
"Development Status :: 5 - Production/Stable",
|
|
7
|
+
"Environment :: Console",
|
|
8
|
+
"License :: OSI Approved :: MIT License",
|
|
9
|
+
"Operating System :: OS Independent",
|
|
10
|
+
"Programming Language :: Python",
|
|
11
|
+
"Programming Language :: Python :: 3.11",
|
|
12
|
+
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Programming Language :: Python :: 3.13",
|
|
14
|
+
"Topic :: Communications :: File Sharing",
|
|
15
|
+
"Topic :: Utilities",
|
|
16
|
+
]
|
|
17
|
+
authors = [
|
|
18
|
+
{name = "Liz Balbuena"},
|
|
19
|
+
]
|
|
20
|
+
license = "MIT"
|
|
21
|
+
readme = "README.md"
|
|
22
|
+
requires-python = ">=3.11"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"boto3>=1.40.49",
|
|
25
|
+
"platformdirs>=4.5.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
sobe = "sobe.main:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/Liz4v/sobe"
|
|
33
|
+
Documentation = "https://github.com/Liz4v/sobe/blob/main/README.md"
|
|
34
|
+
Repository = "https://github.com/Liz4v/sobe.git"
|
|
35
|
+
Issues = "https://github.com/Liz4v/sobe/issues"
|
|
36
|
+
Changelog = "https://github.com/Liz4v/sobe/releases"
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 120
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["uv_build"]
|
|
43
|
+
build-backend = "uv_build"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"ruff>=0.14.0",
|
|
48
|
+
]
|
sobe-0.2/src/sobe/aws.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import mimetypes
|
|
4
|
+
import pathlib
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
import botocore.exceptions
|
|
9
|
+
|
|
10
|
+
from .config import AWSConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AWS:
|
|
14
|
+
def __init__(self, config: AWSConfig) -> None:
|
|
15
|
+
self.config = config
|
|
16
|
+
self._session = boto3.Session(**self.config.session)
|
|
17
|
+
self._s3_resource = self._session.resource("s3", **self.config.service)
|
|
18
|
+
self._bucket = self._s3_resource.Bucket(self.config.bucket) # type: ignore[attr-defined]
|
|
19
|
+
self._cloudfront = self._session.client("cloudfront", **self.config.service)
|
|
20
|
+
|
|
21
|
+
def upload(self, year: str, local_path: pathlib.Path) -> None:
|
|
22
|
+
"""Uploads a file."""
|
|
23
|
+
type_guess, _ = mimetypes.guess_type(local_path)
|
|
24
|
+
extra_args = {"ContentType": type_guess or "application/octet-stream"}
|
|
25
|
+
self._bucket.upload_file(str(local_path), f"{year}/{local_path.name}", ExtraArgs=extra_args)
|
|
26
|
+
|
|
27
|
+
def delete(self, year: str, remote_filename: str) -> bool:
|
|
28
|
+
"""Delete a file, if it exists. Returns whether it did."""
|
|
29
|
+
obj = self._bucket.Object(f"{year}/{remote_filename}")
|
|
30
|
+
try:
|
|
31
|
+
obj.load()
|
|
32
|
+
obj.delete()
|
|
33
|
+
return True
|
|
34
|
+
except botocore.exceptions.ClientError as e: # pragma: no cover - network edge
|
|
35
|
+
if e.response.get("Error", {}).get("Code") == "404":
|
|
36
|
+
return False
|
|
37
|
+
raise
|
|
38
|
+
|
|
39
|
+
def invalidate_cache(self):
|
|
40
|
+
"""Create and wait for a full-path CloudFront invalidation. Iterates until completion."""
|
|
41
|
+
ref = datetime.datetime.now().astimezone().isoformat()
|
|
42
|
+
batch = {"Paths": {"Quantity": 1, "Items": ["/*"]}, "CallerReference": ref}
|
|
43
|
+
distribution = self.config.cloudfront
|
|
44
|
+
response = self._cloudfront.create_invalidation(DistributionId=distribution, InvalidationBatch=batch)
|
|
45
|
+
invalidation = response["Invalidation"]["Id"]
|
|
46
|
+
status = "Created"
|
|
47
|
+
while status != "Completed": # pragma: no cover - polling side effect
|
|
48
|
+
yield status
|
|
49
|
+
time.sleep(3)
|
|
50
|
+
response = self._cloudfront.get_invalidation(DistributionId=distribution, Id=invalidation)
|
|
51
|
+
status = response["Invalidation"]["Status"]
|
|
52
|
+
|
|
53
|
+
def generate_needed_permissions(self) -> str:
|
|
54
|
+
"""Return the minimal IAM policy statement required by the tool."""
|
|
55
|
+
try:
|
|
56
|
+
sts = self._session.client("sts", **self.config.service)
|
|
57
|
+
account_id = sts.get_caller_identity()["Account"]
|
|
58
|
+
except botocore.exceptions.ClientError: # pragma: no cover - network edge
|
|
59
|
+
account_id = "YOUR_ACCOUNT_ID"
|
|
60
|
+
|
|
61
|
+
actions = """
|
|
62
|
+
s3:PutObject s3:GetObject s3:ListBucket s3:DeleteObject
|
|
63
|
+
cloudfront:CreateInvalidation cloudfront:GetInvalidation
|
|
64
|
+
""".split()
|
|
65
|
+
resources = [
|
|
66
|
+
f"arn:aws:s3:::{self.config.bucket}",
|
|
67
|
+
f"arn:aws:s3:::{self.config.bucket}/*",
|
|
68
|
+
f"arn:aws:cloudfront::{account_id}:distribution/{self.config.cloudfront}",
|
|
69
|
+
]
|
|
70
|
+
statement = {"Effect": "Allow", "Action": actions, "Resource": resources}
|
|
71
|
+
policy = {"Version": "2012-10-17", "Statement": [statement]}
|
|
72
|
+
return json.dumps(policy, indent=2)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
from typing import Any, NamedTuple, Self
|
|
3
|
+
|
|
4
|
+
from platformdirs import PlatformDirs
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AWSConfig(NamedTuple):
|
|
8
|
+
bucket: str
|
|
9
|
+
cloudfront: str
|
|
10
|
+
session: dict[str, Any]
|
|
11
|
+
service: dict[str, Any]
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def from_dict(cls, raw: dict[str, Any]) -> Self:
|
|
15
|
+
return cls(
|
|
16
|
+
bucket=raw.get("bucket", "example-bucket"),
|
|
17
|
+
cloudfront=raw.get("cloudfront", "E1111111111111"),
|
|
18
|
+
session=raw.get("session", {}),
|
|
19
|
+
service=raw.get("service", {}),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Config(NamedTuple):
|
|
24
|
+
url: str
|
|
25
|
+
aws: AWSConfig
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_dict(cls, raw: dict[str, Any]) -> Self:
|
|
29
|
+
return cls(
|
|
30
|
+
url=raw.get("url", "https://example.com/"),
|
|
31
|
+
aws=AWSConfig.from_dict(raw.get("aws", {})),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
DEFAULT_TEMPLATE = """
|
|
36
|
+
# sobe configuration
|
|
37
|
+
|
|
38
|
+
url = "https://example.com/"
|
|
39
|
+
|
|
40
|
+
[aws]
|
|
41
|
+
bucket = "example-bucket"
|
|
42
|
+
cloudfront = "E1111111111111"
|
|
43
|
+
|
|
44
|
+
[aws.session]
|
|
45
|
+
# If you already have AWS CLI set up, don't fill keys here.
|
|
46
|
+
# region_name = "..."
|
|
47
|
+
# profile_name = "..."
|
|
48
|
+
# aws_access_key_id = "..."
|
|
49
|
+
# aws_secret_access_key = "..."
|
|
50
|
+
|
|
51
|
+
[aws.service]
|
|
52
|
+
verify = true
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_config() -> Config:
|
|
57
|
+
path = PlatformDirs("sobe", "balbuena.ca").user_config_path / "config.toml"
|
|
58
|
+
if path.exists():
|
|
59
|
+
with path.open("rb") as f:
|
|
60
|
+
payload = tomllib.load(f)
|
|
61
|
+
if payload.get("aws", {}).get("bucket", "example-bucket") != "example-bucket":
|
|
62
|
+
return Config.from_dict(payload)
|
|
63
|
+
|
|
64
|
+
# create default file and exit for user to customize
|
|
65
|
+
defaults = "\n".join(line.strip() for line in DEFAULT_TEMPLATE.lstrip().splitlines())
|
|
66
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
path.write_text(defaults)
|
|
68
|
+
print("Created config file at the path below. You must edit it before use.")
|
|
69
|
+
print(path)
|
|
70
|
+
raise SystemExit(1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
CONFIG: Config = load_config()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import datetime
|
|
3
|
+
import functools
|
|
4
|
+
import pathlib
|
|
5
|
+
import sys
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
import urllib3.exceptions
|
|
9
|
+
|
|
10
|
+
from .aws import AWS
|
|
11
|
+
from .config import CONFIG
|
|
12
|
+
|
|
13
|
+
write = functools.partial(print, flush=True, end="")
|
|
14
|
+
print = functools.partial(print, flush=True) # type: ignore
|
|
15
|
+
warnings.filterwarnings("ignore", category=urllib3.exceptions.InsecureRequestWarning)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
args = parse_args()
|
|
20
|
+
aws = AWS(CONFIG.aws)
|
|
21
|
+
|
|
22
|
+
for path in args.paths:
|
|
23
|
+
write(f"{CONFIG.url}{args.year}/{path.name} ...")
|
|
24
|
+
if args.delete:
|
|
25
|
+
existed = aws.delete(args.year, path.name)
|
|
26
|
+
print("deleted." if existed else "didn't exist.")
|
|
27
|
+
else:
|
|
28
|
+
aws.upload(args.year, path)
|
|
29
|
+
print("ok.")
|
|
30
|
+
if args.invalidate:
|
|
31
|
+
write("Clearing cache...")
|
|
32
|
+
for _ in aws.invalidate_cache():
|
|
33
|
+
write(".")
|
|
34
|
+
print("complete.")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_args() -> argparse.Namespace:
|
|
38
|
+
parser = argparse.ArgumentParser(description="Upload files to your AWS drop box.")
|
|
39
|
+
parser.add_argument("-y", "--year", type=int, default=datetime.date.today().year, help="change year directory")
|
|
40
|
+
parser.add_argument("-i", "--invalidate", action="store_true", help="invalidate CloudFront cache")
|
|
41
|
+
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")
|
|
43
|
+
parser.add_argument("files", nargs="*", help="Source files.")
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
|
|
46
|
+
if args.policy:
|
|
47
|
+
aws = AWS(CONFIG.aws)
|
|
48
|
+
print(aws.generate_needed_permissions())
|
|
49
|
+
sys.exit(0)
|
|
50
|
+
|
|
51
|
+
if not args.files and not args.invalidate:
|
|
52
|
+
parser.print_help()
|
|
53
|
+
sys.exit(0)
|
|
54
|
+
|
|
55
|
+
args.paths = [pathlib.Path(p) for p in args.files]
|
|
56
|
+
if not args.delete:
|
|
57
|
+
missing = [p for p in args.paths if not p.exists()]
|
|
58
|
+
if missing:
|
|
59
|
+
print("The following files do not exist:")
|
|
60
|
+
for p in missing:
|
|
61
|
+
print(f" {p}")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
return args
|
sobe-0.1/pyproject.toml
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "sobe"
|
|
3
|
-
version = "0.1"
|
|
4
|
-
description = "AWS-based drop box uploader"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
requires-python = "==3.13.*"
|
|
7
|
-
dependencies = [
|
|
8
|
-
"boto3>=1.40.49",
|
|
9
|
-
"platformdirs>=4.5.0",
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
[project.scripts]
|
|
13
|
-
sobe = "sobe.main:main"
|
|
14
|
-
|
|
15
|
-
[tool.ruff]
|
|
16
|
-
line-length = 120
|
|
17
|
-
|
|
18
|
-
[build-system]
|
|
19
|
-
requires = ["uv_build"]
|
|
20
|
-
build-backend = "uv_build"
|
|
21
|
-
|
|
22
|
-
[dependency-groups]
|
|
23
|
-
dev = [
|
|
24
|
-
"ruff>=0.14.0",
|
|
25
|
-
]
|
sobe-0.1/src/sobe/main.py
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
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))
|
|
File without changes
|