whycode-cli 0.2.1__py3-none-any.whl → 0.2.2__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.
- whycode/__init__.py +1 -1
- whycode/cli.py +172 -1
- whycode/risk_card.py +12 -0
- whycode/suppressions.py +123 -0
- {whycode_cli-0.2.1.dist-info → whycode_cli-0.2.2.dist-info}/METADATA +4 -2
- {whycode_cli-0.2.1.dist-info → whycode_cli-0.2.2.dist-info}/RECORD +10 -9
- {whycode_cli-0.2.1.dist-info → whycode_cli-0.2.2.dist-info}/WHEEL +0 -0
- {whycode_cli-0.2.1.dist-info → whycode_cli-0.2.2.dist-info}/entry_points.txt +0 -0
- {whycode_cli-0.2.1.dist-info → whycode_cli-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {whycode_cli-0.2.1.dist-info → whycode_cli-0.2.2.dist-info}/top_level.txt +0 -0
whycode/__init__.py
CHANGED
whycode/cli.py
CHANGED
|
@@ -4,6 +4,8 @@ Commands
|
|
|
4
4
|
--------
|
|
5
5
|
- ``whycode why <path>`` — print the Risk Card for a single file.
|
|
6
6
|
- ``whycode why <path> --at SHA`` — risk card as of a past commit.
|
|
7
|
+
- ``whycode why <path> --mute KIND`` — locally suppress a noisy signal kind.
|
|
8
|
+
- ``whycode highlights`` — repo-wide treasure map of decisions and incidents.
|
|
7
9
|
- ``whycode diff [--base REF]`` — risk-rank files changed against a base ref.
|
|
8
10
|
- ``whycode show <sha>`` — risk-flavored summary for one commit.
|
|
9
11
|
- ``whycode timeline <path>`` — risk score evolution across the file's history.
|
|
@@ -29,6 +31,7 @@ from whycode import __version__
|
|
|
29
31
|
from whycode import git_facts as gf
|
|
30
32
|
from whycode import risk_card as rc
|
|
31
33
|
from whycode import signals as sig
|
|
34
|
+
from whycode import suppressions as supp
|
|
32
35
|
|
|
33
36
|
app = typer.Typer(
|
|
34
37
|
add_completion=False,
|
|
@@ -119,6 +122,20 @@ def why(
|
|
|
119
122
|
"--at",
|
|
120
123
|
help="Show the Risk Card as of this commit / ref (postmortem queries).",
|
|
121
124
|
),
|
|
125
|
+
mute: list[str] = typer.Option(
|
|
126
|
+
[],
|
|
127
|
+
"--mute",
|
|
128
|
+
help=(
|
|
129
|
+
"Suppress a signal kind for this path going forward "
|
|
130
|
+
"(stored in .whycode/suppressed.json). Accepts kind name or "
|
|
131
|
+
"unique prefix: incident, revert, ghost, coupling, silence, …"
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
no_mutes: bool = typer.Option(
|
|
135
|
+
False,
|
|
136
|
+
"--no-mutes",
|
|
137
|
+
help="Bypass the local suppression list — show all signals.",
|
|
138
|
+
),
|
|
122
139
|
max_commits: int | None = typer.Option(
|
|
123
140
|
None, "--max-commits", help="Cap the number of commits scanned (debug)."
|
|
124
141
|
),
|
|
@@ -140,7 +157,31 @@ def why(
|
|
|
140
157
|
except gf.GitError:
|
|
141
158
|
err.print(f"[red]error:[/red] unknown commit / ref: {at!r}")
|
|
142
159
|
raise typer.Exit(2) from None
|
|
143
|
-
|
|
160
|
+
if mute:
|
|
161
|
+
sl = supp.load(repo_root)
|
|
162
|
+
added: list[str] = []
|
|
163
|
+
for token in mute:
|
|
164
|
+
try:
|
|
165
|
+
kind = supp.resolve_kind(token)
|
|
166
|
+
except ValueError as exc:
|
|
167
|
+
err.print(f"[red]error:[/red] {exc}")
|
|
168
|
+
raise typer.Exit(2) from None
|
|
169
|
+
if sl.add(rel, kind):
|
|
170
|
+
added.append(kind.value)
|
|
171
|
+
if added:
|
|
172
|
+
supp.save(repo_root, sl)
|
|
173
|
+
if not json_out:
|
|
174
|
+
err.print(
|
|
175
|
+
f"[dim]muted on {rel}: {', '.join(added)} "
|
|
176
|
+
f"(stored in .whycode/suppressed.json — edit to undo)[/dim]"
|
|
177
|
+
)
|
|
178
|
+
card = rc.build(
|
|
179
|
+
repo_root,
|
|
180
|
+
rel,
|
|
181
|
+
max_commits=max_commits,
|
|
182
|
+
ref=resolved_ref,
|
|
183
|
+
apply_suppressions=not no_mutes,
|
|
184
|
+
)
|
|
144
185
|
if json_out:
|
|
145
186
|
console.print_json(json.dumps(card.to_dict()))
|
|
146
187
|
return
|
|
@@ -286,6 +327,136 @@ def diff(
|
|
|
286
327
|
raise typer.Exit(1)
|
|
287
328
|
|
|
288
329
|
|
|
330
|
+
@app.command()
|
|
331
|
+
def highlights(
|
|
332
|
+
invariants: int = typer.Option(
|
|
333
|
+
5, "--invariants", help="How many invariant lines to surface."
|
|
334
|
+
),
|
|
335
|
+
incidents: int = typer.Option(
|
|
336
|
+
5, "--incidents", help="How many incident commits to surface."
|
|
337
|
+
),
|
|
338
|
+
max_commits: int | None = typer.Option(
|
|
339
|
+
None,
|
|
340
|
+
"--max-commits",
|
|
341
|
+
help="Cap on commits scanned (defaults to no cap; tune for very large repos).",
|
|
342
|
+
),
|
|
343
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Path inside the repo."),
|
|
344
|
+
json_out: bool = typer.Option(
|
|
345
|
+
False, "--json", help="Emit machine-readable JSON instead of a card."
|
|
346
|
+
),
|
|
347
|
+
) -> None:
|
|
348
|
+
"""The first-run treasure map: top decisions and incidents across the repo.
|
|
349
|
+
|
|
350
|
+
Surfaces the highest-value commit-message lines (invariants stated by past
|
|
351
|
+
authors) and the most recent incident-flavoured commits — the things a
|
|
352
|
+
reader most wants to know about the codebase before touching anything.
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
repo_root = gf.discover_repo_root(repo.resolve())
|
|
356
|
+
except gf.GitError as exc:
|
|
357
|
+
err.print(f"[red]error:[/red] {exc}")
|
|
358
|
+
raise typer.Exit(2) from exc
|
|
359
|
+
|
|
360
|
+
with console.status("Reading repo history…", spinner="dots"):
|
|
361
|
+
commits = gf.all_commits(repo_root, max_count=max_commits)
|
|
362
|
+
if not commits:
|
|
363
|
+
console.print("[yellow]no commits in this repo[/yellow]")
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
inv_pairs = gf.extract_invariant_quotes(commits)
|
|
367
|
+
sha_to_commit = {c.sha: c for c in commits}
|
|
368
|
+
seen_lines: dict[str, str] = {}
|
|
369
|
+
for sha, line in inv_pairs:
|
|
370
|
+
seen_lines.setdefault(line, sha)
|
|
371
|
+
inv_records: list[tuple[str, str, gf.Commit]] = []
|
|
372
|
+
for line, sha in seen_lines.items():
|
|
373
|
+
commit = sha_to_commit.get(sha)
|
|
374
|
+
if commit is None:
|
|
375
|
+
continue
|
|
376
|
+
inv_records.append((line, sha, commit))
|
|
377
|
+
inv_records.sort(key=lambda t: t[2].authored_at, reverse=True)
|
|
378
|
+
inv_records = inv_records[:invariants]
|
|
379
|
+
|
|
380
|
+
incident_records = gf.find_incidents(commits)[:incidents]
|
|
381
|
+
|
|
382
|
+
if json_out:
|
|
383
|
+
console.print_json(
|
|
384
|
+
json.dumps(
|
|
385
|
+
{
|
|
386
|
+
"repo": str(repo_root),
|
|
387
|
+
"scanned_commits": len(commits),
|
|
388
|
+
"invariants": [
|
|
389
|
+
{
|
|
390
|
+
"sha": c.sha[:12],
|
|
391
|
+
"subject": c.subject,
|
|
392
|
+
"author": c.author_name,
|
|
393
|
+
"authored_at": c.authored_at.isoformat(),
|
|
394
|
+
"line": line,
|
|
395
|
+
}
|
|
396
|
+
for line, _, c in inv_records
|
|
397
|
+
],
|
|
398
|
+
"incidents": [
|
|
399
|
+
{
|
|
400
|
+
"sha": c.sha[:12],
|
|
401
|
+
"subject": c.subject,
|
|
402
|
+
"author": c.author_name,
|
|
403
|
+
"authored_at": c.authored_at.isoformat(),
|
|
404
|
+
}
|
|
405
|
+
for c in incident_records
|
|
406
|
+
],
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
console.print(
|
|
413
|
+
f"[bold]WhyCode highlights[/bold] "
|
|
414
|
+
f"[dim]{len(commits)} commits scanned in this repo[/dim]\n"
|
|
415
|
+
)
|
|
416
|
+
if inv_records:
|
|
417
|
+
console.print(
|
|
418
|
+
f"[bold yellow]INVARIANTS[/bold yellow] "
|
|
419
|
+
f"[dim]({len(inv_records)} most recent stated by past authors)[/dim]"
|
|
420
|
+
)
|
|
421
|
+
for i, (line, sha, commit) in enumerate(inv_records, 1):
|
|
422
|
+
short = sha[:7]
|
|
423
|
+
date = str(commit.authored_at.date())
|
|
424
|
+
console.print(
|
|
425
|
+
f" {i}. [dim]{short} {date} {commit.author_name}[/dim]"
|
|
426
|
+
)
|
|
427
|
+
console.print(f" [italic]{line}[/italic]")
|
|
428
|
+
console.print()
|
|
429
|
+
else:
|
|
430
|
+
console.print(
|
|
431
|
+
"[dim]INVARIANTS: none found. The repo's commit bodies don't use "
|
|
432
|
+
"cautionary language like 'do not', 'must not', 'warning:', etc.[/dim]\n"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if incident_records:
|
|
436
|
+
console.print(
|
|
437
|
+
f"[bold red]INCIDENTS[/bold red] "
|
|
438
|
+
f"[dim]({len(incident_records)} most recent incident-flavoured commits)[/dim]"
|
|
439
|
+
)
|
|
440
|
+
for i, c in enumerate(incident_records, 1):
|
|
441
|
+
short = c.sha[:7]
|
|
442
|
+
date = str(c.authored_at.date())
|
|
443
|
+
subj = c.subject if len(c.subject) <= 70 else c.subject[:69] + "…"
|
|
444
|
+
console.print(
|
|
445
|
+
f" {i}. [dim]{short} {date} {c.author_name}[/dim]\n"
|
|
446
|
+
f" {subj}"
|
|
447
|
+
)
|
|
448
|
+
console.print()
|
|
449
|
+
else:
|
|
450
|
+
console.print(
|
|
451
|
+
"[dim]INCIDENTS: none found. No commit subject contains 'hotfix', "
|
|
452
|
+
"'incident', 'regression', etc.[/dim]\n"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
console.print(
|
|
456
|
+
"[dim]→ whycode why <path> to dig into the file behind any of these.[/dim]"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
289
460
|
def _sample_indices(total: int, max_samples: int) -> list[int]:
|
|
290
461
|
"""Pick at most ``max_samples`` indices spread across [0, total).
|
|
291
462
|
|
whycode/risk_card.py
CHANGED
|
@@ -18,6 +18,7 @@ from rich.text import Text
|
|
|
18
18
|
|
|
19
19
|
from whycode import git_facts as gf
|
|
20
20
|
from whycode import signals as sig
|
|
21
|
+
from whycode import suppressions as supp
|
|
21
22
|
from whycode.scorer import Band, Score, score
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
@@ -73,9 +74,20 @@ def build(
|
|
|
73
74
|
*,
|
|
74
75
|
max_commits: int | None = None,
|
|
75
76
|
ref: str | None = None,
|
|
77
|
+
apply_suppressions: bool = True,
|
|
76
78
|
) -> RiskCard:
|
|
79
|
+
"""Build a Risk Card.
|
|
80
|
+
|
|
81
|
+
By default, signals matching the local ``.whycode/suppressed.json`` list
|
|
82
|
+
are dropped — that file is the user's "this signal is wrong, hide it"
|
|
83
|
+
feedback. Pass ``apply_suppressions=False`` to bypass it (useful for
|
|
84
|
+
debug or auditing what was hidden).
|
|
85
|
+
"""
|
|
77
86
|
facts = gf.gather(repo_root, path, max_commits=max_commits, ref=ref)
|
|
78
87
|
signals = sig.all_signals(facts)
|
|
88
|
+
if apply_suppressions:
|
|
89
|
+
suppressions = supp.load(repo_root)
|
|
90
|
+
signals = supp.filter_signals(signals, suppressions, path)
|
|
79
91
|
s = score(signals)
|
|
80
92
|
head = facts.commits[0] if facts.commits else None
|
|
81
93
|
return RiskCard(
|
whycode/suppressions.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Local suppression list — the "this signal is wrong, hide it" feedback loop.
|
|
2
|
+
|
|
3
|
+
Stored at ``<repo>/.whycode/suppressed.json``. ``.whycode/`` is gitignored,
|
|
4
|
+
so the list is per-developer and never travels with the repo. This honours
|
|
5
|
+
the privacy contract: no central DB, no telemetry, no cross-team sharing of
|
|
6
|
+
"these signals are bogus" data.
|
|
7
|
+
|
|
8
|
+
Schema (intentionally tiny — easy to hand-edit):
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
"suppressed": [
|
|
12
|
+
["src/foo.py", "incident_history"],
|
|
13
|
+
["src/bar.py", "ghost_keeper"]
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Match is exact ``(path, kind)`` — no globs. We can add patterns if real
|
|
18
|
+
users ask for them; for now precision beats convenience.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from collections.abc import Iterable, Iterator
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
from whycode.signals import SignalKind
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from whycode.signals import Signal
|
|
33
|
+
|
|
34
|
+
_FILENAME = "suppressed.json"
|
|
35
|
+
_DIRNAME = ".whycode"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class SuppressionList:
|
|
40
|
+
entries: list[tuple[str, str]] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
def is_suppressed(self, path: str, kind: SignalKind) -> bool:
|
|
43
|
+
return (path, kind.value) in self.entries
|
|
44
|
+
|
|
45
|
+
def add(self, path: str, kind: SignalKind) -> bool:
|
|
46
|
+
"""Idempotent. Returns True if a new entry was added."""
|
|
47
|
+
pair = (path, kind.value)
|
|
48
|
+
if pair in self.entries:
|
|
49
|
+
return False
|
|
50
|
+
self.entries.append(pair)
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
def __iter__(self) -> Iterator[tuple[str, str]]:
|
|
54
|
+
return iter(self.entries)
|
|
55
|
+
|
|
56
|
+
def __len__(self) -> int:
|
|
57
|
+
return len(self.entries)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _path(repo_root: Path) -> Path:
|
|
61
|
+
return repo_root / _DIRNAME / _FILENAME
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load(repo_root: Path) -> SuppressionList:
|
|
65
|
+
"""Return the suppression list for ``repo_root``. Empty if no file exists."""
|
|
66
|
+
target = _path(repo_root)
|
|
67
|
+
if not target.exists():
|
|
68
|
+
return SuppressionList()
|
|
69
|
+
try:
|
|
70
|
+
raw = json.loads(target.read_text())
|
|
71
|
+
except (OSError, json.JSONDecodeError):
|
|
72
|
+
# A corrupt file should not stop ``whycode why`` from running.
|
|
73
|
+
# Treat as empty and let the user see what happened on next save.
|
|
74
|
+
return SuppressionList()
|
|
75
|
+
items = raw.get("suppressed", [])
|
|
76
|
+
out: list[tuple[str, str]] = []
|
|
77
|
+
for item in items:
|
|
78
|
+
if isinstance(item, list) and len(item) == 2:
|
|
79
|
+
out.append((str(item[0]), str(item[1])))
|
|
80
|
+
return SuppressionList(entries=out)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def save(repo_root: Path, sl: SuppressionList) -> None:
|
|
84
|
+
target = _path(repo_root)
|
|
85
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
payload = {"suppressed": [list(pair) for pair in sl.entries]}
|
|
87
|
+
target.write_text(json.dumps(payload, indent=2) + "\n")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def resolve_kind(token: str) -> SignalKind:
|
|
91
|
+
"""Resolve a user-supplied kind name (or unique prefix) to a SignalKind.
|
|
92
|
+
|
|
93
|
+
Raises ValueError when the token is unknown or matches multiple kinds.
|
|
94
|
+
"""
|
|
95
|
+
needle = token.lower().replace("-", "_").strip()
|
|
96
|
+
if not needle:
|
|
97
|
+
raise ValueError("empty signal kind")
|
|
98
|
+
matches = [k for k in SignalKind if needle in k.value]
|
|
99
|
+
if not matches:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"unknown signal kind: {token!r}. "
|
|
102
|
+
f"Known: {', '.join(k.value for k in SignalKind)}"
|
|
103
|
+
)
|
|
104
|
+
if len(matches) > 1:
|
|
105
|
+
names = ", ".join(k.value for k in matches)
|
|
106
|
+
raise ValueError(f"ambiguous: {token!r} matches multiple kinds: {names}")
|
|
107
|
+
return matches[0]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def filter_signals(
|
|
111
|
+
signals: Iterable[Signal], suppressions: SuppressionList, path: str
|
|
112
|
+
) -> list[Signal]:
|
|
113
|
+
"""Drop signals whose ``(path, kind)`` is in the suppression list."""
|
|
114
|
+
return [s for s in signals if not suppressions.is_suppressed(path, s.kind)]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"SuppressionList",
|
|
119
|
+
"filter_signals",
|
|
120
|
+
"load",
|
|
121
|
+
"resolve_kind",
|
|
122
|
+
"save",
|
|
123
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: whycode-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Tells you what to be afraid of before you touch a file.
|
|
5
5
|
Author: Kevin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -88,9 +88,11 @@ Requires Python 3.11+.
|
|
|
88
88
|
cd /path/to/your/repo
|
|
89
89
|
|
|
90
90
|
whycode init # one-command setup: CI workflow + pre-commit gate
|
|
91
|
+
whycode highlights # first-run treasure map: top decisions + incidents
|
|
91
92
|
whycode why src/some/file.py # the Risk Card for one file
|
|
92
93
|
whycode why src/some/file.py -b # one-line summary (for triage / scripts)
|
|
93
|
-
whycode why src/some/file.py --at <sha>
|
|
94
|
+
whycode why src/some/file.py --at <sha> # risk as of a past commit
|
|
95
|
+
whycode why src/some/file.py --mute <kind> # local "this signal is wrong, hide it" feedback
|
|
94
96
|
whycode diff # rank everything you changed vs origin/main
|
|
95
97
|
whycode diff --staged # ditto, for files staged for commit
|
|
96
98
|
whycode diff --fail-on history # CI gate: exit 1 if any file is ≥ READ HISTORY FIRST
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
whycode/__init__.py,sha256=
|
|
1
|
+
whycode/__init__.py,sha256=jKEx86xuF42y5emGUY2ejvZ2Ro2LMyRzCr1520lsWKo,96
|
|
2
2
|
whycode/__main__.py,sha256=dqAk6746YpuM-FTIH4TBOULegGc5WweojiZjce0VYgQ,105
|
|
3
|
-
whycode/cli.py,sha256=
|
|
3
|
+
whycode/cli.py,sha256=vs1O8riLYKbW3qGKOLx_bZuJi8pN_t5P6Xx0l8r-drc,30600
|
|
4
4
|
whycode/git_facts.py,sha256=qn4ctE6RkkGa5sFq9ZYjT7UlKIcZnmjSQkOE8NkgvcY,15177
|
|
5
5
|
whycode/mcp_server.py,sha256=56csOHSP90Zk59-_Puvk4WTSlCJ6xQAm-K10b_qmyAQ,7105
|
|
6
|
-
whycode/risk_card.py,sha256=
|
|
6
|
+
whycode/risk_card.py,sha256=iIk4MkQQrlnj782dxdfoogUcByunI5j6y8vUnuhByAA,6996
|
|
7
7
|
whycode/scorer.py,sha256=4pBejunfxzYhGUzMeL8uGEMQzC6DWiqwcTeMdo3eras,1444
|
|
8
8
|
whycode/signals.py,sha256=14KziRolXvhmOnMnluXpPPInoBRO5uDu0tm024EYik0,13066
|
|
9
|
+
whycode/suppressions.py,sha256=1lKSs-kCgpnJbcxozcgiSP8ZAfjEDMHXuM3sw4FaY78,3836
|
|
9
10
|
whycode/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
11
|
whycode/templates/github-workflow.yml,sha256=yy87tbYKCexNYFso4e4OxGAdIIYOLn2cVxEt-FzP2oo,1095
|
|
11
12
|
whycode/templates/pre-commit,sha256=IhU11CvoDwqRAAsvHwUo-BwaNbdgy1cpXc54Z_phrmQ,316
|
|
12
|
-
whycode_cli-0.2.
|
|
13
|
-
whycode_cli-0.2.
|
|
14
|
-
whycode_cli-0.2.
|
|
15
|
-
whycode_cli-0.2.
|
|
16
|
-
whycode_cli-0.2.
|
|
17
|
-
whycode_cli-0.2.
|
|
13
|
+
whycode_cli-0.2.2.dist-info/licenses/LICENSE,sha256=U6LN5qg5kJXSJf7KFPm9KJhmiGn3qK_GsTVWXdt1DFA,1062
|
|
14
|
+
whycode_cli-0.2.2.dist-info/METADATA,sha256=ghJkHcW2uJmn8Jb1SoEo9zBlY7slOmPx4fEO_Pl7mm0,9260
|
|
15
|
+
whycode_cli-0.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
+
whycode_cli-0.2.2.dist-info/entry_points.txt,sha256=xrNWc4CQn3ZhQFJxsGIPiTqpN19K4pRpgaj6qGaEzSQ,44
|
|
17
|
+
whycode_cli-0.2.2.dist-info/top_level.txt,sha256=6yIL5rxW-4DbARHQYrPlGQVqKddZ88sjvmNosDh1w3A,8
|
|
18
|
+
whycode_cli-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|