rawget 0.1.0__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.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: rawget
3
+ Version: 0.1.0
4
+ Summary: A lightweight CLI tool for downloading files from a URL.
5
+ Author-email: MJ Anglin <contact@mjanglin.com>
6
+ Maintainer-email: MJ Anglin <contact@mjanglin.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 MJ Anglin
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: homepage, https://github.com/clxrityy/rawget
30
+ Project-URL: issues, https://github.com/clxrityy/rawget/issues
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.8
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Operating System :: OS Independent
40
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Provides-Extra: dev
44
+ Requires-Dist: twine; extra == "dev"
45
+ Requires-Dist: build; extra == "dev"
46
+ Requires-Dist: pytest; extra == "dev"
47
+ Dynamic: license-file
48
+
49
+ # rawget
50
+
51
+ A lightweight CLI tool for downloading files from a URL.
52
+
53
+ ## Features
54
+
55
+ - [x] No external dependencies.
56
+ - [x] Cross-platform default download directory detection.
57
+ - [x] Simple command-line interface.
58
+ - [x] Automatic file extension detection based on content.
59
+ - [x] Works on all major platforms (Linux, macOS, Windows)
60
+
61
+ > Note: Streaming platform downloads (e.g., YouTube) are not supported.
62
+
63
+ ## Usage
64
+
65
+ ```bash
66
+ rawget <URL> [output_file_name]
67
+ ```
@@ -0,0 +1,12 @@
1
+ rawget-0.1.0.dist-info/licenses/LICENSE,sha256=bN-2TZD4Im41yYPVqwydlIUlGOPIuL4HFLoSwk7ylvk,1066
2
+ src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ src/__main__.py,sha256=zMquuPLGxuL26yuBdpu0ZUAbtl9VZdB2Qr4Mm5HcV7Q,466
4
+ src/download.py,sha256=-ACvYHuaUFN-l0O9s2inWGDnvEDkZWlEZAjUh_M0N3c,2689
5
+ src/extension.py,sha256=g196LWctS4eXkAFsRdymyDRj_Mz4D1zGGtWXkANv3lA,1342
6
+ tests/conftest.py,sha256=CJnq3_lQUVlHEnXt-vx3b8dUJto3j_UuCdRx3rlamn4,1507
7
+ tests/test_download.py,sha256=uMB_tqPayWPpwlLylPnQrbb0WzGao82ZmGKiWopI0qE,3043
8
+ rawget-0.1.0.dist-info/METADATA,sha256=6Pvyg5El0r6qEdqzgGB_4ru0rCsJLjjDZh_-up4XWNw,2760
9
+ rawget-0.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
10
+ rawget-0.1.0.dist-info/entry_points.txt,sha256=rfaqxTp2Z3zKjhbj12oO_VhQsvafJ1BuRdMwkJPf4tA,45
11
+ rawget-0.1.0.dist-info/top_level.txt,sha256=KW3xgkz9NLMTcmmzgKvW8RFpCFkRIQ085qzq2diFf68,10
12
+ rawget-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rawget = src.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MJ Anglin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ src
2
+ tests
src/__init__.py ADDED
File without changes
src/__main__.py ADDED
@@ -0,0 +1,22 @@
1
+ #! /usr/bin/env python3
2
+
3
+ """
4
+ Lightweight CLI tool for downloading files from a URL.
5
+ File type detection from raw content.
6
+ No external dependencies.
7
+ """
8
+ import sys
9
+ from .download import download_file
10
+
11
+ def main():
12
+ if len(sys.argv) < 2:
13
+ print(f"Usage: rawget <URL> [output_file_name]")
14
+ sys.exit(1)
15
+
16
+ url = sys.argv[1]
17
+ output = sys.argv[2] if len(sys.argv) >= 3 else None
18
+
19
+ download_file(url, output)
20
+
21
+ if __name__ == "__main__":
22
+ main()
src/download.py ADDED
@@ -0,0 +1,72 @@
1
+ import os
2
+ import urllib.request
3
+ from .extension import detect_file_extension
4
+ from pathlib import Path
5
+
6
+ def get_default_download_dir():
7
+ # Linux (XDG spec)
8
+ xdg = os.environ.get("XDG_DOWNLOAD_DIR")
9
+ if xdg:
10
+ return Path(xdg).expanduser()
11
+
12
+ # macOS / Windows / fallback
13
+ return Path.home() / "Downloads"
14
+
15
+ def download_file(url: str, output = None):
16
+ try:
17
+ with urllib.request.urlopen(url) as response:
18
+ content_type = response.headers.get('Content-Type', '')
19
+ data = response.read()
20
+
21
+ ext = detect_file_extension(data, url, content_type)
22
+ # ensure ext starts with a dot
23
+ if ext and not ext.startswith("."):
24
+ ext = f".{ext}"
25
+
26
+ # Decide destination
27
+ if not output:
28
+ download_dir = get_default_download_dir()
29
+ download_dir.mkdir(parents=True, exist_ok=True)
30
+ filename = Path(url).name
31
+ filepath = download_dir / filename
32
+ elif output.startswith("/"): # Absolute path
33
+ cleaned = output.rstrip("/")
34
+ # Case: only a leading slash and a filename -> treat as Downloads
35
+ if "/" not in cleaned[1:]:
36
+ download_dir = get_default_download_dir()
37
+ download_dir.mkdir(parents=True, exist_ok=True)
38
+ filename = Path(cleaned).name or Path(url).name
39
+ filepath = download_dir / filename
40
+ else:
41
+ output_path = Path(cleaned)
42
+ if output.endswith("/"):
43
+ output_path.mkdir(parents=True, exist_ok=True)
44
+ filename = Path(url).name
45
+ filepath = output_path / filename
46
+ else:
47
+ output_path.parent.mkdir(parents=True, exist_ok=True)
48
+ filepath = output_path
49
+ else:
50
+ # relative: place inside default Downloads
51
+ base_dir = get_default_download_dir()
52
+ output_path = (base_dir / output.rstrip("/"))
53
+ if output.endswith("/"):
54
+ output_path.mkdir(parents=True, exist_ok=True)
55
+ filename = Path(url).name
56
+ filepath = output_path / filename
57
+ else:
58
+ output_path.parent.mkdir(parents=True, exist_ok=True)
59
+ filepath = output_path
60
+
61
+ # Apply extension if missing
62
+ if ext and not filepath.name.endswith(ext):
63
+ filepath = filepath.with_suffix(ext)
64
+
65
+ with open(filepath, "wb") as f:
66
+ f.write(data)
67
+
68
+ print(f"Downloaded {url} to {filepath}")
69
+
70
+ except Exception as e:
71
+ print(f"Failed to download from URL {url}\n\n{e}")
72
+ raise e
src/extension.py ADDED
@@ -0,0 +1,53 @@
1
+ from pathlib import Path
2
+
3
+ FILE_SIGNATURES = {
4
+ # Images
5
+ b"\x89PNG\r\n\x1a\n": ".png",
6
+ b"\xff\xd8\xff": ".jpg",
7
+ b"GIF87a": ".gif",
8
+ b"GIF89a": ".gif",
9
+ b"RIFF": ".wav", # WAV & AVI start with RIFF; AVI handled below
10
+ b"OggS": ".ogg",
11
+ b"fLaC": ".flac",
12
+ b"BM": ".bmp",
13
+ b"RIFF": ".avi", # AVI (overlaps WAV, check later)
14
+ b"WEBP": ".webp",
15
+ # Audio
16
+ b"ID3": ".mp3",
17
+ b"\xff\xfb": ".mp3",
18
+ # Video
19
+ b"\x00\x00\x00\x18ftyp": ".mp4",
20
+ b"\x00\x00\x00\x14ftyp": ".mp4",
21
+ b"ftyp": ".mp4",
22
+ # Documents
23
+ b"%PDF-": ".pdf",
24
+ }
25
+
26
+ def detect_file_extension(data: bytes, url: str, content_type: str) -> str:
27
+ import mimetypes
28
+
29
+ # 1. Try Content-Type header
30
+ ext = mimetypes.guess_extension(content_type)
31
+
32
+ if ext:
33
+ return ext
34
+
35
+ # 2. Try file signature (magic numbers)
36
+ for sig, sig_ext in FILE_SIGNATURES.items():
37
+ if data.startswith(sig):
38
+ # Special case: RIFF can be WAV or AVI
39
+ if sig == b"RIFF":
40
+ if data[8:12] == b"WAVE":
41
+ return ".wav"
42
+ elif data[8:12] == b"AVI ":
43
+ return ".avi"
44
+ else:
45
+ return sig_ext
46
+
47
+ # 3. Try URL suffix
48
+ path_ext = Path(url).suffix
49
+ if path_ext:
50
+ return path_ext
51
+
52
+ # 4. Fallback
53
+ return ".bin"
tests/conftest.py ADDED
@@ -0,0 +1,38 @@
1
+ import pytest
2
+ import os
3
+ from pathlib import Path
4
+ from src.download import download_file
5
+
6
+ options = {
7
+ "url": "",
8
+ "file_name": "",
9
+ "expected_suffix": "",
10
+ "expected_download": Path("")
11
+ }
12
+
13
+ # Helper function to test downloading files
14
+ # - The file should be placed in the expected download location
15
+ # - The file should have the correct extension
16
+ # - The file should have non-zero size
17
+ # - The file should be removed after the test
18
+ @pytest.mark.skip
19
+ def test_download(capsys: pytest.CaptureFixture[str], opts: dict = options):
20
+
21
+ url = opts["url"]
22
+ file_name = opts["file_name"]
23
+ expected_suffix = opts["expected_suffix"]
24
+ expected_download = opts["expected_download"]
25
+
26
+ try:
27
+ download_file(url, file_name)
28
+ captured = capsys.readouterr()
29
+ assert "Downloaded" in captured.out, f"Expected 'Downloading' message in output, got: {captured.out}"
30
+ assert expected_download.exists(), f"Expected file {expected_download} to exist"
31
+ assert expected_download.suffix == expected_suffix, f"Expected file {expected_download} to have {expected_suffix} extension"
32
+ assert expected_download.stat().st_size > 0, f"Expected file {expected_download} to have non-zero size"
33
+ except Exception as e:
34
+ assert False, f"Download failed with exception: {e}"
35
+ finally:
36
+ if expected_download.exists():
37
+ os.remove(expected_download)
38
+ assert not expected_download.exists(), f"Expected file {expected_download} to be removed"
tests/test_download.py ADDED
@@ -0,0 +1,85 @@
1
+ import tempfile
2
+ from pathlib import Path
3
+ from src.download import get_default_download_dir
4
+ from conftest import test_download
5
+
6
+ def test_get_default_download_dir():
7
+ dir_path = get_default_download_dir()
8
+ assert dir_path is not None
9
+
10
+ png_url = "https://avatars.githubusercontent.com/u/97744702?v=4"
11
+ mp3_url = "https://github.com/clxrityy/clxrity.xyz/blob/wav/public/assets/audio/previews/dreamy-guitar-loop.mp3"
12
+ wav_url = "https://github.com/clxrityy/clxrity.xyz/blob/wav/public/assets/audio/yearbook/awards/bring-it-in-%5B87%5D.wav"
13
+ webp_url = "https://github.com/clxrityy/clxrity.xyz/blob/wav/public/assets/img/musictable.webp"
14
+
15
+ # Test downloading a PNG file with default behavior
16
+ # - The file should be placed in the default download directory
17
+ def test_download_file_default_behavior(capsys):
18
+ expected_download = Path(get_default_download_dir()) / "default.png"
19
+ opts = {
20
+ "url": png_url,
21
+ "file_name": "default.png",
22
+ "expected_suffix": ".png",
23
+ "expected_download": expected_download
24
+ }
25
+ test_download(capsys, opts)
26
+
27
+
28
+ # Test downloading a PNG file with a leading slash in the filename
29
+ # - The leading slash should be ignored and the file should be placed in the default download directory
30
+ def test_download_file_ignore_leading_slash(capsys):
31
+ expected_download = Path(get_default_download_dir()) / "default.png"
32
+ opts = {
33
+ "url": png_url,
34
+ "file_name": "/default.png",
35
+ "expected_suffix": ".png",
36
+ "expected_download": expected_download
37
+ }
38
+ test_download(capsys, opts)
39
+
40
+ # Test downloading a PNG file to a temporary directory
41
+ # - The file should be placed in the specified temporary directory
42
+ def test_download_file_temp_dir(capsys):
43
+ temp_dir = tempfile.gettempdir()
44
+ output_path = f"{temp_dir}/default.png"
45
+ expected_download = Path(output_path)
46
+ opts = {
47
+ "url": png_url,
48
+ "file_name": output_path,
49
+ "expected_suffix": ".png",
50
+ "expected_download": expected_download
51
+ }
52
+ test_download(capsys, opts)
53
+
54
+ # Test downloading MP3 file
55
+ def test_download_mp3_file(capsys):
56
+ expected_download = Path(get_default_download_dir()) / "default.mp3"
57
+ opts = {
58
+ "url": mp3_url,
59
+ "file_name": "default.mp3",
60
+ "expected_suffix": ".mp3",
61
+ "expected_download": expected_download
62
+ }
63
+ test_download(capsys, opts)
64
+
65
+ # Test downloading WAV file
66
+ def test_download_wav_file(capsys):
67
+ expected_download = Path(get_default_download_dir()) / "default.wav"
68
+ opts = {
69
+ "url": wav_url,
70
+ "file_name": "default.wav",
71
+ "expected_suffix": ".wav",
72
+ "expected_download": expected_download
73
+ }
74
+ test_download(capsys, opts)
75
+
76
+ # Test downloading WEBP file
77
+ def test_download_webp_file(capsys):
78
+ expected_download = Path(get_default_download_dir()) / "default.webp"
79
+ opts = {
80
+ "url": webp_url,
81
+ "file_name": "default.webp",
82
+ "expected_suffix": ".webp",
83
+ "expected_download": expected_download
84
+ }
85
+ test_download(capsys, opts)