266 lines
9.3 KiB
Python
266 lines
9.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
|
|
ETHNICITY_FILTER_CHOICES = [
|
|
"any",
|
|
"european",
|
|
"mediterranean_mena",
|
|
"latina",
|
|
"east_asian",
|
|
"southeast_asian",
|
|
"south_asian",
|
|
"black_african",
|
|
"indigenous",
|
|
"mixed",
|
|
"asian",
|
|
"white_asian",
|
|
"western_european",
|
|
"french_european",
|
|
"germanic_european",
|
|
"nordic_european",
|
|
"celtic_european",
|
|
"slavic_european",
|
|
"baltic_european",
|
|
"alpine_european",
|
|
"balkan_european",
|
|
"greek_mediterranean",
|
|
"italian_mediterranean",
|
|
"iberian_mediterranean",
|
|
]
|
|
ETHNICITY_LIST_KEYS = tuple(choice for choice in ETHNICITY_FILTER_CHOICES if choice != "any")
|
|
ETHNICITY_BASE_LIST_KEYS = (
|
|
"european",
|
|
"mediterranean_mena",
|
|
"latina",
|
|
"east_asian",
|
|
"southeast_asian",
|
|
"south_asian",
|
|
"black_african",
|
|
"indigenous",
|
|
"mixed",
|
|
)
|
|
EUROPEAN_REGIONAL_LIST_KEYS = (
|
|
"western_european",
|
|
"french_european",
|
|
"germanic_european",
|
|
"nordic_european",
|
|
"celtic_european",
|
|
"slavic_european",
|
|
"baltic_european",
|
|
"alpine_european",
|
|
"balkan_european",
|
|
)
|
|
MEDITERRANEAN_REGIONAL_LIST_KEYS = (
|
|
"greek_mediterranean",
|
|
"italian_mediterranean",
|
|
"iberian_mediterranean",
|
|
)
|
|
ETHNICITY_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
|
|
|
|
|
|
def ethnicity_text_from_value(value: Any) -> str:
|
|
if isinstance(value, dict):
|
|
return str(value.get("ethnicity") or "").strip()
|
|
text = str(value or "").strip()
|
|
if not text:
|
|
return ""
|
|
if text.startswith("{"):
|
|
try:
|
|
raw = json.loads(text)
|
|
except json.JSONDecodeError:
|
|
return text
|
|
if isinstance(raw, dict):
|
|
return str(raw.get("ethnicity") or "").strip()
|
|
return text
|
|
|
|
|
|
def is_valid_ethnicity_filter(value: Any) -> bool:
|
|
text = ethnicity_text_from_value(value)
|
|
return text == "any" or text in ETHNICITY_FILTER_CHOICES or "+" in text
|
|
|
|
|
|
def normalize_ethnicity_filter(value: Any, default: str = "any", allow_random: bool = False) -> str:
|
|
text = ethnicity_text_from_value(value)
|
|
if text.lower() in ETHNICITY_RANDOM_TOKENS:
|
|
return "random" if allow_random else default
|
|
return text if is_valid_ethnicity_filter(text) else default
|
|
|
|
|
|
def build_filter_config_json(
|
|
ethnicity: str = "any",
|
|
figure: str = "curvy",
|
|
no_plus_women: bool = False,
|
|
no_black: bool = False,
|
|
include_european: bool = True,
|
|
include_mediterranean_mena: bool = True,
|
|
include_latina: bool = True,
|
|
include_east_asian: bool = True,
|
|
include_southeast_asian: bool = True,
|
|
include_south_asian: bool = True,
|
|
include_black_african: bool = True,
|
|
include_indigenous: bool = True,
|
|
include_mixed: bool = True,
|
|
include_plus_size: bool = True,
|
|
) -> str:
|
|
include_flags = {
|
|
"european": include_european,
|
|
"mediterranean_mena": include_mediterranean_mena,
|
|
"latina": include_latina,
|
|
"east_asian": include_east_asian,
|
|
"southeast_asian": include_southeast_asian,
|
|
"south_asian": include_south_asian,
|
|
"black_african": include_black_african,
|
|
"indigenous": include_indigenous,
|
|
"mixed": include_mixed,
|
|
}
|
|
selected_ethnicities = [key for key, enabled in include_flags.items() if enabled]
|
|
disabled_ethnicities = [key for key, enabled in include_flags.items() if not enabled]
|
|
enabled_ethnicities = list(selected_ethnicities)
|
|
if enabled_ethnicities:
|
|
enabled_ethnicities.extend(f"exclude_{key}" for key in disabled_ethnicities)
|
|
if 0 < len(selected_ethnicities) < len(include_flags):
|
|
ethnicity = "+".join(enabled_ethnicities)
|
|
elif not is_valid_ethnicity_filter(ethnicity):
|
|
ethnicity = "any"
|
|
return json.dumps(
|
|
{
|
|
"ethnicity": ethnicity,
|
|
"ethnicity_includes": selected_ethnicities,
|
|
"figure": figure if figure in ("curvy", "balanced", "bombshell", "random") else "curvy",
|
|
"include_plus_size": bool(include_plus_size),
|
|
"include_black_african": bool(include_black_african),
|
|
"no_plus_women": not bool(include_plus_size) or bool(no_plus_women),
|
|
"no_black": not bool(include_black_african) or bool(no_black),
|
|
},
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
)
|
|
|
|
|
|
def build_ethnicity_list_json(
|
|
include_european: bool = False,
|
|
include_mediterranean_mena: bool = False,
|
|
include_latina: bool = False,
|
|
include_east_asian: bool = False,
|
|
include_southeast_asian: bool = False,
|
|
include_south_asian: bool = False,
|
|
include_black_african: bool = False,
|
|
include_indigenous: bool = False,
|
|
include_mixed: bool = False,
|
|
include_asian: bool = False,
|
|
include_white_asian: bool = False,
|
|
include_western_european: bool = False,
|
|
include_french_european: bool = False,
|
|
include_germanic_european: bool = False,
|
|
include_nordic_european: bool = False,
|
|
include_celtic_european: bool = False,
|
|
include_slavic_european: bool = False,
|
|
include_baltic_european: bool = False,
|
|
include_alpine_european: bool = False,
|
|
include_balkan_european: bool = False,
|
|
include_greek_mediterranean: bool = False,
|
|
include_italian_mediterranean: bool = False,
|
|
include_iberian_mediterranean: bool = False,
|
|
strict_excludes: bool = True,
|
|
) -> dict[str, str]:
|
|
include_flags = {
|
|
"european": include_european,
|
|
"mediterranean_mena": include_mediterranean_mena,
|
|
"latina": include_latina,
|
|
"east_asian": include_east_asian,
|
|
"southeast_asian": include_southeast_asian,
|
|
"south_asian": include_south_asian,
|
|
"black_african": include_black_african,
|
|
"indigenous": include_indigenous,
|
|
"mixed": include_mixed,
|
|
"asian": include_asian,
|
|
"white_asian": include_white_asian,
|
|
"western_european": include_western_european,
|
|
"french_european": include_french_european,
|
|
"germanic_european": include_germanic_european,
|
|
"nordic_european": include_nordic_european,
|
|
"celtic_european": include_celtic_european,
|
|
"slavic_european": include_slavic_european,
|
|
"baltic_european": include_baltic_european,
|
|
"alpine_european": include_alpine_european,
|
|
"balkan_european": include_balkan_european,
|
|
"greek_mediterranean": include_greek_mediterranean,
|
|
"italian_mediterranean": include_italian_mediterranean,
|
|
"iberian_mediterranean": include_iberian_mediterranean,
|
|
}
|
|
selected = [key for key in ETHNICITY_LIST_KEYS if include_flags.get(key)]
|
|
if not selected or set(selected) == set(ETHNICITY_LIST_KEYS):
|
|
ethnicity = "any"
|
|
else:
|
|
tokens = list(selected)
|
|
if strict_excludes:
|
|
protected: set[str] = set()
|
|
if "asian" in selected:
|
|
protected.update(("east_asian", "southeast_asian", "south_asian"))
|
|
if "white_asian" in selected:
|
|
protected.update(("european", "east_asian", "southeast_asian", "south_asian", "mixed"))
|
|
if any(key in selected for key in EUROPEAN_REGIONAL_LIST_KEYS):
|
|
protected.add("european")
|
|
if any(key in selected for key in MEDITERRANEAN_REGIONAL_LIST_KEYS):
|
|
protected.add("mediterranean_mena")
|
|
if "mixed" in selected:
|
|
protected.update(ETHNICITY_BASE_LIST_KEYS)
|
|
tokens.extend(
|
|
f"exclude_{key}"
|
|
for key in ETHNICITY_BASE_LIST_KEYS
|
|
if key not in selected and key not in protected
|
|
)
|
|
ethnicity = "+".join(tokens)
|
|
filter_config = {
|
|
"ethnicity": ethnicity,
|
|
"ethnicity_includes": selected,
|
|
}
|
|
summary = "any ethnicity" if ethnicity == "any" else "ethnicity list: " + ", ".join(selected)
|
|
return {
|
|
"ethnicity": ethnicity,
|
|
"filter_config": json.dumps(filter_config, ensure_ascii=True, sort_keys=True),
|
|
"summary": summary,
|
|
}
|
|
|
|
|
|
def parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
defaults = {
|
|
"ethnicity": "any",
|
|
"figure": "curvy",
|
|
"no_plus_women": False,
|
|
"no_black": False,
|
|
"include_plus_size": True,
|
|
"include_black_african": True,
|
|
}
|
|
if not filter_config:
|
|
return defaults
|
|
if isinstance(filter_config, dict):
|
|
raw = filter_config
|
|
else:
|
|
text = str(filter_config).strip()
|
|
if not text.startswith("{"):
|
|
raw = {"ethnicity": text}
|
|
else:
|
|
try:
|
|
raw = json.loads(text)
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid filter_config JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("filter_config must be a JSON object")
|
|
parsed = {**defaults, **raw}
|
|
parsed["ethnicity"] = normalize_ethnicity_filter(parsed.get("ethnicity"), "any")
|
|
parsed["figure"] = parsed["figure"] if parsed.get("figure") in ("curvy", "balanced", "bombshell", "random") else "curvy"
|
|
parsed["include_plus_size"] = bool(parsed.get("include_plus_size"))
|
|
parsed["include_black_african"] = bool(parsed.get("include_black_african"))
|
|
parsed["no_plus_women"] = bool(parsed.get("no_plus_women"))
|
|
parsed["no_black"] = bool(parsed.get("no_black"))
|
|
return parsed
|
|
|
|
|
|
_ethnicity_text_from_value = ethnicity_text_from_value
|
|
_is_valid_ethnicity_filter = is_valid_ethnicity_filter
|
|
_parse_filter_config = parse_filter_config
|