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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- yuho-5.0.0.dist-info/entry_points.txt +2 -0
yuho/library/install.py
ADDED
|
@@ -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
|