Add formatters, utils, schemas and version
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.8) (push) Has been cancelled
CI / test (3.9) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / build-package (push) Has been cancelled

This commit is contained in:
2026-01-29 10:50:33 +00:00
parent ef344031f7
commit 6470e7fe5c

View File

@@ -0,0 +1,147 @@
"""File parsing utilities for ConfigForge."""
import os
from pathlib import Path
from typing import Any, Optional
from configforge.exceptions import (
ConversionError,
FileOperationError,
InvalidConfigFormatError,
)
SUPPORTED_FORMATS = ["json", "yaml", "yml", "toml", "env", "ini"]
def detect_format(filepath: str) -> str:
"""Detect the format of a configuration file based on its extension."""
ext = Path(filepath).suffix.lower()
format_mapping = {
".json": "json",
".yaml": "yaml",
".yml": "yaml",
".toml": "toml",
".env": "env",
".ini": "ini",
}
if ext not in format_mapping:
raise InvalidConfigFormatError(
f"Unsupported file format: {ext}. Supported formats: {', '.join(SUPPORTED_FORMATS)}"
)
return format_mapping[ext]
class FileParser:
"""Parser for configuration files in various formats."""
@staticmethod
def parse(filepath: str) -> dict:
"""Parse a configuration file and return its contents as a dictionary."""
try:
format_type = detect_format(filepath)
parser_method = getattr(FileParser, f"_parse_{format_type}", None)
if parser_method is None:
raise InvalidConfigFormatError(f"No parser available for format: {format_type}")
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
return parser_method(content, filepath)
except FileNotFoundError:
raise FileOperationError(f"File not found: {filepath}", filepath=filepath)
except PermissionError:
raise FileOperationError(f"Permission denied: {filepath}", filepath=filepath)
except Exception as e:
if isinstance(e, (InvalidConfigFormatError, FileOperationError, ConversionError)):
raise
raise ConversionError(f"Failed to parse {filepath}: {str(e)}") from e
@staticmethod
def _parse_json(content: str, filepath: str) -> dict:
"""Parse JSON configuration file."""
import json
try:
return json.loads(content)
except json.JSONDecodeError as e:
raise ConversionError(f"Invalid JSON in {filepath}: {str(e)}") from e
@staticmethod
def _parse_yaml(content: str, filepath: str) -> dict:
"""Parse YAML configuration file."""
import yaml
try:
return yaml.safe_load(content) or {}
except yaml.YAMLError as e:
raise ConversionError(f"Invalid YAML in {filepath}: {str(e)}") from e
@staticmethod
def _parse_toml(content: str, filepath: str) -> dict:
"""Parse TOML configuration file."""
import tomlkit
try:
return tomlkit.loads(content)
except tomlkit.exceptions.ParseError as e:
raise ConversionError(f"Invalid TOML in {filepath}: {str(e)}") from e
@staticmethod
def _parse_env(content: str, filepath: str) -> dict:
"""Parse ENV configuration file."""
result = {}
for line_num, line in enumerate(content.splitlines(), 1):
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, _, value = line.partition("=")
key = key.strip()
value = value.strip()
if not key:
continue
value = value.strip('"').strip("'")
result[key] = value
return result
@staticmethod
def _parse_ini(content: str, filepath: str) -> dict:
"""Parse INI configuration file."""
result = {}
current_section = "default"
for line_num, line in enumerate(content.splitlines(), 1):
line = line.strip()
if not line or line.startswith("#") or line.startswith(";"):
continue
if line.startswith("[") and line.endswith("]"):
current_section = line[1:-1].strip()
if current_section not in result:
result[current_section] = {}
elif "=" in line:
key, _, value = line.partition("=")
key = key.strip()
value = value.strip()
if current_section == "default":
result[key] = value
else:
result[current_section][key] = value
return result
def parse_file(filepath: str) -> dict:
"""Parse a configuration file and return its contents as a dictionary."""
return FileParser.parse(filepath)