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 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: # pragma: no cover - network edge
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": # pragma: no cover - polling side effect
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: # pragma: no cover - network edge
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", "balbuena.ca").user_config_path / "config.toml"
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(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()
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 CONFIG
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
- aws = AWS(CONFIG.aws)
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"{CONFIG.url}{args.year}/{path.name} ...")
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=int, default=datetime.date.today().year, help="change year directory")
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
- aws = AWS(CONFIG.aws)
48
- print(aws.generate_needed_permissions())
49
- sys.exit(0)
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
- sys.exit(0)
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
- sys.exit(1)
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
- 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.
33
+ [![Documentation Status](https://readthedocs.org/projects/sobe/badge/?version=latest)](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
- # sobe configuration
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
- Upload files to a specific year:
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
- Upload and invalidate CloudFront cache:
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
- Get required AWS IAM policy:
74
+ The basic example is uploading files to current year directory:
104
75
  ```bash
105
- sobe --policy
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
- ## AWS Permissions
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.2
2
+ Generator: uv 0.9.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
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,,