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,431 @@
1
+ """
2
+ API server command - HTTP REST API for Yuho operations.
3
+
4
+ Provides a lightweight HTTP API for:
5
+ - Parsing and validating Yuho source code
6
+ - Transpiling to various formats
7
+ - Running lint checks
8
+ - Health checks
9
+ """
10
+
11
+ import sys
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Optional, Dict, Any, List
15
+ from dataclasses import dataclass, asdict
16
+ from http.server import HTTPServer, BaseHTTPRequestHandler
17
+ from urllib.parse import urlparse, parse_qs
18
+ import threading
19
+
20
+ import click
21
+
22
+ from yuho.parser import Parser
23
+ from yuho.ast import ASTBuilder
24
+ from yuho.transpile.base import TranspileTarget
25
+ from yuho.transpile.registry import TranspilerRegistry
26
+ from yuho.cli.error_formatter import Colors, colorize
27
+
28
+
29
+ @dataclass
30
+ class APIResponse:
31
+ """Standard API response structure."""
32
+ success: bool
33
+ data: Optional[Dict[str, Any]] = None
34
+ error: Optional[str] = None
35
+
36
+ def to_json(self) -> str:
37
+ return json.dumps(asdict(self), indent=2)
38
+
39
+
40
+ class YuhoAPIHandler(BaseHTTPRequestHandler):
41
+ """HTTP request handler for Yuho API."""
42
+
43
+ # Class-level parser and registry (shared across requests)
44
+ parser = Parser()
45
+ registry = TranspilerRegistry.instance()
46
+
47
+ def _send_json_response(self, status: int, response: APIResponse) -> None:
48
+ """Send a JSON response."""
49
+ body = response.to_json().encode('utf-8')
50
+ self.send_response(status)
51
+ self.send_header('Content-Type', 'application/json')
52
+ self.send_header('Content-Length', str(len(body)))
53
+ self.send_header('Access-Control-Allow-Origin', '*')
54
+ self.end_headers()
55
+ self.wfile.write(body)
56
+
57
+ def _read_body(self) -> bytes:
58
+ """Read request body."""
59
+ content_length = int(self.headers.get('Content-Length', 0))
60
+ return self.rfile.read(content_length)
61
+
62
+ def do_OPTIONS(self) -> None:
63
+ """Handle CORS preflight requests."""
64
+ self.send_response(204)
65
+ self.send_header('Access-Control-Allow-Origin', '*')
66
+ self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
67
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
68
+ self.end_headers()
69
+
70
+ def do_GET(self) -> None:
71
+ """Handle GET requests."""
72
+ parsed = urlparse(self.path)
73
+ path = parsed.path
74
+
75
+ if path == '/health' or path == '/':
76
+ self._handle_health()
77
+ elif path == '/targets':
78
+ self._handle_targets()
79
+ elif path == '/rules':
80
+ self._handle_rules()
81
+ else:
82
+ self._send_json_response(404, APIResponse(
83
+ success=False,
84
+ error=f"Not found: {path}"
85
+ ))
86
+
87
+ def do_POST(self) -> None:
88
+ """Handle POST requests."""
89
+ parsed = urlparse(self.path)
90
+ path = parsed.path
91
+
92
+ try:
93
+ body = self._read_body()
94
+ data = json.loads(body) if body else {}
95
+ except json.JSONDecodeError as e:
96
+ self._send_json_response(400, APIResponse(
97
+ success=False,
98
+ error=f"Invalid JSON: {e}"
99
+ ))
100
+ return
101
+
102
+ if path == '/parse':
103
+ self._handle_parse(data)
104
+ elif path == '/validate':
105
+ self._handle_validate(data)
106
+ elif path == '/transpile':
107
+ self._handle_transpile(data)
108
+ elif path == '/lint':
109
+ self._handle_lint(data)
110
+ else:
111
+ self._send_json_response(404, APIResponse(
112
+ success=False,
113
+ error=f"Not found: {path}"
114
+ ))
115
+
116
+ def _handle_health(self) -> None:
117
+ """Health check endpoint."""
118
+ self._send_json_response(200, APIResponse(
119
+ success=True,
120
+ data={
121
+ "status": "healthy",
122
+ "version": "5.0.0",
123
+ "endpoints": [
124
+ "GET /health",
125
+ "GET /targets",
126
+ "GET /rules",
127
+ "POST /parse",
128
+ "POST /validate",
129
+ "POST /transpile",
130
+ "POST /lint",
131
+ ]
132
+ }
133
+ ))
134
+
135
+ def _handle_targets(self) -> None:
136
+ """List available transpile targets."""
137
+ targets = [
138
+ {
139
+ "name": t.name.lower(),
140
+ "extension": t.file_extension,
141
+ }
142
+ for t in TranspileTarget
143
+ ]
144
+ self._send_json_response(200, APIResponse(
145
+ success=True,
146
+ data={"targets": targets}
147
+ ))
148
+
149
+ def _handle_rules(self) -> None:
150
+ """List available lint rules."""
151
+ from yuho.cli.commands.lint import ALL_RULES
152
+ rules = [
153
+ {
154
+ "id": r.id,
155
+ "severity": r.severity.name.lower(),
156
+ "description": r.description,
157
+ }
158
+ for r in ALL_RULES
159
+ ]
160
+ self._send_json_response(200, APIResponse(
161
+ success=True,
162
+ data={"rules": rules}
163
+ ))
164
+
165
+ def _handle_parse(self, data: Dict[str, Any]) -> None:
166
+ """Parse Yuho source code."""
167
+ source = data.get('source', '')
168
+ filename = data.get('filename', '<api>')
169
+
170
+ if not source:
171
+ self._send_json_response(400, APIResponse(
172
+ success=False,
173
+ error="Missing 'source' field"
174
+ ))
175
+ return
176
+
177
+ result = self.parser.parse_string(source, filename)
178
+
179
+ if result.errors:
180
+ errors = [
181
+ {
182
+ "message": e.message,
183
+ "line": e.location.line if e.location else None,
184
+ "column": e.location.col if e.location else None,
185
+ }
186
+ for e in result.errors
187
+ ]
188
+ self._send_json_response(200, APIResponse(
189
+ success=False,
190
+ data={"errors": errors}
191
+ ))
192
+ return
193
+
194
+ # Build AST
195
+ try:
196
+ builder = ASTBuilder()
197
+ ast = builder.build(result.tree)
198
+
199
+ # Return AST summary
200
+ self._send_json_response(200, APIResponse(
201
+ success=True,
202
+ data={
203
+ "statutes": len(ast.statutes),
204
+ "types": len(ast.type_defs),
205
+ "functions": len(ast.function_defs),
206
+ "imports": len(ast.imports),
207
+ "statute_sections": [s.section_number for s in ast.statutes],
208
+ }
209
+ ))
210
+ except Exception as e:
211
+ self._send_json_response(500, APIResponse(
212
+ success=False,
213
+ error=f"AST build error: {e}"
214
+ ))
215
+
216
+ def _handle_validate(self, data: Dict[str, Any]) -> None:
217
+ """Validate Yuho source code."""
218
+ source = data.get('source', '')
219
+ filename = data.get('filename', '<api>')
220
+
221
+ if not source:
222
+ self._send_json_response(400, APIResponse(
223
+ success=False,
224
+ error="Missing 'source' field"
225
+ ))
226
+ return
227
+
228
+ result = self.parser.parse_string(source, filename)
229
+
230
+ errors = []
231
+ if result.errors:
232
+ errors = [
233
+ {
234
+ "type": "parse",
235
+ "message": e.message,
236
+ "line": e.location.line if e.location else None,
237
+ "column": e.location.col if e.location else None,
238
+ }
239
+ for e in result.errors
240
+ ]
241
+
242
+ self._send_json_response(200, APIResponse(
243
+ success=len(errors) == 0,
244
+ data={
245
+ "valid": len(errors) == 0,
246
+ "errors": errors,
247
+ }
248
+ ))
249
+
250
+ def _handle_transpile(self, data: Dict[str, Any]) -> None:
251
+ """Transpile Yuho source code."""
252
+ source = data.get('source', '')
253
+ target_name = data.get('target', 'json')
254
+ filename = data.get('filename', '<api>')
255
+
256
+ if not source:
257
+ self._send_json_response(400, APIResponse(
258
+ success=False,
259
+ error="Missing 'source' field"
260
+ ))
261
+ return
262
+
263
+ # Parse
264
+ result = self.parser.parse_string(source, filename)
265
+
266
+ if result.errors:
267
+ errors = [{"message": e.message} for e in result.errors]
268
+ self._send_json_response(200, APIResponse(
269
+ success=False,
270
+ data={"errors": errors}
271
+ ))
272
+ return
273
+
274
+ # Build AST
275
+ try:
276
+ builder = ASTBuilder()
277
+ ast = builder.build(result.tree)
278
+ except Exception as e:
279
+ self._send_json_response(500, APIResponse(
280
+ success=False,
281
+ error=f"AST build error: {e}"
282
+ ))
283
+ return
284
+
285
+ # Transpile
286
+ try:
287
+ target = TranspileTarget.from_string(target_name)
288
+ transpiler = self.registry.get(target)
289
+ output = transpiler.transpile(ast)
290
+
291
+ self._send_json_response(200, APIResponse(
292
+ success=True,
293
+ data={
294
+ "target": target.name.lower(),
295
+ "output": output,
296
+ }
297
+ ))
298
+ except ValueError as e:
299
+ self._send_json_response(400, APIResponse(
300
+ success=False,
301
+ error=f"Invalid target: {target_name}"
302
+ ))
303
+ except Exception as e:
304
+ self._send_json_response(500, APIResponse(
305
+ success=False,
306
+ error=f"Transpilation error: {e}"
307
+ ))
308
+
309
+ def _handle_lint(self, data: Dict[str, Any]) -> None:
310
+ """Run lint checks on Yuho source code."""
311
+ source = data.get('source', '')
312
+ rules = data.get('rules', None) # Optional rule filter
313
+ filename = data.get('filename', '<api>')
314
+
315
+ if not source:
316
+ self._send_json_response(400, APIResponse(
317
+ success=False,
318
+ error="Missing 'source' field"
319
+ ))
320
+ return
321
+
322
+ # Parse
323
+ result = self.parser.parse_string(source, filename)
324
+
325
+ if result.errors:
326
+ errors = [{"message": e.message} for e in result.errors]
327
+ self._send_json_response(200, APIResponse(
328
+ success=False,
329
+ data={"parse_errors": errors}
330
+ ))
331
+ return
332
+
333
+ # Build AST
334
+ try:
335
+ builder = ASTBuilder()
336
+ ast = builder.build(result.tree)
337
+ except Exception as e:
338
+ self._send_json_response(500, APIResponse(
339
+ success=False,
340
+ error=f"AST build error: {e}"
341
+ ))
342
+ return
343
+
344
+ # Run lint
345
+ from yuho.cli.commands.lint import ALL_RULES, Severity
346
+
347
+ active_rules = ALL_RULES
348
+ if rules:
349
+ active_rules = [r for r in ALL_RULES if r.id in rules]
350
+
351
+ all_issues = []
352
+ for rule in active_rules:
353
+ issues = rule.check(ast, source)
354
+ for issue in issues:
355
+ all_issues.append({
356
+ "rule": issue.rule,
357
+ "severity": issue.severity.name.lower(),
358
+ "message": issue.message,
359
+ "line": issue.line,
360
+ "suggestion": issue.suggestion,
361
+ })
362
+
363
+ self._send_json_response(200, APIResponse(
364
+ success=True,
365
+ data={
366
+ "issues": all_issues,
367
+ "summary": {
368
+ "errors": len([i for i in all_issues if i["severity"] == "error"]),
369
+ "warnings": len([i for i in all_issues if i["severity"] == "warning"]),
370
+ "infos": len([i for i in all_issues if i["severity"] == "info"]),
371
+ "hints": len([i for i in all_issues if i["severity"] == "hint"]),
372
+ }
373
+ }
374
+ ))
375
+
376
+ def log_message(self, format: str, *args) -> None:
377
+ """Override to use our logging."""
378
+ if hasattr(self.server, 'verbose') and self.server.verbose:
379
+ message = format % args
380
+ click.echo(f"[API] {self.address_string()} - {message}")
381
+
382
+
383
+ class YuhoAPIServer(HTTPServer):
384
+ """Custom HTTP server with additional configuration."""
385
+
386
+ def __init__(self, server_address, RequestHandlerClass, verbose: bool = False):
387
+ super().__init__(server_address, RequestHandlerClass)
388
+ self.verbose = verbose
389
+
390
+
391
+ def run_api(
392
+ host: str = "127.0.0.1",
393
+ port: int = 8080,
394
+ verbose: bool = False,
395
+ color: bool = True,
396
+ ) -> None:
397
+ """
398
+ Start the Yuho API server.
399
+
400
+ Args:
401
+ host: Host to bind to
402
+ port: Port to listen on
403
+ verbose: Enable verbose output
404
+ color: Use colored output
405
+ """
406
+ server_address = (host, port)
407
+
408
+ try:
409
+ httpd = YuhoAPIServer(server_address, YuhoAPIHandler, verbose=verbose)
410
+ except OSError as e:
411
+ click.echo(colorize(f"error: Could not start server: {e}", Colors.RED), err=True)
412
+ sys.exit(1)
413
+
414
+ url = f"http://{host}:{port}"
415
+ click.echo(f"Starting Yuho API server at {colorize(url, Colors.CYAN) if color else url}")
416
+ click.echo("Endpoints:")
417
+ click.echo(" GET /health - Health check")
418
+ click.echo(" GET /targets - List transpile targets")
419
+ click.echo(" GET /rules - List lint rules")
420
+ click.echo(" POST /parse - Parse Yuho source")
421
+ click.echo(" POST /validate - Validate Yuho source")
422
+ click.echo(" POST /transpile - Transpile to target format")
423
+ click.echo(" POST /lint - Run lint checks")
424
+ click.echo("")
425
+ click.echo("Press Ctrl+C to stop")
426
+
427
+ try:
428
+ httpd.serve_forever()
429
+ except KeyboardInterrupt:
430
+ click.echo("\nShutting down...")
431
+ httpd.shutdown()