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/config/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yuho configuration module.
|
|
3
|
+
|
|
4
|
+
Handles configuration loading from:
|
|
5
|
+
1. Default values
|
|
6
|
+
2. Config file (~/.config/yuho/config.toml)
|
|
7
|
+
3. Environment variables (YUHO_*)
|
|
8
|
+
4. CLI flags
|
|
9
|
+
|
|
10
|
+
Later sources override earlier ones.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from yuho.config.loader import Config, load_config, get_config
|
|
14
|
+
from yuho.config.schema import (
|
|
15
|
+
LLMSection,
|
|
16
|
+
TranspileSection,
|
|
17
|
+
LSPSection,
|
|
18
|
+
MCPSection,
|
|
19
|
+
)
|
|
20
|
+
from yuho.config.mask import (
|
|
21
|
+
mask_value,
|
|
22
|
+
mask_dict,
|
|
23
|
+
mask_string,
|
|
24
|
+
mask_error,
|
|
25
|
+
mask_url,
|
|
26
|
+
safe_repr,
|
|
27
|
+
is_sensitive_key,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Config",
|
|
32
|
+
"load_config",
|
|
33
|
+
"get_config",
|
|
34
|
+
"LLMSection",
|
|
35
|
+
"TranspileSection",
|
|
36
|
+
"LSPSection",
|
|
37
|
+
"MCPSection",
|
|
38
|
+
# Masking utilities
|
|
39
|
+
"mask_value",
|
|
40
|
+
"mask_dict",
|
|
41
|
+
"mask_string",
|
|
42
|
+
"mask_error",
|
|
43
|
+
"mask_url",
|
|
44
|
+
"safe_repr",
|
|
45
|
+
"is_sensitive_key",
|
|
46
|
+
]
|
yuho/config/loader.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration loader with multi-source override support.
|
|
3
|
+
|
|
4
|
+
Load order (later overrides earlier):
|
|
5
|
+
1. Built-in defaults
|
|
6
|
+
2. Config file (~/.config/yuho/config.toml)
|
|
7
|
+
3. Environment variables (YUHO_*)
|
|
8
|
+
4. CLI flags
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Dict, Any
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from yuho.config.schema import ConfigSchema
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Default config file location
|
|
21
|
+
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "yuho" / "config.toml"
|
|
22
|
+
|
|
23
|
+
# Singleton config instance
|
|
24
|
+
_config: Optional["Config"] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Config:
|
|
28
|
+
"""
|
|
29
|
+
Yuho configuration manager.
|
|
30
|
+
|
|
31
|
+
Handles loading and accessing configuration from multiple sources.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, schema: ConfigSchema):
|
|
35
|
+
self._schema = schema
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def llm(self):
|
|
39
|
+
"""Get LLM configuration section."""
|
|
40
|
+
return self._schema.llm
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def transpile(self):
|
|
44
|
+
"""Get transpile configuration section."""
|
|
45
|
+
return self._schema.transpile
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def lsp(self):
|
|
49
|
+
"""Get LSP configuration section."""
|
|
50
|
+
return self._schema.lsp
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def mcp(self):
|
|
54
|
+
"""Get MCP configuration section."""
|
|
55
|
+
return self._schema.mcp
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
58
|
+
"""Convert to dictionary."""
|
|
59
|
+
return self._schema.to_dict()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_config(
|
|
63
|
+
config_path: Optional[Path] = None,
|
|
64
|
+
env_prefix: str = "YUHO_",
|
|
65
|
+
cli_overrides: Optional[Dict[str, Any]] = None,
|
|
66
|
+
) -> Config:
|
|
67
|
+
"""
|
|
68
|
+
Load configuration from all sources.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
config_path: Path to config file (default: ~/.config/yuho/config.toml)
|
|
72
|
+
env_prefix: Prefix for environment variables
|
|
73
|
+
cli_overrides: Dictionary of CLI flag overrides
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Loaded Config instance
|
|
77
|
+
"""
|
|
78
|
+
# Start with defaults
|
|
79
|
+
config_data: Dict[str, Any] = {}
|
|
80
|
+
|
|
81
|
+
# Load from file
|
|
82
|
+
file_path = config_path or DEFAULT_CONFIG_PATH
|
|
83
|
+
if file_path.exists():
|
|
84
|
+
config_data = _load_from_file(file_path)
|
|
85
|
+
logger.debug(f"Loaded config from {file_path}")
|
|
86
|
+
|
|
87
|
+
# Override from environment
|
|
88
|
+
env_data = _load_from_env(env_prefix)
|
|
89
|
+
config_data = _merge_dicts(config_data, env_data)
|
|
90
|
+
|
|
91
|
+
# Override from CLI
|
|
92
|
+
if cli_overrides:
|
|
93
|
+
config_data = _merge_dicts(config_data, cli_overrides)
|
|
94
|
+
|
|
95
|
+
# Create schema
|
|
96
|
+
schema = ConfigSchema.from_dict(config_data)
|
|
97
|
+
|
|
98
|
+
return Config(schema)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_config() -> Config:
|
|
102
|
+
"""
|
|
103
|
+
Get the global config instance.
|
|
104
|
+
|
|
105
|
+
Loads config on first call, then returns cached instance.
|
|
106
|
+
"""
|
|
107
|
+
global _config
|
|
108
|
+
if _config is None:
|
|
109
|
+
_config = load_config()
|
|
110
|
+
return _config
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _load_from_file(path: Path) -> Dict[str, Any]:
|
|
114
|
+
"""Load config from TOML file."""
|
|
115
|
+
try:
|
|
116
|
+
import tomllib
|
|
117
|
+
except ImportError:
|
|
118
|
+
try:
|
|
119
|
+
import tomli as tomllib
|
|
120
|
+
except ImportError:
|
|
121
|
+
logger.warning("tomllib/tomli not available, skipping config file")
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
with open(path, "rb") as f:
|
|
126
|
+
return tomllib.load(f)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning(f"Failed to load config file: {e}")
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _load_from_env(prefix: str) -> Dict[str, Any]:
|
|
133
|
+
"""Load config from environment variables."""
|
|
134
|
+
config: Dict[str, Any] = {}
|
|
135
|
+
|
|
136
|
+
# Map environment variables to config paths
|
|
137
|
+
env_mapping = {
|
|
138
|
+
f"{prefix}LLM_PROVIDER": ("llm", "provider"),
|
|
139
|
+
f"{prefix}LLM_MODEL": ("llm", "model"),
|
|
140
|
+
f"{prefix}LLM_OLLAMA_HOST": ("llm", "ollama_host"),
|
|
141
|
+
f"{prefix}LLM_OLLAMA_PORT": ("llm", "ollama_port"),
|
|
142
|
+
f"{prefix}LLM_HUGGINGFACE_CACHE": ("llm", "huggingface_cache"),
|
|
143
|
+
f"{prefix}LLM_OPENAI_API_KEY": ("llm", "openai_api_key"),
|
|
144
|
+
f"{prefix}LLM_ANTHROPIC_API_KEY": ("llm", "anthropic_api_key"),
|
|
145
|
+
f"{prefix}LLM_MAX_TOKENS": ("llm", "max_tokens"),
|
|
146
|
+
f"{prefix}LLM_TEMPERATURE": ("llm", "temperature"),
|
|
147
|
+
f"{prefix}TRANSPILE_DEFAULT_TARGET": ("transpile", "default_target"),
|
|
148
|
+
f"{prefix}TRANSPILE_LATEX_COMPILER": ("transpile", "latex_compiler"),
|
|
149
|
+
f"{prefix}TRANSPILE_OUTPUT_DIR": ("transpile", "output_dir"),
|
|
150
|
+
f"{prefix}MCP_HOST": ("mcp", "host"),
|
|
151
|
+
f"{prefix}MCP_PORT": ("mcp", "port"),
|
|
152
|
+
f"{prefix}MCP_AUTH_TOKEN": ("mcp", "auth_token"),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for env_var, (section, key) in env_mapping.items():
|
|
156
|
+
value = os.environ.get(env_var)
|
|
157
|
+
if value is not None:
|
|
158
|
+
if section not in config:
|
|
159
|
+
config[section] = {}
|
|
160
|
+
|
|
161
|
+
# Type conversion
|
|
162
|
+
if key in ("ollama_port", "max_tokens", "port"):
|
|
163
|
+
value = int(value)
|
|
164
|
+
elif key == "temperature":
|
|
165
|
+
value = float(value)
|
|
166
|
+
|
|
167
|
+
config[section][key] = value
|
|
168
|
+
|
|
169
|
+
return config
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _merge_dicts(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
173
|
+
"""Deep merge two dictionaries."""
|
|
174
|
+
result = base.copy()
|
|
175
|
+
|
|
176
|
+
for key, value in override.items():
|
|
177
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
178
|
+
result[key] = _merge_dicts(result[key], value)
|
|
179
|
+
else:
|
|
180
|
+
result[key] = value
|
|
181
|
+
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def create_default_config(path: Optional[Path] = None) -> Path:
|
|
186
|
+
"""
|
|
187
|
+
Create a default config file.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
path: Path to create file at (default: ~/.config/yuho/config.toml)
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Path to created file
|
|
194
|
+
"""
|
|
195
|
+
file_path = path or DEFAULT_CONFIG_PATH
|
|
196
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
|
|
198
|
+
default_config = """# Yuho Configuration
|
|
199
|
+
# https://yuho.dev/docs/configuration
|
|
200
|
+
|
|
201
|
+
[llm]
|
|
202
|
+
# LLM provider: ollama, huggingface, openai, anthropic
|
|
203
|
+
provider = "ollama"
|
|
204
|
+
model = "llama3"
|
|
205
|
+
ollama_host = "localhost"
|
|
206
|
+
ollama_port = 11434
|
|
207
|
+
# huggingface_cache = "~/.cache/huggingface"
|
|
208
|
+
# openai_api_key = ""
|
|
209
|
+
# anthropic_api_key = ""
|
|
210
|
+
max_tokens = 2048
|
|
211
|
+
temperature = 0.7
|
|
212
|
+
fallback_providers = ["huggingface"]
|
|
213
|
+
|
|
214
|
+
[transpile]
|
|
215
|
+
default_target = "json"
|
|
216
|
+
latex_compiler = "pdflatex"
|
|
217
|
+
# output_dir = "./output"
|
|
218
|
+
include_source_locations = true
|
|
219
|
+
|
|
220
|
+
[lsp]
|
|
221
|
+
diagnostic_severity_error = true
|
|
222
|
+
diagnostic_severity_warning = true
|
|
223
|
+
diagnostic_severity_info = true
|
|
224
|
+
diagnostic_severity_hint = true
|
|
225
|
+
completion_trigger_chars = [".", ":"]
|
|
226
|
+
|
|
227
|
+
[mcp]
|
|
228
|
+
host = "127.0.0.1"
|
|
229
|
+
port = 8080
|
|
230
|
+
allowed_origins = ["*"]
|
|
231
|
+
# auth_token = ""
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
file_path.write_text(default_config)
|
|
235
|
+
return file_path
|
yuho/config/mask.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for masking sensitive data in logs, errors, and output.
|
|
3
|
+
|
|
4
|
+
This module provides functions to redact sensitive information like
|
|
5
|
+
API keys, tokens, and passwords before they appear in logs or error messages.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Dict, List, Optional, Set
|
|
10
|
+
|
|
11
|
+
# Sensitive field names that should always be masked
|
|
12
|
+
SENSITIVE_FIELDS: Set[str] = {
|
|
13
|
+
"api_key",
|
|
14
|
+
"apikey",
|
|
15
|
+
"api-key",
|
|
16
|
+
"auth_token",
|
|
17
|
+
"authtoken",
|
|
18
|
+
"auth-token",
|
|
19
|
+
"token",
|
|
20
|
+
"password",
|
|
21
|
+
"secret",
|
|
22
|
+
"credential",
|
|
23
|
+
"openai_api_key",
|
|
24
|
+
"anthropic_api_key",
|
|
25
|
+
"authorization",
|
|
26
|
+
"bearer",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Patterns that look like secrets (for string scanning)
|
|
30
|
+
SECRET_PATTERNS = [
|
|
31
|
+
# API keys (various formats)
|
|
32
|
+
re.compile(r"sk-[a-zA-Z0-9]{20,}"), # OpenAI-style
|
|
33
|
+
re.compile(r"sk-ant-[a-zA-Z0-9-]{20,}"), # Anthropic-style
|
|
34
|
+
re.compile(r"hf_[a-zA-Z0-9]{20,}"), # HuggingFace-style
|
|
35
|
+
# Bearer tokens in headers
|
|
36
|
+
re.compile(r"Bearer\s+[a-zA-Z0-9._-]{20,}", re.IGNORECASE),
|
|
37
|
+
# Generic long alphanumeric strings that look like tokens
|
|
38
|
+
re.compile(r"[a-zA-Z0-9]{32,}"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def mask_value(value: str, visible_chars: int = 4) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Mask a sensitive value, showing only the first few characters.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
value: The sensitive value to mask
|
|
48
|
+
visible_chars: Number of characters to show at the start
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Masked value like "sk-a***" or "***" if too short
|
|
52
|
+
"""
|
|
53
|
+
if not value or len(value) <= visible_chars:
|
|
54
|
+
return "***"
|
|
55
|
+
return value[:visible_chars] + "***"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_sensitive_key(key: str) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Check if a key name indicates sensitive data.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
key: The key/field name to check
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if the key appears to be sensitive
|
|
67
|
+
"""
|
|
68
|
+
key_lower = key.lower().replace("_", "").replace("-", "")
|
|
69
|
+
for sensitive in SENSITIVE_FIELDS:
|
|
70
|
+
if sensitive.replace("_", "").replace("-", "") in key_lower:
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def mask_dict(data: Dict[str, Any], deep: bool = True) -> Dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Mask sensitive values in a dictionary.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
data: Dictionary potentially containing sensitive values
|
|
81
|
+
deep: Whether to recursively mask nested dictionaries
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
New dictionary with sensitive values masked
|
|
85
|
+
"""
|
|
86
|
+
result = {}
|
|
87
|
+
for key, value in data.items():
|
|
88
|
+
if is_sensitive_key(key):
|
|
89
|
+
if isinstance(value, str) and value:
|
|
90
|
+
result[key] = mask_value(value)
|
|
91
|
+
elif value is not None:
|
|
92
|
+
result[key] = "***"
|
|
93
|
+
else:
|
|
94
|
+
result[key] = None
|
|
95
|
+
elif deep and isinstance(value, dict):
|
|
96
|
+
result[key] = mask_dict(value, deep=True)
|
|
97
|
+
elif deep and isinstance(value, list):
|
|
98
|
+
result[key] = [
|
|
99
|
+
mask_dict(v, deep=True) if isinstance(v, dict) else v
|
|
100
|
+
for v in value
|
|
101
|
+
]
|
|
102
|
+
else:
|
|
103
|
+
result[key] = value
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def mask_string(text: str) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Scan a string and mask anything that looks like a secret.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
text: Text that might contain secrets
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Text with secret-like patterns masked
|
|
116
|
+
"""
|
|
117
|
+
result = text
|
|
118
|
+
for pattern in SECRET_PATTERNS:
|
|
119
|
+
result = pattern.sub(lambda m: mask_value(m.group(0)), result)
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def mask_error(error: Exception) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Get a safe string representation of an error, masking any secrets.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
error: The exception to format
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Error message with potential secrets masked
|
|
132
|
+
"""
|
|
133
|
+
return mask_string(str(error))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def mask_url(url: str) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Mask sensitive parts of a URL (query params with sensitive names).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
url: URL that might contain sensitive query parameters
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
URL with sensitive query parameters masked
|
|
145
|
+
"""
|
|
146
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
parsed = urlparse(url)
|
|
150
|
+
if not parsed.query:
|
|
151
|
+
return url
|
|
152
|
+
|
|
153
|
+
params = parse_qs(parsed.query, keep_blank_values=True)
|
|
154
|
+
masked_params = {}
|
|
155
|
+
|
|
156
|
+
for key, values in params.items():
|
|
157
|
+
if is_sensitive_key(key):
|
|
158
|
+
masked_params[key] = ["***" for _ in values]
|
|
159
|
+
else:
|
|
160
|
+
masked_params[key] = values
|
|
161
|
+
|
|
162
|
+
# Rebuild URL with masked params
|
|
163
|
+
masked_query = urlencode(masked_params, doseq=True)
|
|
164
|
+
return urlunparse((
|
|
165
|
+
parsed.scheme,
|
|
166
|
+
parsed.netloc,
|
|
167
|
+
parsed.path,
|
|
168
|
+
parsed.params,
|
|
169
|
+
masked_query,
|
|
170
|
+
parsed.fragment,
|
|
171
|
+
))
|
|
172
|
+
except Exception:
|
|
173
|
+
# If URL parsing fails, just return original
|
|
174
|
+
return url
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def safe_repr(obj: Any) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Get a safe string representation of any object, masking secrets.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
obj: Object to represent
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Safe string representation
|
|
186
|
+
"""
|
|
187
|
+
if isinstance(obj, dict):
|
|
188
|
+
return str(mask_dict(obj))
|
|
189
|
+
elif isinstance(obj, str):
|
|
190
|
+
return mask_string(obj)
|
|
191
|
+
elif isinstance(obj, Exception):
|
|
192
|
+
return mask_error(obj)
|
|
193
|
+
else:
|
|
194
|
+
return mask_string(repr(obj))
|
yuho/config/schema.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration schema definitions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Optional, List, Dict, Any
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class LLMSection:
|
|
12
|
+
"""[llm] configuration section."""
|
|
13
|
+
|
|
14
|
+
provider: str = "ollama"
|
|
15
|
+
model: str = "llama3"
|
|
16
|
+
ollama_host: str = "localhost"
|
|
17
|
+
ollama_port: int = 11434
|
|
18
|
+
huggingface_cache: Optional[str] = None
|
|
19
|
+
openai_api_key: Optional[str] = None
|
|
20
|
+
anthropic_api_key: Optional[str] = None
|
|
21
|
+
max_tokens: int = 2048
|
|
22
|
+
temperature: float = 0.7
|
|
23
|
+
fallback_providers: List[str] = field(default_factory=lambda: ["huggingface"])
|
|
24
|
+
|
|
25
|
+
def to_llm_config(self):
|
|
26
|
+
"""Convert to LLMConfig."""
|
|
27
|
+
from yuho.llm import LLMConfig
|
|
28
|
+
return LLMConfig(
|
|
29
|
+
provider=self.provider,
|
|
30
|
+
model_name=self.model,
|
|
31
|
+
ollama_host=self.ollama_host,
|
|
32
|
+
ollama_port=self.ollama_port,
|
|
33
|
+
huggingface_cache=self.huggingface_cache,
|
|
34
|
+
api_key=self.openai_api_key or self.anthropic_api_key,
|
|
35
|
+
max_tokens=self.max_tokens,
|
|
36
|
+
temperature=self.temperature,
|
|
37
|
+
fallback_providers=self.fallback_providers,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class TranspileSection:
|
|
43
|
+
"""[transpile] configuration section."""
|
|
44
|
+
|
|
45
|
+
default_target: str = "json"
|
|
46
|
+
latex_compiler: str = "pdflatex"
|
|
47
|
+
output_dir: Optional[str] = None
|
|
48
|
+
include_source_locations: bool = True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class LSPSection:
|
|
53
|
+
"""[lsp] configuration section."""
|
|
54
|
+
|
|
55
|
+
diagnostic_severity_error: bool = True
|
|
56
|
+
diagnostic_severity_warning: bool = True
|
|
57
|
+
diagnostic_severity_info: bool = True
|
|
58
|
+
diagnostic_severity_hint: bool = True
|
|
59
|
+
completion_trigger_chars: List[str] = field(default_factory=lambda: [".", ":"])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class MCPSection:
|
|
64
|
+
"""[mcp] configuration section."""
|
|
65
|
+
|
|
66
|
+
host: str = "127.0.0.1"
|
|
67
|
+
port: int = 8080
|
|
68
|
+
allowed_origins: List[str] = field(default_factory=lambda: ["*"])
|
|
69
|
+
auth_token: Optional[str] = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class LibrarySection:
|
|
74
|
+
"""[library] configuration section."""
|
|
75
|
+
|
|
76
|
+
registry_url: str = "https://registry.yuho.dev"
|
|
77
|
+
registry_api_version: str = "v1"
|
|
78
|
+
auth_token: Optional[str] = None
|
|
79
|
+
timeout: int = 30
|
|
80
|
+
verify_ssl: bool = True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ConfigSchema:
|
|
85
|
+
"""Complete configuration schema."""
|
|
86
|
+
|
|
87
|
+
llm: LLMSection = field(default_factory=LLMSection)
|
|
88
|
+
transpile: TranspileSection = field(default_factory=TranspileSection)
|
|
89
|
+
lsp: LSPSection = field(default_factory=LSPSection)
|
|
90
|
+
mcp: MCPSection = field(default_factory=MCPSection)
|
|
91
|
+
library: LibrarySection = field(default_factory=LibrarySection)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ConfigSchema":
|
|
95
|
+
"""Create config from dictionary."""
|
|
96
|
+
llm_data = data.get("llm", {})
|
|
97
|
+
transpile_data = data.get("transpile", {})
|
|
98
|
+
lsp_data = data.get("lsp", {})
|
|
99
|
+
mcp_data = data.get("mcp", {})
|
|
100
|
+
library_data = data.get("library", {})
|
|
101
|
+
|
|
102
|
+
return cls(
|
|
103
|
+
llm=LLMSection(**{k: v for k, v in llm_data.items() if k in LLMSection.__dataclass_fields__}),
|
|
104
|
+
transpile=TranspileSection(**{k: v for k, v in transpile_data.items() if k in TranspileSection.__dataclass_fields__}),
|
|
105
|
+
lsp=LSPSection(**{k: v for k, v in lsp_data.items() if k in LSPSection.__dataclass_fields__}),
|
|
106
|
+
mcp=MCPSection(**{k: v for k, v in mcp_data.items() if k in MCPSection.__dataclass_fields__}),
|
|
107
|
+
library=LibrarySection(**{k: v for k, v in library_data.items() if k in LibrarySection.__dataclass_fields__}),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
111
|
+
"""Convert to dictionary."""
|
|
112
|
+
return {
|
|
113
|
+
"llm": {
|
|
114
|
+
"provider": self.llm.provider,
|
|
115
|
+
"model": self.llm.model,
|
|
116
|
+
"ollama_host": self.llm.ollama_host,
|
|
117
|
+
"ollama_port": self.llm.ollama_port,
|
|
118
|
+
"huggingface_cache": self.llm.huggingface_cache,
|
|
119
|
+
"max_tokens": self.llm.max_tokens,
|
|
120
|
+
"temperature": self.llm.temperature,
|
|
121
|
+
"fallback_providers": self.llm.fallback_providers,
|
|
122
|
+
},
|
|
123
|
+
"transpile": {
|
|
124
|
+
"default_target": self.transpile.default_target,
|
|
125
|
+
"latex_compiler": self.transpile.latex_compiler,
|
|
126
|
+
"output_dir": self.transpile.output_dir,
|
|
127
|
+
"include_source_locations": self.transpile.include_source_locations,
|
|
128
|
+
},
|
|
129
|
+
"lsp": {
|
|
130
|
+
"diagnostic_severity_error": self.lsp.diagnostic_severity_error,
|
|
131
|
+
"diagnostic_severity_warning": self.lsp.diagnostic_severity_warning,
|
|
132
|
+
"diagnostic_severity_info": self.lsp.diagnostic_severity_info,
|
|
133
|
+
"diagnostic_severity_hint": self.lsp.diagnostic_severity_hint,
|
|
134
|
+
"completion_trigger_chars": self.lsp.completion_trigger_chars,
|
|
135
|
+
},
|
|
136
|
+
"mcp": {
|
|
137
|
+
"host": self.mcp.host,
|
|
138
|
+
"port": self.mcp.port,
|
|
139
|
+
"allowed_origins": self.mcp.allowed_origins,
|
|
140
|
+
},
|
|
141
|
+
"library": {
|
|
142
|
+
"registry_url": self.library.registry_url,
|
|
143
|
+
"registry_api_version": self.library.registry_api_version,
|
|
144
|
+
"timeout": self.library.timeout,
|
|
145
|
+
"verify_ssl": self.library.verify_ssl,
|
|
146
|
+
},
|
|
147
|
+
}
|
yuho/library/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yuho library module - user-contributed statute repository.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Package format definitions (.yhpkg)
|
|
6
|
+
- Contribution validation
|
|
7
|
+
- Library indexing and search
|
|
8
|
+
- Package installation and management
|
|
9
|
+
- Dependency resolution with version constraints
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from yuho.library.package import (
|
|
13
|
+
Package,
|
|
14
|
+
PackageMetadata,
|
|
15
|
+
PackageValidator,
|
|
16
|
+
DeprecationInfo,
|
|
17
|
+
)
|
|
18
|
+
from yuho.library.index import (
|
|
19
|
+
LibraryIndex,
|
|
20
|
+
search_library,
|
|
21
|
+
)
|
|
22
|
+
from yuho.library.install import (
|
|
23
|
+
install_package,
|
|
24
|
+
uninstall_package,
|
|
25
|
+
list_installed,
|
|
26
|
+
update_package,
|
|
27
|
+
check_updates,
|
|
28
|
+
download_package,
|
|
29
|
+
update_all_packages,
|
|
30
|
+
publish_package,
|
|
31
|
+
)
|
|
32
|
+
from yuho.library.resolver import (
|
|
33
|
+
DependencyResolver,
|
|
34
|
+
Resolution,
|
|
35
|
+
Dependency,
|
|
36
|
+
Version,
|
|
37
|
+
VersionConstraint,
|
|
38
|
+
resolve_dependencies,
|
|
39
|
+
validate_semver,
|
|
40
|
+
SemverValidation,
|
|
41
|
+
SemverValidationError,
|
|
42
|
+
CompatibilityChecker,
|
|
43
|
+
CompatibilityResult,
|
|
44
|
+
)
|
|
45
|
+
from yuho.library.lockfile import (
|
|
46
|
+
LockFile,
|
|
47
|
+
LockFileManager,
|
|
48
|
+
LockedPackage,
|
|
49
|
+
load_lock_file,
|
|
50
|
+
create_lock_file,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"Package",
|
|
55
|
+
"PackageMetadata",
|
|
56
|
+
"PackageValidator",
|
|
57
|
+
"DeprecationInfo",
|
|
58
|
+
"LibraryIndex",
|
|
59
|
+
"search_library",
|
|
60
|
+
"install_package",
|
|
61
|
+
"uninstall_package",
|
|
62
|
+
"list_installed",
|
|
63
|
+
"update_package",
|
|
64
|
+
"check_updates",
|
|
65
|
+
"download_package",
|
|
66
|
+
"update_all_packages",
|
|
67
|
+
"publish_package",
|
|
68
|
+
"DependencyResolver",
|
|
69
|
+
"Resolution",
|
|
70
|
+
"Dependency",
|
|
71
|
+
"Version",
|
|
72
|
+
"VersionConstraint",
|
|
73
|
+
"resolve_dependencies",
|
|
74
|
+
"validate_semver",
|
|
75
|
+
"SemverValidation",
|
|
76
|
+
"SemverValidationError",
|
|
77
|
+
"CompatibilityChecker",
|
|
78
|
+
"CompatibilityResult",
|
|
79
|
+
"LockFile",
|
|
80
|
+
"LockFileManager",
|
|
81
|
+
"LockedPackage",
|
|
82
|
+
"load_lock_file",
|
|
83
|
+
"create_lock_file",
|
|
84
|
+
]
|