woolly 0.2.0__py3-none-any.whl → 0.4.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 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
- if time.time() - data.get("timestamp", 0) > ttl:
47
+ entry = CacheEntry.model_validate(data)
48
+ if time.time() - entry.timestamp > ttl:
39
49
  return None # Expired
40
- return data.get("value")
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
- data = {"timestamp": time.time(), "value": value}
49
- path.write_text(json.dumps(data))
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
@@ -2,9 +2,12 @@
2
2
  Check command - analyze package dependencies for Fedora availability.
3
3
  """
4
4
 
5
+ import fnmatch
6
+ from pathlib import Path
5
7
  from typing import Annotated, Optional
6
8
 
7
9
  import cyclopts
10
+ from pydantic import BaseModel, Field
8
11
  from rich.tree import Tree
9
12
 
10
13
  from woolly.cache import CACHE_DIR
@@ -16,6 +19,20 @@ from woolly.progress import ProgressTracker
16
19
  from woolly.reporters import ReportData, get_available_formats, get_reporter
17
20
 
18
21
 
22
+ class TreeStats(BaseModel):
23
+ """Statistics collected from dependency tree analysis."""
24
+
25
+ total: int = 0
26
+ packaged: int = 0
27
+ missing: int = 0
28
+ missing_list: list[str] = Field(default_factory=list)
29
+ packaged_list: list[str] = Field(default_factory=list)
30
+ optional_total: int = 0
31
+ optional_packaged: int = 0
32
+ optional_missing: int = 0
33
+ optional_missing_list: list[str] = Field(default_factory=list)
34
+
35
+
19
36
  def build_tree(
20
37
  provider: LanguageProvider,
21
38
  package_name: str,
@@ -24,6 +41,9 @@ def build_tree(
24
41
  depth: int = 0,
25
42
  max_depth: int = 50,
26
43
  tracker: Optional[ProgressTracker] = None,
44
+ include_optional: bool = False,
45
+ is_optional_dep: bool = False,
46
+ exclude_patterns: Optional[list[str]] = None,
27
47
  ):
28
48
  """
29
49
  Recursively build a dependency tree for a package.
@@ -44,6 +64,12 @@ def build_tree(
44
64
  Maximum recursion depth.
45
65
  tracker
46
66
  Optional progress tracker.
67
+ include_optional
68
+ If True, include optional dependencies in the analysis.
69
+ is_optional_dep
70
+ If True, this package is an optional dependency.
71
+ exclude_patterns
72
+ List of glob patterns to exclude from the dependency tree.
47
73
 
48
74
  Returns
49
75
  -------
@@ -53,9 +79,11 @@ def build_tree(
53
79
  if visited is None:
54
80
  visited = {}
55
81
 
82
+ optional_marker = " [yellow](optional)[/yellow]" if is_optional_dep else ""
83
+
56
84
  if depth > max_depth:
57
85
  log(f"Max depth reached for {package_name}", level="warning", depth=depth)
58
- return f"[dim]{package_name} (max depth reached)[/dim]"
86
+ return f"[dim]{package_name}{optional_marker} (max depth reached)[/dim]"
59
87
 
60
88
  if package_name in visited:
61
89
  is_packaged, cached_version = visited[package_name]
@@ -65,11 +93,9 @@ def build_tree(
65
93
  result="packaged" if is_packaged else "not packaged",
66
94
  )
67
95
  if is_packaged:
68
- return f"[dim]{package_name}[/dim] [dim]v{cached_version}[/dim] • [green]✓[/green] [dim](already visited)[/dim]"
96
+ return f"[dim]{package_name}[/dim] [dim]v{cached_version}[/dim]{optional_marker} • [green]✓[/green] [dim](already visited)[/dim]"
69
97
  else:
70
- return (
71
- f"[dim]{package_name}[/dim] • [red]✗[/red] [dim](already visited)[/dim]"
72
- )
98
+ return f"[dim]{package_name}[/dim]{optional_marker} • [red]✗[/red] [dim](already visited)[/dim]"
73
99
 
74
100
  if tracker:
75
101
  tracker.update(package_name)
@@ -84,7 +110,7 @@ def build_tree(
84
110
  package_name, "Not found", source=provider.registry_name, result="error"
85
111
  )
86
112
  return (
87
- f"[bold red]{package_name}[/bold red] • "
113
+ f"[bold red]{package_name}[/bold red]{optional_marker} • "
88
114
  f"[red]not found on {provider.registry_name}[/red]"
89
115
  )
90
116
 
@@ -107,14 +133,14 @@ def build_tree(
107
133
  ver_str = ", ".join(status.versions) if status.versions else "unknown"
108
134
  pkg_str = ", ".join(status.package_names) if status.package_names else ""
109
135
  label = (
110
- f"[bold]{package_name}[/bold] [dim]v{version}[/dim] • "
136
+ f"[bold]{package_name}[/bold] [dim]v{version}[/dim]{optional_marker} • "
111
137
  f"[green]✓ packaged[/green] [dim]({ver_str})[/dim]"
112
138
  )
113
139
  if pkg_str:
114
140
  label += f" [dim cyan][{pkg_str}][/dim cyan]"
115
141
  else:
116
142
  label = (
117
- f"[bold]{package_name}[/bold] [dim]v{version}[/dim] • "
143
+ f"[bold]{package_name}[/bold] [dim]v{version}[/dim]{optional_marker} • "
118
144
  f"[red]✗ not packaged[/red]"
119
145
  )
120
146
 
@@ -125,16 +151,33 @@ def build_tree(
125
151
  package_name, "Fetching dependencies", source=provider.registry_name
126
152
  )
127
153
 
128
- deps = provider.get_normal_dependencies(package_name, version)
154
+ deps = provider.get_normal_dependencies(
155
+ package_name, version, include_optional=include_optional
156
+ )
129
157
 
130
158
  log(f"Found {len(deps)} dependencies for {package_name}", deps=len(deps))
131
159
 
132
160
  if tracker and deps:
133
161
  tracker.update(package_name, discovered=len(deps))
134
162
 
135
- for dep_name, _dep_req in deps:
163
+ for dep_name, _dep_req, dep_is_optional in deps:
164
+ # Skip dependencies matching exclude patterns
165
+ if exclude_patterns:
166
+ if any(fnmatch.fnmatch(dep_name, pattern) for pattern in exclude_patterns):
167
+ log(f"Filtered out dependency: {dep_name}", level="info", depth=depth)
168
+ continue
169
+
136
170
  child = build_tree(
137
- provider, dep_name, None, visited, depth + 1, max_depth, tracker
171
+ provider,
172
+ dep_name,
173
+ None,
174
+ visited,
175
+ depth + 1,
176
+ max_depth,
177
+ tracker,
178
+ include_optional=include_optional,
179
+ is_optional_dep=dep_is_optional,
180
+ exclude_patterns=exclude_patterns,
138
181
  )
139
182
  if isinstance(child, str):
140
183
  node.add(child)
@@ -148,51 +191,65 @@ def build_tree(
148
191
  return node
149
192
 
150
193
 
151
- def collect_stats(tree, stats=None):
194
+ def collect_stats(tree, stats: Optional[TreeStats] = None) -> TreeStats:
152
195
  """Walk the tree and collect statistics."""
153
196
  if stats is None:
154
- stats = {
155
- "total": 0,
156
- "packaged": 0,
157
- "missing": 0,
158
- "missing_list": [],
159
- "packaged_list": [],
160
- }
197
+ stats = TreeStats()
161
198
 
162
199
  def walk(t):
163
200
  if isinstance(t, str):
164
- stats["total"] += 1
201
+ stats.total += 1
202
+ is_optional = "(optional)" in t
203
+ if is_optional:
204
+ stats.optional_total += 1
165
205
  if "not packaged" in t or "not found" in t:
166
- stats["missing"] += 1
167
- name = (
168
- t.split("[/bold]")[0].split("]")[-1]
169
- if "[/bold]" in t
170
- else t.split()[0]
171
- )
172
- stats["missing_list"].append(name)
206
+ stats.missing += 1
207
+ # Handle both [bold] and [bold red] formats
208
+ if "[/bold]" in t:
209
+ name = t.split("[/bold]")[0].split("]")[-1]
210
+ elif "[/bold red]" in t:
211
+ name = t.split("[/bold red]")[0].split("[bold red]")[-1]
212
+ else:
213
+ name = t.split()[0]
214
+ stats.missing_list.append(name)
215
+ if is_optional:
216
+ stats.optional_missing += 1
217
+ stats.optional_missing_list.append(name)
173
218
  elif "packaged" in t:
174
- stats["packaged"] += 1
219
+ stats.packaged += 1
220
+ if is_optional:
221
+ stats.optional_packaged += 1
175
222
  return
176
223
 
177
224
  if hasattr(t, "label"):
178
225
  label = str(t.label)
179
- stats["total"] += 1
226
+ stats.total += 1
227
+ is_optional = "(optional)" in label
228
+ if is_optional:
229
+ stats.optional_total += 1
180
230
  if "not packaged" in label or "not found" in label:
181
- stats["missing"] += 1
182
- name = (
183
- label.split("[/bold]")[0].split("[bold]")[-1]
184
- if "[bold]" in label
185
- else "unknown"
186
- )
187
- stats["missing_list"].append(name)
231
+ stats.missing += 1
232
+ # Handle both [bold] and [bold red] formats
233
+ if "[bold]" in label and "[bold red]" not in label:
234
+ name = label.split("[/bold]")[0].split("[bold]")[-1]
235
+ elif "[bold red]" in label:
236
+ name = label.split("[/bold red]")[0].split("[bold red]")[-1]
237
+ else:
238
+ name = "unknown"
239
+ stats.missing_list.append(name)
240
+ if is_optional:
241
+ stats.optional_missing += 1
242
+ stats.optional_missing_list.append(name)
188
243
  elif "packaged" in label:
189
- stats["packaged"] += 1
244
+ stats.packaged += 1
190
245
  name = (
191
246
  label.split("[/bold]")[0].split("[bold]")[-1]
192
247
  if "[bold]" in label
193
248
  else "unknown"
194
249
  )
195
- stats["packaged_list"].append(name)
250
+ stats.packaged_list.append(name)
251
+ if is_optional:
252
+ stats.optional_packaged += 1
196
253
 
197
254
  if hasattr(t, "children"):
198
255
  for child in t.children:
@@ -232,6 +289,14 @@ def check(
232
289
  help="Maximum recursion depth.",
233
290
  ),
234
291
  ] = 50,
292
+ optional: Annotated[
293
+ bool,
294
+ cyclopts.Parameter(
295
+ ("--optional", "-o"),
296
+ negative=(),
297
+ help="Include optional dependencies in the analysis.",
298
+ ),
299
+ ] = False,
235
300
  no_progress: Annotated[
236
301
  bool,
237
302
  cyclopts.Parameter(
@@ -253,6 +318,28 @@ def check(
253
318
  help="Report format: stdout, json, markdown. Use 'list-formats' for all options.",
254
319
  ),
255
320
  ] = "stdout",
321
+ missing_only: Annotated[
322
+ bool,
323
+ cyclopts.Parameter(
324
+ ("--missing-only", "-m"),
325
+ negative=(),
326
+ help="Only display packages that are missing from Fedora.",
327
+ ),
328
+ ] = False,
329
+ exclude: Annotated[
330
+ tuple[str, ...],
331
+ cyclopts.Parameter(
332
+ ("--exclude", "-e"),
333
+ help="Glob pattern(s) to exclude dependencies (e.g., '*-windows', 'win*'). Can be specified multiple times.",
334
+ ),
335
+ ] = (),
336
+ template: Annotated[
337
+ Optional[str],
338
+ cyclopts.Parameter(
339
+ ("--template", "-t"),
340
+ help="Path to a Jinja2 template file for custom report format. Only used with --report=template.",
341
+ ),
342
+ ] = None,
256
343
  ):
257
344
  """Check if a package's dependencies are available in Fedora.
258
345
 
@@ -266,12 +353,20 @@ def check(
266
353
  Specific version to check (default: latest).
267
354
  max_depth
268
355
  Maximum recursion depth for dependency tree.
356
+ optional
357
+ Include optional dependencies in the analysis.
269
358
  no_progress
270
359
  Disable progress bar during analysis.
271
360
  debug
272
361
  Enable verbose debug logging.
273
362
  report
274
363
  Output format for the report.
364
+ missing_only
365
+ Only display packages that are missing from Fedora.
366
+ exclude
367
+ Glob pattern(s) to exclude dependencies from the analysis.
368
+ template
369
+ Path to a Jinja2 template file for custom report format.
275
370
  """
276
371
  # Get the language provider
277
372
  provider = get_provider(lang)
@@ -281,12 +376,33 @@ def check(
281
376
  raise SystemExit(1)
282
377
 
283
378
  # Get the reporter
284
- reporter = get_reporter(report, console=console)
379
+ template_path = Path(template) if template else None
380
+
381
+ # Validate template reporter requirements
382
+ if report.lower() in ("template", "tpl", "jinja", "jinja2"):
383
+ if template_path is None:
384
+ console.print("[red]Template reporter requires --template parameter.[/red]")
385
+ console.print(
386
+ "Example: woolly check mypackage --report=template --template=my_template.md"
387
+ )
388
+ raise SystemExit(1)
389
+ if not template_path.exists():
390
+ console.print(f"[red]Template file not found: {template_path}[/red]")
391
+ raise SystemExit(1)
392
+ elif template_path is not None:
393
+ console.print(
394
+ "[yellow]Warning: --template is only used with --report=template[/yellow]"
395
+ )
396
+
397
+ reporter = get_reporter(report, console=console, template_path=template_path)
285
398
  if reporter is None:
286
399
  console.print(f"[red]Unknown report format: {report}[/red]")
287
400
  console.print(f"Available formats: {', '.join(get_available_formats())}")
288
401
  raise SystemExit(1)
289
402
 
403
+ # Convert exclude tuple to list for consistency
404
+ exclude_patterns = list(exclude) if exclude else None
405
+
290
406
  # Initialize logging
291
407
  setup_logger(debug=debug)
292
408
  log(
@@ -294,13 +410,21 @@ def check(
294
410
  package=package,
295
411
  language=lang,
296
412
  max_depth=max_depth,
413
+ include_optional=optional,
297
414
  debug=debug,
298
415
  report_format=report,
416
+ exclude_patterns=exclude_patterns,
299
417
  )
300
418
 
301
419
  console.print(
302
420
  f"\n[bold underline]Analyzing {provider.display_name} package:[/] {package}"
303
421
  )
422
+ if optional:
423
+ console.print("[yellow]Including optional dependencies[/yellow]")
424
+ if exclude_patterns:
425
+ console.print(
426
+ f"[yellow]Excluding dependencies matching: {', '.join(exclude_patterns)}[/yellow]"
427
+ )
304
428
  console.print(f"[dim]Registry: {provider.registry_name}[/dim]")
305
429
  console.print(f"[dim]Cache directory: {CACHE_DIR}[/dim]")
306
430
  console.print()
@@ -317,6 +441,8 @@ def check(
317
441
  version,
318
442
  max_depth=max_depth,
319
443
  tracker=tracker,
444
+ include_optional=optional,
445
+ exclude_patterns=exclude_patterns,
320
446
  )
321
447
  if tracker:
322
448
  tracker.finish()
@@ -335,14 +461,20 @@ def check(
335
461
  root_package=package,
336
462
  language=provider.display_name,
337
463
  registry=provider.registry_name,
338
- total_dependencies=stats["total"],
339
- packaged_count=stats["packaged"],
340
- missing_count=stats["missing"],
341
- missing_packages=stats["missing_list"],
342
- packaged_packages=stats["packaged_list"],
464
+ total_dependencies=stats.total,
465
+ packaged_count=stats.packaged,
466
+ missing_count=stats.missing,
467
+ missing_packages=stats.missing_list,
468
+ packaged_packages=stats.packaged_list,
343
469
  tree=tree,
344
470
  max_depth=max_depth,
345
471
  version=version,
472
+ include_optional=optional,
473
+ optional_total=stats.optional_total,
474
+ optional_packaged=stats.optional_packaged,
475
+ optional_missing=stats.optional_missing,
476
+ optional_missing_packages=stats.optional_missing_list,
477
+ missing_only=missing_only,
346
478
  )
347
479
 
348
480
  # Generate report
@@ -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 format_id, description, aliases in list_reporters():
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 lang_id, display_name, aliases in list_providers():
21
- provider = get_provider(lang_id)
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} ({lang_id})", registry, alias_str)
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)
@@ -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[tuple[str, str, list[str]]]:
66
+ def list_providers() -> list[ProviderInfo]:
56
67
  """
57
68
  List all available providers.
58
69
 
59
70
  Returns:
60
- List of tuples: (language_id, display_name, aliases)
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((lang_id, provider_class.display_name, aliases))
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, package_name: str, version: Optional[str] = None
137
- ) -> list[tuple[str, str]]:
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 non-optional, runtime dependencies for a package.
142
+ Get runtime dependencies for a package.
140
143
 
141
144
  This method filters dependencies to only include normal (runtime)
142
- dependencies that are not optional.
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:
@@ -8,8 +8,7 @@ Fedora repositories for Python packages.
8
8
  import re
9
9
  from typing import Optional
10
10
 
11
- import httpx
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 = httpx.get(url, headers=HEADERS)
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 = httpx.get(url, headers=HEADERS)
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 = "dev" # Treat extras as dev dependencies
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 ';')