nibble-cli 0.3.0__tar.gz → 0.4.0__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,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nibble-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Fast local network scanner with hardware identification and a terminal UI
5
5
  Home-page: https://github.com/backendsystems/nibble
6
6
  Author: saberd
7
- Author-email: your-email@example.com
7
+ Author-email: mail@saberd.com
8
8
  Classifier: Development Status :: 4 - Beta
9
9
  Classifier: Environment :: Console
10
10
  Classifier: Intended Audience :: System Administrators
File without changes
@@ -0,0 +1,50 @@
1
+ import hashlib
2
+
3
+
4
+ def get_checksum(text, archive_name):
5
+ for raw_line in text.splitlines():
6
+ line = raw_line.strip()
7
+ if not line or line.startswith("#"):
8
+ continue
9
+ parts = line.split()
10
+ if len(parts) < 2:
11
+ continue
12
+ checksum = parts[0].lower()
13
+ filename = parts[-1].lstrip("*")
14
+ if filename == archive_name:
15
+ return checksum
16
+ return None
17
+
18
+
19
+ def sha256_file(path):
20
+ with open(path, "rb") as f:
21
+ return hashlib.sha256(f.read()).hexdigest()
22
+
23
+
24
+ def verify_release_checksum(base_url, project, version, archive_name, archive_path, download_text):
25
+ checksum_candidates = [
26
+ "checksums.txt",
27
+ f"{project}_{version}_checksums.txt",
28
+ ]
29
+
30
+ checksum_text = None
31
+ checksum_name = None
32
+ for candidate in checksum_candidates:
33
+ text = download_text(f"{base_url}/{candidate}")
34
+ if text is not None:
35
+ checksum_text = text
36
+ checksum_name = candidate
37
+ break
38
+
39
+ if checksum_text is None:
40
+ raise RuntimeError(
41
+ f"no checksum file found for v{version} (tried: {', '.join(checksum_candidates)})"
42
+ )
43
+
44
+ expected = get_checksum(checksum_text, archive_name)
45
+ if expected is None:
46
+ raise RuntimeError(f"checksum for {archive_name} not found in {checksum_name}")
47
+
48
+ actual = sha256_file(archive_path)
49
+ if actual != expected:
50
+ raise RuntimeError(f"checksum mismatch for {archive_name}")
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import os
4
+ import shutil
5
+ import sys
6
+ from importlib import metadata
7
+ from pathlib import Path
8
+
9
+ PROJECT = "nibble"
10
+ DIST_NAME = "nibble-cli"
11
+
12
+
13
+ def cache_root():
14
+ if os.name == "nt":
15
+ base = Path(os.environ.get("LOCALAPPDATA", str(Path.home())))
16
+ else:
17
+ base = Path.home() / ".local" / "share"
18
+ return base / PROJECT
19
+
20
+
21
+ def installed_version():
22
+ try:
23
+ version = metadata.version(DIST_NAME)
24
+ except metadata.PackageNotFoundError:
25
+ return None
26
+ return version[1:] if version.startswith("v") else version
27
+
28
+
29
+ def remove_path(path):
30
+ if not path.exists():
31
+ return False
32
+ shutil.rmtree(path)
33
+ return True
34
+
35
+
36
+ def main():
37
+ parser = argparse.ArgumentParser(description="Clean cached nibble binaries.")
38
+ parser.add_argument(
39
+ "--all",
40
+ action="store_true",
41
+ help="Remove all cached nibble versions.",
42
+ )
43
+ args = parser.parse_args()
44
+
45
+ root = cache_root()
46
+ if args.all:
47
+ removed = remove_path(root)
48
+ if removed:
49
+ print(f"Removed cache: {root}")
50
+ else:
51
+ print(f"No cache found at: {root}")
52
+ return 0
53
+
54
+ version = installed_version()
55
+ if version is None:
56
+ print("nibble-cli is not installed. Use --all to remove cache root.", file=sys.stderr)
57
+ return 1
58
+
59
+ target = root / version
60
+ removed = remove_path(target)
61
+ if removed:
62
+ print(f"Removed cache for v{version}: {target}")
63
+ else:
64
+ print(f"No cache found for v{version}: {target}")
65
+ return 0
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main())
@@ -0,0 +1,24 @@
1
+ import shutil
2
+ import urllib.error
3
+ import urllib.request
4
+
5
+
6
+ def download_asset(url, out_path):
7
+ try:
8
+ with urllib.request.urlopen(url) as resp, open(out_path, "wb") as f:
9
+ shutil.copyfileobj(resp, f)
10
+ return True
11
+ except urllib.error.HTTPError as e:
12
+ if e.code == 404:
13
+ return False
14
+ raise
15
+
16
+
17
+ def download_text(url):
18
+ try:
19
+ with urllib.request.urlopen(url) as resp:
20
+ return resp.read().decode("utf-8", errors="replace")
21
+ except urllib.error.HTTPError as e:
22
+ if e.code == 404:
23
+ return None
24
+ raise
@@ -0,0 +1,117 @@
1
+ import os
2
+ import platform
3
+ import shutil
4
+ import tarfile
5
+ import tempfile
6
+ from importlib import metadata
7
+ from pathlib import Path
8
+
9
+ from nibble_cli.checksum import verify_release_checksum
10
+ from nibble_cli.download import download_asset, download_text
11
+
12
+ OWNER = "backendsystems"
13
+ PROJECT = "nibble"
14
+ DIST_NAME = "nibble-cli"
15
+
16
+
17
+ def platform_triplet():
18
+ system = platform.system().lower()
19
+ machine = platform.machine().lower()
20
+
21
+ os_map = {
22
+ "linux": "linux",
23
+ "darwin": "darwin",
24
+ "windows": "windows",
25
+ }
26
+ arch_map = {
27
+ "x86_64": "amd64",
28
+ "amd64": "amd64",
29
+ "aarch64": "arm64",
30
+ "arm64": "arm64",
31
+ }
32
+
33
+ os_name = os_map.get(system)
34
+ arch = arch_map.get(machine)
35
+ if not os_name or not arch:
36
+ raise RuntimeError(f"unsupported platform: system={system}, arch={machine}")
37
+ return os_name, arch
38
+
39
+
40
+ def install_dir():
41
+ if os.name == "nt":
42
+ base = Path(os.environ.get("LOCALAPPDATA", str(Path.home())))
43
+ else:
44
+ base = Path.home() / ".local" / "share"
45
+ path = base / PROJECT
46
+ path.mkdir(parents=True, exist_ok=True)
47
+ return path
48
+
49
+
50
+ def dist_version():
51
+ version = metadata.version(DIST_NAME)
52
+ return version[1:] if version.startswith("v") else version
53
+
54
+
55
+ def binary_name():
56
+ return f"{PROJECT}.exe" if os.name == "nt" else PROJECT
57
+
58
+
59
+ def release_base(version):
60
+ return f"https://github.com/{OWNER}/{PROJECT}/releases/download/v{version}"
61
+
62
+
63
+ def extract_binary(archive_path, dest_binary):
64
+ wanted = {binary_name(), PROJECT, f"{PROJECT}.exe"}
65
+
66
+ with tarfile.open(archive_path, "r:*") as tf:
67
+ member = next(
68
+ (m for m in tf.getmembers() if m.isfile() and Path(m.name).name in wanted),
69
+ None,
70
+ )
71
+ if member is None:
72
+ raise RuntimeError("binary not found inside release archive")
73
+
74
+ src = tf.extractfile(member)
75
+ if src is None:
76
+ raise RuntimeError("failed to extract binary from release archive")
77
+
78
+ with src, open(dest_binary, "wb") as dst:
79
+ shutil.copyfileobj(src, dst)
80
+
81
+
82
+ def ensure_installed():
83
+ version = dist_version()
84
+ os_name, arch = platform_triplet()
85
+
86
+ # Keep binaries in a versioned local cache directory.
87
+ install_root = install_dir()
88
+ binary_path = install_root / version / "bin" / binary_name()
89
+ binary_path.parent.mkdir(parents=True, exist_ok=True)
90
+ if binary_path.exists():
91
+ return binary_path
92
+
93
+ asset = f"{PROJECT}_{os_name}_{arch}.tar.gz"
94
+ release_url = release_base(version)
95
+ archive_url = f"{release_url}/{asset}"
96
+
97
+ with tempfile.TemporaryDirectory() as tmp:
98
+ # Download and verify the release asset before extracting it.
99
+ archive_path = Path(tmp) / asset
100
+ if not download_asset(archive_url, archive_path):
101
+ raise RuntimeError(
102
+ f"release asset not found for {os_name}/{arch} at v{version}: {asset}"
103
+ )
104
+ verify_release_checksum(
105
+ base_url=release_url,
106
+ project=PROJECT,
107
+ version=version,
108
+ archive_name=asset,
109
+ archive_path=archive_path,
110
+ download_text=download_text,
111
+ )
112
+ # Extract the nibble binary from the archive into the cache path.
113
+ extract_binary(archive_path, binary_path)
114
+
115
+ if os.name != "nt":
116
+ binary_path.chmod(0o755)
117
+ return binary_path
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ import subprocess
3
+ import sys
4
+
5
+ from nibble_cli.install import ensure_installed
6
+
7
+
8
+ def main():
9
+ try:
10
+ binary = ensure_installed()
11
+ except Exception as e:
12
+ print(f"nibble install error: {e}", file=sys.stderr)
13
+ return 1
14
+
15
+ result = subprocess.run([str(binary)] + sys.argv[1:])
16
+ return result.returncode
17
+
18
+
19
+ if __name__ == "__main__":
20
+ raise SystemExit(main())
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nibble-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Fast local network scanner with hardware identification and a terminal UI
5
5
  Home-page: https://github.com/backendsystems/nibble
6
6
  Author: saberd
7
- Author-email: your-email@example.com
7
+ Author-email: mail@saberd.com
8
8
  Classifier: Development Status :: 4 - Beta
9
9
  Classifier: Environment :: Console
10
10
  Classifier: Intended Audience :: System Administrators
@@ -0,0 +1,12 @@
1
+ setup.py
2
+ nibble_cli/__init__.py
3
+ nibble_cli/checksum.py
4
+ nibble_cli/cleanup.py
5
+ nibble_cli/download.py
6
+ nibble_cli/install.py
7
+ nibble_cli/installer.py
8
+ nibble_cli.egg-info/PKG-INFO
9
+ nibble_cli.egg-info/SOURCES.txt
10
+ nibble_cli.egg-info/dependency_links.txt
11
+ nibble_cli.egg-info/entry_points.txt
12
+ nibble_cli.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ nibble = nibble_cli.installer:main
3
+ nibble-cleanup = nibble_cli.cleanup:main
@@ -0,0 +1 @@
1
+ nibble_cli
@@ -1,4 +1,4 @@
1
- from setuptools import setup
1
+ from setuptools import find_packages, setup
2
2
  import os
3
3
  import subprocess
4
4
 
@@ -21,12 +21,13 @@ setup(
21
21
  long_description=open("README.md").read() if os.path.exists("README.md") else "",
22
22
  long_description_content_type="text/markdown",
23
23
  author="saberd",
24
- author_email="your-email@example.com",
24
+ author_email="mail@saberd.com",
25
25
  url="https://github.com/backendsystems/nibble",
26
- py_modules=["nibble_installer"],
26
+ packages=find_packages(),
27
27
  entry_points={
28
28
  "console_scripts": [
29
- "nibble=nibble_installer:main",
29
+ "nibble=nibble_cli.installer:main",
30
+ "nibble-cleanup=nibble_cli.cleanup:main",
30
31
  ],
31
32
  },
32
33
  install_requires=[],
@@ -1,7 +0,0 @@
1
- nibble_installer.py
2
- setup.py
3
- nibble_cli.egg-info/PKG-INFO
4
- nibble_cli.egg-info/SOURCES.txt
5
- nibble_cli.egg-info/dependency_links.txt
6
- nibble_cli.egg-info/entry_points.txt
7
- nibble_cli.egg-info/top_level.txt
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- nibble = nibble_installer:main
@@ -1 +0,0 @@
1
- nibble_installer
@@ -1,152 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Nibble CLI wrapper for pip.
4
- Downloads the matching GitHub Release binary and executes it.
5
- """
6
-
7
- import os
8
- import platform
9
- import shutil
10
- import subprocess
11
- import sys
12
- import tarfile
13
- import tempfile
14
- import urllib.error
15
- import urllib.request
16
- import zipfile
17
- from importlib import metadata
18
- from pathlib import Path
19
-
20
- REPO = "backendsystems/nibble"
21
- PROJECT = "nibble"
22
- DIST_NAME = "nibble-cli"
23
-
24
-
25
- def _platform_triplet():
26
- system = platform.system().lower()
27
- machine = platform.machine().lower()
28
-
29
- os_map = {
30
- "linux": "linux",
31
- "darwin": "darwin",
32
- "windows": "windows",
33
- }
34
- arch_map = {
35
- "x86_64": "amd64",
36
- "amd64": "amd64",
37
- "aarch64": "arm64",
38
- "arm64": "arm64",
39
- }
40
-
41
- os_name = os_map.get(system)
42
- arch = arch_map.get(machine)
43
- if not os_name or not arch:
44
- raise RuntimeError(f"unsupported platform: system={system}, arch={machine}")
45
- return os_name, arch
46
-
47
-
48
- def _install_dir():
49
- if os.name == "nt":
50
- base = Path(os.environ.get("LOCALAPPDATA", str(Path.home())))
51
- else:
52
- base = Path.home() / ".local" / "share"
53
- path = base / PROJECT
54
- path.mkdir(parents=True, exist_ok=True)
55
- return path
56
-
57
-
58
- def _dist_version():
59
- version = metadata.version(DIST_NAME)
60
- return version[1:] if version.startswith("v") else version
61
-
62
-
63
- def _binary_name():
64
- return f"{PROJECT}.exe" if os.name == "nt" else PROJECT
65
-
66
-
67
- def _download_asset(url, out_path):
68
- try:
69
- with urllib.request.urlopen(url) as resp, open(out_path, "wb") as f:
70
- shutil.copyfileobj(resp, f)
71
- return True
72
- except urllib.error.HTTPError as e:
73
- if e.code == 404:
74
- return False
75
- raise
76
-
77
-
78
- def _extract_binary(archive_path, dest_binary):
79
- bin_names = {_binary_name(), PROJECT, f"{PROJECT}.exe"}
80
-
81
- if str(archive_path).endswith(".zip"):
82
- with zipfile.ZipFile(archive_path, "r") as zf:
83
- for member in zf.namelist():
84
- name = Path(member).name
85
- if name in bin_names:
86
- with zf.open(member) as src, open(dest_binary, "wb") as dst:
87
- shutil.copyfileobj(src, dst)
88
- return
89
- else:
90
- with tarfile.open(archive_path, "r:*") as tf:
91
- for member in tf.getmembers():
92
- if not member.isfile():
93
- continue
94
- name = Path(member.name).name
95
- if name in bin_names:
96
- src = tf.extractfile(member)
97
- if src is None:
98
- continue
99
- with src, open(dest_binary, "wb") as dst:
100
- shutil.copyfileobj(src, dst)
101
- return
102
-
103
- raise RuntimeError("binary not found inside release archive")
104
-
105
-
106
- def ensure_installed():
107
- install_dir = _install_dir()
108
- version = _dist_version()
109
- version_dir = install_dir / version / "bin"
110
- version_dir.mkdir(parents=True, exist_ok=True)
111
- binary_path = version_dir / _binary_name()
112
- if binary_path.exists():
113
- return binary_path
114
-
115
- os_name, arch = _platform_triplet()
116
- asset_base = f"{PROJECT}_{os_name}_{arch}"
117
- candidates = [f"{asset_base}.tar.gz", f"{asset_base}.zip"]
118
-
119
- with tempfile.TemporaryDirectory() as tmp:
120
- tmpdir = Path(tmp)
121
- archive_path = None
122
- for asset in candidates:
123
- url = f"https://github.com/{REPO}/releases/download/v{version}/{asset}"
124
- local = tmpdir / asset
125
- if _download_asset(url, local):
126
- archive_path = local
127
- break
128
- if archive_path is None:
129
- raise RuntimeError(
130
- f"no release asset found for {os_name}/{arch} at v{version} (tried: {', '.join(candidates)})"
131
- )
132
-
133
- _extract_binary(archive_path, binary_path)
134
-
135
- if os.name != "nt":
136
- binary_path.chmod(0o755)
137
- return binary_path
138
-
139
-
140
- def main():
141
- try:
142
- binary = ensure_installed()
143
- except Exception as e:
144
- print(f"nibble install error: {e}", file=sys.stderr)
145
- return 1
146
-
147
- result = subprocess.run([str(binary)] + sys.argv[1:])
148
- return result.returncode
149
-
150
-
151
- if __name__ == "__main__":
152
- raise SystemExit(main())
File without changes