nibble-cli 0.3.1__tar.gz → 0.4.1__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.1
3
+ Version: 0.4.1
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.1
3
+ Version: 0.4.1
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,213 +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
- import hashlib
18
- import re
19
- from importlib import metadata
20
- from pathlib import Path
21
-
22
- REPO = "backendsystems/nibble"
23
- PROJECT = "nibble"
24
- DIST_NAME = "nibble-cli"
25
-
26
-
27
- def _platform_triplet():
28
- system = platform.system().lower()
29
- machine = platform.machine().lower()
30
-
31
- os_map = {
32
- "linux": "linux",
33
- "darwin": "darwin",
34
- "windows": "windows",
35
- }
36
- arch_map = {
37
- "x86_64": "amd64",
38
- "amd64": "amd64",
39
- "aarch64": "arm64",
40
- "arm64": "arm64",
41
- }
42
-
43
- os_name = os_map.get(system)
44
- arch = arch_map.get(machine)
45
- if not os_name or not arch:
46
- raise RuntimeError(f"unsupported platform: system={system}, arch={machine}")
47
- return os_name, arch
48
-
49
-
50
- def _install_dir():
51
- if os.name == "nt":
52
- base = Path(os.environ.get("LOCALAPPDATA", str(Path.home())))
53
- else:
54
- base = Path.home() / ".local" / "share"
55
- path = base / PROJECT
56
- path.mkdir(parents=True, exist_ok=True)
57
- return path
58
-
59
-
60
- def _dist_version():
61
- version = metadata.version(DIST_NAME)
62
- return version[1:] if version.startswith("v") else version
63
-
64
-
65
- def _binary_name():
66
- return f"{PROJECT}.exe" if os.name == "nt" else PROJECT
67
-
68
-
69
- def _download_asset(url, out_path):
70
- try:
71
- with urllib.request.urlopen(url) as resp, open(out_path, "wb") as f:
72
- shutil.copyfileobj(resp, f)
73
- return True
74
- except urllib.error.HTTPError as e:
75
- if e.code == 404:
76
- return False
77
- raise
78
-
79
- def _download_text(url):
80
- try:
81
- with urllib.request.urlopen(url) as resp:
82
- return resp.read().decode("utf-8", errors="replace")
83
- except urllib.error.HTTPError as e:
84
- if e.code == 404:
85
- return None
86
- raise
87
-
88
- def _parse_checksums(text):
89
- checksums = {}
90
- for raw_line in text.splitlines():
91
- line = raw_line.strip()
92
- if not line or line.startswith("#"):
93
- continue
94
- match = re.match(r"^([A-Fa-f0-9]{64})\s+\*?(.+)$", line)
95
- if not match:
96
- continue
97
- checksums[match.group(2).strip()] = match.group(1).lower()
98
- return checksums
99
-
100
- def _sha256_file(path):
101
- h = hashlib.sha256()
102
- with open(path, "rb") as f:
103
- for chunk in iter(lambda: f.read(1024 * 64), b""):
104
- h.update(chunk)
105
- return h.hexdigest()
106
-
107
- def _verify_checksum(version, archive_name, archive_path):
108
- base_url = f"https://github.com/{REPO}/releases/download/v{version}"
109
- checksum_candidates = [
110
- "checksums.txt",
111
- f"{PROJECT}_{version}_checksums.txt",
112
- ]
113
-
114
- checksum_text = None
115
- checksum_name = None
116
- for candidate in checksum_candidates:
117
- text = _download_text(f"{base_url}/{candidate}")
118
- if text is not None:
119
- checksum_text = text
120
- checksum_name = candidate
121
- break
122
-
123
- if checksum_text is None:
124
- raise RuntimeError(
125
- f"no checksum file found for v{version} (tried: {', '.join(checksum_candidates)})"
126
- )
127
-
128
- checksums = _parse_checksums(checksum_text)
129
- expected = checksums.get(archive_name)
130
- if expected is None:
131
- raise RuntimeError(f"checksum for {archive_name} not found in {checksum_name}")
132
-
133
- actual = _sha256_file(archive_path)
134
- if actual != expected:
135
- raise RuntimeError(f"checksum mismatch for {archive_name}")
136
-
137
-
138
- def _extract_binary(archive_path, dest_binary):
139
- bin_names = {_binary_name(), PROJECT, f"{PROJECT}.exe"}
140
-
141
- if str(archive_path).endswith(".zip"):
142
- with zipfile.ZipFile(archive_path, "r") as zf:
143
- for member in zf.namelist():
144
- name = Path(member).name
145
- if name in bin_names:
146
- with zf.open(member) as src, open(dest_binary, "wb") as dst:
147
- shutil.copyfileobj(src, dst)
148
- return
149
- else:
150
- with tarfile.open(archive_path, "r:*") as tf:
151
- for member in tf.getmembers():
152
- if not member.isfile():
153
- continue
154
- name = Path(member.name).name
155
- if name in bin_names:
156
- src = tf.extractfile(member)
157
- if src is None:
158
- continue
159
- with src, open(dest_binary, "wb") as dst:
160
- shutil.copyfileobj(src, dst)
161
- return
162
-
163
- raise RuntimeError("binary not found inside release archive")
164
-
165
-
166
- def ensure_installed():
167
- install_dir = _install_dir()
168
- version = _dist_version()
169
- version_dir = install_dir / version / "bin"
170
- version_dir.mkdir(parents=True, exist_ok=True)
171
- binary_path = version_dir / _binary_name()
172
- if binary_path.exists():
173
- return binary_path
174
-
175
- os_name, arch = _platform_triplet()
176
- asset_base = f"{PROJECT}_{os_name}_{arch}"
177
- candidates = [f"{asset_base}.tar.gz", f"{asset_base}.zip"]
178
-
179
- with tempfile.TemporaryDirectory() as tmp:
180
- tmpdir = Path(tmp)
181
- archive_path = None
182
- for asset in candidates:
183
- url = f"https://github.com/{REPO}/releases/download/v{version}/{asset}"
184
- local = tmpdir / asset
185
- if _download_asset(url, local):
186
- archive_path = local
187
- _verify_checksum(version, asset, local)
188
- break
189
- if archive_path is None:
190
- raise RuntimeError(
191
- f"no release asset found for {os_name}/{arch} at v{version} (tried: {', '.join(candidates)})"
192
- )
193
-
194
- _extract_binary(archive_path, binary_path)
195
-
196
- if os.name != "nt":
197
- binary_path.chmod(0o755)
198
- return binary_path
199
-
200
-
201
- def main():
202
- try:
203
- binary = ensure_installed()
204
- except Exception as e:
205
- print(f"nibble install error: {e}", file=sys.stderr)
206
- return 1
207
-
208
- result = subprocess.run([str(binary)] + sys.argv[1:])
209
- return result.returncode
210
-
211
-
212
- if __name__ == "__main__":
213
- raise SystemExit(main())
File without changes