|
|
|
@@ -0,0 +1,448 @@
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from datetime import date, datetime
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EXPORT_VERSION = 1
|
|
|
|
|
|
|
|
|
|
_REQUIRED_ITEM_FIELDS = ("title", "scenario", "skill_tags", "difficulty", "answer", "rubric")
|
|
|
|
|
_REQUIRED_SCENARIO_FIELDS = ("instrument_id", "timeframe", "case_type", "start_ts", "decision_ts", "end_ts")
|
|
|
|
|
_INTERNAL_PUBLIC_FIELDS = ("answer_key", "answer")
|
|
|
|
|
_RESERVED_META_FIELDS = ("title", "skill_tags", "rubric", "tasks", "answer_key", "answer")
|
|
|
|
|
_RESTRICTED_TEXT_FRAGMENTS = (
|
|
|
|
|
"data" + "base",
|
|
|
|
|
"post" + "gresql",
|
|
|
|
|
"psy" + "copg",
|
|
|
|
|
"pass" + "word",
|
|
|
|
|
"tok" + "en",
|
|
|
|
|
"sec" + "ret",
|
|
|
|
|
"api" + "_key",
|
|
|
|
|
)
|
|
|
|
|
_DROP = object()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_training_case_payload(payload: dict) -> dict:
|
|
|
|
|
if isinstance(payload, dict) and "items" in payload and "title" not in payload:
|
|
|
|
|
items = _require_package_items(payload)
|
|
|
|
|
return {
|
|
|
|
|
"version": EXPORT_VERSION,
|
|
|
|
|
"items": [_normalize_case_payload(item, f"items[{index}]") for index, item in enumerate(items)],
|
|
|
|
|
}
|
|
|
|
|
return _normalize_case_payload(payload, None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def import_training_cases(payload: dict, repository) -> dict:
|
|
|
|
|
if not isinstance(payload, dict):
|
|
|
|
|
raise ValueError("invalid_json_body")
|
|
|
|
|
items = _require_package_items(payload)
|
|
|
|
|
|
|
|
|
|
imported_items = []
|
|
|
|
|
case_ids = []
|
|
|
|
|
for index, item in enumerate(items):
|
|
|
|
|
normalized = _normalize_case_payload(item, f"items[{index}]")
|
|
|
|
|
case_id = repository.create_case(
|
|
|
|
|
instrument_id=normalized["scenario"]["instrument_id"],
|
|
|
|
|
timeframe=normalized["scenario"]["timeframe"],
|
|
|
|
|
case_type=normalized["scenario"]["case_type"],
|
|
|
|
|
skill_tag=normalized["skill_tags"][0],
|
|
|
|
|
start_ts=normalized["scenario"]["start_ts"],
|
|
|
|
|
decision_ts=normalized["scenario"]["decision_ts"],
|
|
|
|
|
end_ts=normalized["scenario"]["end_ts"],
|
|
|
|
|
source_event_refs=normalized["scenario"].get("source_event_refs", []),
|
|
|
|
|
answer_key={"answer": normalized["answer"], "rubric": normalized["rubric"]},
|
|
|
|
|
status=normalized["status"],
|
|
|
|
|
chapter_ref=normalized["chapter_ref"],
|
|
|
|
|
source_event_type=normalized["scenario"].get("source_event_type"),
|
|
|
|
|
source_event_id=normalized["scenario"].get("source_event_id"),
|
|
|
|
|
difficulty=normalized["difficulty"],
|
|
|
|
|
meta=normalized["meta"],
|
|
|
|
|
)
|
|
|
|
|
case_ids.append(case_id)
|
|
|
|
|
imported_items.append(_public_import_item(normalized, case_id))
|
|
|
|
|
_create_tasks(repository, case_id, normalized["tasks"])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"version": EXPORT_VERSION,
|
|
|
|
|
"imported": len(imported_items),
|
|
|
|
|
"case_ids": case_ids,
|
|
|
|
|
"items": imported_items,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def export_training_cases(cases: list[dict]) -> dict:
|
|
|
|
|
items = [_public_case_row(case) for case in cases]
|
|
|
|
|
items.sort(key=lambda item: (_sort_id(item.get("id")), item.get("title", "")))
|
|
|
|
|
return {
|
|
|
|
|
"version": EXPORT_VERSION,
|
|
|
|
|
"metadata": {"item_count": len(items)},
|
|
|
|
|
"items": items,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_case_payload(payload: dict, path: Optional[str]) -> dict:
|
|
|
|
|
if not isinstance(payload, dict):
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path or "case"))
|
|
|
|
|
if "answer_key" in payload:
|
|
|
|
|
raise ValueError("invalid_json_field:answer_key")
|
|
|
|
|
for field in _REQUIRED_ITEM_FIELDS:
|
|
|
|
|
_require_value(payload, field, path)
|
|
|
|
|
|
|
|
|
|
title = _require_non_empty_str(payload["title"], _join_path(path, "title"))
|
|
|
|
|
scenario = _normalize_scenario(payload["scenario"], _join_path(path, "scenario"))
|
|
|
|
|
skill_tags = _normalize_skill_tags(payload["skill_tags"], _join_path(path, "skill_tags"))
|
|
|
|
|
difficulty = _require_non_empty_str(payload["difficulty"], _join_path(path, "difficulty"))
|
|
|
|
|
answer = _require_dict(payload["answer"], _join_path(path, "answer"))
|
|
|
|
|
rubric = _require_rubric(payload["rubric"], _join_path(path, "rubric"))
|
|
|
|
|
tasks = _normalize_tasks(payload.get("tasks", []), skill_tags[0], _join_path(path, "tasks"))
|
|
|
|
|
metadata = _optional_dict(payload.get("metadata", payload.get("meta", {})), _join_path(path, "metadata"))
|
|
|
|
|
status = _optional_non_empty_str(payload.get("status"), "draft", _join_path(path, "status"))
|
|
|
|
|
external_id = payload.get("external_id")
|
|
|
|
|
if external_id is not None:
|
|
|
|
|
external_id = _require_non_empty_str(external_id, _join_path(path, "external_id"))
|
|
|
|
|
|
|
|
|
|
chapter_ref = scenario.get("chapter_ref") or metadata.get("chapter_ref")
|
|
|
|
|
meta = _public_json_value(metadata)
|
|
|
|
|
if meta is _DROP or not isinstance(meta, dict):
|
|
|
|
|
meta = {}
|
|
|
|
|
meta.update(
|
|
|
|
|
{
|
|
|
|
|
"title": title,
|
|
|
|
|
"skill_tags": skill_tags,
|
|
|
|
|
"rubric": _json_value(rubric),
|
|
|
|
|
"tasks": _json_value(tasks),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
if external_id is not None:
|
|
|
|
|
meta["external_id"] = external_id
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"external_id": external_id,
|
|
|
|
|
"title": title,
|
|
|
|
|
"scenario": scenario,
|
|
|
|
|
"skill_tags": skill_tags,
|
|
|
|
|
"difficulty": difficulty,
|
|
|
|
|
"answer": answer,
|
|
|
|
|
"rubric": rubric,
|
|
|
|
|
"tasks": tasks,
|
|
|
|
|
"status": status,
|
|
|
|
|
"chapter_ref": chapter_ref,
|
|
|
|
|
"meta": meta,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_package_items(payload: dict) -> list:
|
|
|
|
|
items = _require_value(payload, "items", None)
|
|
|
|
|
if not isinstance(items, list) or not items:
|
|
|
|
|
raise ValueError("invalid_json_field:items")
|
|
|
|
|
return items
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_scenario(value, path: str) -> dict:
|
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
for field in _REQUIRED_SCENARIO_FIELDS:
|
|
|
|
|
_require_value(value, field, path)
|
|
|
|
|
|
|
|
|
|
scenario = {
|
|
|
|
|
"instrument_id": _coerce_int(value["instrument_id"], _join_path(path, "instrument_id")),
|
|
|
|
|
"timeframe": _require_non_empty_str(value["timeframe"], _join_path(path, "timeframe")),
|
|
|
|
|
"case_type": _require_non_empty_str(value["case_type"], _join_path(path, "case_type")),
|
|
|
|
|
"start_ts": _require_timestamp(value["start_ts"], _join_path(path, "start_ts")),
|
|
|
|
|
"decision_ts": _require_timestamp(value["decision_ts"], _join_path(path, "decision_ts")),
|
|
|
|
|
"end_ts": _require_timestamp(value["end_ts"], _join_path(path, "end_ts")),
|
|
|
|
|
"source_event_refs": _optional_list(value.get("source_event_refs", []), _join_path(path, "source_event_refs")),
|
|
|
|
|
}
|
|
|
|
|
for optional_field in ("source_event_type", "chapter_ref"):
|
|
|
|
|
optional_value = value.get(optional_field)
|
|
|
|
|
if optional_value is not None:
|
|
|
|
|
scenario[optional_field] = _require_non_empty_str(optional_value, _join_path(path, optional_field))
|
|
|
|
|
if value.get("source_event_id") is not None:
|
|
|
|
|
scenario["source_event_id"] = _coerce_int(value["source_event_id"], _join_path(path, "source_event_id"))
|
|
|
|
|
return scenario
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_skill_tags(value, path: str) -> list[str]:
|
|
|
|
|
if not isinstance(value, list) or not value:
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
tags = [_require_non_empty_str(item, f"{path}[{index}]") for index, item in enumerate(value)]
|
|
|
|
|
return list(dict.fromkeys(tags))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_tasks(value, default_skill_tag: str, path: str) -> list[dict]:
|
|
|
|
|
if not isinstance(value, list):
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
tasks = []
|
|
|
|
|
for index, item in enumerate(value):
|
|
|
|
|
item_path = f"{path}[{index}]"
|
|
|
|
|
if not isinstance(item, dict):
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", item_path))
|
|
|
|
|
task_type = _require_non_empty_str(_require_value(item, "task_type", item_path), _join_path(item_path, "task_type"))
|
|
|
|
|
prompt = _require_non_empty_str(_require_value(item, "prompt", item_path), _join_path(item_path, "prompt"))
|
|
|
|
|
task = {
|
|
|
|
|
"task_type": task_type,
|
|
|
|
|
"skill_tag": _optional_non_empty_str(item.get("skill_tag"), default_skill_tag, _join_path(item_path, "skill_tag")),
|
|
|
|
|
"prompt": prompt,
|
|
|
|
|
"required_annotation_types": _optional_list(
|
|
|
|
|
item.get("required_annotation_types", []),
|
|
|
|
|
_join_path(item_path, "required_annotation_types"),
|
|
|
|
|
),
|
|
|
|
|
"max_score": _coerce_float(item.get("max_score", 0), _join_path(item_path, "max_score")),
|
|
|
|
|
"sort_order": _coerce_int(item.get("sort_order", index), _join_path(item_path, "sort_order")),
|
|
|
|
|
"status": _optional_non_empty_str(item.get("status"), "active", _join_path(item_path, "status")),
|
|
|
|
|
"meta": _public_task_meta(_optional_dict(item.get("meta", {}), _join_path(item_path, "meta"))),
|
|
|
|
|
}
|
|
|
|
|
tasks.append(task)
|
|
|
|
|
return sorted(tasks, key=lambda task: (task["sort_order"], task["task_type"], task["prompt"]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _create_tasks(repository, case_id: int, tasks: list[dict]) -> None:
|
|
|
|
|
if not hasattr(repository, "add_task"):
|
|
|
|
|
return
|
|
|
|
|
for task in tasks:
|
|
|
|
|
repository.add_task(
|
|
|
|
|
case_id=case_id,
|
|
|
|
|
task_type=task["task_type"],
|
|
|
|
|
skill_tag=task["skill_tag"],
|
|
|
|
|
prompt=task["prompt"],
|
|
|
|
|
required_annotation_types=task["required_annotation_types"],
|
|
|
|
|
max_score=task["max_score"],
|
|
|
|
|
sort_order=task["sort_order"],
|
|
|
|
|
status=task["status"],
|
|
|
|
|
meta=task["meta"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _public_import_item(item: dict, case_id: int) -> dict:
|
|
|
|
|
public_item = {
|
|
|
|
|
"id": case_id,
|
|
|
|
|
"title": item["title"],
|
|
|
|
|
"scenario": _public_scenario(item["scenario"]),
|
|
|
|
|
"skill_tags": item["skill_tags"],
|
|
|
|
|
"difficulty": item["difficulty"],
|
|
|
|
|
"status": item["status"],
|
|
|
|
|
}
|
|
|
|
|
if item.get("external_id") is not None:
|
|
|
|
|
public_item["external_id"] = item["external_id"]
|
|
|
|
|
return public_item
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _public_case_row(row: dict) -> dict:
|
|
|
|
|
meta = row.get("meta") if isinstance(row.get("meta"), dict) else {}
|
|
|
|
|
title = row.get("title") or meta.get("title") or _default_title(row.get("id"))
|
|
|
|
|
public_title = _public_json_value(title)
|
|
|
|
|
if public_title is _DROP:
|
|
|
|
|
public_title = _default_title(row.get("id"))
|
|
|
|
|
skill_tags = _normalize_public_skill_tags(row, meta)
|
|
|
|
|
rubric = row.get("rubric")
|
|
|
|
|
if rubric in (None, [], {}):
|
|
|
|
|
rubric = meta.get("rubric")
|
|
|
|
|
tasks = row.get("tasks", meta.get("tasks", []))
|
|
|
|
|
item = {
|
|
|
|
|
"id": _json_value(row.get("id")),
|
|
|
|
|
"title": public_title,
|
|
|
|
|
"scenario": _public_scenario(row),
|
|
|
|
|
"skill_tags": skill_tags,
|
|
|
|
|
"difficulty": _public_json_value(row.get("difficulty") or "medium"),
|
|
|
|
|
"status": _public_json_value(row.get("status") or "draft"),
|
|
|
|
|
}
|
|
|
|
|
public_rubric = _public_json_value(rubric)
|
|
|
|
|
if public_rubric not in (_DROP, None, [], {}):
|
|
|
|
|
item["rubric"] = public_rubric
|
|
|
|
|
public_tasks = _public_json_value(tasks)
|
|
|
|
|
if public_tasks not in (_DROP, None, [], {}):
|
|
|
|
|
item["tasks"] = public_tasks
|
|
|
|
|
public_meta = _public_metadata(meta)
|
|
|
|
|
if public_meta:
|
|
|
|
|
item["metadata"] = public_meta
|
|
|
|
|
return item
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _public_scenario(source: dict) -> dict:
|
|
|
|
|
scenario = {
|
|
|
|
|
"instrument_id": _json_value(source.get("instrument_id")),
|
|
|
|
|
"timeframe": _public_json_value(source.get("timeframe")),
|
|
|
|
|
"case_type": _public_json_value(source.get("case_type")),
|
|
|
|
|
"start_ts": _json_value(source.get("start_ts")),
|
|
|
|
|
"decision_ts": _json_value(source.get("decision_ts")),
|
|
|
|
|
"end_ts": _json_value(source.get("end_ts")),
|
|
|
|
|
"source_event_refs": _public_json_value(source.get("source_event_refs", [])),
|
|
|
|
|
}
|
|
|
|
|
for optional_field in ("source_event_type", "source_event_id", "chapter_ref"):
|
|
|
|
|
if source.get(optional_field) is not None:
|
|
|
|
|
value = _public_json_value(source.get(optional_field))
|
|
|
|
|
if value is not _DROP:
|
|
|
|
|
scenario[optional_field] = value
|
|
|
|
|
return scenario
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _public_metadata(meta: dict) -> dict:
|
|
|
|
|
public_meta = {}
|
|
|
|
|
for key, value in sorted(meta.items(), key=lambda item: str(item[0])):
|
|
|
|
|
key_text = str(key)
|
|
|
|
|
if key_text in _RESERVED_META_FIELDS or _has_restricted_text(key_text):
|
|
|
|
|
continue
|
|
|
|
|
public_value = _public_json_value(value)
|
|
|
|
|
if public_value is not _DROP:
|
|
|
|
|
public_meta[key_text] = public_value
|
|
|
|
|
return public_meta
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _public_task_meta(meta: dict) -> dict:
|
|
|
|
|
public_meta = _public_json_value(meta)
|
|
|
|
|
if public_meta is _DROP or not isinstance(public_meta, dict):
|
|
|
|
|
return {}
|
|
|
|
|
return public_meta
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_public_skill_tags(row: dict, meta: dict) -> list[str]:
|
|
|
|
|
raw_tags = row.get("skill_tags") or meta.get("skill_tags")
|
|
|
|
|
if raw_tags is None:
|
|
|
|
|
raw_tags = [row.get("skill_tag")] if row.get("skill_tag") else []
|
|
|
|
|
if isinstance(raw_tags, str):
|
|
|
|
|
raw_tags = [raw_tags]
|
|
|
|
|
if not isinstance(raw_tags, list):
|
|
|
|
|
return []
|
|
|
|
|
tags = []
|
|
|
|
|
for item in raw_tags:
|
|
|
|
|
if isinstance(item, str) and item:
|
|
|
|
|
public_item = _public_json_value(item)
|
|
|
|
|
if public_item is not _DROP:
|
|
|
|
|
tags.append(public_item)
|
|
|
|
|
return sorted(dict.fromkeys(tags))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_value(payload: dict, key: str, path: Optional[str]):
|
|
|
|
|
if not isinstance(payload, dict):
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path or key))
|
|
|
|
|
if key not in payload or payload.get(key) is None:
|
|
|
|
|
raise ValueError(_field_error("missing_required_json_field", _join_path(path, key)))
|
|
|
|
|
return payload[key]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_non_empty_str(value, path: str) -> str:
|
|
|
|
|
if not isinstance(value, str) or value == "":
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _optional_non_empty_str(value, default: str, path: str) -> str:
|
|
|
|
|
if value is None:
|
|
|
|
|
return default
|
|
|
|
|
return _require_non_empty_str(value, path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_dict(value, path: str) -> dict:
|
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _optional_dict(value, path: str) -> dict:
|
|
|
|
|
if value is None:
|
|
|
|
|
return {}
|
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _optional_list(value, path: str) -> list:
|
|
|
|
|
if value is None:
|
|
|
|
|
return []
|
|
|
|
|
if not isinstance(value, list):
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_rubric(value, path: str):
|
|
|
|
|
if not isinstance(value, (dict, list)):
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
if value == {} or value == []:
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_timestamp(value, path: str):
|
|
|
|
|
if isinstance(value, datetime):
|
|
|
|
|
return value
|
|
|
|
|
if isinstance(value, str) and value:
|
|
|
|
|
return value
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _coerce_int(value, path: str) -> int:
|
|
|
|
|
try:
|
|
|
|
|
return int(value)
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _coerce_float(value, path: str) -> float:
|
|
|
|
|
try:
|
|
|
|
|
return float(value)
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
raise ValueError(_field_error("invalid_json_field", path)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _public_json_value(value):
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
result = {}
|
|
|
|
|
for key, item in sorted(value.items(), key=lambda pair: str(pair[0])):
|
|
|
|
|
key_text = str(key)
|
|
|
|
|
if key_text in _INTERNAL_PUBLIC_FIELDS or _has_restricted_text(key_text):
|
|
|
|
|
continue
|
|
|
|
|
public_item = _public_json_value(item)
|
|
|
|
|
if public_item is not _DROP:
|
|
|
|
|
result[key_text] = public_item
|
|
|
|
|
return result
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
result = []
|
|
|
|
|
for item in value:
|
|
|
|
|
public_item = _public_json_value(item)
|
|
|
|
|
if public_item is not _DROP:
|
|
|
|
|
result.append(public_item)
|
|
|
|
|
return result
|
|
|
|
|
if isinstance(value, str) and _has_restricted_text(value):
|
|
|
|
|
return _DROP
|
|
|
|
|
return _json_value(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _json_value(value):
|
|
|
|
|
if isinstance(value, datetime):
|
|
|
|
|
return value.isoformat()
|
|
|
|
|
if isinstance(value, date):
|
|
|
|
|
return value.isoformat()
|
|
|
|
|
if isinstance(value, Decimal):
|
|
|
|
|
return str(value)
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
return {str(key): _json_value(item) for key, item in sorted(value.items(), key=lambda pair: str(pair[0]))}
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
return [_json_value(item) for item in value]
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _has_restricted_text(value: str) -> bool:
|
|
|
|
|
lower_value = value.lower()
|
|
|
|
|
return any(fragment in lower_value for fragment in _RESTRICTED_TEXT_FRAGMENTS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sort_id(value) -> tuple[int, object]:
|
|
|
|
|
if value is None:
|
|
|
|
|
return (1, "")
|
|
|
|
|
try:
|
|
|
|
|
return (0, int(value))
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return (0, str(value))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _default_title(case_id) -> str:
|
|
|
|
|
return f"training-case-{case_id}" if case_id is not None else "training-case"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _join_path(path: Optional[str], key: str) -> str:
|
|
|
|
|
return f"{path}.{key}" if path else key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _field_error(prefix: str, path: str) -> str:
|
|
|
|
|
return f"{prefix}:{path}"
|