woolly 0.1.0__py3-none-any.whl → 0.3.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.
- woolly/__main__.py +12 -473
- woolly/cache.py +88 -0
- woolly/commands/__init__.py +27 -0
- woolly/commands/check.py +422 -0
- woolly/commands/clear_cache.py +42 -0
- woolly/commands/list_formats.py +24 -0
- woolly/commands/list_languages.py +26 -0
- woolly/debug.py +195 -0
- woolly/http.py +34 -0
- woolly/languages/__init__.py +102 -0
- woolly/languages/base.py +374 -0
- woolly/languages/python.py +200 -0
- woolly/languages/rust.py +130 -0
- woolly/progress.py +69 -0
- woolly/reporters/__init__.py +111 -0
- woolly/reporters/base.py +213 -0
- woolly/reporters/json.py +175 -0
- woolly/reporters/markdown.py +117 -0
- woolly/reporters/stdout.py +75 -0
- woolly-0.3.0.dist-info/METADATA +322 -0
- woolly-0.3.0.dist-info/RECORD +25 -0
- {woolly-0.1.0.dist-info → woolly-0.3.0.dist-info}/WHEEL +1 -1
- woolly-0.1.0.dist-info/METADATA +0 -101
- woolly-0.1.0.dist-info/RECORD +0 -7
- {woolly-0.1.0.dist-info → woolly-0.3.0.dist-info}/entry_points.txt +0 -0
- {woolly-0.1.0.dist-info → woolly-0.3.0.dist-info}/licenses/LICENSE +0 -0
woolly/commands/check.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check command - analyze package dependencies for Fedora availability.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Optional
|
|
6
|
+
|
|
7
|
+
import cyclopts
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
from rich.tree import Tree
|
|
10
|
+
|
|
11
|
+
from woolly.cache import CACHE_DIR
|
|
12
|
+
from woolly.commands import app, console
|
|
13
|
+
from woolly.debug import get_log_file, log, log_package_check, setup_logger
|
|
14
|
+
from woolly.languages import get_available_languages, get_provider
|
|
15
|
+
from woolly.languages.base import LanguageProvider
|
|
16
|
+
from woolly.progress import ProgressTracker
|
|
17
|
+
from woolly.reporters import ReportData, get_available_formats, get_reporter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TreeStats(BaseModel):
|
|
21
|
+
"""Statistics collected from dependency tree analysis."""
|
|
22
|
+
|
|
23
|
+
total: int = 0
|
|
24
|
+
packaged: int = 0
|
|
25
|
+
missing: int = 0
|
|
26
|
+
missing_list: list[str] = Field(default_factory=list)
|
|
27
|
+
packaged_list: list[str] = Field(default_factory=list)
|
|
28
|
+
optional_total: int = 0
|
|
29
|
+
optional_packaged: int = 0
|
|
30
|
+
optional_missing: int = 0
|
|
31
|
+
optional_missing_list: list[str] = Field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_tree(
|
|
35
|
+
provider: LanguageProvider,
|
|
36
|
+
package_name: str,
|
|
37
|
+
version: Optional[str] = None,
|
|
38
|
+
visited: Optional[dict] = None,
|
|
39
|
+
depth: int = 0,
|
|
40
|
+
max_depth: int = 50,
|
|
41
|
+
tracker: Optional[ProgressTracker] = None,
|
|
42
|
+
include_optional: bool = False,
|
|
43
|
+
is_optional_dep: bool = False,
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Recursively build a dependency tree for a package.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
provider
|
|
51
|
+
The language provider to use.
|
|
52
|
+
package_name
|
|
53
|
+
Name of the package to analyze.
|
|
54
|
+
version
|
|
55
|
+
Specific version, or None for latest.
|
|
56
|
+
visited
|
|
57
|
+
Dict of already-visited packages mapping to their status.
|
|
58
|
+
depth
|
|
59
|
+
Current recursion depth.
|
|
60
|
+
max_depth
|
|
61
|
+
Maximum recursion depth.
|
|
62
|
+
tracker
|
|
63
|
+
Optional progress tracker.
|
|
64
|
+
include_optional
|
|
65
|
+
If True, include optional dependencies in the analysis.
|
|
66
|
+
is_optional_dep
|
|
67
|
+
If True, this package is an optional dependency.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
Tree
|
|
72
|
+
Rich Tree object representing the dependency tree.
|
|
73
|
+
"""
|
|
74
|
+
if visited is None:
|
|
75
|
+
visited = {}
|
|
76
|
+
|
|
77
|
+
optional_marker = " [yellow](optional)[/yellow]" if is_optional_dep else ""
|
|
78
|
+
|
|
79
|
+
if depth > max_depth:
|
|
80
|
+
log(f"Max depth reached for {package_name}", level="warning", depth=depth)
|
|
81
|
+
return f"[dim]{package_name}{optional_marker} (max depth reached)[/dim]"
|
|
82
|
+
|
|
83
|
+
if package_name in visited:
|
|
84
|
+
is_packaged, cached_version = visited[package_name]
|
|
85
|
+
log_package_check(
|
|
86
|
+
package_name,
|
|
87
|
+
"Skip (already visited)",
|
|
88
|
+
result="packaged" if is_packaged else "not packaged",
|
|
89
|
+
)
|
|
90
|
+
if is_packaged:
|
|
91
|
+
return f"[dim]{package_name}[/dim] [dim]v{cached_version}[/dim]{optional_marker} • [green]✓[/green] [dim](already visited)[/dim]"
|
|
92
|
+
else:
|
|
93
|
+
return f"[dim]{package_name}[/dim]{optional_marker} • [red]✗[/red] [dim](already visited)[/dim]"
|
|
94
|
+
|
|
95
|
+
if tracker:
|
|
96
|
+
tracker.update(package_name)
|
|
97
|
+
|
|
98
|
+
log_package_check(package_name, "Fetching version", source=provider.registry_name)
|
|
99
|
+
|
|
100
|
+
if version is None:
|
|
101
|
+
version = provider.get_latest_version(package_name)
|
|
102
|
+
if version is None:
|
|
103
|
+
visited[package_name] = (False, None)
|
|
104
|
+
log_package_check(
|
|
105
|
+
package_name, "Not found", source=provider.registry_name, result="error"
|
|
106
|
+
)
|
|
107
|
+
return (
|
|
108
|
+
f"[bold red]{package_name}[/bold red]{optional_marker} • "
|
|
109
|
+
f"[red]not found on {provider.registry_name}[/red]"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
log_package_check(package_name, "Checking Fedora", source="dnf repoquery")
|
|
113
|
+
|
|
114
|
+
# Check Fedora packaging status
|
|
115
|
+
status = provider.check_fedora_packaging(package_name)
|
|
116
|
+
visited[package_name] = (status.is_packaged, version)
|
|
117
|
+
|
|
118
|
+
if status.is_packaged:
|
|
119
|
+
log_package_check(
|
|
120
|
+
package_name,
|
|
121
|
+
"Fedora status",
|
|
122
|
+
result=f"packaged ({', '.join(status.versions)})",
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
log_package_check(package_name, "Fedora status", result="not packaged")
|
|
126
|
+
|
|
127
|
+
if status.is_packaged:
|
|
128
|
+
ver_str = ", ".join(status.versions) if status.versions else "unknown"
|
|
129
|
+
pkg_str = ", ".join(status.package_names) if status.package_names else ""
|
|
130
|
+
label = (
|
|
131
|
+
f"[bold]{package_name}[/bold] [dim]v{version}[/dim]{optional_marker} • "
|
|
132
|
+
f"[green]✓ packaged[/green] [dim]({ver_str})[/dim]"
|
|
133
|
+
)
|
|
134
|
+
if pkg_str:
|
|
135
|
+
label += f" [dim cyan][{pkg_str}][/dim cyan]"
|
|
136
|
+
else:
|
|
137
|
+
label = (
|
|
138
|
+
f"[bold]{package_name}[/bold] [dim]v{version}[/dim]{optional_marker} • "
|
|
139
|
+
f"[red]✗ not packaged[/red]"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
node = Tree(label)
|
|
143
|
+
|
|
144
|
+
# ALWAYS recurse into dependencies regardless of packaging status
|
|
145
|
+
log_package_check(
|
|
146
|
+
package_name, "Fetching dependencies", source=provider.registry_name
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
deps = provider.get_normal_dependencies(
|
|
150
|
+
package_name, version, include_optional=include_optional
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
log(f"Found {len(deps)} dependencies for {package_name}", deps=len(deps))
|
|
154
|
+
|
|
155
|
+
if tracker and deps:
|
|
156
|
+
tracker.update(package_name, discovered=len(deps))
|
|
157
|
+
|
|
158
|
+
for dep_name, _dep_req, dep_is_optional in deps:
|
|
159
|
+
child = build_tree(
|
|
160
|
+
provider,
|
|
161
|
+
dep_name,
|
|
162
|
+
None,
|
|
163
|
+
visited,
|
|
164
|
+
depth + 1,
|
|
165
|
+
max_depth,
|
|
166
|
+
tracker,
|
|
167
|
+
include_optional=include_optional,
|
|
168
|
+
is_optional_dep=dep_is_optional,
|
|
169
|
+
)
|
|
170
|
+
if isinstance(child, str):
|
|
171
|
+
node.add(child)
|
|
172
|
+
elif isinstance(child, Tree):
|
|
173
|
+
# Directly append Tree children to avoid wrapping
|
|
174
|
+
# Rich's add() would wrap the Tree in another node
|
|
175
|
+
node.children.append(child)
|
|
176
|
+
else:
|
|
177
|
+
node.add(child)
|
|
178
|
+
|
|
179
|
+
return node
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def collect_stats(tree, stats: Optional[TreeStats] = None) -> TreeStats:
|
|
183
|
+
"""Walk the tree and collect statistics."""
|
|
184
|
+
if stats is None:
|
|
185
|
+
stats = TreeStats()
|
|
186
|
+
|
|
187
|
+
def walk(t):
|
|
188
|
+
if isinstance(t, str):
|
|
189
|
+
stats.total += 1
|
|
190
|
+
is_optional = "(optional)" in t
|
|
191
|
+
if is_optional:
|
|
192
|
+
stats.optional_total += 1
|
|
193
|
+
if "not packaged" in t or "not found" in t:
|
|
194
|
+
stats.missing += 1
|
|
195
|
+
# Handle both [bold] and [bold red] formats
|
|
196
|
+
if "[/bold]" in t:
|
|
197
|
+
name = t.split("[/bold]")[0].split("]")[-1]
|
|
198
|
+
elif "[/bold red]" in t:
|
|
199
|
+
name = t.split("[/bold red]")[0].split("[bold red]")[-1]
|
|
200
|
+
else:
|
|
201
|
+
name = t.split()[0]
|
|
202
|
+
stats.missing_list.append(name)
|
|
203
|
+
if is_optional:
|
|
204
|
+
stats.optional_missing += 1
|
|
205
|
+
stats.optional_missing_list.append(name)
|
|
206
|
+
elif "packaged" in t:
|
|
207
|
+
stats.packaged += 1
|
|
208
|
+
if is_optional:
|
|
209
|
+
stats.optional_packaged += 1
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
if hasattr(t, "label"):
|
|
213
|
+
label = str(t.label)
|
|
214
|
+
stats.total += 1
|
|
215
|
+
is_optional = "(optional)" in label
|
|
216
|
+
if is_optional:
|
|
217
|
+
stats.optional_total += 1
|
|
218
|
+
if "not packaged" in label or "not found" in label:
|
|
219
|
+
stats.missing += 1
|
|
220
|
+
# Handle both [bold] and [bold red] formats
|
|
221
|
+
if "[bold]" in label and "[bold red]" not in label:
|
|
222
|
+
name = label.split("[/bold]")[0].split("[bold]")[-1]
|
|
223
|
+
elif "[bold red]" in label:
|
|
224
|
+
name = label.split("[/bold red]")[0].split("[bold red]")[-1]
|
|
225
|
+
else:
|
|
226
|
+
name = "unknown"
|
|
227
|
+
stats.missing_list.append(name)
|
|
228
|
+
if is_optional:
|
|
229
|
+
stats.optional_missing += 1
|
|
230
|
+
stats.optional_missing_list.append(name)
|
|
231
|
+
elif "packaged" in label:
|
|
232
|
+
stats.packaged += 1
|
|
233
|
+
name = (
|
|
234
|
+
label.split("[/bold]")[0].split("[bold]")[-1]
|
|
235
|
+
if "[bold]" in label
|
|
236
|
+
else "unknown"
|
|
237
|
+
)
|
|
238
|
+
stats.packaged_list.append(name)
|
|
239
|
+
if is_optional:
|
|
240
|
+
stats.optional_packaged += 1
|
|
241
|
+
|
|
242
|
+
if hasattr(t, "children"):
|
|
243
|
+
for child in t.children:
|
|
244
|
+
walk(child)
|
|
245
|
+
|
|
246
|
+
walk(tree)
|
|
247
|
+
return stats
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@app.command(name="check")
|
|
251
|
+
def check(
|
|
252
|
+
package: Annotated[
|
|
253
|
+
str,
|
|
254
|
+
cyclopts.Parameter(
|
|
255
|
+
help="Package name to check.",
|
|
256
|
+
),
|
|
257
|
+
],
|
|
258
|
+
*,
|
|
259
|
+
lang: Annotated[
|
|
260
|
+
str,
|
|
261
|
+
cyclopts.Parameter(
|
|
262
|
+
("--lang", "-l"),
|
|
263
|
+
help="Language/ecosystem. Use 'list-languages' to see options.",
|
|
264
|
+
),
|
|
265
|
+
] = "rust",
|
|
266
|
+
version: Annotated[
|
|
267
|
+
Optional[str],
|
|
268
|
+
cyclopts.Parameter(
|
|
269
|
+
("--version", "-v"),
|
|
270
|
+
help="Specific version to check.",
|
|
271
|
+
),
|
|
272
|
+
] = None,
|
|
273
|
+
max_depth: Annotated[
|
|
274
|
+
int,
|
|
275
|
+
cyclopts.Parameter(
|
|
276
|
+
("--max-depth", "-d"),
|
|
277
|
+
help="Maximum recursion depth.",
|
|
278
|
+
),
|
|
279
|
+
] = 50,
|
|
280
|
+
optional: Annotated[
|
|
281
|
+
bool,
|
|
282
|
+
cyclopts.Parameter(
|
|
283
|
+
("--optional", "-o"),
|
|
284
|
+
negative=(),
|
|
285
|
+
help="Include optional dependencies in the analysis.",
|
|
286
|
+
),
|
|
287
|
+
] = False,
|
|
288
|
+
no_progress: Annotated[
|
|
289
|
+
bool,
|
|
290
|
+
cyclopts.Parameter(
|
|
291
|
+
negative=(),
|
|
292
|
+
help="Disable progress bar.",
|
|
293
|
+
),
|
|
294
|
+
] = False,
|
|
295
|
+
debug: Annotated[
|
|
296
|
+
bool,
|
|
297
|
+
cyclopts.Parameter(
|
|
298
|
+
negative=(),
|
|
299
|
+
help="Enable verbose debug logging (includes command outputs and API responses).",
|
|
300
|
+
),
|
|
301
|
+
] = False,
|
|
302
|
+
report: Annotated[
|
|
303
|
+
str,
|
|
304
|
+
cyclopts.Parameter(
|
|
305
|
+
("--report", "-r"),
|
|
306
|
+
help="Report format: stdout, json, markdown. Use 'list-formats' for all options.",
|
|
307
|
+
),
|
|
308
|
+
] = "stdout",
|
|
309
|
+
):
|
|
310
|
+
"""Check if a package's dependencies are available in Fedora.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
package
|
|
315
|
+
The name of the package to analyze.
|
|
316
|
+
lang
|
|
317
|
+
Language/ecosystem (default: rust).
|
|
318
|
+
version
|
|
319
|
+
Specific version to check (default: latest).
|
|
320
|
+
max_depth
|
|
321
|
+
Maximum recursion depth for dependency tree.
|
|
322
|
+
optional
|
|
323
|
+
Include optional dependencies in the analysis.
|
|
324
|
+
no_progress
|
|
325
|
+
Disable progress bar during analysis.
|
|
326
|
+
debug
|
|
327
|
+
Enable verbose debug logging.
|
|
328
|
+
report
|
|
329
|
+
Output format for the report.
|
|
330
|
+
"""
|
|
331
|
+
# Get the language provider
|
|
332
|
+
provider = get_provider(lang)
|
|
333
|
+
if provider is None:
|
|
334
|
+
console.print(f"[red]Unknown language: {lang}[/red]")
|
|
335
|
+
console.print(f"Available languages: {', '.join(get_available_languages())}")
|
|
336
|
+
raise SystemExit(1)
|
|
337
|
+
|
|
338
|
+
# Get the reporter
|
|
339
|
+
reporter = get_reporter(report, console=console)
|
|
340
|
+
if reporter is None:
|
|
341
|
+
console.print(f"[red]Unknown report format: {report}[/red]")
|
|
342
|
+
console.print(f"Available formats: {', '.join(get_available_formats())}")
|
|
343
|
+
raise SystemExit(1)
|
|
344
|
+
|
|
345
|
+
# Initialize logging
|
|
346
|
+
setup_logger(debug=debug)
|
|
347
|
+
log(
|
|
348
|
+
"Analysis started",
|
|
349
|
+
package=package,
|
|
350
|
+
language=lang,
|
|
351
|
+
max_depth=max_depth,
|
|
352
|
+
include_optional=optional,
|
|
353
|
+
debug=debug,
|
|
354
|
+
report_format=report,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
console.print(
|
|
358
|
+
f"\n[bold underline]Analyzing {provider.display_name} package:[/] {package}"
|
|
359
|
+
)
|
|
360
|
+
if optional:
|
|
361
|
+
console.print("[yellow]Including optional dependencies[/yellow]")
|
|
362
|
+
console.print(f"[dim]Registry: {provider.registry_name}[/dim]")
|
|
363
|
+
console.print(f"[dim]Cache directory: {CACHE_DIR}[/dim]")
|
|
364
|
+
console.print()
|
|
365
|
+
|
|
366
|
+
tracker = None if no_progress else ProgressTracker(console)
|
|
367
|
+
|
|
368
|
+
if tracker:
|
|
369
|
+
tracker.start(f"Analyzing {provider.display_name} dependencies")
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
tree = build_tree(
|
|
373
|
+
provider,
|
|
374
|
+
package,
|
|
375
|
+
version,
|
|
376
|
+
max_depth=max_depth,
|
|
377
|
+
tracker=tracker,
|
|
378
|
+
include_optional=optional,
|
|
379
|
+
)
|
|
380
|
+
if tracker:
|
|
381
|
+
tracker.finish()
|
|
382
|
+
finally:
|
|
383
|
+
if tracker:
|
|
384
|
+
tracker.stop()
|
|
385
|
+
log("Analysis complete")
|
|
386
|
+
|
|
387
|
+
console.print()
|
|
388
|
+
|
|
389
|
+
# Collect statistics
|
|
390
|
+
stats = collect_stats(tree)
|
|
391
|
+
|
|
392
|
+
# Create report data
|
|
393
|
+
report_data = ReportData(
|
|
394
|
+
root_package=package,
|
|
395
|
+
language=provider.display_name,
|
|
396
|
+
registry=provider.registry_name,
|
|
397
|
+
total_dependencies=stats.total,
|
|
398
|
+
packaged_count=stats.packaged,
|
|
399
|
+
missing_count=stats.missing,
|
|
400
|
+
missing_packages=stats.missing_list,
|
|
401
|
+
packaged_packages=stats.packaged_list,
|
|
402
|
+
tree=tree,
|
|
403
|
+
max_depth=max_depth,
|
|
404
|
+
version=version,
|
|
405
|
+
include_optional=optional,
|
|
406
|
+
optional_total=stats.optional_total,
|
|
407
|
+
optional_packaged=stats.optional_packaged,
|
|
408
|
+
optional_missing=stats.optional_missing,
|
|
409
|
+
optional_missing_packages=stats.optional_missing_list,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Generate report
|
|
413
|
+
if reporter.writes_to_file:
|
|
414
|
+
output_path = reporter.write_report(report_data)
|
|
415
|
+
console.print(f"[green]Report saved to: {output_path}[/green]")
|
|
416
|
+
else:
|
|
417
|
+
reporter.generate(report_data)
|
|
418
|
+
|
|
419
|
+
# Show log file path
|
|
420
|
+
log_file = get_log_file()
|
|
421
|
+
if log_file:
|
|
422
|
+
console.print(f"[dim]Log saved to: {log_file}[/dim]\n")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clear cache command - clear cached data.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import cyclopts
|
|
8
|
+
|
|
9
|
+
from woolly.cache import clear_cache
|
|
10
|
+
from woolly.commands import app, console
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command(name="clear-cache")
|
|
14
|
+
def clear_cache_cmd(
|
|
15
|
+
fedora_only: Annotated[
|
|
16
|
+
bool,
|
|
17
|
+
cyclopts.Parameter(
|
|
18
|
+
("--fedora-only", "-f"),
|
|
19
|
+
negative=(),
|
|
20
|
+
help="Clear only Fedora repoquery cache.",
|
|
21
|
+
),
|
|
22
|
+
] = False,
|
|
23
|
+
):
|
|
24
|
+
"""Clear cached data.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
fedora_only
|
|
29
|
+
If set, only clear the Fedora repoquery cache.
|
|
30
|
+
"""
|
|
31
|
+
if fedora_only:
|
|
32
|
+
cleared = clear_cache("fedora")
|
|
33
|
+
if cleared:
|
|
34
|
+
console.print("[yellow]Cleared Fedora cache[/yellow]")
|
|
35
|
+
else:
|
|
36
|
+
console.print("[yellow]No Fedora cache to clear[/yellow]")
|
|
37
|
+
else:
|
|
38
|
+
cleared = clear_cache()
|
|
39
|
+
if cleared:
|
|
40
|
+
console.print(f"[yellow]Cleared caches: {', '.join(cleared)}[/yellow]")
|
|
41
|
+
else:
|
|
42
|
+
console.print("[yellow]No cache to clear[/yellow]")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
List formats command - display available report formats.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rich import box
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from woolly.commands import app, console
|
|
9
|
+
from woolly.reporters import list_reporters
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command(name="list-formats")
|
|
13
|
+
def list_formats_cmd():
|
|
14
|
+
"""List available report formats."""
|
|
15
|
+
table = Table(title="Available Report Formats", box=box.ROUNDED)
|
|
16
|
+
table.add_column("Format", style="bold")
|
|
17
|
+
table.add_column("Description")
|
|
18
|
+
table.add_column("Aliases", style="dim")
|
|
19
|
+
|
|
20
|
+
for info in list_reporters():
|
|
21
|
+
alias_str = ", ".join(info.aliases) if info.aliases else "-"
|
|
22
|
+
table.add_row(info.format_id, info.description, alias_str)
|
|
23
|
+
|
|
24
|
+
console.print(table)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
List languages command - display available language providers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rich import box
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from woolly.commands import app, console
|
|
9
|
+
from woolly.languages import get_provider, list_providers
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command(name="list-languages")
|
|
13
|
+
def list_languages_cmd():
|
|
14
|
+
"""List available language providers."""
|
|
15
|
+
table = Table(title="Available Languages", box=box.ROUNDED)
|
|
16
|
+
table.add_column("Language", style="bold")
|
|
17
|
+
table.add_column("Registry")
|
|
18
|
+
table.add_column("Aliases", style="dim")
|
|
19
|
+
|
|
20
|
+
for info in list_providers():
|
|
21
|
+
provider = get_provider(info.language_id)
|
|
22
|
+
registry = provider.registry_name if provider else "Unknown"
|
|
23
|
+
alias_str = ", ".join(info.aliases) if info.aliases else "-"
|
|
24
|
+
table.add_row(f"{info.display_name} ({info.language_id})", registry, alias_str)
|
|
25
|
+
|
|
26
|
+
console.print(table)
|
woolly/debug.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging utilities for woolly.
|
|
3
|
+
|
|
4
|
+
All operations are logged to ~/.local/state/woolly/logs/
|
|
5
|
+
- INFO level: Basic operation info (always logged)
|
|
6
|
+
- DEBUG level: Detailed output from commands and API calls (with --debug)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
# Log directory follows XDG Base Directory specification
|
|
15
|
+
LOG_DIR = Path.home() / ".local" / "state" / "woolly" / "logs"
|
|
16
|
+
|
|
17
|
+
# Global logger instance
|
|
18
|
+
_logger: Optional[logging.Logger] = None
|
|
19
|
+
_log_file: Optional[Path] = None
|
|
20
|
+
_debug_enabled: bool = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def setup_logger(debug: bool = False) -> logging.Logger:
|
|
24
|
+
"""
|
|
25
|
+
Set up the file logger.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
debug: If True, log DEBUG level messages (command outputs, API responses).
|
|
29
|
+
If False, only log INFO level messages.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Configured logger instance.
|
|
33
|
+
"""
|
|
34
|
+
global _logger, _log_file, _debug_enabled
|
|
35
|
+
|
|
36
|
+
if _logger is not None:
|
|
37
|
+
return _logger
|
|
38
|
+
|
|
39
|
+
_debug_enabled = debug
|
|
40
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# Create log file with timestamp
|
|
43
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
44
|
+
_log_file = LOG_DIR / f"woolly_{timestamp}.log"
|
|
45
|
+
|
|
46
|
+
_logger = logging.getLogger("woolly")
|
|
47
|
+
_logger.setLevel(logging.DEBUG) # Logger accepts all, handler filters
|
|
48
|
+
|
|
49
|
+
# Clear any existing handlers
|
|
50
|
+
_logger.handlers.clear()
|
|
51
|
+
|
|
52
|
+
# File handler - level depends on debug flag
|
|
53
|
+
file_handler = logging.FileHandler(_log_file)
|
|
54
|
+
file_handler.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
55
|
+
file_format = logging.Formatter(
|
|
56
|
+
"%(asctime)s | %(levelname)-8s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
57
|
+
)
|
|
58
|
+
file_handler.setFormatter(file_format)
|
|
59
|
+
_logger.addHandler(file_handler)
|
|
60
|
+
|
|
61
|
+
return _logger
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_debug_enabled() -> bool:
|
|
65
|
+
"""Check if debug logging is enabled."""
|
|
66
|
+
return _debug_enabled
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_logger() -> logging.Logger:
|
|
70
|
+
"""Get the global logger instance, creating it if needed."""
|
|
71
|
+
if _logger is None:
|
|
72
|
+
return setup_logger()
|
|
73
|
+
return _logger
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_log_file() -> Optional[Path]:
|
|
77
|
+
"""Get the path to the current log file."""
|
|
78
|
+
return _log_file
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def log(message: str, level: str = "info", **kwargs) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Log a message to the file.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
message: Message to log.
|
|
87
|
+
level: Log level (debug, info, warning, error).
|
|
88
|
+
**kwargs: Additional context to include.
|
|
89
|
+
"""
|
|
90
|
+
logger = get_logger()
|
|
91
|
+
|
|
92
|
+
# Format context as key=value pairs
|
|
93
|
+
if kwargs:
|
|
94
|
+
context = " | " + " ".join(f"{k}={v}" for k, v in kwargs.items())
|
|
95
|
+
message = message + context
|
|
96
|
+
|
|
97
|
+
log_method = getattr(logger, level, logger.info)
|
|
98
|
+
log_method(message)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def log_debug(message: str, **kwargs) -> None:
|
|
102
|
+
"""Log a DEBUG level message (only shown with --debug)."""
|
|
103
|
+
log(message, level="debug", **kwargs)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def log_info(message: str, **kwargs) -> None:
|
|
107
|
+
"""Log an INFO level message (always shown)."""
|
|
108
|
+
log(message, level="info", **kwargs)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def log_warning(message: str, **kwargs) -> None:
|
|
112
|
+
"""Log a WARNING level message."""
|
|
113
|
+
log(message, level="warning", **kwargs)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def log_error(message: str, **kwargs) -> None:
|
|
117
|
+
"""Log an ERROR level message."""
|
|
118
|
+
log(message, level="error", **kwargs)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def log_package_check(
|
|
122
|
+
package: str,
|
|
123
|
+
action: str,
|
|
124
|
+
source: Optional[str] = None,
|
|
125
|
+
result: Optional[str] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Log a package check operation (INFO level).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
package: Package name being checked.
|
|
132
|
+
action: Action being performed.
|
|
133
|
+
source: Source of data (cache, api, repoquery).
|
|
134
|
+
result: Result of the operation.
|
|
135
|
+
"""
|
|
136
|
+
log_info(
|
|
137
|
+
f"{action}: {package}",
|
|
138
|
+
package=package,
|
|
139
|
+
source=source,
|
|
140
|
+
result=result,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def log_command_output(command: str, output: str, exit_code: int = 0) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Log command execution and output (DEBUG level).
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
command: The command that was executed.
|
|
150
|
+
output: The command output.
|
|
151
|
+
exit_code: The command exit code.
|
|
152
|
+
"""
|
|
153
|
+
log_debug(f"Command: {command}")
|
|
154
|
+
log_debug(f"Exit code: {exit_code}")
|
|
155
|
+
if output:
|
|
156
|
+
for line in output.strip().split("\n"):
|
|
157
|
+
log_debug(f" > {line}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def log_api_request(method: str, url: str) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Log an API request (DEBUG level).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
method: HTTP method (GET, POST, etc.)
|
|
166
|
+
url: The URL being requested.
|
|
167
|
+
"""
|
|
168
|
+
log_debug(f"API {method}: {url}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def log_api_response(status_code: int, body: Optional[str] = None) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Log an API response (DEBUG level).
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
status_code: HTTP status code.
|
|
177
|
+
body: Response body (truncated if too long).
|
|
178
|
+
"""
|
|
179
|
+
log_debug(f"Response: {status_code}")
|
|
180
|
+
if body and is_debug_enabled():
|
|
181
|
+
# Truncate very long responses
|
|
182
|
+
if len(body) > 500:
|
|
183
|
+
body = body[:500] + "... (truncated)"
|
|
184
|
+
for line in body.strip().split("\n")[:10]: # Max 10 lines
|
|
185
|
+
log_debug(f" > {line}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def log_cache_hit(namespace: str, key: str) -> None:
|
|
189
|
+
"""Log a cache hit (DEBUG level)."""
|
|
190
|
+
log_debug(f"Cache HIT: {namespace}/{key}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def log_cache_miss(namespace: str, key: str) -> None:
|
|
194
|
+
"""Log a cache miss (DEBUG level)."""
|
|
195
|
+
log_debug(f"Cache MISS: {namespace}/{key}")
|