mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-12 15:50:01 -08:00
fix: config update logic
This commit is contained in:
@@ -2,6 +2,7 @@ import click
|
||||
from click.core import ParameterSource
|
||||
|
||||
from .. import __version__
|
||||
from ..core.constants import APP_NAME
|
||||
from .config import AppConfig, ConfigLoader
|
||||
from .constants import USER_CONFIG_PATH
|
||||
from .options import options_from_model
|
||||
@@ -15,7 +16,12 @@ commands = {
|
||||
|
||||
@click.version_option(__version__, "--version")
|
||||
@click.option("--no-config", is_flag=True, help="Don't load the user config file.")
|
||||
@click.group(cls=LazyGroup, root="fastanime.cli.commands", lazy_subcommands=commands)
|
||||
@click.group(
|
||||
cls=LazyGroup,
|
||||
root="fastanime.cli.commands",
|
||||
lazy_subcommands=commands,
|
||||
context_settings=dict(auto_envvar_prefix=APP_NAME),
|
||||
)
|
||||
@options_from_model(AppConfig)
|
||||
@click.pass_context
|
||||
def cli(ctx: click.Context, no_config: bool, **kwargs):
|
||||
@@ -34,7 +40,10 @@ def cli(ctx: click.Context, no_config: bool, **kwargs):
|
||||
# update app config with command line parameters
|
||||
for param_name, param_value in ctx.params.items():
|
||||
source = ctx.get_parameter_source(param_name)
|
||||
if source == ParameterSource.COMMANDLINE:
|
||||
if (
|
||||
source == ParameterSource.ENVIRONMENT
|
||||
or source == ParameterSource.COMMANDLINE
|
||||
):
|
||||
parameter = None
|
||||
for param in ctx.command.params:
|
||||
if param.name == param_name:
|
||||
|
||||
@@ -21,25 +21,27 @@ CONFIG_HEADER = f"""
|
||||
def generate_config_ini_from_app_model(app_model: AppConfig) -> str:
|
||||
"""Generate a configuration file content from a Pydantic model."""
|
||||
|
||||
model_schema = AppConfig.model_json_schema()
|
||||
|
||||
model_schema = AppConfig.model_json_schema(mode="serialization")
|
||||
app_model_dict = app_model.model_dump()
|
||||
config_ini_content = [CONFIG_HEADER]
|
||||
|
||||
for section_name, section_model in app_model:
|
||||
section_class_name = model_schema["properties"][section_name]["$ref"].split(
|
||||
"/"
|
||||
)[-1]
|
||||
section_comment = model_schema["$defs"][section_class_name]["description"]
|
||||
for section_name, section_dict in app_model_dict.items():
|
||||
section_ref = model_schema["properties"][section_name].get("$ref")
|
||||
if not section_ref:
|
||||
continue
|
||||
|
||||
section_class_name = section_ref.split("/")[-1]
|
||||
section_schema = model_schema["$defs"][section_class_name]
|
||||
section_comment = section_schema.get("description", "")
|
||||
|
||||
config_ini_content.append(f"\n#\n# {section_comment}\n#")
|
||||
config_ini_content.append(f"[{section_name}]")
|
||||
|
||||
for field_name, field_value in section_model:
|
||||
description = model_schema["$defs"][section_class_name]["properties"][
|
||||
field_name
|
||||
].get("description", "")
|
||||
for field_name, field_value in section_dict.items():
|
||||
field_properties = section_schema.get("properties", {}).get(field_name, {})
|
||||
description = field_properties.get("description", "")
|
||||
|
||||
if description:
|
||||
# Wrap long comments for better readability in the .ini file
|
||||
wrapped_comment = textwrap.fill(
|
||||
description,
|
||||
width=78,
|
||||
@@ -58,4 +60,5 @@ def generate_config_ini_from_app_model(app_model: AppConfig) -> str:
|
||||
value_str = str(field_value)
|
||||
|
||||
config_ini_content.append(f"{field_name} = {value_str}")
|
||||
|
||||
return "\n".join(config_ini_content)
|
||||
|
||||
@@ -78,7 +78,6 @@ class ConfigLoader:
|
||||
section: dict(self.parser.items(section))
|
||||
for section in self.parser.sections()
|
||||
}
|
||||
|
||||
try:
|
||||
app_config = AppConfig.model_validate(config_dict)
|
||||
return app_config
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic import BaseModel, Field, PrivateAttr, computed_field, field_validator
|
||||
|
||||
from ...core.constants import (
|
||||
FZF_DEFAULT_OPTS,
|
||||
@@ -23,23 +23,11 @@ class OtherConfig(BaseModel):
|
||||
class FzfConfig(OtherConfig):
|
||||
"""Configuration specific to the FZF selector."""
|
||||
|
||||
opts: str = Field(
|
||||
default_factory=lambda: "\n"
|
||||
+ "\n".join(
|
||||
[
|
||||
f"\t{line}"
|
||||
for line in FZF_DEFAULT_OPTS.read_text(encoding="utf-8").split()
|
||||
]
|
||||
),
|
||||
description="Command-line options to pass to FZF for theming and behavior.",
|
||||
)
|
||||
_opts: str = PrivateAttr(default=FZF_DEFAULT_OPTS.read_text(encoding="utf-8"))
|
||||
header_color: str = Field(
|
||||
default="95,135,175", description="RGB color for the main TUI header."
|
||||
)
|
||||
header_ascii_art: str = Field(
|
||||
default="\n" + "\n".join([f"\t{line}" for line in APP_ASCII_ART.split("\n")]),
|
||||
description="The ASCII art to display in TUI headers.",
|
||||
)
|
||||
_header_ascii_art: str = PrivateAttr(default=APP_ASCII_ART)
|
||||
preview_header_color: str = Field(
|
||||
default="215,0,95", description="RGB color for preview pane headers."
|
||||
)
|
||||
@@ -47,6 +35,32 @@ class FzfConfig(OtherConfig):
|
||||
default="208,208,208", description="RGB color for preview pane separators."
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
opts = kwargs.pop("opts", None)
|
||||
header_ascii_art = kwargs.pop("header_ascii_art", None)
|
||||
|
||||
super().__init__(**kwargs)
|
||||
if opts:
|
||||
self._opts = opts
|
||||
if header_ascii_art:
|
||||
self._header_ascii_art = header_ascii_art
|
||||
|
||||
@computed_field(
|
||||
description="The FZF options, formatted with leading tabs for the config file."
|
||||
)
|
||||
@property
|
||||
def opts(self) -> str:
|
||||
return "\n" + "\n".join([f"\t{line}" for line in self._opts.split()])
|
||||
|
||||
@computed_field(
|
||||
description="The ASCII art to display as a header in the FZF interface."
|
||||
)
|
||||
@property
|
||||
def header_ascii_art(self) -> str:
|
||||
return "\n" + "\n".join(
|
||||
[f"\t{line}" for line in self._header_ascii_art.split()]
|
||||
)
|
||||
|
||||
|
||||
class RofiConfig(OtherConfig):
|
||||
"""Configuration specific to the Rofi selector."""
|
||||
@@ -203,7 +217,7 @@ class StreamConfig(BaseModel):
|
||||
default="sub", description="Preferred audio/subtitle language type."
|
||||
)
|
||||
server: str = Field(
|
||||
default="top",
|
||||
default="TOP",
|
||||
description="The default server to use from a provider. 'top' uses the first available.",
|
||||
examples=SERVERS_AVAILABLE,
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ from pydantic_core import PydanticUndefined
|
||||
|
||||
from .config.model import OtherConfig
|
||||
|
||||
# Mapping from Python/Pydantic types to Click types
|
||||
TYPE_MAP = {
|
||||
str: click.STRING,
|
||||
int: click.INT,
|
||||
@@ -49,38 +48,29 @@ def options_from_model(model: type[BaseModel], parent_name: str = "") -> Callabl
|
||||
"""
|
||||
decorators = []
|
||||
|
||||
# Check if this model inherits from ExternalTool
|
||||
is_external_tool = issubclass(model, OtherConfig)
|
||||
model_name = model.__name__.lower().replace("config", "")
|
||||
|
||||
# Introspect the model's fields
|
||||
for field_name, field_info in model.model_fields.items():
|
||||
# Handle nested models by calling this function recursively
|
||||
if isinstance(field_info.annotation, type) and issubclass(
|
||||
field_info.annotation, BaseModel
|
||||
):
|
||||
# Apply decorators from the nested model with current model as parent
|
||||
nested_decorators = options_from_model(field_info.annotation, field_name)
|
||||
nested_decorator_list = getattr(nested_decorators, "decorators", [])
|
||||
decorators.extend(nested_decorator_list)
|
||||
continue
|
||||
|
||||
# Determine the option name for the CLI
|
||||
if is_external_tool:
|
||||
# For ExternalTool subclasses, use --model_name-field_name format
|
||||
cli_name = f"--{model_name}-{field_name.replace('_', '-')}"
|
||||
else:
|
||||
cli_name = f"--{field_name.replace('_', '-')}"
|
||||
|
||||
# Build the arguments for the click.option decorator
|
||||
kwargs = {
|
||||
"type": _get_click_type(field_info),
|
||||
"help": field_info.description or "",
|
||||
}
|
||||
|
||||
# Handle boolean flags (e.g., --foo/--no-foo)
|
||||
if field_info.annotation is bool:
|
||||
# Set default value for boolean flags
|
||||
if field_info.default is not PydanticUndefined:
|
||||
kwargs["default"] = field_info.default
|
||||
kwargs["show_default"] = True
|
||||
@@ -89,9 +79,7 @@ def options_from_model(model: type[BaseModel], parent_name: str = "") -> Callabl
|
||||
f"{cli_name}/--no-{model_name}-{field_name.replace('_', '-')}"
|
||||
)
|
||||
else:
|
||||
# For non-external tools, we use the --no- prefix directly
|
||||
cli_name = f"{cli_name}/--no-{field_name.replace('_', '-')}"
|
||||
# For other types, set default if one is provided in the model
|
||||
elif field_info.default is not PydanticUndefined:
|
||||
kwargs["default"] = field_info.default
|
||||
kwargs["show_default"] = True
|
||||
@@ -106,6 +94,27 @@ def options_from_model(model: type[BaseModel], parent_name: str = "") -> Callabl
|
||||
)
|
||||
)
|
||||
|
||||
for field_name, computed_field_info in model.model_computed_fields.items():
|
||||
if is_external_tool:
|
||||
cli_name = f"--{model_name}-{field_name.replace('_', '-')}"
|
||||
else:
|
||||
cli_name = f"--{field_name.replace('_', '-')}"
|
||||
|
||||
kwargs = {
|
||||
"type": TYPE_MAP[computed_field_info.return_type],
|
||||
"help": computed_field_info.description or "",
|
||||
}
|
||||
|
||||
decorators.append(
|
||||
click.option(
|
||||
cli_name,
|
||||
cls=ConfigOption,
|
||||
model_name=model_name,
|
||||
field_name=field_name,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
def decorator(f: Callable) -> Callable:
|
||||
# Apply the decorators in reverse order to the function
|
||||
for deco in reversed(decorators):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from importlib import resources
|
||||
|
||||
APP_NAME = os.environ.get("FASTANIME_APPNAME", "fastanime")
|
||||
APP_NAME = os.environ.get("FASTANIME_APP_NAME", "fastanime")
|
||||
|
||||
try:
|
||||
pkg = resources.files("fastanime")
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
"""
|
||||
his module contains an abstraction for interaction with the anilist api making it easy and efficient
|
||||
"""
|
||||
|
||||
from .api import AniListApi
|
||||
|
||||
@@ -28,7 +28,7 @@ MP4_SERVER_JUICY_STREAM_REGEX = re.compile(
|
||||
)
|
||||
|
||||
# graphql files
|
||||
GQLS = resources.files("fastanime.libs.anime_provider.allanime")
|
||||
GQLS = resources.files("fastanime.libs.providers.anime.allanime") / "queries"
|
||||
SEARCH_GQL = Path(str(GQLS / "search.gql"))
|
||||
ANIME_GQL = Path(str(GQLS / "anime.gql"))
|
||||
EPISODE_GQL = Path(str(GQLS / "episode.gql"))
|
||||
|
||||
Reference in New Issue
Block a user