verscan 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.
- verscan/__init__.py +1 -0
- verscan/__main__.py +3 -0
- verscan/cli.py +246 -0
- verscan/core.py +110 -0
- verscan-0.1.0.dist-info/METADATA +84 -0
- verscan-0.1.0.dist-info/RECORD +10 -0
- verscan-0.1.0.dist-info/WHEEL +5 -0
- verscan-0.1.0.dist-info/entry_points.txt +2 -0
- verscan-0.1.0.dist-info/licenses/LICENSE +21 -0
- verscan-0.1.0.dist-info/top_level.txt +1 -0
verscan/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""verscan — verify package.json / CHANGELOG.md / git tag versions match."""
|
verscan/__main__.py
ADDED
verscan/cli.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""verscan CLI — verify package.json / CHANGELOG.md / git tag versions match."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from . import core
|
|
13
|
+
|
|
14
|
+
VERSION = "0.1.0"
|
|
15
|
+
|
|
16
|
+
HELP = """\
|
|
17
|
+
verscan — verify package.json / CHANGELOG.md / git tag all share the same version.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
verscan [options] [dir]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--no-git skip git tag check
|
|
24
|
+
--no-changelog skip CHANGELOG.md check
|
|
25
|
+
--manifest <file> manifest path (default: auto-detect package.json or pyproject.toml)
|
|
26
|
+
--changelog <file> changelog path (default: CHANGELOG.md)
|
|
27
|
+
--json output JSON
|
|
28
|
+
--version, -v print verscan version
|
|
29
|
+
--help, -h show this help
|
|
30
|
+
|
|
31
|
+
Exit codes: 0 all match 1 mismatch 2 error
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
_USE_COLOR = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _col(code: str, s: str) -> str:
|
|
38
|
+
return f"\x1b[{code}m{s}\x1b[0m" if _USE_COLOR else s
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _green(s: str) -> str: return _col("32", s)
|
|
42
|
+
def _red(s: str) -> str: return _col("31", s)
|
|
43
|
+
def _yellow(s: str) -> str: return _col("33", s)
|
|
44
|
+
def _dim(s: str) -> str: return _col("2", s)
|
|
45
|
+
def _bold(s: str) -> str: return _col("1", s)
|
|
46
|
+
def _cyan(s: str) -> str: return _col("36", s)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _die(msg: str, code: int = 2) -> None:
|
|
50
|
+
sys.stderr.write(_red(f"verscan: {msg}\n"))
|
|
51
|
+
sys.exit(code)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _try_read(path: Path) -> Optional[str]:
|
|
55
|
+
try:
|
|
56
|
+
return path.read_text(encoding="utf-8")
|
|
57
|
+
except OSError:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _resolve_manifest(
|
|
62
|
+
directory: Path, explicit: Optional[str]
|
|
63
|
+
) -> Tuple[str, Path, str]:
|
|
64
|
+
"""Returns (type, path, content) where type is 'package' or 'pyproject'."""
|
|
65
|
+
if explicit:
|
|
66
|
+
p = (directory / explicit).resolve()
|
|
67
|
+
content = _try_read(p)
|
|
68
|
+
if content is None:
|
|
69
|
+
_die(f"manifest not found: {p}")
|
|
70
|
+
base = p.name
|
|
71
|
+
if base == "pyproject.toml":
|
|
72
|
+
return "pyproject", p, content # type: ignore[return-value]
|
|
73
|
+
if base == "package.json":
|
|
74
|
+
return "package", p, content # type: ignore[return-value]
|
|
75
|
+
_die(f"unrecognised manifest type: {base} (use package.json or pyproject.toml)")
|
|
76
|
+
|
|
77
|
+
pkg = directory / "package.json"
|
|
78
|
+
pyproj = directory / "pyproject.toml"
|
|
79
|
+
pkg_content = _try_read(pkg)
|
|
80
|
+
if pkg_content:
|
|
81
|
+
return "package", pkg, pkg_content
|
|
82
|
+
py_content = _try_read(pyproj)
|
|
83
|
+
if py_content:
|
|
84
|
+
return "pyproject", pyproj, py_content
|
|
85
|
+
_die(f"no package.json or pyproject.toml found in {directory}")
|
|
86
|
+
raise SystemExit(2) # unreachable, satisfies type checker
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _read_manifest_version(mtype: str, content: str) -> str:
|
|
90
|
+
if mtype == "package":
|
|
91
|
+
return core.read_pkg_version(content)
|
|
92
|
+
return core.read_pyproject_version(content)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _read_git_tag(directory: Path) -> str:
|
|
96
|
+
try:
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
["git", "describe", "--tags", "--abbrev=0"],
|
|
99
|
+
cwd=str(directory),
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
timeout=5,
|
|
103
|
+
)
|
|
104
|
+
if result.returncode != 0:
|
|
105
|
+
msg = result.stderr.strip()
|
|
106
|
+
if "no names found" in msg or "no tag" in msg or result.returncode == 128:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"no git tags found in this repository (create one with `git tag v0.1.0`)"
|
|
109
|
+
)
|
|
110
|
+
raise ValueError(f"git describe failed: {msg}")
|
|
111
|
+
return core.parse_git_tag(result.stdout)
|
|
112
|
+
except FileNotFoundError:
|
|
113
|
+
raise ValueError("git executable not found")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _label(mtype: str) -> str:
|
|
117
|
+
return {"package": "package.json", "pyproject": "pyproject.toml"}.get(mtype, mtype)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main() -> None:
|
|
121
|
+
argv = sys.argv[1:]
|
|
122
|
+
|
|
123
|
+
if not argv or "--help" in argv or "-h" in argv:
|
|
124
|
+
sys.stdout.write(HELP)
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
if "--version" in argv or "-v" in argv:
|
|
127
|
+
sys.stdout.write(VERSION + "\n")
|
|
128
|
+
sys.exit(0)
|
|
129
|
+
|
|
130
|
+
skip_git = "--no-git" in argv
|
|
131
|
+
skip_changelog = "--no-changelog" in argv
|
|
132
|
+
json_mode = "--json" in argv
|
|
133
|
+
|
|
134
|
+
def _flag_val(name: str) -> Optional[str]:
|
|
135
|
+
try:
|
|
136
|
+
i = argv.index(name)
|
|
137
|
+
return argv[i + 1]
|
|
138
|
+
except (ValueError, IndexError):
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
manifest_flag = _flag_val("--manifest")
|
|
142
|
+
changelog_flag = _flag_val("--changelog")
|
|
143
|
+
|
|
144
|
+
flag_names = {"--manifest", "--changelog"}
|
|
145
|
+
consumed = set()
|
|
146
|
+
for i, a in enumerate(argv):
|
|
147
|
+
if a in flag_names:
|
|
148
|
+
consumed.add(i + 1)
|
|
149
|
+
positional = [
|
|
150
|
+
a for i, a in enumerate(argv)
|
|
151
|
+
if not a.startswith("-") and i not in consumed
|
|
152
|
+
]
|
|
153
|
+
directory = Path(positional[0]).resolve() if positional else Path.cwd()
|
|
154
|
+
|
|
155
|
+
errors: Dict[str, str] = {}
|
|
156
|
+
versions: Dict[str, Optional[str]] = {}
|
|
157
|
+
|
|
158
|
+
# 1. Manifest version
|
|
159
|
+
mtype, mpath, mcontent = _resolve_manifest(directory, manifest_flag) # exits on error
|
|
160
|
+
try:
|
|
161
|
+
versions[mtype] = _read_manifest_version(mtype, mcontent)
|
|
162
|
+
except ValueError as exc:
|
|
163
|
+
errors[mtype] = str(exc)
|
|
164
|
+
|
|
165
|
+
# 2. CHANGELOG version
|
|
166
|
+
if not skip_changelog:
|
|
167
|
+
cl_path = (directory / (changelog_flag or "CHANGELOG.md")).resolve()
|
|
168
|
+
cl_content = _try_read(cl_path)
|
|
169
|
+
if cl_content is None:
|
|
170
|
+
errors["changelog"] = f"CHANGELOG.md not found at {cl_path} (use --no-changelog to skip)"
|
|
171
|
+
else:
|
|
172
|
+
try:
|
|
173
|
+
versions["changelog"] = core.read_changelog_version(cl_content)
|
|
174
|
+
except ValueError as exc:
|
|
175
|
+
errors["changelog"] = str(exc)
|
|
176
|
+
|
|
177
|
+
# 3. Git tag version
|
|
178
|
+
if not skip_git:
|
|
179
|
+
try:
|
|
180
|
+
versions["git"] = _read_git_tag(directory)
|
|
181
|
+
except ValueError as exc:
|
|
182
|
+
errors["git"] = str(exc)
|
|
183
|
+
|
|
184
|
+
has_errors = bool(errors)
|
|
185
|
+
result = core.compare_versions(versions)
|
|
186
|
+
|
|
187
|
+
if json_mode:
|
|
188
|
+
out = {
|
|
189
|
+
"ok": result["ok"] and not has_errors,
|
|
190
|
+
"versions": {k: v for k, v in versions.items() if v is not None},
|
|
191
|
+
"errors": errors,
|
|
192
|
+
"mismatches": result["mismatches"],
|
|
193
|
+
}
|
|
194
|
+
sys.stdout.write(json.dumps(out, indent=2) + "\n")
|
|
195
|
+
sys.exit(0 if result["ok"] and not has_errors else (2 if has_errors else 1))
|
|
196
|
+
|
|
197
|
+
src_label = {
|
|
198
|
+
"package": _label(mtype),
|
|
199
|
+
"pyproject": _label(mtype),
|
|
200
|
+
"changelog": "CHANGELOG.md",
|
|
201
|
+
"git": "git tag",
|
|
202
|
+
}
|
|
203
|
+
all_keys = list(versions.keys()) + list(errors.keys())
|
|
204
|
+
max_len = max((len(src_label.get(k, k)) for k in all_keys), default=0)
|
|
205
|
+
|
|
206
|
+
def _pad(s: str) -> str:
|
|
207
|
+
return s.ljust(max_len)
|
|
208
|
+
|
|
209
|
+
sys.stdout.write("\n")
|
|
210
|
+
|
|
211
|
+
for idx, (src, ver) in enumerate(result["entries"]):
|
|
212
|
+
marker = _dim(" (reference)") if idx == 0 else ""
|
|
213
|
+
sys.stdout.write(
|
|
214
|
+
f" {_green('✓')} {_bold(_pad(src_label.get(src, src)))} {_cyan(ver)}{marker}\n"
|
|
215
|
+
)
|
|
216
|
+
for src, err in errors.items():
|
|
217
|
+
sys.stdout.write(
|
|
218
|
+
f" {_yellow('!')} {_bold(_pad(src_label.get(src, src)))} {_yellow(f'[{err}]')}\n"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if not result["ok"] and not has_errors:
|
|
222
|
+
sys.stdout.write("\n")
|
|
223
|
+
for src in result["mismatches"]:
|
|
224
|
+
ver = versions.get(src, "?")
|
|
225
|
+
sys.stdout.write(
|
|
226
|
+
f" {_red('✗')} {_bold(src_label.get(src, src))} "
|
|
227
|
+
f"is {_red(str(ver))} — expected {_cyan(str(result['reference']))}\n"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
sys.stdout.write("\n")
|
|
231
|
+
|
|
232
|
+
if has_errors:
|
|
233
|
+
sys.stderr.write(_red(f"verscan: {len(errors)} source(s) could not be read\n"))
|
|
234
|
+
sys.exit(2)
|
|
235
|
+
|
|
236
|
+
if not result["ok"]:
|
|
237
|
+
sys.stderr.write(
|
|
238
|
+
_red(
|
|
239
|
+
f"verscan: version mismatch — "
|
|
240
|
+
f"{', '.join(result['mismatches'])} differ from {result['reference']}\n"
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
sys.exit(1)
|
|
244
|
+
|
|
245
|
+
sys.stdout.write(_green(f"verscan: all sources match → {_cyan(str(result['reference']))}\n"))
|
|
246
|
+
sys.exit(0)
|
verscan/core.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
verscan core — pure parsing and comparison logic. No I/O, no subprocess.
|
|
3
|
+
|
|
4
|
+
Each function takes raw string content so it is trivially testable.
|
|
5
|
+
Version numbers always use millisecond-epoch timestamps for state files
|
|
6
|
+
when sharing state with the Node.js counterpart (verscan).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_pkg_version(content: str) -> str:
|
|
17
|
+
"""Extract ``version`` from a package.json string."""
|
|
18
|
+
try:
|
|
19
|
+
pkg = json.loads(content)
|
|
20
|
+
except json.JSONDecodeError as exc:
|
|
21
|
+
raise ValueError(f"invalid JSON in package.json: {exc}") from exc
|
|
22
|
+
version = pkg.get("version")
|
|
23
|
+
if not isinstance(version, str) or not version:
|
|
24
|
+
raise ValueError('no "version" field in package.json')
|
|
25
|
+
return version.strip()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_pyproject_version(content: str) -> str:
|
|
29
|
+
"""Extract version from a pyproject.toml string via regex.
|
|
30
|
+
|
|
31
|
+
Matches ``version = "x.y.z"`` (or single-quoted) inside the ``[project]``
|
|
32
|
+
section without importing tomllib (which requires Python ≥ 3.11).
|
|
33
|
+
"""
|
|
34
|
+
m = re.search(
|
|
35
|
+
r'^\[project\][^[]*?\bversion\s*=\s*["\']([^"\'\\r\\n]+)["\']',
|
|
36
|
+
content,
|
|
37
|
+
re.MULTILINE | re.DOTALL,
|
|
38
|
+
)
|
|
39
|
+
if not m:
|
|
40
|
+
raise ValueError("no version in [project] section of pyproject.toml")
|
|
41
|
+
return m.group(1).strip()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_changelog_version(content: str) -> str:
|
|
45
|
+
"""Extract the first version heading from CHANGELOG.md content.
|
|
46
|
+
|
|
47
|
+
Understands Keep-a-Changelog ``## [1.2.3]`` and bare forms
|
|
48
|
+
``## 1.2.3``, ``## v1.2.3``, ``## [v1.2.3]``.
|
|
49
|
+
Returns the bare semver (v-prefix and brackets stripped).
|
|
50
|
+
"""
|
|
51
|
+
m = re.search(
|
|
52
|
+
r'^##\s+\[?v?(\d+\.\d+(?:\.\d+)?(?:[-+][^\]\s]*)?)(\])?(?:\s|$)',
|
|
53
|
+
content,
|
|
54
|
+
re.MULTILINE,
|
|
55
|
+
)
|
|
56
|
+
if not m:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"no version heading found in CHANGELOG.md "
|
|
59
|
+
"(expected `## [x.y.z]` or `## x.y.z`)"
|
|
60
|
+
)
|
|
61
|
+
return m.group(1).strip()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_git_tag(output: str) -> str:
|
|
65
|
+
"""Strip leading ``v`` and whitespace from ``git describe`` output."""
|
|
66
|
+
tag = output.strip()
|
|
67
|
+
if not tag:
|
|
68
|
+
raise ValueError("no git tags found in this repository")
|
|
69
|
+
return tag.lstrip("v")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def normalize_version(v: str) -> str:
|
|
73
|
+
"""Strip leading ``v`` and whitespace."""
|
|
74
|
+
return str(v).strip().lstrip("v")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compare_versions(
|
|
78
|
+
versions: Dict[str, Optional[str]],
|
|
79
|
+
) -> Dict:
|
|
80
|
+
"""Compare a dict of named version strings.
|
|
81
|
+
|
|
82
|
+
Returns::
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
"ok": bool,
|
|
86
|
+
"reference": str | None,
|
|
87
|
+
"mismatches": [str, ...],
|
|
88
|
+
"entries": [(name, version), ...],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Null/None values are skipped.
|
|
92
|
+
"""
|
|
93
|
+
entries: List[Tuple[str, str]] = [
|
|
94
|
+
(k, v) for k, v in versions.items() if v is not None
|
|
95
|
+
]
|
|
96
|
+
if len(entries) < 2:
|
|
97
|
+
return {
|
|
98
|
+
"ok": True,
|
|
99
|
+
"reference": entries[0][1] if entries else None,
|
|
100
|
+
"mismatches": [],
|
|
101
|
+
"entries": entries,
|
|
102
|
+
}
|
|
103
|
+
ref = entries[0][1]
|
|
104
|
+
mismatches = [k for k, v in entries if v != ref]
|
|
105
|
+
return {
|
|
106
|
+
"ok": len(mismatches) == 0,
|
|
107
|
+
"reference": ref,
|
|
108
|
+
"mismatches": mismatches,
|
|
109
|
+
"entries": entries,
|
|
110
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verscan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-dependency CLI to verify package.json, CHANGELOG.md, and git tag all share the same version — catch release mistakes before they ship.
|
|
5
|
+
Author: yyfjj
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jjdoor/verscan-py
|
|
8
|
+
Project-URL: Repository, https://github.com/jjdoor/verscan-py
|
|
9
|
+
Project-URL: Issues, https://github.com/jjdoor/verscan-py/issues
|
|
10
|
+
Keywords: version,release,changelog,git,semver,cli,devops,ci,lint,check
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# verscan
|
|
25
|
+
|
|
26
|
+
Zero-dependency CLI to verify that `package.json` (or `pyproject.toml`), `CHANGELOG.md`, and your latest git tag all agree on the same version number — before you ship.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
$ verscan
|
|
30
|
+
|
|
31
|
+
✓ pyproject.toml 0.3.1 (reference)
|
|
32
|
+
✓ CHANGELOG.md 0.3.1
|
|
33
|
+
✓ git tag 0.3.1
|
|
34
|
+
|
|
35
|
+
verscan: all sources match → 0.3.1
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install verscan
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
No dependencies. Python ≥ 3.8 required.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
verscan [options] [dir]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Option | Description |
|
|
53
|
+
|--------|-------------|
|
|
54
|
+
| `--no-git` | Skip git tag check |
|
|
55
|
+
| `--no-changelog` | Skip CHANGELOG.md check |
|
|
56
|
+
| `--manifest <file>` | Custom manifest path (default: auto-detect) |
|
|
57
|
+
| `--changelog <file>` | Custom changelog path (default: `CHANGELOG.md`) |
|
|
58
|
+
| `--json` | Output JSON |
|
|
59
|
+
| `--version` | Print verscan version |
|
|
60
|
+
| `--help` | Show help |
|
|
61
|
+
|
|
62
|
+
**Exit codes:** `0` all match · `1` mismatch · `2` parse/read error
|
|
63
|
+
|
|
64
|
+
## Examples
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
verscan # check current directory
|
|
68
|
+
verscan --no-git # skip git tag (before tagging)
|
|
69
|
+
verscan ./packages/api # check a sub-package
|
|
70
|
+
verscan --json | python3 -c "import sys,json; d=json.load(sys.stdin); sys.exit(0 if d['ok'] else 1)"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## CI integration
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
- name: Verify versions aligned
|
|
77
|
+
run: pip install verscan && verscan
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For the Node.js CLI counterpart, see [verscan](https://github.com/jjdoor/verscan).
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
verscan/__init__.py,sha256=odbbKlfPDmA_4t2RLsxJl_Uk7mjWkClBecNZdD-jQC0,79
|
|
2
|
+
verscan/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
|
|
3
|
+
verscan/cli.py,sha256=5Suj2vn2sPmLojr5-OQwbqTObsiP0tXufPU4dWmNXMc,7860
|
|
4
|
+
verscan/core.py,sha256=TZwY2eNc6-MZ3nmh-GUINbNsPpYxM-igqdjaBF9MMB4,3242
|
|
5
|
+
verscan-0.1.0.dist-info/licenses/LICENSE,sha256=fgx5TFKqwm0KYEWedI7KffCOvMfWFqoPSuXZ_-RyoiI,1080
|
|
6
|
+
verscan-0.1.0.dist-info/METADATA,sha256=e1T5P7NuMLE_WlVaBWJmFyIsQzZhZrQs_jYEJWAia5A,2374
|
|
7
|
+
verscan-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
verscan-0.1.0.dist-info/entry_points.txt,sha256=M8DNihWzaS5OQ2Q6qjQiz3ZbiYaygne4dTesMd6xZL8,45
|
|
9
|
+
verscan-0.1.0.dist-info/top_level.txt,sha256=mqBeND8owR27qE97cTTOsMdtaO85iLfIGSqc8mV5RTk,8
|
|
10
|
+
verscan-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cron-watch contributors
|
|
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 @@
|
|
|
1
|
+
verscan
|