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.
- rawget-0.1.0.dist-info/METADATA +67 -0
- rawget-0.1.0.dist-info/RECORD +12 -0
- rawget-0.1.0.dist-info/WHEEL +5 -0
- rawget-0.1.0.dist-info/entry_points.txt +2 -0
- rawget-0.1.0.dist-info/licenses/LICENSE +21 -0
- rawget-0.1.0.dist-info/top_level.txt +2 -0
- src/__init__.py +0 -0
- src/__main__.py +22 -0
- src/download.py +72 -0
- src/extension.py +53 -0
- tests/conftest.py +38 -0
- tests/test_download.py +85 -0
|
@@ -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,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.
|
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)
|