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/cli/commands/api.py
ADDED
|
@@ -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()
|