mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 10:31:50 +01:00
feat: add type safe Config model
This commit is contained in:
committed by
Sebastian Thomschke
parent
e7a3d46d25
commit
1369da1c34
144
src/kleinanzeigen_bot/model/config_model.py
Normal file
144
src/kleinanzeigen_bot/model/config_model.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import Any, Dict, List, Literal
|
||||
|
||||
from pydantic import Field, model_validator, validator
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from kleinanzeigen_bot.utils import dicts
|
||||
from kleinanzeigen_bot.utils.pydantics import ContextualModel
|
||||
|
||||
|
||||
class ContactDefaults(ContextualModel):
|
||||
name:str | None = None
|
||||
street:str | None = None
|
||||
zipcode:int | str | None = None
|
||||
phone:str | None = None
|
||||
|
||||
|
||||
@deprecated("Use description_prefix/description_suffix instead")
|
||||
class DescriptionAffixes(ContextualModel):
|
||||
prefix:str | None = None
|
||||
suffix:str | None = None
|
||||
|
||||
|
||||
class AdDefaults(ContextualModel):
|
||||
active:bool = True
|
||||
type:Literal["OFFER", "WANTED"] = "OFFER"
|
||||
description:DescriptionAffixes | None = None
|
||||
description_prefix:str | None = Field(default = None, description = "prefix for the ad description")
|
||||
description_suffix:str | None = Field(default = None, description = " suffix for the ad description")
|
||||
price_type:Literal["FIXED", "NEGOTIABLE", "GIVE_AWAY", "NOT_APPLICABLE"] = "NEGOTIABLE"
|
||||
shipping_type:Literal["PICKUP", "SHIPPING", "NOT_APPLICABLE"] = "SHIPPING"
|
||||
sell_directly:bool = Field(default = False, description = "requires shipping_type SHIPPING to take effect")
|
||||
contact:ContactDefaults = Field(default_factory = ContactDefaults)
|
||||
republication_interval:int = 7
|
||||
|
||||
@model_validator(mode = "before")
|
||||
@classmethod
|
||||
def unify_description(cls, values:Dict[str, Any]) -> Dict[str, Any]:
|
||||
# Ensure flat prefix/suffix take precedence over deprecated nested "description"
|
||||
desc = values.get("description")
|
||||
flat_prefix = values.get("description_prefix")
|
||||
flat_suffix = values.get("description_suffix")
|
||||
|
||||
if not flat_prefix and isinstance(desc, dict) and desc.get("prefix") is not None:
|
||||
values["description_prefix"] = desc.get("prefix", "")
|
||||
if not flat_suffix and isinstance(desc, dict) and desc.get("suffix") is not None:
|
||||
values["description_suffix"] = desc.get("suffix", "")
|
||||
return values
|
||||
|
||||
|
||||
class DownloadConfig(ContextualModel):
|
||||
include_all_matching_shipping_options:bool = Field(
|
||||
default = False,
|
||||
description = "if true, all shipping options matching the package size will be included"
|
||||
)
|
||||
excluded_shipping_options:List[str] = Field(
|
||||
default_factory = list,
|
||||
description = "list of shipping options to exclude, e.g. ['DHL_2', 'DHL_5']"
|
||||
)
|
||||
|
||||
|
||||
class BrowserConfig(ContextualModel):
|
||||
arguments:List[str] = Field(
|
||||
default_factory = list,
|
||||
description = "See https://peter.sh/experiments/chromium-command-line-switches/"
|
||||
)
|
||||
binary_location:str | None = Field(
|
||||
default = None,
|
||||
description = "path to custom browser executable, if not specified will be looked up on PATH"
|
||||
)
|
||||
extensions:List[str] = Field(
|
||||
default_factory = list,
|
||||
description = "a list of .crx extension files to be loaded"
|
||||
)
|
||||
use_private_window:bool = True
|
||||
user_data_dir:str | None = Field(
|
||||
default = None,
|
||||
description = "See https://github.com/chromium/chromium/blob/main/docs/user_data_dir.md"
|
||||
)
|
||||
profile_name:str | None = None
|
||||
|
||||
|
||||
class LoginConfig(ContextualModel):
|
||||
username:str = Field(..., min_length = 1)
|
||||
password:str = Field(..., min_length = 1)
|
||||
|
||||
|
||||
class PublishingConfig(ContextualModel):
|
||||
delete_old_ads:Literal["BEFORE_PUBLISH", "AFTER_PUBLISH", "NEVER"] | None = "AFTER_PUBLISH"
|
||||
delete_old_ads_by_title:bool = Field(default = True, description = "only works if delete_old_ads is set to BEFORE_PUBLISH")
|
||||
|
||||
|
||||
class CaptchaConfig(ContextualModel):
|
||||
auto_restart:bool = False
|
||||
restart_delay:str = "6h"
|
||||
|
||||
|
||||
class Config(ContextualModel):
|
||||
ad_files:List[str] = Field(
|
||||
default_factory = lambda: ["./**/ad_*.{json,yml,yaml}"],
|
||||
min_items = 1,
|
||||
description = """
|
||||
glob (wildcard) patterns to select ad configuration files
|
||||
if relative paths are specified, then they are relative to this configuration file
|
||||
"""
|
||||
) # type: ignore[call-overload]
|
||||
|
||||
ad_defaults:AdDefaults = Field(
|
||||
default_factory = AdDefaults,
|
||||
description = "Default values for ads, can be overwritten in each ad configuration file"
|
||||
)
|
||||
|
||||
categories:Dict[str, str] = Field(default_factory = dict, description = """
|
||||
additional name to category ID mappings, see default list at
|
||||
https://github.com/Second-Hand-Friends/kleinanzeigen-bot/blob/main/src/kleinanzeigen_bot/resources/categories.yaml
|
||||
|
||||
Example:
|
||||
categories:
|
||||
Elektronik > Notebooks: 161/278
|
||||
Jobs > Praktika: 102/125
|
||||
""")
|
||||
|
||||
download:DownloadConfig = Field(default_factory = DownloadConfig)
|
||||
publishing:PublishingConfig = Field(default_factory = PublishingConfig)
|
||||
browser:BrowserConfig = Field(default_factory = BrowserConfig, description = "Browser configuration")
|
||||
login:LoginConfig = Field(default_factory = LoginConfig.model_construct, description = "Login credentials")
|
||||
captcha:CaptchaConfig = Field(default_factory = CaptchaConfig)
|
||||
|
||||
def with_values(self, values:dict[str, Any]) -> Config:
|
||||
return Config.model_validate(
|
||||
dicts.apply_defaults(copy.deepcopy(values), defaults = self.model_dump())
|
||||
)
|
||||
|
||||
@validator("ad_files", each_item = True)
|
||||
@classmethod
|
||||
def _non_empty_glob_pattern(cls, v:str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("ad_files entries must be non-empty glob patterns")
|
||||
return v
|
||||
Reference in New Issue
Block a user