yuho 5.0.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.
Files changed (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. yuho-5.0.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,699 @@
1
+ """
2
+ Package installation and management for Yuho library.
3
+
4
+ Handles downloading, verifying, and installing statute packages.
5
+ """
6
+
7
+ from typing import Optional, List, Tuple
8
+ from pathlib import Path
9
+ import shutil
10
+ import logging
11
+ import json
12
+ import tempfile
13
+ from urllib.request import Request, urlopen
14
+ from urllib.error import URLError, HTTPError
15
+ from urllib.parse import urljoin
16
+
17
+ from yuho.library.package import Package, PackageValidator
18
+ from yuho.library.index import LibraryIndex, IndexEntry, DEFAULT_LIBRARY_DIR
19
+ from yuho.config.mask import mask_error, mask_url
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def install_package(
25
+ source: str,
26
+ library_dir: Optional[Path] = None,
27
+ verify_signature: bool = True,
28
+ force: bool = False,
29
+ ) -> Tuple[bool, str]:
30
+ """
31
+ Install a statute package to the library.
32
+
33
+ Args:
34
+ source: Path to .yhpkg file or contribution directory
35
+ library_dir: Library directory (default: ~/.yuho/library/packages)
36
+ verify_signature: Whether to verify package signature
37
+ force: Overwrite existing package
38
+
39
+ Returns:
40
+ Tuple of (success, message)
41
+ """
42
+ library_dir = library_dir or DEFAULT_LIBRARY_DIR
43
+ library_dir.mkdir(parents=True, exist_ok=True)
44
+
45
+ source_path = Path(source)
46
+
47
+ try:
48
+ # Load package
49
+ if source_path.suffix == ".yhpkg":
50
+ package = Package.from_yhpkg(source_path)
51
+ elif source_path.is_dir():
52
+ package = Package.from_directory(source_path)
53
+ else:
54
+ return (False, f"Invalid source: {source}")
55
+
56
+ # Validate
57
+ validator = PackageValidator(strict=False)
58
+ is_valid, errors, warnings = validator.validate(package)
59
+
60
+ if not is_valid:
61
+ return (False, f"Validation failed: {'; '.join(errors)}")
62
+
63
+ if warnings:
64
+ logger.warning(f"Package warnings: {'; '.join(warnings)}")
65
+
66
+ # Check signature if required
67
+ if verify_signature and not package.signature:
68
+ logger.warning("Package has no signature, proceeding anyway")
69
+
70
+ # Check for existing package
71
+ section_safe = package.metadata.section_number.replace("/", "_").replace(".", "_")
72
+ dest_path = library_dir / f"{section_safe}.yhpkg"
73
+
74
+ if dest_path.exists() and not force:
75
+ return (False, f"Package already installed: {package.metadata.section_number}. Use --force to overwrite.")
76
+
77
+ # Create .yhpkg if from directory
78
+ if source_path.is_dir():
79
+ package.to_yhpkg(dest_path)
80
+ else:
81
+ shutil.copy2(source_path, dest_path)
82
+
83
+ # Update index
84
+ index = LibraryIndex()
85
+ entry = IndexEntry.from_metadata(
86
+ package.metadata,
87
+ dest_path.name,
88
+ package.content_hash(),
89
+ )
90
+ index.add(entry)
91
+
92
+ return (True, f"Installed {package.metadata.section_number} v{package.metadata.version}")
93
+
94
+ except FileNotFoundError as e:
95
+ return (False, f"File not found: {mask_error(e)}")
96
+ except Exception as e:
97
+ logger.exception(f"Installation failed: {mask_error(e)}")
98
+ return (False, f"Installation failed: {mask_error(e)}")
99
+
100
+
101
+ def uninstall_package(
102
+ section_number: str,
103
+ library_dir: Optional[Path] = None,
104
+ ) -> Tuple[bool, str]:
105
+ """
106
+ Uninstall a statute package from the library.
107
+
108
+ Args:
109
+ section_number: Section number of package to remove
110
+ library_dir: Library directory
111
+
112
+ Returns:
113
+ Tuple of (success, message)
114
+ """
115
+ library_dir = library_dir or DEFAULT_LIBRARY_DIR
116
+ index = LibraryIndex()
117
+
118
+ entry = index.get(section_number)
119
+ if not entry:
120
+ return (False, f"Package not found: {section_number}")
121
+
122
+ # Remove package file
123
+ pkg_path = library_dir / entry.package_path
124
+ if pkg_path.exists():
125
+ pkg_path.unlink()
126
+
127
+ # Remove from index
128
+ index.remove(section_number)
129
+
130
+ return (True, f"Uninstalled {section_number}")
131
+
132
+
133
+ def list_installed(library_dir: Optional[Path] = None) -> List[dict]:
134
+ """
135
+ List all installed packages.
136
+
137
+ Args:
138
+ library_dir: Library directory
139
+
140
+ Returns:
141
+ List of package metadata dictionaries
142
+ """
143
+ index = LibraryIndex()
144
+ return [e.to_dict() for e in index.list_all()]
145
+
146
+
147
+ def update_package(
148
+ section_number: str,
149
+ new_source: str,
150
+ library_dir: Optional[Path] = None,
151
+ ) -> Tuple[bool, str]:
152
+ """
153
+ Update an installed package.
154
+
155
+ Args:
156
+ section_number: Section number to update
157
+ new_source: Path to new version
158
+ library_dir: Library directory
159
+
160
+ Returns:
161
+ Tuple of (success, message)
162
+ """
163
+ index = LibraryIndex()
164
+
165
+ current = index.get(section_number)
166
+ if not current:
167
+ return (False, f"Package not found: {section_number}")
168
+
169
+ # Install new version (force overwrite)
170
+ success, message = install_package(new_source, library_dir, force=True)
171
+
172
+ if success:
173
+ # Get new version info
174
+ new_entry = index.get(section_number)
175
+ if new_entry:
176
+ message = f"Updated {section_number}: {current.version} -> {new_entry.version}"
177
+
178
+ return (success, message)
179
+
180
+
181
+ def _compare_versions(v1: str, v2: str) -> int:
182
+ """
183
+ Compare semantic version strings.
184
+
185
+ Returns:
186
+ -1 if v1 < v2, 0 if equal, 1 if v1 > v2
187
+ """
188
+ def parse_version(v: str) -> Tuple[int, ...]:
189
+ parts = v.lstrip("v").split(".")
190
+ result = []
191
+ for p in parts:
192
+ # Handle pre-release suffixes like -alpha, -beta
193
+ num = p.split("-")[0]
194
+ try:
195
+ result.append(int(num))
196
+ except ValueError:
197
+ result.append(0)
198
+ return tuple(result)
199
+
200
+ p1 = parse_version(v1)
201
+ p2 = parse_version(v2)
202
+
203
+ # Pad to equal length
204
+ max_len = max(len(p1), len(p2))
205
+ p1 = p1 + (0,) * (max_len - len(p1))
206
+ p2 = p2 + (0,) * (max_len - len(p2))
207
+
208
+ if p1 < p2:
209
+ return -1
210
+ elif p1 > p2:
211
+ return 1
212
+ return 0
213
+
214
+
215
+ def check_updates(
216
+ registry_url: Optional[str] = None,
217
+ auth_token: Optional[str] = None,
218
+ timeout: int = 30,
219
+ verify_ssl: bool = True,
220
+ ) -> List[dict]:
221
+ """
222
+ Check for package updates from registry.
223
+
224
+ Args:
225
+ registry_url: Registry URL to check (default: https://registry.yuho.dev)
226
+ auth_token: Optional authentication token
227
+ timeout: Request timeout in seconds
228
+ verify_ssl: Whether to verify SSL certificates
229
+
230
+ Returns:
231
+ List of packages with updates available, each containing:
232
+ - section_number: Package section number
233
+ - current_version: Currently installed version
234
+ - available_version: Version available in registry
235
+ - title: Package title
236
+ """
237
+ if not registry_url:
238
+ registry_url = "https://registry.yuho.dev"
239
+
240
+ index = LibraryIndex()
241
+ installed = index.list_all()
242
+
243
+ if not installed:
244
+ logger.info("No packages installed")
245
+ return []
246
+
247
+ updates = []
248
+
249
+ try:
250
+ # Fetch registry index
251
+ api_url = urljoin(registry_url.rstrip("/") + "/", "api/v1/packages")
252
+
253
+ headers = {
254
+ "Accept": "application/json",
255
+ "User-Agent": "yuho-library/2.0",
256
+ }
257
+ if auth_token:
258
+ headers["Authorization"] = f"Bearer {auth_token}"
259
+
260
+ request = Request(api_url, headers=headers, method="GET")
261
+
262
+ import ssl
263
+ context = None
264
+ if not verify_ssl:
265
+ context = ssl.create_default_context()
266
+ context.check_hostname = False
267
+ context.verify_mode = ssl.CERT_NONE
268
+
269
+ with urlopen(request, timeout=timeout, context=context) as response:
270
+ registry_data = json.loads(response.read().decode("utf-8"))
271
+
272
+ # Build registry lookup
273
+ registry_packages = {}
274
+ for pkg in registry_data.get("packages", []):
275
+ section = pkg.get("section_number")
276
+ if section:
277
+ registry_packages[section] = pkg
278
+
279
+ # Compare versions
280
+ for entry in installed:
281
+ section = entry.section_number
282
+ if section in registry_packages:
283
+ registry_pkg = registry_packages[section]
284
+ registry_version = registry_pkg.get("version", "0.0.0")
285
+
286
+ if _compare_versions(entry.version, registry_version) < 0:
287
+ updates.append({
288
+ "section_number": section,
289
+ "current_version": entry.version,
290
+ "available_version": registry_version,
291
+ "title": entry.title,
292
+ })
293
+
294
+ return updates
295
+
296
+ except HTTPError as e:
297
+ logger.error(f"Registry request failed: HTTP {e.code}")
298
+ return []
299
+ except URLError as e:
300
+ logger.error(f"Registry connection failed: {mask_error(e)}")
301
+ return []
302
+ except json.JSONDecodeError as e:
303
+ logger.error(f"Invalid registry response: {mask_error(e)}")
304
+ return []
305
+ except Exception as e:
306
+ logger.exception(f"Update check failed: {mask_error(e)}")
307
+ return []
308
+
309
+
310
+ def download_package(
311
+ section_number: str,
312
+ registry_url: Optional[str] = None,
313
+ auth_token: Optional[str] = None,
314
+ timeout: int = 30,
315
+ verify_ssl: bool = True,
316
+ library_dir: Optional[Path] = None,
317
+ ) -> Tuple[bool, str]:
318
+ """
319
+ Download and install a package from the registry.
320
+
321
+ Args:
322
+ section_number: Section number to download
323
+ registry_url: Registry URL
324
+ auth_token: Optional authentication token
325
+ timeout: Request timeout in seconds
326
+ verify_ssl: Whether to verify SSL certificates
327
+ library_dir: Library directory
328
+
329
+ Returns:
330
+ Tuple of (success, message)
331
+ """
332
+ if not registry_url:
333
+ registry_url = "https://registry.yuho.dev"
334
+
335
+ try:
336
+ # Fetch package
337
+ api_url = urljoin(
338
+ registry_url.rstrip("/") + "/",
339
+ f"api/v1/packages/{section_number}/download"
340
+ )
341
+
342
+ headers = {
343
+ "Accept": "application/octet-stream",
344
+ "User-Agent": "yuho-library/2.0",
345
+ }
346
+ if auth_token:
347
+ headers["Authorization"] = f"Bearer {auth_token}"
348
+
349
+ request = Request(api_url, headers=headers, method="GET")
350
+
351
+ import ssl
352
+ context = None
353
+ if not verify_ssl:
354
+ context = ssl.create_default_context()
355
+ context.check_hostname = False
356
+ context.verify_mode = ssl.CERT_NONE
357
+
358
+ with urlopen(request, timeout=timeout, context=context) as response:
359
+ # Save to temp file
360
+ with tempfile.NamedTemporaryFile(suffix=".yhpkg", delete=False) as tmp:
361
+ tmp.write(response.read())
362
+ tmp_path = tmp.name
363
+
364
+ # Install the package
365
+ success, message = install_package(tmp_path, library_dir, force=True)
366
+
367
+ # Cleanup temp file
368
+ Path(tmp_path).unlink(missing_ok=True)
369
+
370
+ return (success, message)
371
+
372
+ except HTTPError as e:
373
+ if e.code == 404:
374
+ return (False, f"Package not found: {section_number}")
375
+ return (False, f"Download failed: HTTP {e.code}")
376
+ except URLError as e:
377
+ return (False, f"Connection failed: {e.reason}")
378
+ except Exception as e:
379
+ return (False, f"Download failed: {e}")
380
+
381
+
382
+ def update_all_packages(
383
+ registry_url: Optional[str] = None,
384
+ auth_token: Optional[str] = None,
385
+ timeout: int = 30,
386
+ verify_ssl: bool = True,
387
+ library_dir: Optional[Path] = None,
388
+ ) -> List[Tuple[str, bool, str]]:
389
+ """
390
+ Update all installed packages to latest versions.
391
+
392
+ Args:
393
+ registry_url: Registry URL
394
+ auth_token: Optional authentication token
395
+ timeout: Request timeout in seconds
396
+ verify_ssl: Whether to verify SSL certificates
397
+ library_dir: Library directory
398
+
399
+ Returns:
400
+ List of (section_number, success, message) tuples
401
+ """
402
+ updates = check_updates(registry_url, auth_token, timeout, verify_ssl)
403
+
404
+ if not updates:
405
+ logger.info("All packages are up to date")
406
+ return []
407
+
408
+ results = []
409
+ for update in updates:
410
+ section = update["section_number"]
411
+ success, message = download_package(
412
+ section,
413
+ registry_url,
414
+ auth_token,
415
+ timeout,
416
+ verify_ssl,
417
+ library_dir,
418
+ )
419
+ results.append((section, success, message))
420
+
421
+ return results
422
+
423
+
424
+ def publish_package(
425
+ source: str,
426
+ registry_url: str,
427
+ auth_token: Optional[str] = None,
428
+ timeout: int = 60,
429
+ verify_ssl: bool = True,
430
+ ) -> Tuple[bool, str]:
431
+ """
432
+ Publish a package to a registry.
433
+
434
+ Args:
435
+ source: Path to package source (.yhpkg file or directory)
436
+ registry_url: Registry URL to publish to
437
+ auth_token: Authentication token (required for publishing)
438
+ timeout: Request timeout in seconds
439
+ verify_ssl: Whether to verify SSL certificates
440
+
441
+ Returns:
442
+ Tuple of (success, message)
443
+ """
444
+ source_path = Path(source)
445
+
446
+ try:
447
+ # Load and validate package
448
+ if source_path.suffix == ".yhpkg":
449
+ package = Package.from_yhpkg(source_path)
450
+ pkg_path = source_path
451
+ elif source_path.is_dir():
452
+ package = Package.from_directory(source_path)
453
+ # Create temporary .yhpkg
454
+ with tempfile.NamedTemporaryFile(suffix=".yhpkg", delete=False) as tmp:
455
+ pkg_path = Path(tmp.name)
456
+ package.to_yhpkg(pkg_path)
457
+ else:
458
+ return (False, f"Invalid source: {source}")
459
+
460
+ # Validate strictly for publishing
461
+ validator = PackageValidator(strict=True)
462
+ is_valid, errors, warnings = validator.validate(package)
463
+
464
+ if not is_valid:
465
+ return (False, f"Package validation failed: {'; '.join(errors)}")
466
+
467
+ if warnings:
468
+ logger.warning(f"Package warnings: {'; '.join(warnings)}")
469
+
470
+ # Require authentication for publishing
471
+ if not auth_token:
472
+ return (False, "Authentication token required for publishing. Set via --auth-token or config.")
473
+
474
+ # Upload to registry
475
+ api_url = urljoin(registry_url.rstrip("/") + "/", "api/v1/packages")
476
+
477
+ # Read package data
478
+ with open(pkg_path, "rb") as f:
479
+ pkg_data = f.read()
480
+
481
+ # Cleanup temp file if created
482
+ if source_path.is_dir():
483
+ pkg_path.unlink(missing_ok=True)
484
+
485
+ # Prepare multipart form data
486
+ boundary = "----YuhoPackageBoundary"
487
+ body_parts = []
488
+
489
+ # Add package file
490
+ body_parts.append(f"--{boundary}".encode())
491
+ body_parts.append(
492
+ f'Content-Disposition: form-data; name="package"; filename="{package.metadata.section_number}.yhpkg"'.encode()
493
+ )
494
+ body_parts.append(b"Content-Type: application/octet-stream")
495
+ body_parts.append(b"")
496
+ body_parts.append(pkg_data)
497
+
498
+ # Add metadata
499
+ body_parts.append(f"--{boundary}".encode())
500
+ body_parts.append(b'Content-Disposition: form-data; name="metadata"')
501
+ body_parts.append(b"Content-Type: application/json")
502
+ body_parts.append(b"")
503
+ body_parts.append(json.dumps(package.metadata.to_dict()).encode())
504
+
505
+ body_parts.append(f"--{boundary}--".encode())
506
+
507
+ body = b"\r\n".join(body_parts)
508
+
509
+ headers = {
510
+ "Content-Type": f"multipart/form-data; boundary={boundary}",
511
+ "Authorization": f"Bearer {auth_token}",
512
+ "User-Agent": "yuho-library/2.0",
513
+ }
514
+
515
+ request = Request(api_url, data=body, headers=headers, method="POST")
516
+
517
+ import ssl
518
+ context = None
519
+ if not verify_ssl:
520
+ context = ssl.create_default_context()
521
+ context.check_hostname = False
522
+ context.verify_mode = ssl.CERT_NONE
523
+
524
+ with urlopen(request, timeout=timeout, context=context) as response:
525
+ result = json.loads(response.read().decode("utf-8"))
526
+
527
+ if result.get("success"):
528
+ return (True, f"Published {package.metadata.section_number} v{package.metadata.version}")
529
+ else:
530
+ return (False, result.get("error", "Unknown error"))
531
+
532
+ except HTTPError as e:
533
+ error_body = ""
534
+ try:
535
+ error_body = e.read().decode("utf-8")
536
+ error_data = json.loads(error_body)
537
+ error_msg = error_data.get("error", f"HTTP {e.code}")
538
+ except Exception:
539
+ error_msg = f"HTTP {e.code}: {error_body or e.reason}"
540
+ return (False, f"Publish failed: {error_msg}")
541
+ except URLError as e:
542
+ return (False, f"Connection failed: {mask_error(e)}")
543
+ except Exception as e:
544
+ logger.exception(f"Publish failed: {mask_error(e)}")
545
+ return (False, f"Publish failed: {mask_error(e)}")
546
+
547
+
548
+ def browse_registry(
549
+ page: int = 1,
550
+ per_page: int = 20,
551
+ search: Optional[str] = None,
552
+ jurisdiction: Optional[str] = None,
553
+ tags: Optional[List[str]] = None,
554
+ sort_by: str = "updated",
555
+ registry_url: Optional[str] = None,
556
+ timeout: int = 30,
557
+ verify_ssl: bool = True,
558
+ ) -> dict:
559
+ """
560
+ Browse packages in the registry with pagination and filtering.
561
+
562
+ Args:
563
+ page: Page number (1-indexed)
564
+ per_page: Results per page (max 100)
565
+ search: Search query for title/description
566
+ jurisdiction: Filter by jurisdiction code
567
+ tags: Filter by tags
568
+ sort_by: Sort order: 'updated', 'name', 'downloads'
569
+ registry_url: Registry base URL
570
+ timeout: Request timeout
571
+ verify_ssl: Verify SSL certificates
572
+
573
+ Returns:
574
+ Dict with:
575
+ - packages: List of package metadata
576
+ - total: Total number of matching packages
577
+ - page: Current page number
578
+ - per_page: Results per page
579
+ - pages: Total number of pages
580
+ """
581
+ registry = registry_url or "https://registry.yuho.dev"
582
+
583
+ # Build query parameters
584
+ params = {
585
+ "page": str(page),
586
+ "per_page": str(min(per_page, 100)),
587
+ "sort": sort_by,
588
+ }
589
+
590
+ if search:
591
+ params["q"] = search
592
+ if jurisdiction:
593
+ params["jurisdiction"] = jurisdiction
594
+ if tags:
595
+ params["tags"] = ",".join(tags)
596
+
597
+ # Build URL
598
+ query_string = "&".join(f"{k}={v}" for k, v in params.items())
599
+ api_url = f"{registry.rstrip('/')}/api/v1/packages?{query_string}"
600
+
601
+ try:
602
+ request = Request(api_url, headers={"User-Agent": "yuho-library/2.0"})
603
+
604
+ import ssl
605
+ context = None
606
+ if not verify_ssl:
607
+ context = ssl.create_default_context()
608
+ context.check_hostname = False
609
+ context.verify_mode = ssl.CERT_NONE
610
+
611
+ with urlopen(request, timeout=timeout, context=context) as response:
612
+ data = json.loads(response.read().decode("utf-8"))
613
+
614
+ return {
615
+ "packages": data.get("packages", []),
616
+ "total": data.get("total", 0),
617
+ "page": data.get("page", page),
618
+ "per_page": data.get("per_page", per_page),
619
+ "pages": data.get("pages", 1),
620
+ "success": True,
621
+ }
622
+
623
+ except HTTPError as e:
624
+ return {
625
+ "packages": [],
626
+ "total": 0,
627
+ "page": page,
628
+ "per_page": per_page,
629
+ "pages": 0,
630
+ "success": False,
631
+ "error": f"HTTP {e.code}: {e.reason}",
632
+ }
633
+ except URLError as e:
634
+ return {
635
+ "packages": [],
636
+ "total": 0,
637
+ "page": page,
638
+ "per_page": per_page,
639
+ "pages": 0,
640
+ "success": False,
641
+ "error": f"Connection failed: {e.reason}",
642
+ }
643
+ except Exception as e:
644
+ return {
645
+ "packages": [],
646
+ "total": 0,
647
+ "page": page,
648
+ "per_page": per_page,
649
+ "pages": 0,
650
+ "success": False,
651
+ "error": str(e),
652
+ }
653
+
654
+
655
+ def get_registry_package_info(
656
+ section_number: str,
657
+ registry_url: Optional[str] = None,
658
+ timeout: int = 30,
659
+ verify_ssl: bool = True,
660
+ ) -> Optional[dict]:
661
+ """
662
+ Get detailed package information from registry.
663
+
664
+ Args:
665
+ section_number: Package section number
666
+ registry_url: Registry base URL
667
+ timeout: Request timeout
668
+ verify_ssl: Verify SSL certificates
669
+
670
+ Returns:
671
+ Package metadata dict or None if not found
672
+ """
673
+ registry = registry_url or "https://registry.yuho.dev"
674
+ api_url = f"{registry.rstrip('/')}/api/v1/packages/{section_number}"
675
+
676
+ try:
677
+ request = Request(api_url, headers={"User-Agent": "yuho-library/2.0"})
678
+
679
+ import ssl
680
+ context = None
681
+ if not verify_ssl:
682
+ context = ssl.create_default_context()
683
+ context.check_hostname = False
684
+ context.verify_mode = ssl.CERT_NONE
685
+
686
+ with urlopen(request, timeout=timeout, context=context) as response:
687
+ return json.loads(response.read().decode("utf-8"))
688
+
689
+ except HTTPError as e:
690
+ if e.code == 404:
691
+ return None
692
+ logger.warning(f"Registry error: HTTP {e.code}")
693
+ return None
694
+ except URLError as e:
695
+ logger.warning(f"Connection failed: {mask_error(e)}")
696
+ return None
697
+ except Exception as e:
698
+ logger.exception(f"Failed to get package info: {mask_error(e)}")
699
+ return None