woolly 0.2.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/cache.py +15 -5
- woolly/commands/check.py +107 -43
- woolly/commands/list_formats.py +3 -3
- woolly/commands/list_languages.py +4 -4
- woolly/http.py +34 -0
- woolly/languages/__init__.py +33 -3
- woolly/languages/base.py +11 -7
- woolly/languages/python.py +5 -6
- woolly/languages/rust.py +3 -5
- woolly/reporters/__init__.py +29 -8
- woolly/reporters/base.py +92 -10
- woolly/reporters/json.py +119 -89
- woolly/reporters/markdown.py +30 -53
- woolly/reporters/stdout.py +27 -6
- woolly-0.3.0.dist-info/METADATA +322 -0
- woolly-0.3.0.dist-info/RECORD +25 -0
- woolly-0.2.0.dist-info/METADATA +0 -213
- woolly-0.2.0.dist-info/RECORD +0 -24
- {woolly-0.2.0.dist-info → woolly-0.3.0.dist-info}/WHEEL +0 -0
- {woolly-0.2.0.dist-info → woolly-0.3.0.dist-info}/entry_points.txt +0 -0
- {woolly-0.2.0.dist-info → woolly-0.3.0.dist-info}/licenses/LICENSE +0 -0
woolly/cache.py
CHANGED
|
@@ -8,11 +8,20 @@ import time
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any, Optional
|
|
10
10
|
|
|
11
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
12
|
+
|
|
11
13
|
CACHE_DIR = Path.home() / ".cache" / "woolly"
|
|
12
14
|
DEFAULT_CACHE_TTL = 86400 * 7 # 7 days
|
|
13
15
|
FEDORA_CACHE_TTL = 86400 # 1 day for Fedora repoquery data
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
class CacheEntry(BaseModel):
|
|
19
|
+
"""A cached value with timestamp."""
|
|
20
|
+
|
|
21
|
+
timestamp: float = Field(default_factory=time.time)
|
|
22
|
+
value: Any
|
|
23
|
+
|
|
24
|
+
|
|
16
25
|
def ensure_cache_dir() -> None:
|
|
17
26
|
"""Create cache directory if it doesn't exist."""
|
|
18
27
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -35,18 +44,19 @@ def read_cache(namespace: str, key: str, ttl: int = DEFAULT_CACHE_TTL) -> Option
|
|
|
35
44
|
|
|
36
45
|
try:
|
|
37
46
|
data = json.loads(path.read_text())
|
|
38
|
-
|
|
47
|
+
entry = CacheEntry.model_validate(data)
|
|
48
|
+
if time.time() - entry.timestamp > ttl:
|
|
39
49
|
return None # Expired
|
|
40
|
-
return
|
|
41
|
-
except (json.JSONDecodeError, KeyError):
|
|
50
|
+
return entry.value
|
|
51
|
+
except (json.JSONDecodeError, KeyError, ValidationError):
|
|
42
52
|
return None
|
|
43
53
|
|
|
44
54
|
|
|
45
55
|
def write_cache(namespace: str, key: str, value: Any) -> None:
|
|
46
56
|
"""Write to disk cache."""
|
|
47
57
|
path = get_cache_path(namespace, key)
|
|
48
|
-
|
|
49
|
-
path.write_text(
|
|
58
|
+
entry = CacheEntry(value=value)
|
|
59
|
+
path.write_text(entry.model_dump_json())
|
|
50
60
|
|
|
51
61
|
|
|
52
62
|
def clear_cache(namespace: Optional[str] = None) -> list[str]:
|
woolly/commands/check.py
CHANGED
|
@@ -5,6 +5,7 @@ Check command - analyze package dependencies for Fedora availability.
|
|
|
5
5
|
from typing import Annotated, Optional
|
|
6
6
|
|
|
7
7
|
import cyclopts
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
8
9
|
from rich.tree import Tree
|
|
9
10
|
|
|
10
11
|
from woolly.cache import CACHE_DIR
|
|
@@ -16,6 +17,20 @@ from woolly.progress import ProgressTracker
|
|
|
16
17
|
from woolly.reporters import ReportData, get_available_formats, get_reporter
|
|
17
18
|
|
|
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
|
+
|
|
19
34
|
def build_tree(
|
|
20
35
|
provider: LanguageProvider,
|
|
21
36
|
package_name: str,
|
|
@@ -24,6 +39,8 @@ def build_tree(
|
|
|
24
39
|
depth: int = 0,
|
|
25
40
|
max_depth: int = 50,
|
|
26
41
|
tracker: Optional[ProgressTracker] = None,
|
|
42
|
+
include_optional: bool = False,
|
|
43
|
+
is_optional_dep: bool = False,
|
|
27
44
|
):
|
|
28
45
|
"""
|
|
29
46
|
Recursively build a dependency tree for a package.
|
|
@@ -44,6 +61,10 @@ def build_tree(
|
|
|
44
61
|
Maximum recursion depth.
|
|
45
62
|
tracker
|
|
46
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.
|
|
47
68
|
|
|
48
69
|
Returns
|
|
49
70
|
-------
|
|
@@ -53,9 +74,11 @@ def build_tree(
|
|
|
53
74
|
if visited is None:
|
|
54
75
|
visited = {}
|
|
55
76
|
|
|
77
|
+
optional_marker = " [yellow](optional)[/yellow]" if is_optional_dep else ""
|
|
78
|
+
|
|
56
79
|
if depth > max_depth:
|
|
57
80
|
log(f"Max depth reached for {package_name}", level="warning", depth=depth)
|
|
58
|
-
return f"[dim]{package_name} (max depth reached)[/dim]"
|
|
81
|
+
return f"[dim]{package_name}{optional_marker} (max depth reached)[/dim]"
|
|
59
82
|
|
|
60
83
|
if package_name in visited:
|
|
61
84
|
is_packaged, cached_version = visited[package_name]
|
|
@@ -65,11 +88,9 @@ def build_tree(
|
|
|
65
88
|
result="packaged" if is_packaged else "not packaged",
|
|
66
89
|
)
|
|
67
90
|
if is_packaged:
|
|
68
|
-
return f"[dim]{package_name}[/dim] [dim]v{cached_version}[/dim] • [green]✓[/green] [dim](already visited)[/dim]"
|
|
91
|
+
return f"[dim]{package_name}[/dim] [dim]v{cached_version}[/dim]{optional_marker} • [green]✓[/green] [dim](already visited)[/dim]"
|
|
69
92
|
else:
|
|
70
|
-
return (
|
|
71
|
-
f"[dim]{package_name}[/dim] • [red]✗[/red] [dim](already visited)[/dim]"
|
|
72
|
-
)
|
|
93
|
+
return f"[dim]{package_name}[/dim]{optional_marker} • [red]✗[/red] [dim](already visited)[/dim]"
|
|
73
94
|
|
|
74
95
|
if tracker:
|
|
75
96
|
tracker.update(package_name)
|
|
@@ -84,7 +105,7 @@ def build_tree(
|
|
|
84
105
|
package_name, "Not found", source=provider.registry_name, result="error"
|
|
85
106
|
)
|
|
86
107
|
return (
|
|
87
|
-
f"[bold red]{package_name}[/bold red] • "
|
|
108
|
+
f"[bold red]{package_name}[/bold red]{optional_marker} • "
|
|
88
109
|
f"[red]not found on {provider.registry_name}[/red]"
|
|
89
110
|
)
|
|
90
111
|
|
|
@@ -107,14 +128,14 @@ def build_tree(
|
|
|
107
128
|
ver_str = ", ".join(status.versions) if status.versions else "unknown"
|
|
108
129
|
pkg_str = ", ".join(status.package_names) if status.package_names else ""
|
|
109
130
|
label = (
|
|
110
|
-
f"[bold]{package_name}[/bold] [dim]v{version}[/dim] • "
|
|
131
|
+
f"[bold]{package_name}[/bold] [dim]v{version}[/dim]{optional_marker} • "
|
|
111
132
|
f"[green]✓ packaged[/green] [dim]({ver_str})[/dim]"
|
|
112
133
|
)
|
|
113
134
|
if pkg_str:
|
|
114
135
|
label += f" [dim cyan][{pkg_str}][/dim cyan]"
|
|
115
136
|
else:
|
|
116
137
|
label = (
|
|
117
|
-
f"[bold]{package_name}[/bold] [dim]v{version}[/dim] • "
|
|
138
|
+
f"[bold]{package_name}[/bold] [dim]v{version}[/dim]{optional_marker} • "
|
|
118
139
|
f"[red]✗ not packaged[/red]"
|
|
119
140
|
)
|
|
120
141
|
|
|
@@ -125,16 +146,26 @@ def build_tree(
|
|
|
125
146
|
package_name, "Fetching dependencies", source=provider.registry_name
|
|
126
147
|
)
|
|
127
148
|
|
|
128
|
-
deps = provider.get_normal_dependencies(
|
|
149
|
+
deps = provider.get_normal_dependencies(
|
|
150
|
+
package_name, version, include_optional=include_optional
|
|
151
|
+
)
|
|
129
152
|
|
|
130
153
|
log(f"Found {len(deps)} dependencies for {package_name}", deps=len(deps))
|
|
131
154
|
|
|
132
155
|
if tracker and deps:
|
|
133
156
|
tracker.update(package_name, discovered=len(deps))
|
|
134
157
|
|
|
135
|
-
for dep_name, _dep_req in deps:
|
|
158
|
+
for dep_name, _dep_req, dep_is_optional in deps:
|
|
136
159
|
child = build_tree(
|
|
137
|
-
provider,
|
|
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,
|
|
138
169
|
)
|
|
139
170
|
if isinstance(child, str):
|
|
140
171
|
node.add(child)
|
|
@@ -148,51 +179,65 @@ def build_tree(
|
|
|
148
179
|
return node
|
|
149
180
|
|
|
150
181
|
|
|
151
|
-
def collect_stats(tree, stats=None):
|
|
182
|
+
def collect_stats(tree, stats: Optional[TreeStats] = None) -> TreeStats:
|
|
152
183
|
"""Walk the tree and collect statistics."""
|
|
153
184
|
if stats is None:
|
|
154
|
-
stats =
|
|
155
|
-
"total": 0,
|
|
156
|
-
"packaged": 0,
|
|
157
|
-
"missing": 0,
|
|
158
|
-
"missing_list": [],
|
|
159
|
-
"packaged_list": [],
|
|
160
|
-
}
|
|
185
|
+
stats = TreeStats()
|
|
161
186
|
|
|
162
187
|
def walk(t):
|
|
163
188
|
if isinstance(t, str):
|
|
164
|
-
stats
|
|
189
|
+
stats.total += 1
|
|
190
|
+
is_optional = "(optional)" in t
|
|
191
|
+
if is_optional:
|
|
192
|
+
stats.optional_total += 1
|
|
165
193
|
if "not packaged" in t or "not found" in t:
|
|
166
|
-
stats
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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)
|
|
173
206
|
elif "packaged" in t:
|
|
174
|
-
stats
|
|
207
|
+
stats.packaged += 1
|
|
208
|
+
if is_optional:
|
|
209
|
+
stats.optional_packaged += 1
|
|
175
210
|
return
|
|
176
211
|
|
|
177
212
|
if hasattr(t, "label"):
|
|
178
213
|
label = str(t.label)
|
|
179
|
-
stats
|
|
214
|
+
stats.total += 1
|
|
215
|
+
is_optional = "(optional)" in label
|
|
216
|
+
if is_optional:
|
|
217
|
+
stats.optional_total += 1
|
|
180
218
|
if "not packaged" in label or "not found" in label:
|
|
181
|
-
stats
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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)
|
|
188
231
|
elif "packaged" in label:
|
|
189
|
-
stats
|
|
232
|
+
stats.packaged += 1
|
|
190
233
|
name = (
|
|
191
234
|
label.split("[/bold]")[0].split("[bold]")[-1]
|
|
192
235
|
if "[bold]" in label
|
|
193
236
|
else "unknown"
|
|
194
237
|
)
|
|
195
|
-
stats
|
|
238
|
+
stats.packaged_list.append(name)
|
|
239
|
+
if is_optional:
|
|
240
|
+
stats.optional_packaged += 1
|
|
196
241
|
|
|
197
242
|
if hasattr(t, "children"):
|
|
198
243
|
for child in t.children:
|
|
@@ -232,6 +277,14 @@ def check(
|
|
|
232
277
|
help="Maximum recursion depth.",
|
|
233
278
|
),
|
|
234
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,
|
|
235
288
|
no_progress: Annotated[
|
|
236
289
|
bool,
|
|
237
290
|
cyclopts.Parameter(
|
|
@@ -266,6 +319,8 @@ def check(
|
|
|
266
319
|
Specific version to check (default: latest).
|
|
267
320
|
max_depth
|
|
268
321
|
Maximum recursion depth for dependency tree.
|
|
322
|
+
optional
|
|
323
|
+
Include optional dependencies in the analysis.
|
|
269
324
|
no_progress
|
|
270
325
|
Disable progress bar during analysis.
|
|
271
326
|
debug
|
|
@@ -294,6 +349,7 @@ def check(
|
|
|
294
349
|
package=package,
|
|
295
350
|
language=lang,
|
|
296
351
|
max_depth=max_depth,
|
|
352
|
+
include_optional=optional,
|
|
297
353
|
debug=debug,
|
|
298
354
|
report_format=report,
|
|
299
355
|
)
|
|
@@ -301,6 +357,8 @@ def check(
|
|
|
301
357
|
console.print(
|
|
302
358
|
f"\n[bold underline]Analyzing {provider.display_name} package:[/] {package}"
|
|
303
359
|
)
|
|
360
|
+
if optional:
|
|
361
|
+
console.print("[yellow]Including optional dependencies[/yellow]")
|
|
304
362
|
console.print(f"[dim]Registry: {provider.registry_name}[/dim]")
|
|
305
363
|
console.print(f"[dim]Cache directory: {CACHE_DIR}[/dim]")
|
|
306
364
|
console.print()
|
|
@@ -317,6 +375,7 @@ def check(
|
|
|
317
375
|
version,
|
|
318
376
|
max_depth=max_depth,
|
|
319
377
|
tracker=tracker,
|
|
378
|
+
include_optional=optional,
|
|
320
379
|
)
|
|
321
380
|
if tracker:
|
|
322
381
|
tracker.finish()
|
|
@@ -335,14 +394,19 @@ def check(
|
|
|
335
394
|
root_package=package,
|
|
336
395
|
language=provider.display_name,
|
|
337
396
|
registry=provider.registry_name,
|
|
338
|
-
total_dependencies=stats
|
|
339
|
-
packaged_count=stats
|
|
340
|
-
missing_count=stats
|
|
341
|
-
missing_packages=stats
|
|
342
|
-
packaged_packages=stats
|
|
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,
|
|
343
402
|
tree=tree,
|
|
344
403
|
max_depth=max_depth,
|
|
345
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,
|
|
346
410
|
)
|
|
347
411
|
|
|
348
412
|
# Generate report
|
woolly/commands/list_formats.py
CHANGED
|
@@ -17,8 +17,8 @@ def list_formats_cmd():
|
|
|
17
17
|
table.add_column("Description")
|
|
18
18
|
table.add_column("Aliases", style="dim")
|
|
19
19
|
|
|
20
|
-
for
|
|
21
|
-
alias_str = ", ".join(aliases) if aliases else "-"
|
|
22
|
-
table.add_row(format_id, description, alias_str)
|
|
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
23
|
|
|
24
24
|
console.print(table)
|
|
@@ -17,10 +17,10 @@ def list_languages_cmd():
|
|
|
17
17
|
table.add_column("Registry")
|
|
18
18
|
table.add_column("Aliases", style="dim")
|
|
19
19
|
|
|
20
|
-
for
|
|
21
|
-
provider = get_provider(
|
|
20
|
+
for info in list_providers():
|
|
21
|
+
provider = get_provider(info.language_id)
|
|
22
22
|
registry = provider.registry_name if provider else "Unknown"
|
|
23
|
-
alias_str = ", ".join(aliases) if aliases else "-"
|
|
24
|
-
table.add_row(f"{display_name} ({
|
|
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
25
|
|
|
26
26
|
console.print(table)
|
woolly/http.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared HTTP client configuration for API requests.
|
|
3
|
+
|
|
4
|
+
This module provides centralized HTTP configuration including
|
|
5
|
+
the User-Agent header and common request settings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
|
|
11
|
+
# Version identifier for the User-Agent
|
|
12
|
+
VERSION = version("woolly")
|
|
13
|
+
PROJECT_URL = "https://github.com/r0x0d/woolly"
|
|
14
|
+
|
|
15
|
+
# Shared headers for all API requests
|
|
16
|
+
DEFAULT_HEADERS = {
|
|
17
|
+
"User-Agent": f"woolly/{VERSION} ({PROJECT_URL})",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get(url: str, **kwargs) -> httpx.Response:
|
|
22
|
+
"""
|
|
23
|
+
Make a GET request with default headers.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
url: The URL to request.
|
|
27
|
+
**kwargs: Additional arguments passed to httpx.get().
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
httpx.Response object.
|
|
31
|
+
"""
|
|
32
|
+
headers = kwargs.pop("headers", {})
|
|
33
|
+
merged_headers = {**DEFAULT_HEADERS, **headers}
|
|
34
|
+
return httpx.get(url, headers=merged_headers, **kwargs)
|
woolly/languages/__init__.py
CHANGED
|
@@ -8,10 +8,21 @@ inheriting from LanguageProvider and add it to PROVIDERS dict.
|
|
|
8
8
|
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
11
13
|
from woolly.languages.base import LanguageProvider
|
|
12
14
|
from woolly.languages.python import PythonProvider
|
|
13
15
|
from woolly.languages.rust import RustProvider
|
|
14
16
|
|
|
17
|
+
|
|
18
|
+
class ProviderInfo(BaseModel):
|
|
19
|
+
"""Information about an available language provider."""
|
|
20
|
+
|
|
21
|
+
language_id: str
|
|
22
|
+
display_name: str
|
|
23
|
+
aliases: list[str] = Field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
15
26
|
# Registry of available language providers
|
|
16
27
|
# Key: language identifier (used in CLI)
|
|
17
28
|
# Value: Provider class
|
|
@@ -52,21 +63,40 @@ def get_provider(language: str) -> Optional[LanguageProvider]:
|
|
|
52
63
|
return provider_class()
|
|
53
64
|
|
|
54
65
|
|
|
55
|
-
def list_providers() -> list[
|
|
66
|
+
def list_providers() -> list[ProviderInfo]:
|
|
56
67
|
"""
|
|
57
68
|
List all available providers.
|
|
58
69
|
|
|
59
70
|
Returns:
|
|
60
|
-
List of
|
|
71
|
+
List of ProviderInfo objects with language details.
|
|
61
72
|
"""
|
|
62
73
|
result = []
|
|
63
74
|
for lang_id, provider_class in PROVIDERS.items():
|
|
64
75
|
# Find aliases for this language
|
|
65
76
|
aliases = [alias for alias, target in ALIASES.items() if target == lang_id]
|
|
66
|
-
result.append(
|
|
77
|
+
result.append(
|
|
78
|
+
ProviderInfo(
|
|
79
|
+
language_id=lang_id,
|
|
80
|
+
display_name=provider_class.display_name,
|
|
81
|
+
aliases=aliases,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
67
84
|
return result
|
|
68
85
|
|
|
69
86
|
|
|
70
87
|
def get_available_languages() -> list[str]:
|
|
71
88
|
"""Get list of available language identifiers."""
|
|
72
89
|
return list(PROVIDERS.keys())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"LanguageProvider",
|
|
94
|
+
"ProviderInfo",
|
|
95
|
+
"PythonProvider",
|
|
96
|
+
"RustProvider",
|
|
97
|
+
"get_provider",
|
|
98
|
+
"list_providers",
|
|
99
|
+
"get_available_languages",
|
|
100
|
+
"PROVIDERS",
|
|
101
|
+
"ALIASES",
|
|
102
|
+
]
|
woolly/languages/base.py
CHANGED
|
@@ -133,20 +133,24 @@ class LanguageProvider(ABC):
|
|
|
133
133
|
return info.latest_version
|
|
134
134
|
|
|
135
135
|
def get_normal_dependencies(
|
|
136
|
-
self,
|
|
137
|
-
|
|
136
|
+
self,
|
|
137
|
+
package_name: str,
|
|
138
|
+
version: Optional[str] = None,
|
|
139
|
+
include_optional: bool = False,
|
|
140
|
+
) -> list[tuple[str, str, bool]]:
|
|
138
141
|
"""
|
|
139
|
-
Get
|
|
142
|
+
Get runtime dependencies for a package.
|
|
140
143
|
|
|
141
144
|
This method filters dependencies to only include normal (runtime)
|
|
142
|
-
dependencies
|
|
145
|
+
dependencies. By default, optional dependencies are excluded.
|
|
143
146
|
|
|
144
147
|
Args:
|
|
145
148
|
package_name: The name of the package.
|
|
146
149
|
version: Specific version, or None for latest.
|
|
150
|
+
include_optional: If True, include optional dependencies.
|
|
147
151
|
|
|
148
152
|
Returns:
|
|
149
|
-
List of tuples: (dependency_name, version_requirement)
|
|
153
|
+
List of tuples: (dependency_name, version_requirement, is_optional)
|
|
150
154
|
"""
|
|
151
155
|
if version is None:
|
|
152
156
|
version = self.get_latest_version(package_name)
|
|
@@ -155,9 +159,9 @@ class LanguageProvider(ABC):
|
|
|
155
159
|
|
|
156
160
|
deps = self.fetch_dependencies(package_name, version)
|
|
157
161
|
return [
|
|
158
|
-
(d.name, d.version_requirement)
|
|
162
|
+
(d.name, d.version_requirement, d.optional)
|
|
159
163
|
for d in deps
|
|
160
|
-
if d.kind == "normal" and not d.optional
|
|
164
|
+
if d.kind == "normal" and (include_optional or not d.optional)
|
|
161
165
|
]
|
|
162
166
|
|
|
163
167
|
def get_fedora_provides_pattern(self, package_name: str) -> str:
|
woolly/languages/python.py
CHANGED
|
@@ -8,8 +8,7 @@ Fedora repositories for Python packages.
|
|
|
8
8
|
import re
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
|
-
import
|
|
12
|
-
|
|
11
|
+
from woolly import http
|
|
13
12
|
from woolly.cache import DEFAULT_CACHE_TTL, read_cache, write_cache
|
|
14
13
|
from woolly.debug import (
|
|
15
14
|
log_api_request,
|
|
@@ -20,7 +19,6 @@ from woolly.debug import (
|
|
|
20
19
|
from woolly.languages.base import Dependency, LanguageProvider, PackageInfo
|
|
21
20
|
|
|
22
21
|
PYPI_API = "https://pypi.org/pypi"
|
|
23
|
-
HEADERS = {"User-Agent": "woolly/0.1.0 (https://github.com/r0x0d/woolly)"}
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
class PythonProvider(LanguageProvider):
|
|
@@ -51,7 +49,7 @@ class PythonProvider(LanguageProvider):
|
|
|
51
49
|
log_cache_miss(self.cache_namespace, cache_key)
|
|
52
50
|
url = f"{PYPI_API}/{package_name}/json"
|
|
53
51
|
log_api_request("GET", url)
|
|
54
|
-
r =
|
|
52
|
+
r = http.get(url)
|
|
55
53
|
log_api_response(r.status_code, r.text[:500] if r.text else None)
|
|
56
54
|
|
|
57
55
|
if r.status_code == 404:
|
|
@@ -96,7 +94,7 @@ class PythonProvider(LanguageProvider):
|
|
|
96
94
|
log_cache_miss(self.cache_namespace, cache_key)
|
|
97
95
|
url = f"{PYPI_API}/{package_name}/{version}/json"
|
|
98
96
|
log_api_request("GET", url)
|
|
99
|
-
r =
|
|
97
|
+
r = http.get(url)
|
|
100
98
|
log_api_response(r.status_code, r.text[:500] if r.text else None)
|
|
101
99
|
|
|
102
100
|
if r.status_code != 200:
|
|
@@ -141,7 +139,8 @@ class PythonProvider(LanguageProvider):
|
|
|
141
139
|
|
|
142
140
|
if "extra ==" in req_string or "extra==" in req_string:
|
|
143
141
|
is_optional = True
|
|
144
|
-
kind
|
|
142
|
+
# Keep kind as "normal" so optional dependencies can be included
|
|
143
|
+
# when --optional flag is used
|
|
145
144
|
|
|
146
145
|
# Extract the package name and version requirement
|
|
147
146
|
# Handle environment markers (everything after ';')
|
woolly/languages/rust.py
CHANGED
|
@@ -7,8 +7,7 @@ Fedora repositories for Rust crate packages.
|
|
|
7
7
|
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
|
|
10
|
+
from woolly import http
|
|
12
11
|
from woolly.cache import DEFAULT_CACHE_TTL, read_cache, write_cache
|
|
13
12
|
from woolly.debug import (
|
|
14
13
|
log_api_request,
|
|
@@ -19,7 +18,6 @@ from woolly.debug import (
|
|
|
19
18
|
from woolly.languages.base import Dependency, LanguageProvider, PackageInfo
|
|
20
19
|
|
|
21
20
|
CRATES_API = "https://crates.io/api/v1/crates"
|
|
22
|
-
HEADERS = {"User-Agent": "woolly/0.1.0 (https://github.com/r0x0d/woolly)"}
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
class RustProvider(LanguageProvider):
|
|
@@ -50,7 +48,7 @@ class RustProvider(LanguageProvider):
|
|
|
50
48
|
log_cache_miss(self.cache_namespace, cache_key)
|
|
51
49
|
url = f"{CRATES_API}/{package_name}"
|
|
52
50
|
log_api_request("GET", url)
|
|
53
|
-
r =
|
|
51
|
+
r = http.get(url)
|
|
54
52
|
log_api_response(r.status_code, r.text[:500] if r.text else None)
|
|
55
53
|
|
|
56
54
|
if r.status_code == 404:
|
|
@@ -91,7 +89,7 @@ class RustProvider(LanguageProvider):
|
|
|
91
89
|
log_cache_miss(self.cache_namespace, cache_key)
|
|
92
90
|
url = f"{CRATES_API}/{package_name}/{version}/dependencies"
|
|
93
91
|
log_api_request("GET", url)
|
|
94
|
-
r =
|
|
92
|
+
r = http.get(url)
|
|
95
93
|
log_api_response(r.status_code, r.text[:500] if r.text else None)
|
|
96
94
|
|
|
97
95
|
if r.status_code != 200:
|
woolly/reporters/__init__.py
CHANGED
|
@@ -6,12 +6,24 @@ To add a new format, create a module in this directory that defines a class
|
|
|
6
6
|
inheriting from Reporter and add it to REPORTERS dict.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
9
12
|
from rich.console import Console
|
|
10
13
|
|
|
11
|
-
from woolly.reporters.base import
|
|
12
|
-
from woolly.reporters.stdout import StdoutReporter
|
|
13
|
-
from woolly.reporters.markdown import MarkdownReporter
|
|
14
|
+
from woolly.reporters.base import ReportData, Reporter, strip_markup
|
|
14
15
|
from woolly.reporters.json import JsonReporter
|
|
16
|
+
from woolly.reporters.markdown import MarkdownReporter
|
|
17
|
+
from woolly.reporters.stdout import StdoutReporter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReporterInfo(BaseModel):
|
|
21
|
+
"""Information about an available reporter."""
|
|
22
|
+
|
|
23
|
+
format_id: str
|
|
24
|
+
description: str
|
|
25
|
+
aliases: list[str] = Field(default_factory=list)
|
|
26
|
+
|
|
15
27
|
|
|
16
28
|
# Registry of available reporters
|
|
17
29
|
# Key: format identifier (used in CLI)
|
|
@@ -30,7 +42,9 @@ ALIASES: dict[str, str] = {
|
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
|
|
33
|
-
def get_reporter(
|
|
45
|
+
def get_reporter(
|
|
46
|
+
format_name: str, console: Optional[Console] = None
|
|
47
|
+
) -> Optional[Reporter]:
|
|
34
48
|
"""
|
|
35
49
|
Get an instantiated reporter for the specified format.
|
|
36
50
|
|
|
@@ -57,18 +71,24 @@ def get_reporter(format_name: str, console: Console | None = None) -> Reporter |
|
|
|
57
71
|
return reporter_class()
|
|
58
72
|
|
|
59
73
|
|
|
60
|
-
def list_reporters() -> list[
|
|
74
|
+
def list_reporters() -> list[ReporterInfo]:
|
|
61
75
|
"""
|
|
62
76
|
List all available reporters.
|
|
63
77
|
|
|
64
78
|
Returns:
|
|
65
|
-
List of
|
|
79
|
+
List of ReporterInfo objects with format details.
|
|
66
80
|
"""
|
|
67
81
|
result = []
|
|
68
82
|
for format_id, reporter_class in REPORTERS.items():
|
|
69
83
|
# Find aliases for this format
|
|
70
84
|
aliases = [alias for alias, target in ALIASES.items() if target == format_id]
|
|
71
|
-
result.append(
|
|
85
|
+
result.append(
|
|
86
|
+
ReporterInfo(
|
|
87
|
+
format_id=format_id,
|
|
88
|
+
description=reporter_class.description,
|
|
89
|
+
aliases=aliases,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
72
92
|
return result
|
|
73
93
|
|
|
74
94
|
|
|
@@ -80,11 +100,12 @@ def get_available_formats() -> list[str]:
|
|
|
80
100
|
__all__ = [
|
|
81
101
|
"Reporter",
|
|
82
102
|
"ReportData",
|
|
83
|
-
"
|
|
103
|
+
"ReporterInfo",
|
|
84
104
|
"StdoutReporter",
|
|
85
105
|
"MarkdownReporter",
|
|
86
106
|
"JsonReporter",
|
|
87
107
|
"get_reporter",
|
|
88
108
|
"list_reporters",
|
|
89
109
|
"get_available_formats",
|
|
110
|
+
"strip_markup",
|
|
90
111
|
]
|