woolly 0.3.0__py3-none-any.whl → 0.5.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.
@@ -0,0 +1,245 @@
1
+ """
2
+ Template-based report generator using Jinja2.
3
+
4
+ Allows users to provide a custom markdown template file for report generation.
5
+ Only a limited set of variables are exposed for security and simplicity.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ from woolly.reporters.base import ReportData, Reporter, strip_markup
12
+
13
+
14
+ class TemplateReporter(Reporter):
15
+ """Reporter that generates reports from user-provided Jinja2 templates.
16
+
17
+ This reporter allows users to customize the output format by providing
18
+ a markdown template file. Only a limited, safe set of variables are
19
+ exposed to the template.
20
+
21
+ Available template variables:
22
+ Metadata:
23
+ - root_package: Name of the analyzed package
24
+ - language: Language/ecosystem name (e.g., "Rust", "Python")
25
+ - registry: Registry name (e.g., "crates.io", "PyPI")
26
+ - root_license: License of the root package (or None)
27
+ - version: Package version (if specified)
28
+ - timestamp: Formatted timestamp string (YYYY-MM-DD HH:MM:SS)
29
+ - max_depth: Maximum recursion depth used
30
+
31
+ Statistics:
32
+ - total_dependencies: Total number of dependencies analyzed
33
+ - packaged_count: Number of packages available in Fedora
34
+ - missing_count: Number of packages missing from Fedora
35
+
36
+ Optional dependency statistics:
37
+ - include_optional: Whether optional deps were included
38
+ - optional_total: Total optional dependencies
39
+ - optional_packaged: Optional deps available in Fedora
40
+ - optional_missing: Optional deps missing from Fedora
41
+
42
+ Dev/Build dependency statistics:
43
+ - dev_total, dev_packaged, dev_missing: Dev dep counts
44
+ - build_total, build_packaged, build_missing: Build dep counts
45
+ - dev_dependencies: List of dev dep dicts with Fedora status
46
+ - build_dependencies: List of build dep dicts with Fedora status
47
+
48
+ Features / Extras:
49
+ - features: List of feature/extra objects with name and dependencies
50
+
51
+ Package lists (sorted):
52
+ - missing_packages: List of missing required package names
53
+ - packaged_packages: List of packaged package names
54
+ - optional_missing_packages: List of missing optional package names
55
+
56
+ Flags:
57
+ - missing_only: Whether missing-only mode was enabled
58
+
59
+ Example template:
60
+ # Report for {{ root_package }}
61
+
62
+ Generated: {{ timestamp }}
63
+ Language: {{ language }}
64
+
65
+ ## Summary
66
+ - Total: {{ total_dependencies }}
67
+ - Packaged: {{ packaged_count }}
68
+ - Missing: {{ missing_count }}
69
+
70
+ {% if missing_packages %}
71
+ ## Missing Packages
72
+ {% for pkg in missing_packages %}
73
+ - {{ pkg }}
74
+ {% endfor %}
75
+ {% endif %}
76
+ """
77
+
78
+ name = "template"
79
+ description = "Custom template-based report (requires --template)"
80
+ file_extension = "md"
81
+ writes_to_file = True
82
+
83
+ def __init__(self, template_path: Optional[Path] = None):
84
+ """Initialize the template reporter.
85
+
86
+ Args:
87
+ template_path: Path to the Jinja2 template file.
88
+ """
89
+ self._template_path = template_path
90
+ self._jinja2_available: Optional[bool] = None
91
+
92
+ @property
93
+ def template_path(self) -> Optional[Path]:
94
+ """Get the template path."""
95
+ return self._template_path
96
+
97
+ @template_path.setter
98
+ def template_path(self, value: Optional[Path]) -> None:
99
+ """Set the template path."""
100
+ self._template_path = value
101
+
102
+ def _check_jinja2(self) -> bool:
103
+ """Check if Jinja2 is available.
104
+
105
+ Returns:
106
+ True if Jinja2 is available, False otherwise.
107
+ """
108
+ if self._jinja2_available is None:
109
+ try:
110
+ import jinja2 # type: ignore[import-not-found] # noqa: F401
111
+
112
+ self._jinja2_available = True
113
+ except ImportError:
114
+ self._jinja2_available = False
115
+ return self._jinja2_available
116
+
117
+ def _get_template_context(self, data: ReportData) -> dict:
118
+ """Build the template context with allowed variables only.
119
+
120
+ This method creates a safe, limited context for template rendering.
121
+ Only specific variables from ReportData are exposed.
122
+
123
+ Args:
124
+ data: Report data containing all information.
125
+
126
+ Returns:
127
+ Dictionary of template variables.
128
+ """
129
+ # Convert features to plain dicts for template use
130
+ features_list = []
131
+ for f in data.features:
132
+ if hasattr(f, "name"):
133
+ features_list.append({"name": f.name, "dependencies": f.dependencies})
134
+ else:
135
+ features_list.append(f)
136
+
137
+ return {
138
+ # Metadata
139
+ "root_package": data.root_package,
140
+ "language": data.language,
141
+ "registry": data.registry,
142
+ "root_license": data.root_license,
143
+ "version": data.version,
144
+ "timestamp": data.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
145
+ "max_depth": data.max_depth,
146
+ # Statistics
147
+ "total_dependencies": data.total_dependencies,
148
+ "packaged_count": data.packaged_count,
149
+ "missing_count": data.missing_count,
150
+ # Optional dependency statistics
151
+ "include_optional": data.include_optional,
152
+ "optional_total": data.optional_total,
153
+ "optional_packaged": data.optional_packaged,
154
+ "optional_missing": data.optional_missing,
155
+ # Dev/Build dependency statistics
156
+ "dev_total": data.dev_total,
157
+ "dev_packaged": data.dev_packaged,
158
+ "dev_missing": data.dev_missing,
159
+ "build_total": data.build_total,
160
+ "build_packaged": data.build_packaged,
161
+ "build_missing": data.build_missing,
162
+ "dev_dependencies": data.dev_dependencies,
163
+ "build_dependencies": data.build_dependencies,
164
+ # Features / Extras
165
+ "features": features_list,
166
+ # Package lists (sorted for consistent output)
167
+ "missing_packages": sorted(data.required_missing_packages),
168
+ "packaged_packages": sorted(data.unique_packaged_packages),
169
+ "optional_missing_packages": sorted(data.optional_missing_set),
170
+ # Flags
171
+ "missing_only": data.missing_only,
172
+ }
173
+
174
+ def generate(self, data: ReportData) -> str:
175
+ """Generate report content from the template.
176
+
177
+ Args:
178
+ data: Report data containing all information.
179
+
180
+ Returns:
181
+ Rendered template content as a string.
182
+
183
+ Raises:
184
+ RuntimeError: If Jinja2 is not installed or template is not set.
185
+ FileNotFoundError: If the template file doesn't exist.
186
+ jinja2.TemplateError: If there's a template syntax error.
187
+ """
188
+ if not self._check_jinja2():
189
+ raise RuntimeError(
190
+ "Jinja2 is required for template reports. "
191
+ "Install it with: pip install jinja2"
192
+ )
193
+
194
+ if self._template_path is None:
195
+ raise RuntimeError(
196
+ "No template path specified. Use --template to provide a template file."
197
+ )
198
+
199
+ if not self._template_path.exists():
200
+ raise FileNotFoundError(f"Template file not found: {self._template_path}")
201
+
202
+ # Import Jinja2 here (we've already checked it's available)
203
+ from jinja2 import ( # type: ignore[import-not-found]
204
+ Environment,
205
+ FileSystemLoader,
206
+ StrictUndefined,
207
+ select_autoescape,
208
+ )
209
+
210
+ # Create Jinja2 environment with security settings
211
+ env = Environment(
212
+ loader=FileSystemLoader(self._template_path.parent),
213
+ autoescape=select_autoescape(default=False),
214
+ undefined=StrictUndefined, # Raise error on undefined variables
215
+ # Disable dangerous features
216
+ extensions=[],
217
+ )
218
+
219
+ # Add custom filter to strip Rich markup
220
+ env.filters["strip_markup"] = strip_markup
221
+
222
+ # Load and render template
223
+ template = env.get_template(self._template_path.name)
224
+ context = self._get_template_context(data)
225
+
226
+ return template.render(**context)
227
+
228
+ def get_output_filename(self, data: ReportData) -> str:
229
+ """Get the output filename for the template report.
230
+
231
+ Uses the template filename as a base if available, otherwise
232
+ falls back to the default naming convention.
233
+
234
+ Args:
235
+ data: Report data.
236
+
237
+ Returns:
238
+ Filename string.
239
+ """
240
+ timestamp = data.timestamp.strftime("%Y%m%d_%H%M%S")
241
+ if self._template_path:
242
+ # Use template name as base (without extension)
243
+ base_name = self._template_path.stem
244
+ return f"woolly_{data.root_package}_{base_name}_{timestamp}.md"
245
+ return f"woolly_{data.root_package}_template_{timestamp}.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: woolly
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Check if package dependencies are available in Fedora. Supports Rust, Python, and more.
5
5
  Author-email: Rodolfo Olivieri <rodolfo.olivieri3@gmail.com>
6
6
  License-File: LICENSE
@@ -26,6 +26,8 @@ Requires-Dist: cyclopts>=4.3.0
26
26
  Requires-Dist: httpx>=0.28.0
27
27
  Requires-Dist: pydantic>=2.10.0
28
28
  Requires-Dist: rich>=14.2.0
29
+ Provides-Extra: template
30
+ Requires-Dist: jinja2>=3.1.0; extra == 'template'
29
31
  Description-Content-Type: text/markdown
30
32
 
31
33
  # 🐑 Woolly
@@ -0,0 +1,26 @@
1
+ woolly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ woolly/__main__.py,sha256=em9-CiwyaP3N5FKr_Srv6Eh8Vm6TOC3VWSwXR1BjzV8,304
3
+ woolly/cache.py,sha256=lUoTUd_Ui8052QoOHIc3CcNLQc8ErDwwRK_uP8Qzxjo,2933
4
+ woolly/debug.py,sha256=85_0_lsqccT8TAkyWFinOIqkPVPpcmrKuNfIYQOEqZ4,5294
5
+ woolly/http.py,sha256=bG1AjyF5Umjej-R2ywEPivGarTBiW9Q7bMwy2JsULJU,1521
6
+ woolly/progress.py,sha256=Ij-K3-6-qeVLFKD_WdSfuclZh6uiso9XSSVRlpbB4jA,2040
7
+ woolly/commands/__init__.py,sha256=3xzeIPYD0dmVWwRXHkdPmsF2IIRU6KyvDwgemKdyghI,840
8
+ woolly/commands/check.py,sha256=w3bc8eAdhfrUJeFTGOb7nCsFwBY0B6hQtbOPfInWgRk,20520
9
+ woolly/commands/clear_cache.py,sha256=nCS52v_hNvKTwj62CKrQRfjX53fGpZnWpfB0GMmi8e4,1039
10
+ woolly/commands/list_formats.py,sha256=f4UAWjoBbDu8YhobQyccErwpmOqElrCxBg0xcnJVDUQ,695
11
+ woolly/commands/list_languages.py,sha256=VFjvMgXnvaW3O-MBEpzf5nd89yQ9TqLqcULPDCDUthg,857
12
+ woolly/languages/__init__.py,sha256=CwcPDIMvFpCK1tFbx5rew_5mLrrMy6s4HT6C-ZZz_7M,2527
13
+ woolly/languages/base.py,sha256=OmpMuVSZZo8xjAp6JAZAyr98fH6FQhsPL-BI6YnvOPw,18288
14
+ woolly/languages/python.py,sha256=4EhHKlcDxjyk6_rKaLNmCfLReavJfm1DKyV0SdoJMqY,15303
15
+ woolly/languages/rust.py,sha256=gMCcFRGcNCyEbLruRHk9oxWbW9oYXmVIWXbDVGjnfeg,6674
16
+ woolly/reporters/__init__.py,sha256=-85BHuDAQwyrgWtJqME76FT4jyrvS4cuas7CxaWDODU,3301
17
+ woolly/reporters/base.py,sha256=M8A3vZLPGR7btR10P9Ac56UYDFpH2xC2zCvwL7_xAR8,6911
18
+ woolly/reporters/json.py,sha256=B_QSUs-ldVFhx4ijcRGCLmF0umxQHk2M76E40k-8PUU,9265
19
+ woolly/reporters/markdown.py,sha256=CfYD9t31IgRTU4GfYytWLv-jVTsv5KpZ6J6yEFrv_nE,7454
20
+ woolly/reporters/stdout.py,sha256=1O7jz2u_6Onr5dLnWqcHamxID-fHBaIAfqnE5ZEC0i0,8126
21
+ woolly/reporters/template.py,sha256=C9amNJ3kJg6Cv8DCcLM3QiOIV_Sa8I1qAWIoAODv0D8,9066
22
+ woolly-0.5.0.dist-info/METADATA,sha256=BZ0GnVZv7N1lyQrTZB35jwMtEmK0yotyoKpEepE4qbA,10477
23
+ woolly-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
+ woolly-0.5.0.dist-info/entry_points.txt,sha256=tMSCR0PXsaIWiiY5fwQ5eowwNqPSrH0j9QriPDoE75k,48
25
+ woolly-0.5.0.dist-info/licenses/LICENSE,sha256=TnfrdmTYydTTenujKk2LdAuDuLSMwjpXhW7EU2VR0e8,1064
26
+ woolly-0.5.0.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- woolly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- woolly/__main__.py,sha256=em9-CiwyaP3N5FKr_Srv6Eh8Vm6TOC3VWSwXR1BjzV8,304
3
- woolly/cache.py,sha256=tzZE0FrymWQZSpJ5ZFgv2G17CVYq7SzsxpVWsF7ByXI,2405
4
- woolly/debug.py,sha256=85_0_lsqccT8TAkyWFinOIqkPVPpcmrKuNfIYQOEqZ4,5294
5
- woolly/http.py,sha256=xPFbHPWEWTN4mjEONyJ9hERmb62mMWun0jRjTAaaNE8,872
6
- woolly/progress.py,sha256=Ij-K3-6-qeVLFKD_WdSfuclZh6uiso9XSSVRlpbB4jA,2040
7
- woolly/commands/__init__.py,sha256=3xzeIPYD0dmVWwRXHkdPmsF2IIRU6KyvDwgemKdyghI,840
8
- woolly/commands/check.py,sha256=kCFQClTOiBAP-DRpAQFbDGgIKiPv8SlMYKoQzx6Fq4E,13312
9
- woolly/commands/clear_cache.py,sha256=nCS52v_hNvKTwj62CKrQRfjX53fGpZnWpfB0GMmi8e4,1039
10
- woolly/commands/list_formats.py,sha256=f4UAWjoBbDu8YhobQyccErwpmOqElrCxBg0xcnJVDUQ,695
11
- woolly/commands/list_languages.py,sha256=VFjvMgXnvaW3O-MBEpzf5nd89yQ9TqLqcULPDCDUthg,857
12
- woolly/languages/__init__.py,sha256=CwcPDIMvFpCK1tFbx5rew_5mLrrMy6s4HT6C-ZZz_7M,2527
13
- woolly/languages/base.py,sha256=Sjm9wXWeM36w6eYx_YyHm0jM6TqAUshlCiSPczW1_IA,12365
14
- woolly/languages/python.py,sha256=aA1fmHO0VWV_sHjZF92G30frv916Veh6cqjIj354UgM,6723
15
- woolly/languages/rust.py,sha256=DdhflO0PDvpgGy41lst9vUW9vF1Xrf94RJIMAsWSRDc,4508
16
- woolly/reporters/__init__.py,sha256=dS1XZGPTjvCkDetiWikkmBn7OnpvnmMXGZDYz2LVQr4,2833
17
- woolly/reporters/base.py,sha256=xXwrNb2NVVktQHjYCBT4nCzdqpvQsVCVsSs5-hB0D8I,6264
18
- woolly/reporters/json.py,sha256=mJb3tZDxvzEZ2zeOKELKJaU1E3b4TG86_DtsEJ8JHGE,5707
19
- woolly/reporters/markdown.py,sha256=fh--BH1rQP46qhTXiWcTHX3X9kjQJsAAXFFiToqK-RA,4300
20
- woolly/reporters/stdout.py,sha256=Xzku45BElUFyHh_pCWqAW4efb2mhC54CEja0xN6Xoe0,2784
21
- woolly-0.3.0.dist-info/METADATA,sha256=SMLGfdfa3_Rk6S36nGkJfiagX2bvmYMpb4llzvE6NCc,10402
22
- woolly-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
23
- woolly-0.3.0.dist-info/entry_points.txt,sha256=tMSCR0PXsaIWiiY5fwQ5eowwNqPSrH0j9QriPDoE75k,48
24
- woolly-0.3.0.dist-info/licenses/LICENSE,sha256=TnfrdmTYydTTenujKk2LdAuDuLSMwjpXhW7EU2VR0e8,1064
25
- woolly-0.3.0.dist-info/RECORD,,
File without changes