harden domain architecture and add lazy Crossref+Unpaywall PDF enrichment with publications workspace refactor
This commit is contained in:
parent
a527e7a535
commit
01454162bb
62 changed files with 2562 additions and 688 deletions
|
|
@ -10,11 +10,100 @@ from app.services.domains.publications.modes import (
|
|||
)
|
||||
from app.services.domains.publications.queries import (
|
||||
get_latest_completed_run_id_for_user,
|
||||
get_publication_item_for_user,
|
||||
publication_list_item_from_row,
|
||||
publications_query,
|
||||
unread_item_from_row,
|
||||
)
|
||||
from app.services.domains.publications.types import PublicationListItem, UnreadPublicationItem
|
||||
from app.services.domains.unpaywall.application import resolve_publication_oa_metadata
|
||||
from sqlalchemy import update
|
||||
from app.db.models import Publication
|
||||
|
||||
|
||||
def _with_oa_overrides(
|
||||
rows: list[PublicationListItem],
|
||||
oa_data: dict[int, tuple[str | None, str | None]],
|
||||
) -> list[PublicationListItem]:
|
||||
return [
|
||||
PublicationListItem(
|
||||
publication_id=row.publication_id,
|
||||
scholar_profile_id=row.scholar_profile_id,
|
||||
scholar_label=row.scholar_label,
|
||||
title=row.title,
|
||||
year=row.year,
|
||||
citation_count=row.citation_count,
|
||||
venue_text=row.venue_text,
|
||||
pub_url=row.pub_url,
|
||||
doi=(oa_data.get(row.publication_id) or (None, None))[0] or row.doi,
|
||||
pdf_url=(oa_data.get(row.publication_id) or (None, None))[1] or row.pdf_url,
|
||||
is_read=row.is_read,
|
||||
first_seen_at=row.first_seen_at,
|
||||
is_new_in_latest_run=row.is_new_in_latest_run,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def _resolved_fields(
|
||||
*,
|
||||
row: PublicationListItem,
|
||||
resolved: tuple[str | None, str | None] | None,
|
||||
) -> tuple[str | None, str | None]:
|
||||
if resolved is None:
|
||||
return row.doi, row.pdf_url
|
||||
resolved_doi, resolved_pdf = resolved
|
||||
return resolved_doi or row.doi, resolved_pdf or row.pdf_url
|
||||
|
||||
|
||||
async def _persist_resolved_metadata(
|
||||
db_session: AsyncSession,
|
||||
*,
|
||||
rows: list[PublicationListItem],
|
||||
oa_data: dict[int, tuple[str | None, str | None]],
|
||||
) -> None:
|
||||
by_id = {row.publication_id: row for row in rows}
|
||||
updates: list[tuple[int, str | None, str | None]] = []
|
||||
for publication_id, resolved in oa_data.items():
|
||||
existing = by_id.get(publication_id)
|
||||
if existing is None:
|
||||
continue
|
||||
next_doi, next_pdf = _resolved_fields(row=existing, resolved=resolved)
|
||||
if next_doi == existing.doi and next_pdf == existing.pdf_url:
|
||||
continue
|
||||
updates.append((publication_id, next_doi, next_pdf))
|
||||
for publication_id, doi, pdf_url in updates:
|
||||
await db_session.execute(
|
||||
update(Publication)
|
||||
.where(Publication.id == publication_id)
|
||||
.values(doi=doi, pdf_url=pdf_url)
|
||||
)
|
||||
if updates:
|
||||
await db_session.commit()
|
||||
|
||||
|
||||
def missing_pdf_items(
|
||||
rows: list[PublicationListItem],
|
||||
*,
|
||||
limit: int,
|
||||
) -> list[PublicationListItem]:
|
||||
bounded_limit = max(0, int(limit))
|
||||
if bounded_limit == 0:
|
||||
return []
|
||||
return [row for row in rows if not row.pdf_url][:bounded_limit]
|
||||
|
||||
|
||||
async def resolve_and_persist_oa_metadata(
|
||||
db_session: AsyncSession,
|
||||
*,
|
||||
rows: list[PublicationListItem],
|
||||
unpaywall_email: str | None = None,
|
||||
) -> dict[int, tuple[str | None, str | None]]:
|
||||
if not rows:
|
||||
return {}
|
||||
oa_data = await resolve_publication_oa_metadata(rows, request_email=unpaywall_email)
|
||||
await _persist_resolved_metadata(db_session, rows=rows, oa_data=oa_data)
|
||||
return oa_data
|
||||
|
||||
|
||||
async def list_for_user(
|
||||
|
|
@ -36,10 +125,35 @@ async def list_for_user(
|
|||
limit=limit,
|
||||
)
|
||||
)
|
||||
return [
|
||||
rows = [
|
||||
publication_list_item_from_row(row, latest_run_id=latest_run_id)
|
||||
for row in result.all()
|
||||
]
|
||||
return rows
|
||||
|
||||
|
||||
async def retry_pdf_for_user(
|
||||
db_session: AsyncSession,
|
||||
*,
|
||||
user_id: int,
|
||||
scholar_profile_id: int,
|
||||
publication_id: int,
|
||||
unpaywall_email: str | None = None,
|
||||
) -> PublicationListItem | None:
|
||||
item = await get_publication_item_for_user(
|
||||
db_session,
|
||||
user_id=user_id,
|
||||
scholar_profile_id=scholar_profile_id,
|
||||
publication_id=publication_id,
|
||||
)
|
||||
if item is None:
|
||||
return None
|
||||
oa_data = await resolve_and_persist_oa_metadata(
|
||||
db_session,
|
||||
rows=[item],
|
||||
unpaywall_email=unpaywall_email,
|
||||
)
|
||||
return _with_oa_overrides([item], oa_data)[0]
|
||||
|
||||
|
||||
async def list_unread_for_user(
|
||||
|
|
@ -73,17 +187,19 @@ async def list_new_for_latest_run_for_user(
|
|||
scholar_profile_id=None,
|
||||
limit=limit,
|
||||
)
|
||||
return [
|
||||
UnreadPublicationItem(
|
||||
publication_id=row.publication_id,
|
||||
scholar_profile_id=row.scholar_profile_id,
|
||||
scholar_label=row.scholar_label,
|
||||
title=row.title,
|
||||
year=row.year,
|
||||
citation_count=row.citation_count,
|
||||
venue_text=row.venue_text,
|
||||
pub_url=row.pub_url,
|
||||
pdf_url=row.pdf_url,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
return [_to_unread_item(row) for row in rows]
|
||||
|
||||
|
||||
def _to_unread_item(row: PublicationListItem) -> UnreadPublicationItem:
|
||||
return UnreadPublicationItem(
|
||||
publication_id=row.publication_id,
|
||||
scholar_profile_id=row.scholar_profile_id,
|
||||
scholar_label=row.scholar_label,
|
||||
title=row.title,
|
||||
year=row.year,
|
||||
citation_count=row.citation_count,
|
||||
venue_text=row.venue_text,
|
||||
pub_url=row.pub_url,
|
||||
doi=row.doi,
|
||||
pdf_url=row.pdf_url,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue