refactor(vault_utils): replace search_simple with search_by_filename and clean unused imports

This commit is contained in:
dimitrievgs 2026-07-29 22:42:29 +03:00
parent d030d87ead
commit 0ab91a6b94
8 changed files with 127 additions and 27 deletions

View File

@ -22,3 +22,15 @@
- [x] Расширить тесты логики ходов фракций в `test_wwn_mechanics.py`.
- [x] Добавлены setup-тесты для перезаписи тестовых файлов фракций (frontmatter и контент) для детерминированности среды.
- [x] Убедиться, что текущий набор pytest проходит без ошибок (`pytest -v`).
# План работ и задачи
## Исправление ошибок импорта фракций
- [x] Удалить импорт `vault_query` в `D:/Work/Software_Development/Projects/ttrpg-obsidian-vault/scripts/faction_turns/resolve_faction_turn.py`
- [x] Удалить импорт `vault_query` в `D:/Work/Software_Development/Projects/ttrpg-obsidian-vault/scripts/faction_turns/propose_faction_turn.py`
- [x] Заменить вызов `vault_query` на прямые вызовы функций в `D:/Work/Software_Development/Projects/ttrpg-obsidian-vault/scripts/faction_turns/tests/verify_factions.py`
## Исправления логики фракций
- [x] Починить логику `Repair Asset` для ремонта `Base of Influence` (цель может не иметь точного совпадения имени) в файле `resolve_faction_turn.py`
- [x] Сделать генерацию названий файлов событий (Event files) более читаемой, убрать длинные подчеркивания и дефисы.
- [✓] Добавить тесты исцеления Base of Influence для обычной и мощной фракций (D:/Work/Software_Development/Projects/ttrpg-obsidian-vault/scripts/faction_turns/tests/test_repair_boi.py)
- [✓] Запустить и проверить прохождение тестов `pytest`

View File

@ -38,9 +38,10 @@ scripts_path = Path(__file__).resolve().parent.parent
if str(scripts_path) not in sys.path:
sys.path.insert(0, str(scripts_path))
# Важно! не удалять неиспользуемые импорты - они используются в propose/resolve_faction_trun
from vault_utils import (
get_file,
search_simple,
search_by_filename,
search_jsonlogic,
list_directory,
get_recent_files,

View File

@ -33,7 +33,6 @@ except ImportError:
from faction_utils import (
WWN_MILES_PER_TURN,
get_travel_distance,
vault_query,
get_all_factions,
get_factions_in_location,
get_assets_in_location,
@ -42,7 +41,7 @@ from faction_utils import (
roll_dice,
roll_attribute_check,
are_within_one_move,
write_faction,
write_faction,
)
from wwn_assets_data import (
get_available_assets,
@ -76,7 +75,10 @@ class FactionTurnProposalRequest(TypedDict, total=False):
Поля:
location_wikilink: Имя локации на русском языке в формате Obsidian wikilink.
Обязательное поле.
Пример: "[[Уотердип]]", "[[Невервинтер]]", название локации на русском.
КРИТИЧЕСКИ ВАЖНО: Копируй название буква в букву из запроса,
СТРОГО в именительном падеже. ЗАПРЕЩЕНО склонять, менять
окончания, переводить или придумывать буквы.
Пример: "[[Даггерфорд]]", "[[Калимшан]]", название локации на русском.
factions_folder: Путь к папке с файлами фракций внутри vault.
По умолчанию: "Фракции".
@ -565,7 +567,7 @@ def _format_faction_section(
return (
f"### {idx}. `{name}` | Initiative: {initiative} | {state_label.upper()}\n"
f"**Файл:** `{name}.md`\n" # Добавьте эту строку
f"<use-this-faction_file-name>faction_file=={name}.md</use-this-faction_file-name>\n" # Добавьте эту строку
f"**Для resolve:** faction_file: \"{name}\""
f"**Статы:** {status_str}"
f"{tags_str}"

View File

@ -46,7 +46,7 @@ except ImportError:
from typing import TypedDict
from faction_utils import (
vault_query,
get_file,
get_all_factions,
write_faction,
create_event_file,
@ -194,8 +194,9 @@ def _load_faction_file(faction_file: str, factions_folder: str) -> tuple[dict, s
if not path:
path = f"{factions_folder}/{faction_file}.md"
data = vault_query("get_frontmatter", {"path": path})
if "error" in data:
try:
data = get_file(path)
except RuntimeError:
raise ValueError(
f"Фракция не найдена: '{faction_file}'. "
f"Obsidian не смог найти путь автоматически, а по пути '{path}' файла нет."
@ -286,16 +287,21 @@ def _apply_damage_to_boi(
def _make_event_filename(game_date: str, faction_file: str, action_type: str) -> str:
"""
Генерирует имя файла события.
Генерирует читаемое имя файла события.
Формат: YYYY-MM-DD_{faction}_{action}.md
Если game_date не в ISO формате использует реальную дату.
Формат: Date_Faction_Action.md
"""
safe_date = re.sub(r'[^\d\-]', '-', game_date)[:10]
if len(safe_date) < 10:
safe_date = date.today().isoformat()
safe_faction = re.sub(r'[^a-zA-Z0-9_]', '_', faction_file)
safe_action = re.sub(r'[^a-zA-Z0-9_]', '_', action_type)
# Извлекаем все буквы и цифры из даты, объединяем через дефис (например: 15-Mirtul-1492-DR)
safe_date_parts = re.findall(r'[a-zA-Zа-яА-Я0-9]+', game_date)
safe_date = "-".join(safe_date_parts) if safe_date_parts else date.today().isoformat()
# Заменяем все не-буквенно-цифровые символы на подчеркивания
safe_faction = re.sub(r'[^a-zA-Zа-яА-Я0-9]', '_', faction_file)
safe_faction = re.sub(r'_+', '_', safe_faction).strip('_')
safe_action = re.sub(r'[^a-zA-Zа-яА-Я0-9]', '_', action_type)
safe_action = re.sub(r'_+', '_', safe_action).strip('_')
return f"{safe_date}_{safe_faction}_{safe_action}.md"
@ -726,7 +732,21 @@ def _resolve_repair_asset(
result["hp_after"] = faction_fm["hp"]
result["treasure_spent"] = cost
else:
asset = _find_asset_in_faction(faction_fm, repair_target)
# Проверяем, является ли цель Base of Influence
asset = None
if repair_target.startswith("Base of Influence"):
location_from_action = action.get("location", "")
if location_from_action:
asset = _find_boi_in_faction(faction_fm, location_from_action)
if not asset:
for a in faction_fm.get("assets", []):
if a.get("is_base_of_influence") and (repair_target in a.get("name", "") or a.get("name", "").startswith("Base of Influence")):
asset = a
break
else:
asset = _find_asset_in_faction(faction_fm, repair_target)
if not asset:
result["error"] = f"Актив '{repair_target}' не найден"
return result

View File

@ -0,0 +1,48 @@
import sys
from pathlib import Path
# Добавляем путь к scripts и faction_turns
scripts_path = Path(__file__).resolve().parent.parent.parent
faction_turns_path = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(scripts_path))
sys.path.insert(0, str(faction_turns_path))
from resolve_faction_turn import _resolve_repair_asset
def test_repair_boi_powerful():
print("--- Тест Repair Asset для мощной фракции ---")
faction_fm = {
"name": "Powerful Faction",
"treasure": 10,
"force": 6,
"assets": [
{
"name": "Base of Influence — [[Лускан]]",
"is_base_of_influence": True,
"hp": 5,
"hp_max": 20,
"location": "[[Лускан]]"
}
]
}
action = {
"faction_file": "Powerful_Faction",
"action_type": "Repair Asset",
"location": "[[Лускан]]",
"repair_asset": "Base of Influence"
}
result = _resolve_repair_asset(action, faction_fm, 1)
print(f"Результат: {result}")
assert "error" not in result, f"Ошибка: {result.get('error')}"
assert result["healed"] == 3, f"Ожидали лечение на 3 (ceil(Force 6 / 2)), получили {result['healed']}"
assert result["hp_after"] == 8, f"Ожидали HP 8, получили {result['hp_after']}"
print("\nТест мощной фракции успешно пройден!")
if __name__ == "__main__":
test_repair_boi_powerful()

View File

@ -11,7 +11,7 @@ import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from vault_utils import (
get_file, write_file, create_file, patch_frontmatter, search_simple, list_directory,
get_file, write_file, create_file, patch_frontmatter, search_by_filename, list_directory,
normalize_wikilink, resolve_wikilink
)
@ -36,9 +36,9 @@ def test_list_directory():
def test_search_simple():
"""Проверяет простой поиск по имени файла (POST)."""
result = search_simple("Test_Vault_Utils")
result = search_by_filename("Test_Vault_Utils")
assert len(result) > 0
assert result[0]["filename"] == TEST_FILE_PATH
assert result[0] == TEST_FILE_PATH
def test_get_file():
"""Проверяет чтение контента и парсинг frontmatter (GET)."""

View File

@ -3,14 +3,14 @@ from pathlib import Path
# Добавляем путь к scripts, чтобы импортировать vault_utils
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from vault_utils import vault_query
from faction_utils import get_all_factions
# Проверяем работу get_all_factions
folder = "Фракции/Tests"
print(f"--- Проверка загрузки для папки: {folder} ---")
# Прямой вызов get_all_factions через vault_query
result = vault_query("get_all_factions", {"folder": folder})
# Вызов функции get_all_factions
result = get_all_factions(folder)
print("Результат:", result)

View File

@ -86,7 +86,24 @@ def patch_frontmatter(path: str, target: str, value: Any, operation: str = "repl
resp = requests.patch(f"{BASE_URL}/vault/{encoded_path}", headers=headers, data=val.encode('utf-8'), **_get_req_kwargs())
return resp.status_code in [200, 204]
def search_simple(query: str) -> list[dict]:
def search_by_filename(filename_query: str) -> list[str]:
"""
Выполняет поиск в Obsidian строго по названию файла (или части пути).
Возвращает список путей к найденным файлам.
"""
# Формируем JsonLogic запрос, который вернет true, если filename_query есть в пути
query = {
"glob": [f"*{filename_query}*.md", {"var": "path"}]
}
# Используем вашу готовую функцию для отправки JsonLogic запроса
raw_results = search_jsonlogic(query)
# Эндпоинт возвращает список словарей вида [{"filename": "...", "result": true}]
# Отфильтруем и вернем только пути (имена файлов)
return [item.get("filename") for item in raw_results if item.get("filename")]
'''def search_simple(query: str) -> list[dict]:
"""Выполняет простой текстовый поиск в Obsidian."""
resp = requests.post(
f"{BASE_URL}/search/simple/",
@ -96,7 +113,7 @@ def search_simple(query: str) -> list[dict]:
)
if resp.status_code == 200:
return resp.json()
return []
return []'''
def search_jsonlogic(query: dict) -> list[dict]:
"""Выполняет поиск по правилам JsonLogic в Obsidian."""
@ -130,8 +147,8 @@ def normalize_wikilink(s: str) -> str:
def resolve_wikilink(name: str) -> Optional[str]:
"""Ищет путь к файлу по имени wikilink через простой поиск."""
original_clean = re.sub(r'^\[\[|\]\]$', '', name).split('|')[0].strip()
results = search_simple(original_clean)
return results[0].get("filename") if results else None
results = search_by_filename(original_clean)
return results[0] if results else None
def get_recent_files(