mirror of
https://github.com/Canner/WrenAI.git
synced 2026-08-30 18:00:36 +08:00
fix: ensure UTF-8 encoding for YAML file operations on Windows (#2357)
Co-authored-by: LeafJJ <leafjj@users.noreply.github.com>
This commit is contained in:
@@ -109,7 +109,7 @@ def _load_conn(
|
||||
typer.echo(f"Error: connection file not found: {path_str}", err=True)
|
||||
raise typer.Exit(1)
|
||||
try:
|
||||
conn = json.loads(path.read_text())
|
||||
conn = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as e:
|
||||
typer.echo(f"Error: invalid JSON in {path_str}: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
@@ -38,7 +38,7 @@ def load_config(wren_home: Path) -> WrenConfig:
|
||||
return WrenConfig()
|
||||
|
||||
try:
|
||||
raw = json.loads(config_path.read_text())
|
||||
raw = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError, OSError) as e:
|
||||
raise WrenError(
|
||||
ErrorCode.GENERIC_USER_ERROR,
|
||||
|
||||
@@ -171,7 +171,10 @@ def convert_mdl_to_project(mdl_json: dict) -> list[ProjectFile]:
|
||||
ProjectFile(
|
||||
relative_path="wren_project.yml",
|
||||
content=yaml.dump(
|
||||
project_config, default_flow_style=False, sort_keys=False
|
||||
project_config,
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -197,7 +200,10 @@ def convert_mdl_to_project(mdl_json: dict) -> list[ProjectFile]:
|
||||
ProjectFile(
|
||||
relative_path=f"{dir_path}/metadata.yml",
|
||||
content=yaml.dump(
|
||||
model_snake, default_flow_style=False, sort_keys=False
|
||||
model_snake,
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -219,6 +225,7 @@ def convert_mdl_to_project(mdl_json: dict) -> list[ProjectFile]:
|
||||
{"statement": statement},
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -229,7 +236,10 @@ def convert_mdl_to_project(mdl_json: dict) -> list[ProjectFile]:
|
||||
ProjectFile(
|
||||
relative_path=f"{dir_path}/metadata.yml",
|
||||
content=yaml.dump(
|
||||
view_snake, default_flow_style=False, sort_keys=False
|
||||
view_snake,
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -245,6 +255,7 @@ def convert_mdl_to_project(mdl_json: dict) -> list[ProjectFile]:
|
||||
{"relationships": rels_snake},
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -335,7 +346,7 @@ def load_global_config() -> dict:
|
||||
config_file = _WREN_HOME / "config.yml"
|
||||
if not config_file.exists():
|
||||
return {}
|
||||
return yaml.safe_load(config_file.read_text()) or {}
|
||||
return yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
|
||||
|
||||
|
||||
def discover_project_path(explicit: str | None = None) -> Path:
|
||||
@@ -382,7 +393,7 @@ def load_project_config(project_path: Path) -> dict:
|
||||
config_file = project_path / PROJECT_FILE
|
||||
if not config_file.exists():
|
||||
return {}
|
||||
return yaml.safe_load(config_file.read_text()) or {}
|
||||
return yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
|
||||
|
||||
|
||||
# Field order preferred when writing wren_project.yml back from a dict —
|
||||
@@ -415,7 +426,9 @@ def save_project_config(project_path: Path, config: dict) -> None:
|
||||
ordered[key] = value
|
||||
|
||||
(project_path / PROJECT_FILE).write_text(
|
||||
yaml.safe_dump(ordered, default_flow_style=False, sort_keys=False)
|
||||
yaml.safe_dump(
|
||||
ordered, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -494,7 +507,7 @@ def _load_models_v1(project_path: Path) -> list[dict]:
|
||||
return []
|
||||
models = []
|
||||
for f in sorted(models_dir.glob("*.yml")):
|
||||
data = yaml.safe_load(f.read_text())
|
||||
data = yaml.safe_load(f.read_text(encoding="utf-8"))
|
||||
if isinstance(data, dict):
|
||||
data["_source_dir"] = f.stem
|
||||
models.append(data)
|
||||
@@ -518,7 +531,7 @@ def _load_models_v2(project_path: Path) -> list[dict]:
|
||||
meta_file = d / "metadata.yml"
|
||||
if not meta_file.exists():
|
||||
continue
|
||||
model = yaml.safe_load(meta_file.read_text()) or {}
|
||||
model = yaml.safe_load(meta_file.read_text(encoding="utf-8")) or {}
|
||||
if not isinstance(model, dict):
|
||||
continue
|
||||
model["_source_dir"] = d.name
|
||||
@@ -526,7 +539,7 @@ def _load_models_v2(project_path: Path) -> list[dict]:
|
||||
# Merge ref_sql.sql if present (takes precedence)
|
||||
ref_sql_file = d / "ref_sql.sql"
|
||||
if ref_sql_file.exists():
|
||||
sql_content = ref_sql_file.read_text().strip()
|
||||
sql_content = ref_sql_file.read_text(encoding="utf-8").strip()
|
||||
if sql_content:
|
||||
model["ref_sql"] = sql_content
|
||||
|
||||
@@ -551,7 +564,7 @@ def _load_views_v1(project_path: Path) -> list[dict]:
|
||||
views_file = project_path / "views.yml"
|
||||
if not views_file.exists():
|
||||
return []
|
||||
data = yaml.safe_load(views_file.read_text()) or {}
|
||||
data = yaml.safe_load(views_file.read_text(encoding="utf-8")) or {}
|
||||
return data.get("views", []) if isinstance(data, dict) else []
|
||||
|
||||
|
||||
@@ -572,7 +585,7 @@ def _load_views_v2(project_path: Path) -> list[dict]:
|
||||
meta_file = d / "metadata.yml"
|
||||
if not meta_file.exists():
|
||||
continue
|
||||
view = yaml.safe_load(meta_file.read_text()) or {}
|
||||
view = yaml.safe_load(meta_file.read_text(encoding="utf-8")) or {}
|
||||
if not isinstance(view, dict):
|
||||
continue
|
||||
view["_source_dir"] = d.name
|
||||
@@ -580,7 +593,7 @@ def _load_views_v2(project_path: Path) -> list[dict]:
|
||||
# Merge sql.yml if present (takes precedence)
|
||||
sql_file = d / "sql.yml"
|
||||
if sql_file.exists():
|
||||
sql_data = yaml.safe_load(sql_file.read_text()) or {}
|
||||
sql_data = yaml.safe_load(sql_file.read_text(encoding="utf-8")) or {}
|
||||
if isinstance(sql_data, dict) and sql_data.get("statement"):
|
||||
view["statement"] = sql_data["statement"]
|
||||
|
||||
@@ -607,7 +620,7 @@ def _load_cubes_v1(project_path: Path) -> list[dict]:
|
||||
return []
|
||||
cubes = []
|
||||
for f in sorted(cubes_dir.glob("*.yml")):
|
||||
data = yaml.safe_load(f.read_text())
|
||||
data = yaml.safe_load(f.read_text(encoding="utf-8"))
|
||||
if isinstance(data, dict):
|
||||
data["_source_file"] = f.name
|
||||
cubes.append(data)
|
||||
@@ -647,7 +660,7 @@ def load_relationships(project_path: Path) -> list[dict]:
|
||||
rel_file = project_path / "relationships.yml"
|
||||
if not rel_file.exists():
|
||||
return []
|
||||
data = yaml.safe_load(rel_file.read_text()) or {}
|
||||
data = yaml.safe_load(rel_file.read_text(encoding="utf-8")) or {}
|
||||
return data.get("relationships", []) if isinstance(data, dict) else []
|
||||
|
||||
|
||||
@@ -656,7 +669,7 @@ def load_instructions(project_path: Path) -> str | None:
|
||||
inst_file = project_path / "instructions.md"
|
||||
if not inst_file.exists():
|
||||
return None
|
||||
return inst_file.read_text().strip() or None
|
||||
return inst_file.read_text(encoding="utf-8").strip() or None
|
||||
|
||||
|
||||
# ── Build ─────────────────────────────────────────────────────────────────
|
||||
@@ -1264,7 +1277,9 @@ def apply_upgrade(project_path: Path, result: UpgradeResult) -> None:
|
||||
config = load_project_config(project_path)
|
||||
config["schema_version"] = result.to_version
|
||||
config_file = project_path / PROJECT_FILE
|
||||
config_file.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False))
|
||||
config_file.write_text(
|
||||
yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
)
|
||||
|
||||
|
||||
def _apply_v1_to_v2(project_path: Path) -> None:
|
||||
@@ -1282,7 +1297,9 @@ def _apply_v1_to_v2(project_path: Path) -> None:
|
||||
(model_dir / "ref_sql.sql").write_text(ref_sql.strip() + "\n")
|
||||
|
||||
(model_dir / "metadata.yml").write_text(
|
||||
yaml.dump(model, default_flow_style=False, sort_keys=False)
|
||||
yaml.dump(
|
||||
model, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
)
|
||||
)
|
||||
|
||||
# Delete old flat file
|
||||
@@ -1307,13 +1324,16 @@ def _apply_v1_to_v2(project_path: Path) -> None:
|
||||
{"statement": statement},
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
)
|
||||
)
|
||||
elif statement:
|
||||
view["statement"] = statement
|
||||
|
||||
(view_dir / "metadata.yml").write_text(
|
||||
yaml.dump(view, default_flow_style=False, sort_keys=False)
|
||||
yaml.dump(
|
||||
view, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
)
|
||||
)
|
||||
|
||||
# Delete old views.yml
|
||||
|
||||
@@ -229,7 +229,7 @@ def init(
|
||||
typer.echo(f"Error: {mdl_path} not found.", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
mdl_json = json.loads(mdl_path.read_text())
|
||||
mdl_json = json.loads(mdl_path.read_text(encoding="utf-8"))
|
||||
files = convert_mdl_to_project(mdl_json)
|
||||
try:
|
||||
write_project_files(files, project_path, force=force)
|
||||
@@ -710,7 +710,11 @@ def show(
|
||||
elif output == "yaml":
|
||||
# YAML output uses snake_case (native)
|
||||
manifest = build_manifest(project_path)
|
||||
typer.echo(_yaml.dump(manifest, default_flow_style=False, sort_keys=False))
|
||||
typer.echo(
|
||||
_yaml.dump(
|
||||
manifest, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Summary view
|
||||
config = load_project_config(project_path)
|
||||
@@ -1158,7 +1162,11 @@ def _show_from_osi(
|
||||
typer.echo(json.dumps(manifest_json, indent=2, ensure_ascii=False))
|
||||
return
|
||||
if output == "yaml":
|
||||
typer.echo(_yaml.dump(manifest, default_flow_style=False, sort_keys=False))
|
||||
typer.echo(
|
||||
_yaml.dump(
|
||||
manifest, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Summary
|
||||
|
||||
@@ -32,7 +32,7 @@ def _load_mdl_json(mdl: str | None) -> str:
|
||||
path_str = _require_mdl(mdl)
|
||||
path = Path(path_str).expanduser()
|
||||
if path.exists():
|
||||
return path.read_text()
|
||||
return path.read_text(encoding="utf-8")
|
||||
typer.echo(f"Error: MDL file not found: {path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
@@ -126,7 +126,7 @@ def _load_cube_query_from(source: str) -> dict:
|
||||
if not p.exists():
|
||||
typer.echo(f"Error: CubeQuery file not found: {p}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raw = p.read_text()
|
||||
raw = p.read_text(encoding="utf-8")
|
||||
label = str(p)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
|
||||
@@ -375,7 +375,10 @@ def convert_dbt_project_to_wren_project(
|
||||
ProjectFile(
|
||||
relative_path="wren_project.yml",
|
||||
content=yaml.dump(
|
||||
project_config, default_flow_style=False, sort_keys=False
|
||||
project_config,
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
),
|
||||
),
|
||||
ProjectFile(
|
||||
@@ -384,6 +387,7 @@ def convert_dbt_project_to_wren_project(
|
||||
{"relationships": relationships},
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
),
|
||||
),
|
||||
ProjectFile(
|
||||
@@ -404,6 +408,7 @@ def convert_dbt_project_to_wren_project(
|
||||
{"version": 1, "pairs": query_pairs},
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -411,7 +416,9 @@ def convert_dbt_project_to_wren_project(
|
||||
files.extend(
|
||||
ProjectFile(
|
||||
relative_path=f"models/{model['name']}/metadata.yml",
|
||||
content=yaml.dump(model, default_flow_style=False, sort_keys=False),
|
||||
content=yaml.dump(
|
||||
model, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
),
|
||||
)
|
||||
for model in imported_models
|
||||
)
|
||||
|
||||
@@ -78,7 +78,7 @@ def _load_manifest(mdl: str | None) -> dict:
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
try:
|
||||
return json.loads(mdl_path.read_text())
|
||||
return json.loads(mdl_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as e:
|
||||
typer.echo(f"Error: invalid JSON in {mdl_path}: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
@@ -86,7 +86,7 @@ def parse_osi(text: str, *, suffix: str = ".yaml") -> dict:
|
||||
|
||||
|
||||
def load_osi_file(path: Path) -> dict:
|
||||
return parse_osi(path.read_text(), suffix=path.suffix)
|
||||
return parse_osi(path.read_text(encoding="utf-8"), suffix=path.suffix)
|
||||
|
||||
|
||||
def _extract_wren_block(custom_extensions: Any) -> dict:
|
||||
|
||||
@@ -149,7 +149,7 @@ def _load_raw() -> dict:
|
||||
if not _PROFILES_FILE.exists():
|
||||
return {"active": None, "profiles": {}}
|
||||
try:
|
||||
data = yaml.safe_load(_PROFILES_FILE.read_text())
|
||||
data = yaml.safe_load(_PROFILES_FILE.read_text(encoding="utf-8"))
|
||||
except yaml.YAMLError as exc:
|
||||
raise ValueError(
|
||||
f"profiles.yml is not valid YAML: {exc}\n"
|
||||
@@ -180,7 +180,9 @@ def _load_raw() -> dict:
|
||||
def _save_raw(data: dict) -> None:
|
||||
"""Write profiles.yml atomically with 0600 permissions."""
|
||||
_WREN_HOME.mkdir(parents=True, exist_ok=True)
|
||||
payload = yaml.dump(data, default_flow_style=False, sort_keys=False)
|
||||
payload = yaml.dump(
|
||||
data, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
)
|
||||
# Write to a temp file in the same directory then atomically replace
|
||||
fd, tmp_path = tempfile.mkstemp(dir=_WREN_HOME, suffix=".yml.tmp")
|
||||
try:
|
||||
@@ -232,7 +234,7 @@ def resolve_profile_for_project(project_path: Path) -> tuple[str | None, dict]:
|
||||
pinned_name: str | None = None
|
||||
if project_yml.exists():
|
||||
try:
|
||||
config = yaml.safe_load(project_yml.read_text()) or {}
|
||||
config = yaml.safe_load(project_yml.read_text(encoding="utf-8")) or {}
|
||||
except yaml.YAMLError as exc:
|
||||
# Fail loudly: a malformed project file shouldn't silently fall
|
||||
# back to the global active profile — that risks running against
|
||||
|
||||
@@ -133,7 +133,7 @@ def add(
|
||||
typer.echo(f"Error: file not found: {from_file}", err=True)
|
||||
raise typer.Exit(1)
|
||||
try:
|
||||
text = path.read_text()
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if path.suffix in (".yml", ".yaml"):
|
||||
raw = yaml.safe_load(text)
|
||||
else:
|
||||
|
||||
@@ -283,7 +283,7 @@ def generate_project_files(
|
||||
"data_source": "duckdb",
|
||||
}
|
||||
files["wren_project.yml"] = yaml.dump(
|
||||
project_config, default_flow_style=False, sort_keys=False
|
||||
project_config, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
)
|
||||
|
||||
# -- models/<table_name>/metadata.yml --
|
||||
@@ -316,7 +316,7 @@ def generate_project_files(
|
||||
|
||||
dir_name = _safe_path_segment(model_name)
|
||||
files[f"models/{dir_name}/metadata.yml"] = yaml.dump(
|
||||
model, default_flow_style=False, sort_keys=False
|
||||
model, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
)
|
||||
|
||||
# -- relationships.yml --
|
||||
@@ -337,6 +337,7 @@ def generate_project_files(
|
||||
{"relationships": rels_yaml},
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
)
|
||||
else:
|
||||
files["relationships.yml"] = "relationships: []\n"
|
||||
|
||||
@@ -51,7 +51,7 @@ def parse_types_cmd(
|
||||
if not path.exists():
|
||||
typer.echo(f"Error: file not found: {input_file}", err=True)
|
||||
raise typer.Exit(1)
|
||||
data = json.loads(path.read_text())
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
else:
|
||||
data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
|
||||
Reference in New Issue
Block a user