zipline-cli 0.2.2__tar.gz → 0.2.3__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.
@@ -1,25 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zipline-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Python 3 CLI File Uploader for Diced/Zipline v3/v4 Upload Server and Django Files.
5
5
  Author: Shane
6
6
  License: MIT License
7
- Project-URL: GitHub, https://github.com/cssnr/zipline-cli
8
- Project-URL: Issues, https://github.com/cssnr/zipline-cli/issues
9
7
  Project-URL: Changelog, https://github.com/cssnr/zipline-cli/releases
8
+ Project-URL: Discord, https://discord.gg/wXy6m2X8wY
10
9
  Project-URL: Documentation, https://zipline-cli.cssnr.com/
11
- Project-URL: Homepage, https://cssnr.com/
12
10
  Project-URL: Funding, https://ko-fi.com/cssnr
13
- Project-URL: Discord, https://discord.gg/wXy6m2X8wY
11
+ Project-URL: GitHub, https://github.com/cssnr/zipline-cli
12
+ Project-URL: Homepage, https://cssnr.com/
13
+ Project-URL: Issues, https://github.com/cssnr/zipline-cli/issues
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
14
17
  Classifier: Programming Language :: Python
15
18
  Classifier: Programming Language :: Python :: 3
16
19
  Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Operating System :: OS Independent
18
- Classifier: License :: OSI Approved :: MIT License
19
- Classifier: Natural Language :: English
20
- Classifier: Typing :: Typed
21
20
  Classifier: Topic :: Communications :: File Sharing
22
21
  Classifier: Topic :: Terminals
22
+ Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.9
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
@@ -55,7 +55,7 @@ Dynamic: license-file
55
55
  # Zipline CLI
56
56
 
57
57
  <a title="Zipline CLI" href="https://zipline-cli.cssnr.com/" target="_blank">
58
- <img alt="Zipline CLI" align="right" width="128" height="auto" src="https://raw.githubusercontent.com/smashedr/repo-images/refs/heads/master/zipline/cli/logo128.png"></a>
58
+ <img alt="Zipline CLI" align="right" width="128" height="auto" src="https://zipline-cli.cssnr.com/assets/images/logo.png"></a>
59
59
 
60
60
  - [Quick Start](#quick-start)
61
61
  - [Install](#install)
@@ -25,7 +25,7 @@
25
25
  # Zipline CLI
26
26
 
27
27
  <a title="Zipline CLI" href="https://zipline-cli.cssnr.com/" target="_blank">
28
- <img alt="Zipline CLI" align="right" width="128" height="auto" src="https://raw.githubusercontent.com/smashedr/repo-images/refs/heads/master/zipline/cli/logo128.png"></a>
28
+ <img alt="Zipline CLI" align="right" width="128" height="auto" src="https://zipline-cli.cssnr.com/assets/images/logo.png"></a>
29
29
 
30
30
  - [Quick Start](#quick-start)
31
31
  - [Install](#install)
@@ -3,80 +3,67 @@
3
3
  [project]
4
4
  name = "zipline-cli"
5
5
  description = "Python 3 CLI File Uploader for Diced/Zipline v3/v4 Upload Server and Django Files."
6
- authors = [{ name="Shane" }]
7
6
  readme = "README.md"
8
- dynamic = ["version"]
9
7
  requires-python = ">=3.9"
8
+ license = { text = "MIT License" }
9
+ authors = [{ name = "Shane" }]
10
10
  classifiers = [
11
- "Programming Language :: Python",
12
- "Programming Language :: Python :: 3",
13
- "Programming Language :: Python :: 3 :: Only",
14
- "Operating System :: OS Independent",
15
- "License :: OSI Approved :: MIT License",
16
- "Natural Language :: English",
17
- "Typing :: Typed",
18
- "Topic :: Communications :: File Sharing",
19
- "Topic :: Terminals",
11
+ "License :: OSI Approved :: MIT License",
12
+ "Natural Language :: English",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Topic :: Communications :: File Sharing",
18
+ "Topic :: Terminals",
19
+ "Typing :: Typed",
20
20
  ]
21
- license = { text = "MIT License" }
22
- #license = { file = "LICENSE" }
23
-
24
21
  dependencies = [
25
- "python-decouple",
26
- "python-dotenv",
27
- "requests",
28
- ]
29
-
30
- [dependency-groups]
31
- dev = [
32
- "bandit",
33
- "black",
34
- "build",
35
- "isort",
36
- "mypy",
37
- "python-decouple-typed",
38
- "psycopg2",
39
- "ruff",
40
- "setuptools",
41
- "ty",
42
- "types-psycopg2",
43
- "types-requests",
44
- "validate-pyproject[all]",
45
- "wheel",
22
+ "python-decouple",
23
+ "python-dotenv",
24
+ "requests",
46
25
  ]
47
-
48
- [project.scripts]
49
- zipline = "zipline.zipline:main"
26
+ dynamic = ["version"]
27
+ # license = { file = "LICENSE" }
50
28
 
51
29
  [project.urls]
52
- GitHub = "https://github.com/cssnr/zipline-cli"
53
- Issues = "https://github.com/cssnr/zipline-cli/issues"
54
30
  Changelog = "https://github.com/cssnr/zipline-cli/releases"
31
+ Discord = "https://discord.gg/wXy6m2X8wY"
55
32
  Documentation = "https://zipline-cli.cssnr.com/"
56
-
57
- Homepage = "https://cssnr.com/"
58
33
  Funding = "https://ko-fi.com/cssnr"
59
- Discord = "https://discord.gg/wXy6m2X8wY"
60
-
61
- # Setup Tools
62
- # https://setuptools.pypa.io/en/latest/
63
- #[tool.setuptools.package-data]
64
- #actions = ["py.typed"]
65
- [tool.setuptools.dynamic]
66
- version = { attr = "zipline.__version__" }
67
- #version = {file = "version.txt"}
34
+ GitHub = "https://github.com/cssnr/zipline-cli"
35
+ Homepage = "https://cssnr.com/"
36
+ Issues = "https://github.com/cssnr/zipline-cli/issues"
68
37
 
69
- [tool.setuptools]
70
- py-modules = ["zipline"]
71
- include-package-data = true
38
+ [project.scripts]
39
+ zipline = "zipline.cli:main"
72
40
 
73
- [tool.setuptools.packages.find]
74
- where = ["src"]
41
+ [dependency-groups]
42
+ dev = [
43
+ "bandit",
44
+ "black",
45
+ "build",
46
+ "isort",
47
+ "mypy",
48
+ "psycopg2",
49
+ "python-decouple",
50
+ "python-decouple-typed",
51
+ "python-dotenv",
52
+ "requests",
53
+ "ruff",
54
+ "setuptools",
55
+ "tombi",
56
+ "ty",
57
+ "types-psycopg2",
58
+ "types-requests",
59
+ "validate-pyproject[all]",
60
+ "wheel",
61
+ ]
75
62
 
76
63
  # Build System
77
64
  [build-system]
78
- build-backend = "setuptools.build_meta"
79
65
  requires = ["setuptools>=61.0", "wheel"]
66
+ build-backend = "setuptools.build_meta"
80
67
 
81
68
  # Bandit
82
69
  # https://bandit.readthedocs.io/en/latest/config.html
@@ -107,7 +94,7 @@ skip = ["venv", ".github"]
107
94
  # Mypy
108
95
  # https://mypy.readthedocs.io/en/stable/config_file.html
109
96
  [tool.mypy]
110
- #ignore_missing_imports = true
97
+ # ignore_missing_imports = true
111
98
  exclude = ["venv"]
112
99
 
113
100
  # Ruff
@@ -119,3 +106,19 @@ extend-exclude = [".github"]
119
106
 
120
107
  [tool.ruff.lint]
121
108
  select = ["E4", "E7", "E9", "F", "B", "Q"]
109
+
110
+ # Setup Tools
111
+ # https://setuptools.pypa.io/en/latest/
112
+ [tool.setuptools]
113
+ py-modules = ["zipline"]
114
+ include-package-data = true
115
+
116
+ [tool.setuptools.package-data]
117
+ zipline = ["py.typed"]
118
+
119
+ [tool.setuptools.dynamic]
120
+ version = { attr = "zipline._version.__version__" }
121
+ # version = {file = "version.txt"}
122
+
123
+ [tool.setuptools.packages.find]
124
+ where = ["src"]
@@ -0,0 +1,10 @@
1
+ """
2
+ ██████ ▄▄ ▄▄▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄▄▄▄ ▄█████ ██ ██
3
+ ▄▄▀▀ ██ ██▄█▀ ██ ██ ███▄██ ██▄▄ ██ ██ ██
4
+ ██████ ██ ██ ██▄▄▄ ██ ██ ▀██ ██▄▄▄ ▀█████ ██████ ██
5
+ """
6
+
7
+ from .zipline import Zipline
8
+
9
+
10
+ __all__ = ["Zipline"]
@@ -2,19 +2,11 @@ import os
2
2
  import re
3
3
 
4
4
 
5
- try:
6
- from .zipline import Zipline
7
- except ImportError:
8
- Zipline = None # type: ignore
9
-
10
-
11
5
  def get_version() -> str:
12
- version = os.environ.get("GITHUB_REF_NAME", "0.0.1")
6
+ version = os.environ.get("GITHUB_REF_NAME", "")
13
7
  pattern = r"^\d+\.\d+\.\d+(?:[abc]\d*)?$"
14
8
  match = re.match(pattern, version)
15
9
  return version if match else "0.0.1"
16
10
 
17
11
 
18
12
  __version__ = get_version()
19
-
20
- __all__ = ["Zipline"]
@@ -0,0 +1,215 @@
1
+ import argparse
2
+ import io
3
+ import os
4
+ import re
5
+ import secrets
6
+ import string
7
+ import sys
8
+ from importlib.metadata import version
9
+ from pathlib import Path
10
+ from typing import Any, List, Optional, TextIO
11
+
12
+ from decouple import config
13
+ from dotenv import find_dotenv, load_dotenv
14
+
15
+ from zipline.zipline import Zipline, ZipURL
16
+
17
+ from . import __doc__ as package_doc
18
+
19
+
20
+ def run() -> None:
21
+ zipline_file = ".zipline"
22
+ env_file = Path(os.path.expanduser("~")) / zipline_file
23
+ dotenv_path = env_file if os.path.isfile(env_file) else find_dotenv(filename=zipline_file)
24
+ env = load_dotenv(dotenv_path=dotenv_path)
25
+
26
+ parser = argparse.ArgumentParser(description="Zipline CLI.", epilog="Docs: https://zipline-cli.cssnr.com/")
27
+ parser.add_argument("files", metavar="file(s)", type=str, nargs="*", help="file(s) to upload")
28
+ parser.add_argument("-s", "--setup", action="store_true", default=False, help="run the interactive setup")
29
+ parser.add_argument(
30
+ "-u", "--url", metavar="ZIPLINE_URL", type=str, default=get_default(["url"]), help="Zipline server url"
31
+ )
32
+ parser.add_argument(
33
+ "-a",
34
+ "-t",
35
+ "--authorization",
36
+ "--token",
37
+ metavar="ZIPLINE_TOKEN",
38
+ type=str,
39
+ default=get_default(["token", "authorization"]),
40
+ help="Zipline access token or ZIPLINE_TOKEN",
41
+ )
42
+ parser.add_argument(
43
+ "-e",
44
+ "-x",
45
+ "--expires_at",
46
+ "--expire",
47
+ type=str,
48
+ default=get_default(["expire", "expire_at"]),
49
+ help="1d, 2w, etc. See: https://zipline.diced.sh/docs/guides/ms",
50
+ )
51
+ parser.add_argument(
52
+ "-E",
53
+ "--embed",
54
+ action="store_true",
55
+ default=get_default(["embed"], False, bool),
56
+ help="enable embeds on uploads",
57
+ )
58
+ parser.add_argument("-i", "--info", action="store_true", help="show application information")
59
+ parser.add_argument("-V", "--version", action="store_true", help="show the installed version")
60
+ args = parser.parse_args()
61
+
62
+ if args.version:
63
+ print(package_doc, file=sys.stderr)
64
+ print(version("zipline-cli"))
65
+ return
66
+
67
+ if args.info:
68
+ print(f"Zipline Version: {version('zipline-cli')}")
69
+ print(f"Config File: {env_file.absolute()}")
70
+ print(f"Server URL: {config('ZIPLINE_URL', '')}")
71
+ print(f"Token (ends in): {config('ZIPLINE_TOKEN', '')[-10:]}")
72
+ print(f"Expire: {config('ZIPLINE_EXPIRE', '')}")
73
+ print(f"Embed: {config('ZIPLINE_EMBED', '')}")
74
+ zipline_format = config("ZIPLINE_FORMAT", "{url}\n{raw_url}")
75
+ print(f"URL Format::\n{zipline_format}")
76
+ return
77
+
78
+ if args.setup:
79
+ setup(env_file)
80
+ return
81
+
82
+ if not env and not args.url and not args.authorization and not os.path.isfile(env_file):
83
+ env_file.touch()
84
+ print("First Run Detected, Entering Setup.")
85
+ setup(env_file)
86
+ return
87
+
88
+ if not args.url:
89
+ parser.print_help()
90
+ exit_error(parser, "Missing URL. Use --setup or specify --url")
91
+
92
+ if not args.authorization:
93
+ parser.print_help()
94
+ exit_error(parser, "Missing Token. Use --setup or specify --token")
95
+
96
+ if args.expires_at:
97
+ args.expires_at = args.expires_at.strip().lower()
98
+ match = re.search(r"^(\d+)(?:ms|s|m|h|d|w|y)$", args.expires_at)
99
+ if not match:
100
+ parser.print_help()
101
+ exit_error(parser, f"Invalid Expire Format: {args.expires_at}.")
102
+
103
+ zipline = Zipline(**vars(args))
104
+
105
+ if not args.files:
106
+ content: str = sys.stdin.read().rstrip("\n") + "\n"
107
+ text_f: TextIO = io.StringIO(content)
108
+ name = f"{gen_rand(8)}.txt"
109
+ url: ZipURL = zipline.send_file(name, text_f)
110
+ print(format_output(name, url))
111
+ return
112
+
113
+ exit_code = 1
114
+ for name in args.files:
115
+ if not os.path.isfile(name):
116
+ print(f"Warning: File Not Found: {name}")
117
+ continue
118
+ # mode: Literal["r", "rb"] = get_mode(name)
119
+ with open(name, "rb") as f:
120
+ # name, ext = os.path.splitext(os.path.basename(filename))
121
+ # ext = f'.{ext}' if ext else ''
122
+ # name = f'{name}-{gen_rand(8)}{ext}'
123
+ # url: str = zipline.send_file(name, f)
124
+ zip_url: ZipURL = zipline.send_file(name, f)
125
+ print(format_output(name, zip_url))
126
+ exit_code = 0
127
+ sys.exit(exit_code)
128
+
129
+
130
+ def setup(env_file: Path) -> None:
131
+ print("Setting up Environment File...")
132
+ url = input("Zipline URL: ").strip()
133
+ token = input("Zipline Authorization Token: ").strip()
134
+ if not url or not token:
135
+ raise ValueError("Missing URL or Token.")
136
+ output = f"ZIPLINE_URL={url}\nZIPLINE_TOKEN={token}\n"
137
+ embed = input("Enabled Embed? [Yes]/No: ").strip()
138
+ if not embed or embed.lower() not in ["n", "o", "no", "noo"]:
139
+ output += "ZIPLINE_EMBED=true\n"
140
+ expire = input("Default Expire? [Blank for None]: ").strip().lower()
141
+ if expire:
142
+ match = re.search(r"^(\d+)(?:ms|s|m|h|d|w|y)$", expire)
143
+ if not match:
144
+ print(f"Warning: invalid expire format: {expire}. See --help")
145
+ else:
146
+ output += f"ZIPLINE_EXPIRE={expire}\n"
147
+ with open(env_file, "w") as f:
148
+ f.write(output)
149
+ print(f"Setup Complete. Variables Saved to: {env_file}")
150
+
151
+
152
+ def get_default(
153
+ values: List[str],
154
+ default: Optional[Any] = None,
155
+ cast: type = str,
156
+ pre: str = "ZIPLINE_",
157
+ suf: str = "",
158
+ ) -> Optional[str]:
159
+ """
160
+ Get Default Environment Variable from List of values
161
+ :param values: List of Values to Check
162
+ :param default: Default Value if None
163
+ :param cast: Type to Cast Value
164
+ :param pre: Environment Variable Prefix
165
+ :param suf: Environment Variable Suffix
166
+ :return: Environment Variable or None
167
+ """
168
+ for value in values:
169
+ result = config(f"{pre}{value.upper()}{suf}", "", cast)
170
+ if result:
171
+ return result
172
+ return default
173
+
174
+
175
+ def format_output(filename: str, url: ZipURL) -> str:
176
+ """
177
+ Format URL Output
178
+ :param filename: Original or File Name
179
+ :param url: ZipURL to Format
180
+ :return: Formatted Output
181
+ """
182
+ zipline_format = config("ZIPLINE_FORMAT", "{filename}\n{url}\n{raw_url}")
183
+ return zipline_format.format(filename=filename, url=url, raw_url=url.raw)
184
+
185
+
186
+ def gen_rand(length: int = 4) -> str:
187
+ """
188
+ Generate Random Streng
189
+ :param length: Length of String
190
+ :return: Random String
191
+ """
192
+ r = "".join(secrets.choice(string.ascii_letters) for _ in range(length))
193
+ return "".join(r)
194
+
195
+
196
+ def exit_error(arg_parser: argparse.ArgumentParser, message: str, print_help=True):
197
+ # print(f"\033[31;1merror: \033[33;1m{message}\033[0m", file=sys.stderr, end="\n\n")
198
+ print(f"error: {message}", file=sys.stderr, end="\n\n")
199
+ if print_help:
200
+ arg_parser.print_help(sys.stderr)
201
+ sys.exit(1)
202
+
203
+
204
+ def main() -> None:
205
+ try:
206
+ run()
207
+ except KeyboardInterrupt:
208
+ sys.exit(1)
209
+ except Exception as error:
210
+ print("\nError: {}".format(str(error)))
211
+ sys.exit(1)
212
+
213
+
214
+ if __name__ == "__main__":
215
+ main()
@@ -0,0 +1,158 @@
1
+ import mimetypes
2
+ from pathlib import Path
3
+ from typing import IO, Dict, Optional
4
+
5
+ import requests
6
+
7
+
8
+ class ZipURL(object):
9
+ """
10
+ Zipline URL Object
11
+ :param file_url: Zipline File Display URL
12
+ """
13
+
14
+ __slots__ = ["url", "raw"]
15
+
16
+ def __init__(self, file_url: str):
17
+ self.url: str = file_url
18
+ self.raw: str = self._get_raw(file_url)
19
+
20
+ def __repr__(self):
21
+ return f"<url={self.url} raw={self.raw}>"
22
+
23
+ def __str__(self):
24
+ return self.url
25
+
26
+ @staticmethod
27
+ def _get_raw(url: str) -> str:
28
+ try:
29
+ s = url.split("/", 4)
30
+ return f"{s[0]}//{s[2]}/r/{s[4]}"
31
+ except Exception: # noqa
32
+ return ""
33
+
34
+
35
+ class Zipline(object):
36
+ """
37
+ Zipline Python API
38
+ :param base_url: Zipline URL
39
+ :param kwargs: Zipline Headers
40
+ """
41
+
42
+ # noinspection SpellCheckingInspection
43
+ allowed_headers = [
44
+ # zipline v3
45
+ "format",
46
+ "image_compression_percent",
47
+ "expires_at",
48
+ "password",
49
+ "zws",
50
+ "embed",
51
+ "max_views",
52
+ "uploadtext",
53
+ "authorization",
54
+ "no_json",
55
+ "x_zipline_filename",
56
+ "original_name",
57
+ "override_domain",
58
+ # zipline v4
59
+ "x-zipline-deletes-at",
60
+ "x-zipline-format",
61
+ "x-zipline-image-compression-percent",
62
+ "x-zipline-password",
63
+ "x-zipline-max-views",
64
+ "x-zipline-no-json",
65
+ "x-zipline-original-name",
66
+ "x-zipline-folder",
67
+ "x-zipline-filename",
68
+ "x-zipline-domain",
69
+ "x-zipline-file-extension",
70
+ ]
71
+
72
+ def __init__(self, url: str, **kwargs):
73
+ self.base_url: str = url.rstrip("/")
74
+ self._headers: Dict[str, str] = {}
75
+ for header, value in kwargs.items():
76
+ if header.lower() not in self.allowed_headers:
77
+ continue
78
+ if value is None:
79
+ continue
80
+ key = header.replace("_", "-").title()
81
+ self._headers[key] = str(value)
82
+
83
+ def send_file(self, file_name: str, file_object: IO, overrides: Optional[dict] = None) -> ZipURL:
84
+ """
85
+ Upload File to Zipline
86
+ :param file_name: Name of File for files tuple
87
+ :param file_object: File to Upload
88
+ :param overrides: Header Overrides
89
+ :return: File URL
90
+ """
91
+ url = self.base_url + "/api/upload"
92
+
93
+ path = Path(file_name)
94
+ # if not path.is_file():
95
+ # raise ValueError(f"Not a File: {path.resolve()}")
96
+
97
+ mime_type = get_type(path)
98
+ # print(f"mime_type: {mime_type}")
99
+ files = {"file": (file_name, file_object, mime_type)}
100
+ headers = self._headers | overrides if overrides else self._headers
101
+ r = requests.post(url, headers=headers, files=files) # nosec
102
+ r.raise_for_status()
103
+ data = r.json()["files"][0]
104
+ if isinstance(data, dict):
105
+ return ZipURL(data["url"])
106
+ elif isinstance(data, list):
107
+ return ZipURL(data[0])
108
+ else:
109
+ return ZipURL(data)
110
+
111
+
112
+ def get_type(file_path: Path) -> str:
113
+ # Deprecated since version 3.13: Passing a file path instead of URL is soft deprecated. Use guess_file_type() for this.
114
+ # https://docs.python.org/3/library/mimetypes.html#mimetypes.guess_type
115
+ mime_type, _ = mimetypes.guess_type(file_path, strict=False)
116
+ if mime_type:
117
+ return mime_type
118
+ return magic_type(file_path)
119
+
120
+
121
+ def magic_type(file_path: Path) -> str: # NOSONAR
122
+ try:
123
+ with open(file_path, "rb") as file:
124
+ chunk = file.read(512)
125
+ # print(chunk)
126
+ if chunk[0:3] == b"\xff\xd8\xff" or chunk[6:10] in (b"JFIF", b"Exif"):
127
+ return "image/jpeg"
128
+ elif chunk.startswith(b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"):
129
+ return "image/png"
130
+ elif chunk.startswith(b"RIFF") and chunk[8:12] == b"WEBP":
131
+ return "image/webp"
132
+ elif chunk.startswith((b"\x47\x49\x46\x38\x37\x61", b"\x47\x49\x46\x38\x39\x61")):
133
+ return "image/gif"
134
+ elif chunk.startswith(b"\x66\x74\x79\x70\x68\x65\x69\x63\x66\x74\79\70\6d"):
135
+ return "image/heic"
136
+ elif chunk.startswith(b"\x00\x00\x01\x00"):
137
+ return "image/ico"
138
+
139
+ elif chunk[3:11] in (b"\x66\x74\x79\x70\x4d\x53\x4e\x56", b"\x66\x74\x79\x70\x69\x73\x6f\x6d"):
140
+ return "video/mp4"
141
+ elif chunk[4:12] == b"ftypisom" or chunk[4:12] == b"ftypMSNV" or chunk[4:12] == b"ftypmp42":
142
+ return "video/mp4"
143
+ elif chunk.startswith(b"\x1a\x45\xdf\xa3"):
144
+ return "video/x-matroska"
145
+ elif chunk.startswith(b"\x6d\x6f\x6f\x76"):
146
+ return "video/quicktime"
147
+
148
+ elif chunk.startswith((b"\xff\xfb", b"\xff\xfb", b"\xff\xfb", b"\x49\x44\x33")):
149
+ return "audio/mp3"
150
+ elif chunk.startswith(b"RIFF") and chunk[8:12] == b"WAVE":
151
+ return "audio/wav"
152
+ elif chunk.startswith(b"OggS"):
153
+ return "application/ogg"
154
+
155
+ chunk.decode("utf-8")
156
+ return "text/plain"
157
+ except UnicodeDecodeError:
158
+ return "application/octet-stream"
@@ -1,25 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zipline-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Python 3 CLI File Uploader for Diced/Zipline v3/v4 Upload Server and Django Files.
5
5
  Author: Shane
6
6
  License: MIT License
7
- Project-URL: GitHub, https://github.com/cssnr/zipline-cli
8
- Project-URL: Issues, https://github.com/cssnr/zipline-cli/issues
9
7
  Project-URL: Changelog, https://github.com/cssnr/zipline-cli/releases
8
+ Project-URL: Discord, https://discord.gg/wXy6m2X8wY
10
9
  Project-URL: Documentation, https://zipline-cli.cssnr.com/
11
- Project-URL: Homepage, https://cssnr.com/
12
10
  Project-URL: Funding, https://ko-fi.com/cssnr
13
- Project-URL: Discord, https://discord.gg/wXy6m2X8wY
11
+ Project-URL: GitHub, https://github.com/cssnr/zipline-cli
12
+ Project-URL: Homepage, https://cssnr.com/
13
+ Project-URL: Issues, https://github.com/cssnr/zipline-cli/issues
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
14
17
  Classifier: Programming Language :: Python
15
18
  Classifier: Programming Language :: Python :: 3
16
19
  Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Operating System :: OS Independent
18
- Classifier: License :: OSI Approved :: MIT License
19
- Classifier: Natural Language :: English
20
- Classifier: Typing :: Typed
21
20
  Classifier: Topic :: Communications :: File Sharing
22
21
  Classifier: Topic :: Terminals
22
+ Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.9
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
@@ -55,7 +55,7 @@ Dynamic: license-file
55
55
  # Zipline CLI
56
56
 
57
57
  <a title="Zipline CLI" href="https://zipline-cli.cssnr.com/" target="_blank">
58
- <img alt="Zipline CLI" align="right" width="128" height="auto" src="https://raw.githubusercontent.com/smashedr/repo-images/refs/heads/master/zipline/cli/logo128.png"></a>
58
+ <img alt="Zipline CLI" align="right" width="128" height="auto" src="https://zipline-cli.cssnr.com/assets/images/logo.png"></a>
59
59
 
60
60
  - [Quick Start](#quick-start)
61
61
  - [Install](#install)
@@ -3,6 +3,8 @@ MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
5
  src/zipline/__init__.py
6
+ src/zipline/_version.py
7
+ src/zipline/cli.py
6
8
  src/zipline/py.typed
7
9
  src/zipline/zipline.py
8
10
  src/zipline_cli.egg-info/PKG-INFO
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zipline = zipline.cli:main
@@ -1,349 +0,0 @@
1
- import argparse
2
- import io
3
- import mimetypes
4
- import os
5
- import re
6
- import secrets
7
- import string
8
- import sys
9
- from importlib.metadata import version
10
- from pathlib import Path
11
- from typing import IO, Any, Dict, List, Optional, TextIO
12
-
13
- import requests
14
- from decouple import config
15
- from dotenv import find_dotenv, load_dotenv
16
-
17
-
18
- class ZipURL(object):
19
- """
20
- Zipline URL Object
21
- :param file_url: Zipline File Display URL
22
- """
23
-
24
- __slots__ = ["url", "raw"]
25
-
26
- def __init__(self, file_url: str):
27
- self.url: str = file_url
28
- self.raw: str = self._get_raw(file_url)
29
-
30
- def __repr__(self):
31
- return f"<url={self.url} raw={self.raw}>"
32
-
33
- def __str__(self):
34
- return self.url
35
-
36
- @staticmethod
37
- def _get_raw(url: str) -> str:
38
- try:
39
- s = url.split("/", 4)
40
- return f"{s[0]}//{s[2]}/r/{s[4]}"
41
- except Exception: # noqa
42
- return ""
43
-
44
-
45
- class Zipline(object):
46
- """
47
- Zipline Python API
48
- :param base_url: Zipline URL
49
- :param kwargs: Zipline Headers
50
- """
51
-
52
- # noinspection SpellCheckingInspection
53
- allowed_headers = [
54
- # zipline v3
55
- "format",
56
- "image_compression_percent",
57
- "expires_at",
58
- "password",
59
- "zws",
60
- "embed",
61
- "max_views",
62
- "uploadtext",
63
- "authorization",
64
- "no_json",
65
- "x_zipline_filename",
66
- "original_name",
67
- "override_domain",
68
- # zipline v4
69
- "x-zipline-deletes-at",
70
- "x-zipline-format",
71
- "x-zipline-image-compression-percent",
72
- "x-zipline-password",
73
- "x-zipline-max-views",
74
- "x-zipline-no-json",
75
- "x-zipline-original-name",
76
- "x-zipline-folder",
77
- "x-zipline-filename",
78
- "x-zipline-domain",
79
- "x-zipline-file-extension",
80
- ]
81
-
82
- def __init__(self, url: str, **kwargs):
83
- self.base_url: str = url.rstrip("/")
84
- self._headers: Dict[str, str] = {}
85
- for header, value in kwargs.items():
86
- if header.lower() not in self.allowed_headers:
87
- continue
88
- if value is None:
89
- continue
90
- key = header.replace("_", "-").title()
91
- self._headers[key] = str(value)
92
-
93
- def send_file(self, file_name: str, file_object: IO, overrides: Optional[dict] = None) -> ZipURL:
94
- """
95
- Upload File to Zipline
96
- :param file_name: Name of File for files tuple
97
- :param file_object: File to Upload
98
- :param overrides: Header Overrides
99
- :return: File URL
100
- """
101
- url = self.base_url + "/api/upload"
102
-
103
- path = Path(file_name)
104
- if not path.is_file():
105
- raise ValueError(f"Not a File: {path.resolve()}")
106
-
107
- mime_type = get_type(path)
108
- # print(f"mime_type: {mime_type}")
109
- files = {"file": (file_name, file_object, mime_type)}
110
- headers = self._headers | overrides if overrides else self._headers
111
- r = requests.post(url, headers=headers, files=files) # nosec
112
- r.raise_for_status()
113
- data = r.json()["files"][0]
114
- if isinstance(data, dict):
115
- return ZipURL(data["url"])
116
- elif isinstance(data, list):
117
- return ZipURL(data[0])
118
- else:
119
- return ZipURL(data)
120
-
121
-
122
- def get_type(file_path: Path) -> str:
123
- # Deprecated since version 3.13: Passing a file path instead of URL is soft deprecated. Use guess_file_type() for this.
124
- # https://docs.python.org/3/library/mimetypes.html#mimetypes.guess_type
125
- mime_type, _ = mimetypes.guess_type(file_path, strict=False)
126
- if mime_type:
127
- return mime_type
128
- return magic_type(file_path)
129
-
130
-
131
- def magic_type(file_path: Path) -> str: # NOSONAR
132
- try:
133
- with open(file_path, "rb") as file:
134
- chunk = file.read(512)
135
- # print(chunk)
136
- if chunk[0:3] == b"\xff\xd8\xff" or chunk[6:10] in (b"JFIF", b"Exif"):
137
- return "image/jpeg"
138
- elif chunk.startswith(b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"):
139
- return "image/png"
140
- elif chunk.startswith(b"RIFF") and chunk[8:12] == b"WEBP":
141
- return "image/webp"
142
- elif chunk.startswith((b"\x47\x49\x46\x38\x37\x61", b"\x47\x49\x46\x38\x39\x61")):
143
- return "image/gif"
144
- elif chunk.startswith(b"\x66\x74\x79\x70\x68\x65\x69\x63\x66\x74\79\70\6d"):
145
- return "image/heic"
146
- elif chunk.startswith(b"\x00\x00\x01\x00"):
147
- return "image/ico"
148
-
149
- elif chunk[3:11] in (b"\x66\x74\x79\x70\x4d\x53\x4e\x56", b"\x66\x74\x79\x70\x69\x73\x6f\x6d"):
150
- return "video/mp4"
151
- elif chunk[4:12] == b"ftypisom" or chunk[4:12] == b"ftypMSNV" or chunk[4:12] == b"ftypmp42":
152
- return "video/mp4"
153
- elif chunk.startswith(b"\x1a\x45\xdf\xa3"):
154
- return "video/x-matroska"
155
- elif chunk.startswith(b"\x6d\x6f\x6f\x76"):
156
- return "video/quicktime"
157
-
158
- elif chunk.startswith((b"\xff\xfb", b"\xff\xfb", b"\xff\xfb", b"\x49\x44\x33")):
159
- return "audio/mp3"
160
- elif chunk.startswith(b"RIFF") and chunk[8:12] == b"WAVE":
161
- return "audio/wav"
162
- elif chunk.startswith(b"OggS"):
163
- return "application/ogg"
164
-
165
- chunk.decode("utf-8")
166
- return "text/plain"
167
- except UnicodeDecodeError:
168
- return "application/octet-stream"
169
-
170
-
171
- def format_output(filename: str, url: ZipURL) -> str:
172
- """
173
- Format URL Output
174
- :param filename: Original or File Name
175
- :param url: ZipURL to Format
176
- :return: Formatted Output
177
- """
178
- zipline_format = config("ZIPLINE_FORMAT", "{filename}\n{url}\n{raw_url}")
179
- return zipline_format.format(filename=filename, url=url, raw_url=url.raw)
180
-
181
-
182
- def gen_rand(length: int = 4) -> str:
183
- """
184
- Generate Random Streng
185
- :param length: Length of String
186
- :return: Random String
187
- """
188
- r = "".join(secrets.choice(string.ascii_letters) for _ in range(length))
189
- return "".join(r)
190
-
191
-
192
- def get_default(
193
- values: List[str],
194
- default: Optional[Any] = None,
195
- cast: type = str,
196
- pre: str = "ZIPLINE_",
197
- suf: str = "",
198
- ) -> Optional[str]:
199
- """
200
- Get Default Environment Variable from List of values
201
- :param values: List of Values to Check
202
- :param default: Default Value if None
203
- :param cast: Type to Cast Value
204
- :param pre: Environment Variable Prefix
205
- :param suf: Environment Variable Suffix
206
- :return: Environment Variable or None
207
- """
208
- for value in values:
209
- result = config(f"{pre}{value.upper()}{suf}", "", cast)
210
- if result:
211
- return result
212
- return default
213
-
214
-
215
- def setup(env_file: Path) -> None:
216
- print("Setting up Environment File...")
217
- url = input("Zipline URL: ").strip()
218
- token = input("Zipline Authorization Token: ").strip()
219
- if not url or not token:
220
- raise ValueError("Missing URL or Token.")
221
- output = f"ZIPLINE_URL={url}\nZIPLINE_TOKEN={token}\n"
222
- embed = input("Enabled Embed? [Yes]/No: ").strip()
223
- if not embed or embed.lower() not in ["n", "o", "no", "noo"]:
224
- output += "ZIPLINE_EMBED=true\n"
225
- expire = input("Default Expire? [Blank for None]: ").strip().lower()
226
- if expire:
227
- match = re.search(r"^(\d+)(?:ms|s|m|h|d|w|y)$", expire)
228
- if not match:
229
- print(f"Warning: invalid expire format: {expire}. See --help")
230
- else:
231
- output += f"ZIPLINE_EXPIRE={expire}\n"
232
- with open(env_file, "w") as f:
233
- f.write(output)
234
- print(f"Setup Complete. Variables Saved to: {env_file}")
235
- sys.exit(0)
236
-
237
-
238
- def run() -> None:
239
- zipline_file = ".zipline"
240
- env_file = Path(os.path.expanduser("~")) / zipline_file
241
- dotenv_path = env_file if os.path.isfile(env_file) else find_dotenv(filename=zipline_file)
242
- env = load_dotenv(dotenv_path=dotenv_path)
243
-
244
- parser = argparse.ArgumentParser(description="Zipline CLI.")
245
- parser.add_argument("files", metavar="Files", type=str, nargs="*", help="Files to Upload.")
246
- parser.add_argument("-s", "--setup", action="store_true", default=False, help="run the interactive setup")
247
- parser.add_argument("-i", "--info", action="store_true", help="show application information")
248
- parser.add_argument("-V", "--version", action="store_true", help="show the installed version")
249
- parser.add_argument("-u", "--url", type=str, default=get_default(["url"]), help="Zipline URL")
250
- parser.add_argument(
251
- "-a",
252
- "-t",
253
- "--authorization",
254
- "--token",
255
- type=str,
256
- default=get_default(["token", "authorization"]),
257
- help="Zipline Access Token for Authorization or ZIPLINE_TOKEN",
258
- )
259
- parser.add_argument(
260
- "-e",
261
- "-x",
262
- "--expires_at",
263
- "--expire",
264
- type=str,
265
- default=get_default(["expire", "expire_at"]),
266
- help="Ex: 1d, 2w. See: https://zipline.diced.sh/docs/guides/upload-options#image-expiration",
267
- )
268
- parser.add_argument(
269
- "--embed", action="store_true", default=get_default(["embed"], False, bool), help="Enable Embeds on Uploads."
270
- )
271
- args = parser.parse_args()
272
-
273
- if args.version:
274
- print(version("zipline-cli"))
275
- return
276
-
277
- if args.info:
278
- print(f"Zipline Version: {version('zipline-cli')}")
279
- print(f"Config File: {env_file.absolute()}")
280
- print(f"Server URL: {config('ZIPLINE_URL', '')}")
281
- print(f"Token (ends in): {config('ZIPLINE_TOKEN', '')[-10:]}")
282
- print(f"Expire: {config('ZIPLINE_EXPIRE', '')}")
283
- print(f"Embed: {config('ZIPLINE_EMBED', '')}")
284
- zipline_format = config("ZIPLINE_FORMAT", "{url}\n{raw_url}")
285
- print(f"URL Format::\n{zipline_format}")
286
- return
287
-
288
- if args.setup:
289
- setup(env_file)
290
-
291
- if not env and not args.url and not args.authorization and not os.path.isfile(env_file):
292
- env_file.touch()
293
- print("First Run Detected, Entering Setup.")
294
- setup(env_file)
295
-
296
- if not args.url:
297
- parser.print_help()
298
- raise ValueError("Missing URL. Use --setup or specify --url")
299
-
300
- if not args.authorization:
301
- parser.print_help()
302
- raise ValueError("Missing Token. Use --setup or specify --token")
303
-
304
- if args.expires_at:
305
- args.expires_at = args.expires_at.strip().lower()
306
- match = re.search(r"^(\d+)(?:ms|s|m|h|d|w|y)$", args.expires_at)
307
- if not match:
308
- parser.print_help()
309
- raise ValueError(f"Invalid Expire Format: {args.expires_at}.")
310
-
311
- zipline = Zipline(**vars(args))
312
-
313
- if not args.files:
314
- content: str = sys.stdin.read().rstrip("\n") + "\n"
315
- text_f: TextIO = io.StringIO(content)
316
- name = f"{gen_rand(8)}.txt"
317
- url: ZipURL = zipline.send_file(name, text_f)
318
- print(format_output(name, url))
319
- sys.exit(0)
320
-
321
- exit_code = 1
322
- for name in args.files:
323
- if not os.path.isfile(name):
324
- print(f"Warning: File Not Found: {name}")
325
- continue
326
- # mode: Literal["r", "rb"] = get_mode(name)
327
- with open(name, "rb") as f:
328
- # name, ext = os.path.splitext(os.path.basename(filename))
329
- # ext = f'.{ext}' if ext else ''
330
- # name = f'{name}-{gen_rand(8)}{ext}'
331
- # url: str = zipline.send_file(name, f)
332
- zip_url: ZipURL = zipline.send_file(name, f)
333
- print(format_output(name, zip_url))
334
- exit_code = 0
335
- sys.exit(exit_code)
336
-
337
-
338
- def main() -> None:
339
- try:
340
- run()
341
- except KeyboardInterrupt:
342
- sys.exit(1)
343
- except Exception as error:
344
- print("\nError: {}".format(str(error)))
345
- sys.exit(1)
346
-
347
-
348
- if __name__ == "__main__":
349
- main()
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- zipline = zipline.zipline:main
File without changes
File without changes
File without changes