8a625bfb27
조회/수정 경로는 deleted_at 을 일관 가드하나 파일/콘텐츠 서빙 5엔드포인트 (get_document_file·image_raw·save_content·preview·content)가 'if not doc' 만 검사 → 삭제 문서 원본/preview/전문/마커이미지가 doc_id(+토큰)만으로 노출·삭제 문서 NAS 재기록. get_live_document(session, doc_id) 헬퍼(없거나 deleted_at 이면 404)로 통일 — '경로마다 deleted_at 기억' 대신 구조 강제(추가될 서빙 경로 자동 보호). save_content 는 삭제 문서 쓰기 차단까지. find_paper_holder 도 deleted_at IS NULL 필터 추가(dedup.find_canonical 대칭). 검증: py_compile 통과. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
40 lines
1.7 KiB
Python
40 lines
1.7 KiB
Python
"""B-3 논문 서지 holder 공유 dedup 조회.
|
|
|
|
모든 논문 수집기(OpenAlex/arXiv/KoreaScience/J-STAGE)·reconcile·구매 PDF 스탬프가
|
|
ingest 전 이 함수로 holder 존재를 확인한다(있으면 skip 또는 child 링크).
|
|
|
|
- 조회 키 = lower(extract_meta #>> '{paper,doi}') == normalize_doi(...) — 라이브 partial-unique
|
|
인덱스 uq_documents_paper_doi 와 동일 식(인덱스 사용).
|
|
- .scalars().first() — 교차게시·다중 landing-page 로 2행 이상 매칭 시 MultipleResultsFound
|
|
raise 방지(scalar_one_or_none 금지, 2026-06 BBC 수집 중단 선례 / news_collector 동일 규율).
|
|
- 서지 holder Document 의 **생성**은 각 수집기/스탬프 경로가 소유한다(초록 signal 문서 vs 구매
|
|
최소 holder 로 shape 가 다름). 이 모듈은 dedup 조회만 공유한다.
|
|
|
|
DB 조회라 본 모듈은 PR2(arXiv 실수집)에서 라이브 검증한다 — PR1 단위 테스트 대상은 doi.py(순수).
|
|
"""
|
|
|
|
from sqlalchemy import func, select
|
|
|
|
from models.document import Document
|
|
from services.papers.doi import normalize_doi
|
|
|
|
# 인덱스 식과 동일: lower(extract_meta #>> '{paper,doi}')
|
|
_DOI_EXPR = func.lower(Document.extract_meta[("paper", "doi")].astext)
|
|
|
|
|
|
async def find_paper_holder(session, raw_or_normalized_doi):
|
|
"""정규화 DOI 로 서지 holder Document 조회. 없으면 None.
|
|
|
|
인자는 raw 든 정규화든 받아 normalize_doi 로 통일(저장=조회 동일 함수 보장).
|
|
"""
|
|
doi = normalize_doi(raw_or_normalized_doi)
|
|
if not doi:
|
|
return None
|
|
result = await session.execute(
|
|
select(Document)
|
|
.where(Document.material_type == "paper", _DOI_EXPR == doi,
|
|
Document.deleted_at.is_(None))
|
|
.limit(1)
|
|
)
|
|
return result.scalars().first()
|