woolly 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.
woolly/__init__.py ADDED
File without changes
woolly/__main__.py ADDED
@@ -0,0 +1,482 @@
1
+ import argparse
2
+ import sys
3
+ from typing import Optional
4
+ import requests
5
+ import subprocess
6
+ import re
7
+ import json
8
+ import hashlib
9
+ import time
10
+ from pathlib import Path
11
+ from rich.tree import Tree
12
+ from rich.console import Console
13
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn, TaskID
14
+ from rich import box
15
+ from rich.table import Table
16
+
17
+ CRATES_API = "https://crates.io/api/v1/crates"
18
+ CACHE_DIR = Path.home() / ".cache" / "fedora-rust-checker"
19
+ CACHE_TTL = 86400 * 7 # 7 days for crates.io data
20
+ FEDORA_CACHE_TTL = 86400 # 1 day for Fedora repoquery data
21
+
22
+ console = Console()
23
+
24
+ # Required headers for crates.io API
25
+ HEADERS = {"User-Agent": "fedora-rust-checker@0.1.0"}
26
+
27
+
28
+ # ------------------------------------------------------------
29
+ # Disk Cache Helpers
30
+ # ------------------------------------------------------------
31
+ def ensure_cache_dir():
32
+ """Create cache directory if it doesn't exist."""
33
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
34
+ (CACHE_DIR / "crates").mkdir(exist_ok=True)
35
+ (CACHE_DIR / "fedora").mkdir(exist_ok=True)
36
+
37
+
38
+ def get_cache_path(namespace: str, key: str) -> Path:
39
+ """Get path for a cache entry."""
40
+ safe_key = hashlib.md5(key.encode()).hexdigest()
41
+ return CACHE_DIR / namespace / f"{safe_key}.json"
42
+
43
+
44
+ def read_cache(namespace: str, key: str, ttl: int = CACHE_TTL):
45
+ """Read from disk cache if not expired."""
46
+ path = get_cache_path(namespace, key)
47
+ if not path.exists():
48
+ return None
49
+
50
+ try:
51
+ data = json.loads(path.read_text())
52
+ if time.time() - data.get("timestamp", 0) > ttl:
53
+ return None # Expired
54
+ return data.get("value")
55
+ except (json.JSONDecodeError, KeyError):
56
+ return None
57
+
58
+
59
+ def write_cache(namespace: str, key: str, value):
60
+ """Write to disk cache."""
61
+ ensure_cache_dir()
62
+ path = get_cache_path(namespace, key)
63
+ data = {"timestamp": time.time(), "value": value}
64
+ path.write_text(json.dumps(data))
65
+
66
+
67
+ # ------------------------------------------------------------
68
+ # Fedora repoquery helpers
69
+ # ------------------------------------------------------------
70
+ def repoquery_crate(crate: str):
71
+ """
72
+ Query Fedora for a crate using the virtual provides format: crate(name)
73
+ Returns (is_packaged, versions_list, package_names)
74
+ """
75
+ cache_key = f"repoquery:{crate}"
76
+ cached = read_cache("fedora", cache_key, FEDORA_CACHE_TTL)
77
+ if cached is not None:
78
+ return tuple(cached)
79
+
80
+ provide_pattern = f"crate({crate})"
81
+
82
+ try:
83
+ out = subprocess.check_output(
84
+ ["dnf", "repoquery", "--whatprovides", provide_pattern,
85
+ "--queryformat", "%{NAME}|%{VERSION}"],
86
+ stderr=subprocess.DEVNULL,
87
+ ).decode().strip()
88
+
89
+ if not out:
90
+ result = (False, [], [])
91
+ write_cache("fedora", cache_key, list(result))
92
+ return result
93
+
94
+ versions = set()
95
+ packages = set()
96
+ for line in out.split("\n"):
97
+ if "|" in line:
98
+ pkg, ver = line.split("|", 1)
99
+ packages.add(pkg)
100
+ versions.add(ver)
101
+
102
+ result = (True, sorted(versions), sorted(packages))
103
+ write_cache("fedora", cache_key, [result[0], result[1], result[2]])
104
+ return result
105
+ except subprocess.CalledProcessError:
106
+ result = (False, [], [])
107
+ write_cache("fedora", cache_key, list(result))
108
+ return result
109
+
110
+
111
+ def get_crate_provides_version(crate: str):
112
+ """
113
+ Get the actual crate version provided by Fedora packages.
114
+ """
115
+ cache_key = f"provides:{crate}"
116
+ cached = read_cache("fedora", cache_key, FEDORA_CACHE_TTL)
117
+ if cached is not None:
118
+ return cached
119
+
120
+ provide_pattern = f"crate({crate})"
121
+
122
+ try:
123
+ out = subprocess.check_output(
124
+ ["dnf", "repoquery", "--provides", "--whatprovides", provide_pattern],
125
+ stderr=subprocess.DEVNULL,
126
+ ).decode().strip()
127
+
128
+ if not out:
129
+ write_cache("fedora", cache_key, [])
130
+ return []
131
+
132
+ versions = set()
133
+ pattern = re.compile(rf"crate\({re.escape(crate)}\)\s*=\s*([\d.]+)")
134
+ for line in out.split("\n"):
135
+ match = pattern.search(line)
136
+ if match:
137
+ versions.add(match.group(1))
138
+
139
+ result = sorted(versions)
140
+ write_cache("fedora", cache_key, result)
141
+ return result
142
+ except subprocess.CalledProcessError:
143
+ write_cache("fedora", cache_key, [])
144
+ return []
145
+
146
+
147
+ def crate_is_packaged(crate: str):
148
+ """
149
+ Check if a crate is packaged in Fedora.
150
+ Returns (is_packaged, crate_versions, package_names)
151
+ """
152
+ is_packaged, pkg_versions, packages = repoquery_crate(crate)
153
+
154
+ if not is_packaged:
155
+ alt_name = crate.replace("-", "_")
156
+ if alt_name != crate:
157
+ is_packaged, pkg_versions, packages = repoquery_crate(alt_name)
158
+
159
+ if not is_packaged:
160
+ alt_name = crate.replace("_", "-")
161
+ if alt_name != crate:
162
+ is_packaged, pkg_versions, packages = repoquery_crate(alt_name)
163
+
164
+ if is_packaged:
165
+ crate_versions = get_crate_provides_version(crate)
166
+ if not crate_versions:
167
+ crate_versions = pkg_versions
168
+ return (True, crate_versions, packages)
169
+
170
+ return (False, [], [])
171
+
172
+
173
+ # ------------------------------------------------------------
174
+ # Crates.io API helpers
175
+ # ------------------------------------------------------------
176
+ def fetch_crate_info(crate_name: str):
177
+ """Fetch basic crate info (latest version, etc.)"""
178
+ cache_key = f"info:{crate_name}"
179
+ cached = read_cache("crates", cache_key)
180
+ if cached is not None:
181
+ return cached
182
+
183
+ url = f"{CRATES_API}/{crate_name}"
184
+ r = requests.get(url, headers=HEADERS)
185
+ if r.status_code == 404:
186
+ write_cache("crates", cache_key, None)
187
+ return None
188
+ if r.status_code != 200:
189
+ raise RuntimeError(f"Failed to fetch metadata for crate {crate_name}: {r.status_code}")
190
+
191
+ data = r.json()
192
+ write_cache("crates", cache_key, data)
193
+ return data
194
+
195
+
196
+ def fetch_dependencies(crate_name: str, version: str):
197
+ """
198
+ Fetch dependencies for a specific crate version.
199
+ """
200
+ cache_key = f"deps:{crate_name}:{version}"
201
+ cached = read_cache("crates", cache_key)
202
+ if cached is not None:
203
+ return cached
204
+
205
+ url = f"{CRATES_API}/{crate_name}/{version}/dependencies"
206
+ r = requests.get(url, headers=HEADERS)
207
+ if r.status_code != 200:
208
+ write_cache("crates", cache_key, [])
209
+ return []
210
+
211
+ data = r.json()
212
+ deps = data.get("dependencies", [])
213
+ write_cache("crates", cache_key, deps)
214
+ return deps
215
+
216
+
217
+ def get_latest_version(crate_name: str):
218
+ """Get the latest (newest) version of a crate."""
219
+ info = fetch_crate_info(crate_name)
220
+ if info is None:
221
+ return None
222
+ return info["crate"]["newest_version"]
223
+
224
+
225
+ def get_normal_dependencies(crate_name: str, version: Optional[str] = None):
226
+ """
227
+ Returns normal (non-dev, non-build) dependencies as list of tuples:
228
+ [("dep_name", "version_req"), ...]
229
+ """
230
+ if version is None:
231
+ version = get_latest_version(crate_name)
232
+ if version is None:
233
+ return []
234
+
235
+ deps = fetch_dependencies(crate_name, version)
236
+ result = []
237
+ for d in deps:
238
+ if d.get("kind") == "normal":
239
+ if d.get("optional", False):
240
+ continue
241
+ result.append((d["crate_id"], d["req"]))
242
+ return result
243
+
244
+
245
+ # ------------------------------------------------------------
246
+ # Progress Tracker
247
+ # ------------------------------------------------------------
248
+ class ProgressTracker:
249
+ def __init__(self):
250
+ self.progress = Progress(
251
+ SpinnerColumn(),
252
+ TextColumn("[bold blue]{task.description}"),
253
+ BarColumn(bar_width=30),
254
+ TaskProgressColumn(),
255
+ TextColumn("•"),
256
+ TimeElapsedColumn(),
257
+ TextColumn("[dim]{task.fields[status]}[/dim]"),
258
+ console=console,
259
+ )
260
+ self.task: TaskID = TaskID(0)
261
+ self.processed = 0
262
+ self.total_discovered = 0
263
+
264
+ def start(self):
265
+ self.task = self.progress.add_task(
266
+ "Analyzing dependencies",
267
+ total=None,
268
+ status="starting..."
269
+ )
270
+ self.progress.start()
271
+
272
+ def stop(self):
273
+ self.progress.stop()
274
+
275
+ def update(self, crate_name: str, discovered: int = 0):
276
+ self.processed += 1
277
+ self.total_discovered += discovered
278
+
279
+ if self.total_discovered > 0:
280
+ self.progress.update(
281
+ self.task,
282
+ completed=self.processed,
283
+ total=self.processed + self.total_discovered,
284
+ status=f"checking: {crate_name}"
285
+ )
286
+ else:
287
+ self.progress.update(
288
+ self.task,
289
+ status=f"checking: {crate_name}"
290
+ )
291
+
292
+ def finish(self):
293
+ self.progress.update(
294
+ self.task,
295
+ completed=self.processed,
296
+ total=self.processed,
297
+ status="[green]complete![/green]"
298
+ )
299
+
300
+
301
+ # ------------------------------------------------------------
302
+ # Tree Builder (FULLY RECURSIVE)
303
+ # ------------------------------------------------------------
304
+ def build_tree(crate_name: str, version: Optional[str] = None, visited=None, depth=0, max_depth=50, tracker=None):
305
+ if visited is None:
306
+ visited = set()
307
+
308
+ if depth > max_depth:
309
+ return f"[dim]{crate_name} (max depth reached)[/dim]"
310
+
311
+ if crate_name in visited:
312
+ return f"[dim]{crate_name} (already visited)[/dim]"
313
+ visited.add(crate_name)
314
+
315
+ if tracker:
316
+ tracker.update(crate_name)
317
+
318
+ if version is None:
319
+ version = get_latest_version(crate_name)
320
+ if version is None:
321
+ return f"[bold red]{crate_name}[/bold red] • [red]not found on crates.io[/red]"
322
+
323
+ # Check Fedora packaging status
324
+ is_packaged, fedora_versions, packages = crate_is_packaged(crate_name)
325
+
326
+ if is_packaged:
327
+ ver_str = ", ".join(fedora_versions) if fedora_versions else "unknown"
328
+ pkg_str = ", ".join(packages) if packages else ""
329
+ label = (f"[bold]{crate_name}[/bold] [dim]v{version}[/dim] • "
330
+ f"[green]✓ packaged[/green] [dim]({ver_str})[/dim]")
331
+ if pkg_str:
332
+ label += f" [dim cyan][{pkg_str}][/dim cyan]"
333
+ else:
334
+ label = f"[bold]{crate_name}[/bold] [dim]v{version}[/dim] • [red]✗ not packaged[/red]"
335
+
336
+ node = Tree(label)
337
+
338
+ # ALWAYS recurse into dependencies regardless of packaging status
339
+ deps = get_normal_dependencies(crate_name, version)
340
+ if tracker and deps:
341
+ tracker.update(crate_name, discovered=len(deps))
342
+
343
+ for (dep_name, dep_req) in deps:
344
+ child = build_tree(dep_name, None, visited, depth + 1, max_depth, tracker)
345
+ if isinstance(child, str):
346
+ node.add(child)
347
+ else:
348
+ node.add(child)
349
+
350
+ return node
351
+
352
+
353
+ # ------------------------------------------------------------
354
+ # Summary Collection
355
+ # ------------------------------------------------------------
356
+ def collect_stats(tree, stats=None):
357
+ """Walk the tree and collect statistics."""
358
+ if stats is None:
359
+ stats = {"total": 0, "packaged": 0, "missing": 0, "missing_list": [], "packaged_list": []}
360
+
361
+ def walk(t):
362
+ if isinstance(t, str):
363
+ stats["total"] += 1
364
+ if "not packaged" in t or "not found" in t:
365
+ stats["missing"] += 1
366
+ name = t.split("[/bold]")[0].split("]")[-1] if "[/bold]" in t else t.split()[0]
367
+ stats["missing_list"].append(name)
368
+ elif "packaged" in t:
369
+ stats["packaged"] += 1
370
+ return
371
+
372
+ if hasattr(t, "label"):
373
+ label = str(t.label)
374
+ stats["total"] += 1
375
+ if "not packaged" in label or "not found" in label:
376
+ stats["missing"] += 1
377
+ name = label.split("[/bold]")[0].split("[bold]")[-1] if "[bold]" in label else "unknown"
378
+ stats["missing_list"].append(name)
379
+ elif "packaged" in label:
380
+ stats["packaged"] += 1
381
+ name = label.split("[/bold]")[0].split("[bold]")[-1] if "[bold]" in label else "unknown"
382
+ stats["packaged_list"].append(name)
383
+
384
+ if hasattr(t, "children"):
385
+ for child in t.children:
386
+ walk(child)
387
+
388
+ walk(tree)
389
+ return stats
390
+
391
+
392
+ def print_summary_table(root_crate, tree):
393
+ stats = collect_stats(tree)
394
+
395
+ table = Table(title=f"Dependency Summary for '{root_crate}'", box=box.ROUNDED)
396
+ table.add_column("Metric", style="bold")
397
+ table.add_column("Value", justify="right")
398
+
399
+ table.add_row("Total dependencies checked", str(stats["total"]))
400
+ table.add_row("[green]Packaged in Fedora[/green]", str(stats["packaged"]))
401
+ table.add_row("[red]Missing from Fedora[/red]", str(stats["missing"]))
402
+
403
+ console.print(table)
404
+ console.print()
405
+
406
+ if stats["missing_list"]:
407
+ console.print("[bold]Missing crates that need packaging:[/bold]")
408
+ for name in sorted(set(stats["missing_list"])):
409
+ console.print(f" • {name}")
410
+ console.print()
411
+
412
+
413
+ def clear_cache(namespace: Optional[str] = None):
414
+ """Clear disk cache."""
415
+ if namespace:
416
+ cache_path = CACHE_DIR / namespace
417
+ if cache_path.exists():
418
+ for f in cache_path.glob("*.json"):
419
+ f.unlink()
420
+ console.print(f"[yellow]Cleared {namespace} cache[/yellow]")
421
+ else:
422
+ for ns in ["crates", "fedora"]:
423
+ cache_path = CACHE_DIR / ns
424
+ if cache_path.exists():
425
+ for f in cache_path.glob("*.json"):
426
+ f.unlink()
427
+ console.print("[yellow]Cleared all caches[/yellow]")
428
+
429
+
430
+ def main() -> int:
431
+ parser = argparse.ArgumentParser(
432
+ description="Check if a Rust crate's dependencies are packaged in Fedora"
433
+ )
434
+ parser.add_argument("crate", nargs="?", help="Crate name on crates.io (e.g., ripgrep)")
435
+ parser.add_argument("--version", "-v", help="Specific version to check")
436
+ parser.add_argument("--max-depth", "-d", type=int, default=50, help="Maximum recursion depth")
437
+ parser.add_argument("--no-progress", action="store_true", help="Disable progress bar")
438
+ parser.add_argument("--clear-cache", action="store_true", help="Clear all cached data")
439
+ parser.add_argument("--clear-fedora-cache", action="store_true", help="Clear Fedora repoquery cache")
440
+ args = parser.parse_args()
441
+
442
+ if args.clear_cache:
443
+ clear_cache()
444
+ if not args.crate:
445
+ exit(0)
446
+
447
+ if args.clear_fedora_cache:
448
+ clear_cache("fedora")
449
+ if not args.crate:
450
+ exit(0)
451
+
452
+ if not args.crate:
453
+ parser.print_help()
454
+ exit(1)
455
+
456
+ console.print(f"\n[bold underline]Analyzing crate:[/] {args.crate}")
457
+ console.print(f"[dim]Cache directory: {CACHE_DIR}[/dim]\n")
458
+
459
+ tracker = None if args.no_progress else ProgressTracker()
460
+
461
+ if tracker:
462
+ tracker.start()
463
+
464
+ try:
465
+ tree = build_tree(args.crate, args.version, max_depth=args.max_depth, tracker=tracker)
466
+ if tracker:
467
+ tracker.finish()
468
+ finally:
469
+ if tracker:
470
+ tracker.stop()
471
+
472
+ console.print()
473
+ print_summary_table(args.crate, tree)
474
+
475
+ console.print("[bold]Dependency Tree:[/bold]")
476
+ console.print(tree)
477
+ console.print()
478
+
479
+ return 0
480
+
481
+ if __name__ == "__main__":
482
+ sys.exit(main())
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: woolly
3
+ Version: 0.1.0
4
+ Summary: Recursively search for RPMs in Fedora for Rust crates.
5
+ Author-email: Rodolfo Olivieri <rodolfo.olivieri3@gmail.com>
6
+ License-File: LICENSE
7
+ Classifier: Development Status :: 2 - Pre-Alpha
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Information Technology
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: System :: Systems Administration
23
+ Classifier: Topic :: Utilities
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: requests>=2.32.5
26
+ Requires-Dist: rich>=14.2.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Woolly
30
+
31
+ Recursively search for RPMs in Fedora for a given Rust [crate](https://crate.io).
32
+
33
+ > This tool is merely a starting point for figuring out how much packaging
34
+ > effort you will need to bring a rust crate over to Fedora.
35
+
36
+ ## What does "woolly" means?
37
+
38
+ Nothing. I just liked the name.
39
+
40
+ ## Running the project
41
+
42
+ ```bash
43
+ $ uv run rust_rpm_inspector
44
+
45
+ Analyzing crate: cliclack
46
+ Cache directory: /home/r0x0d/.cache/fedora-rust-checker
47
+
48
+ Analyzing dependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% • 0:00:15 complete!
49
+
50
+ Dependency Summary for 'cliclack'
51
+ ╭────────────────────────────┬───────╮
52
+ │ Metric │ Value │
53
+ ├────────────────────────────┼───────┤
54
+ │ Total dependencies checked │ 7 │
55
+ │ Packaged in Fedora │ 0 │
56
+ │ Missing from Fedora │ 1 │
57
+ ╰────────────────────────────┴───────╯
58
+
59
+ Missing crates that need packaging:
60
+ • cliclack
61
+
62
+ Dependency Tree:
63
+ cliclack v0.3.6 • ✗ not packaged
64
+ ├── console v0.16.1 • ✓ packaged (0.16.1)
65
+ │ ├── encode_unicode v1.0.0 • ✓ packaged (1.0.0)
66
+ │ └── windows-sys v0.61.2 • ✗ not packaged
67
+ │ └── windows-link v0.2.1 • ✗ not packaged
68
+ ├── indicatif v0.18.3 • ✓ packaged (0.16.2, 0.18.0, 0.18.1)
69
+ │ ├── console (already visited)
70
+ │ ├── portable-atomic v1.11.1 • ✓ packaged (1.11.1)
71
+ │ ├── unit-prefix v0.5.2 • ✓ packaged (0.5.1)
72
+ │ └── web-time v1.1.0 • ✓ packaged (1.1.0)
73
+ │ ├── js-sys v0.3.82 • ✗ not packaged
74
+ │ │ ├── once_cell v1.21.3 • ✓ packaged (1.21.3)
75
+ │ │ └── wasm-bindgen v0.2.105 • ✗ not packaged
76
+ │ │ ├── cfg-if v1.0.4 • ✓ packaged (0.1.10, 1.0.3, 1.0.4)
77
+ │ │ ├── once_cell (already visited)
78
+ │ │ ├── wasm-bindgen-macro v0.2.105 • ✗ not packaged
79
+ │ │ │ ├── quote v1.0.42 • ✓ packaged (0.3.15, 1.0.40, 1.0.41)
80
+ │ │ │ │ └── proc-macro2 v1.0.103 • ✓ packaged (1.0.101, 1.0.103)
81
+ │ │ │ │ └── unicode-ident v1.0.22 • ✓ packaged (1.0.19, 1.0.22)
82
+ │ │ │ └── wasm-bindgen-macro-support v0.2.105 • ✗ not packaged
83
+ │ │ │ ├── bumpalo v3.19.0 • ✓ packaged (3.19.0)
84
+ │ │ │ ├── proc-macro2 (already visited)
85
+ │ │ │ ├── quote (already visited)
86
+ │ │ │ ├── syn v2.0.110 • ✓ packaged (1.0.109, 2.0.106, 2.0.108)
87
+ │ │ │ │ ├── proc-macro2 (already visited)
88
+ │ │ │ │ └── unicode-ident (already visited)
89
+ │ │ │ └── wasm-bindgen-shared v0.2.105 • ✗ not packaged
90
+ │ │ │ └── unicode-ident (already visited)
91
+ │ │ └── wasm-bindgen-shared (already visited)
92
+ │ └── wasm-bindgen (already visited)
93
+ ├── once_cell (already visited)
94
+ ├── strsim v0.11.1 • ✓ packaged (0.10.0, 0.11.1)
95
+ ├── textwrap v0.16.2 • ✓ packaged (0.11.0, 0.15.2, 0.16.2)
96
+ └── zeroize v1.8.2 • ✓ packaged (1.8.1, 1.8.2)
97
+ ```
98
+
99
+ Keep in mind that you may not need all of RPMs to be present in Fedora, like
100
+ the output above, we have `windows*` crates in the dependency tree, but they
101
+ are not used at all.
@@ -0,0 +1,7 @@
1
+ woolly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ woolly/__main__.py,sha256=_XB9nZAFBJptgQYaL6OO9VRLh2SYGjRTPS5DXndJx7w,15592
3
+ woolly-0.1.0.dist-info/METADATA,sha256=H11_hfzIbdrVvqsHeL4fvLcxgQeOeCrnB-FG8G47i2o,4895
4
+ woolly-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ woolly-0.1.0.dist-info/entry_points.txt,sha256=tMSCR0PXsaIWiiY5fwQ5eowwNqPSrH0j9QriPDoE75k,48
6
+ woolly-0.1.0.dist-info/licenses/LICENSE,sha256=TnfrdmTYydTTenujKk2LdAuDuLSMwjpXhW7EU2VR0e8,1064
7
+ woolly-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ woolly = woolly.__main__:main
@@ -0,0 +1,19 @@
1
+ Copyright 2025 Rodolfo Olivieri
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the “Software”), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.