ci: add CodeQL security scanning and Dependabot
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ac002131d6
commit
3866c6d6f0
90 changed files with 40 additions and 1 deletions
107
app/services/portability/normalize.py
Normal file
107
app/services/portability/normalize.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from typing import Any
|
||||
|
||||
from app.services.domains.ingestion.application import normalize_title
|
||||
from app.services.domains.portability.constants import (
|
||||
MAX_IMPORT_PUBLICATIONS,
|
||||
MAX_IMPORT_SCHOLARS,
|
||||
SHA256_RE,
|
||||
WORD_RE,
|
||||
)
|
||||
from app.services.domains.portability.types import ImportExportError
|
||||
|
||||
|
||||
def _normalize_optional_text(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = str(value).strip()
|
||||
return normalized or None
|
||||
|
||||
|
||||
def _normalize_optional_year(value: Any) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
year = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if year < 1500 or year > 3000:
|
||||
return None
|
||||
return year
|
||||
|
||||
|
||||
def _normalize_citation_count(value: Any) -> int:
|
||||
try:
|
||||
parsed = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
return max(0, parsed)
|
||||
|
||||
|
||||
def _first_author_last_name(authors_text: str | None) -> str:
|
||||
if not authors_text:
|
||||
return ""
|
||||
first_author = authors_text.split(",", maxsplit=1)[0].strip().lower()
|
||||
words = WORD_RE.findall(first_author)
|
||||
if not words:
|
||||
return ""
|
||||
return words[-1]
|
||||
|
||||
|
||||
def _first_venue_word(venue_text: str | None) -> str:
|
||||
if not venue_text:
|
||||
return ""
|
||||
words = WORD_RE.findall(venue_text.lower())
|
||||
if not words:
|
||||
return ""
|
||||
return words[0]
|
||||
|
||||
|
||||
def _build_fingerprint(
|
||||
*,
|
||||
title: str,
|
||||
year: int | None,
|
||||
author_text: str | None,
|
||||
venue_text: str | None,
|
||||
) -> str:
|
||||
canonical = "|".join(
|
||||
[
|
||||
normalize_title(title),
|
||||
str(year) if year is not None else "",
|
||||
_first_author_last_name(author_text),
|
||||
_first_venue_word(venue_text),
|
||||
]
|
||||
)
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _resolve_fingerprint(
|
||||
*,
|
||||
title: str,
|
||||
year: int | None,
|
||||
author_text: str | None,
|
||||
venue_text: str | None,
|
||||
provided_fingerprint: Any,
|
||||
) -> str:
|
||||
normalized = _normalize_optional_text(provided_fingerprint)
|
||||
if normalized and SHA256_RE.fullmatch(normalized.lower()):
|
||||
return normalized.lower()
|
||||
return _build_fingerprint(
|
||||
title=title,
|
||||
year=year,
|
||||
author_text=author_text,
|
||||
venue_text=venue_text,
|
||||
)
|
||||
|
||||
|
||||
def _validate_import_sizes(
|
||||
*,
|
||||
scholars: list[dict[str, Any]],
|
||||
publications: list[dict[str, Any]],
|
||||
) -> None:
|
||||
if len(scholars) > MAX_IMPORT_SCHOLARS:
|
||||
raise ImportExportError(f"Import exceeds max scholars ({MAX_IMPORT_SCHOLARS}).")
|
||||
if len(publications) > MAX_IMPORT_PUBLICATIONS:
|
||||
raise ImportExportError(f"Import exceeds max publications ({MAX_IMPORT_PUBLICATIONS}).")
|
||||
Loading…
Add table
Add a link
Reference in a new issue