Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/paperscout/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ class Tier(str, Enum):
COLD = "cold"


class MatchReason(str, Enum):
"""Why a watchlist entry matched a paper or probe hit."""

AUTHOR = "author"
PAPER = "paper"


@dataclass(slots=True)
class ProbeHit:
"""Successful HEAD to an unpublished draft URL plus optional excerpt text."""
Expand Down Expand Up @@ -211,7 +218,7 @@ def __post_init__(self) -> None:

@dataclass
class PerUserMatches:
"""One user's watchlist hits: ``(paper|hit, 'author'|'paper')`` tuples."""
"""One user's watchlist hits: ``(paper|hit, MatchReason)`` tuples."""

papers: list[tuple[Paper, str]] = field(default_factory=list)
probe_hits: list[tuple[ProbeHit, str]] = field(default_factory=list)
papers: list[tuple[Paper, MatchReason]] = field(default_factory=list)
probe_hits: list[tuple[ProbeHit, MatchReason]] = field(default_factory=list)
4 changes: 2 additions & 2 deletions src/paperscout/scout.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,15 +521,15 @@ def notify_users(app: App, result: PollResult, mq: MessageQueue) -> None:
lines.append("*:rotating_light: Papers matching your watchlist:*")
for paper, reason in matches.papers:
p_link = _paper_link(paper)
tag = f"[{reason} match]"
tag = f"[{reason.value} match]"
lines.append(f"• {p_link} — {paper.title} (by *{paper.author}*) {tag}")

if matches.probe_hits:
lines.append("*:rotating_light: New drafts matching your watchlist:*")
for hit, reason in matches.probe_hits:
h_link = _hit_label(hit.url, hit.prefix, hit.number, hit.revision, hit.extension)
lm = _fmt_lm(hit.last_modified)
tag = f"[{reason} match]"
tag = f"[{reason.value} match]"
lines.append(f"• {h_link} — {lm} {tag}")

if not lines:
Expand Down
14 changes: 7 additions & 7 deletions src/paperscout/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, cast

from .models import PerUserMatches
from .models import MatchReason, PerUserMatches

if TYPE_CHECKING:
from psycopg2.pool import ThreadedConnectionPool
Expand Down Expand Up @@ -371,29 +371,29 @@ def matches_for_users(
authors = user_authors.get(uid, [])
paper_nums = user_papers.get(uid, set())

matched_papers: list[tuple[Paper, str]] = []
matched_papers: list[tuple[Paper, MatchReason]] = []
for paper in new_papers:
# Author match
if authors and paper.author:
author_lower = paper.author.lower()
if any(a in author_lower for a in authors):
matched_papers.append((paper, "author"))
matched_papers.append((paper, MatchReason.AUTHOR))
continue
# Paper-number match
if paper_nums and paper.number is not None and paper.number in paper_nums:
matched_papers.append((paper, "paper"))
matched_papers.append((paper, MatchReason.PAPER))

matched_hits: list[tuple[ProbeHit, str]] = []
matched_hits: list[tuple[ProbeHit, MatchReason]] = []
for hit in probe_hits:
# Author match via front_text
if authors and hit.front_text:
text_lower = hit.front_text.lower()
if any(a in text_lower for a in authors):
matched_hits.append((hit, "author"))
matched_hits.append((hit, MatchReason.AUTHOR))
continue
# Paper-number match via probe hit number
if paper_nums and hit.number in paper_nums:
matched_hits.append((hit, "paper"))
matched_hits.append((hit, MatchReason.PAPER))

if matched_papers or matched_hits:
result[uid] = PerUserMatches(
Expand Down
12 changes: 6 additions & 6 deletions tests/test_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import pytest

from paperscout.errors import ConfigurationError
from paperscout.models import CycleResult, CycleStatus, Paper, PerUserMatches, ProbeHit
from paperscout.models import CycleResult, CycleStatus, MatchReason, Paper, PerUserMatches, ProbeHit
from paperscout.monitor import (
DiffResult,
PollResult,
Expand Down Expand Up @@ -189,7 +189,7 @@ def test_explicit_dp_transitions(self):
def test_explicit_per_user_matches(self):
diff = DiffResult(new_papers=[], updated_papers=[])
paper = Paper(id="P2300R11")
pum = PerUserMatches(papers=[(paper, "author")], probe_hits=[])
pum = PerUserMatches(papers=[(paper, MatchReason.AUTHOR)], probe_hits=[])
result = PollResult(diff=diff, probe_hits=[], per_user_matches={"U1": pum})
assert "U1" in result.per_user_matches

Expand Down Expand Up @@ -358,7 +358,7 @@ async def test_poll_once_populates_per_user_matches(self, fake_pool):
prober.run_cycle = AsyncMock(return_value=_empty_cycle())

user_watchlist.matches_for_users.return_value = {
"U123": PerUserMatches(papers=[(new_paper, "author")], probe_hits=[])
"U123": PerUserMatches(papers=[(new_paper, MatchReason.AUTHOR)], probe_hits=[])
}
result = await scheduler.poll_once()
assert "U123" in result.per_user_matches
Expand All @@ -373,7 +373,7 @@ async def test_poll_once_per_user_probe_hit(self, fake_pool):
index.papers = {}

user_watchlist.matches_for_users.return_value = {
"U123": PerUserMatches(papers=[], probe_hits=[(hit, "author")])
"U123": PerUserMatches(papers=[], probe_hits=[(hit, MatchReason.AUTHOR)])
}
result = await scheduler.poll_once()
assert "U123" in result.per_user_matches
Expand Down Expand Up @@ -403,7 +403,7 @@ async def test_restart_with_prior_poll_notifies_seed_hits(self, fake_pool):
hit = _recent_hit()
prober.run_cycle = AsyncMock(return_value=_success_cycle([hit]))
user_watchlist.matches_for_users.return_value = {
"U123": PerUserMatches(papers=[], probe_hits=[(hit, "author")])
"U123": PerUserMatches(papers=[], probe_hits=[(hit, MatchReason.AUTHOR)])
}
result = await scheduler.poll_once()
assert len(notified) == 1
Expand All @@ -418,7 +418,7 @@ async def test_restart_with_discovered_urls_notifies(self, fake_pool):
hit = _recent_hit()
prober.run_cycle = AsyncMock(return_value=_success_cycle([hit]))
user_watchlist.matches_for_users.return_value = {
"U123": PerUserMatches(papers=[], probe_hits=[(hit, "author")])
"U123": PerUserMatches(papers=[], probe_hits=[(hit, MatchReason.AUTHOR)])
}
result = await scheduler.poll_once()
assert len(notified) == 1
Expand Down
10 changes: 5 additions & 5 deletions tests/test_scout.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch

from paperscout.models import Paper, PerUserMatches, ProbeHit
from paperscout.models import MatchReason, Paper, PerUserMatches, ProbeHit
from paperscout.monitor import DiffResult, DPTransition, PollResult
from paperscout.scout import (
_batch_lines,
Expand Down Expand Up @@ -278,7 +278,7 @@ def test_author_match_sends_dm(self):
paper = Paper(
id="P2300R11", title="Senders", author="Eric Niebler", url="https://wg21.link/P2300R11"
)
pum = PerUserMatches(papers=[(paper, "author")], probe_hits=[])
pum = PerUserMatches(papers=[(paper, MatchReason.AUTHOR)], probe_hits=[])
result = _make_result(per_user_matches={"U123": pum})
notify_users(app, result, mq)
mq.enqueue.assert_called_once()
Expand All @@ -291,7 +291,7 @@ def test_paper_match_sends_dm(self):
app = MagicMock()
mq = MagicMock()
paper = Paper(id="P2300R11", title="X", author="Someone", url="https://wg21.link/P2300R11")
pum = PerUserMatches(papers=[(paper, "paper")], probe_hits=[])
pum = PerUserMatches(papers=[(paper, MatchReason.PAPER)], probe_hits=[])
result = _make_result(per_user_matches={"U456": pum})
notify_users(app, result, mq)
channel, text = mq.enqueue.call_args[0]
Expand All @@ -302,7 +302,7 @@ def test_probe_hit_match_sends_dm(self):
app = MagicMock()
mq = MagicMock()
hit = _recent_hit()
pum = PerUserMatches(papers=[], probe_hits=[(hit, "author")])
pum = PerUserMatches(papers=[], probe_hits=[(hit, MatchReason.AUTHOR)])
result = _make_result(per_user_matches={"U789": pum})
notify_users(app, result, mq)
mq.enqueue.assert_called_once()
Expand All @@ -313,7 +313,7 @@ def test_multiple_users_get_separate_dms(self):
app = MagicMock()
mq = MagicMock()
paper = Paper(id="P2300R11", title="X", author="Niebler")
pum = PerUserMatches(papers=[(paper, "author")], probe_hits=[])
pum = PerUserMatches(papers=[(paper, MatchReason.AUTHOR)], probe_hits=[])
result = _make_result(per_user_matches={"U1": pum, "U2": pum})
notify_users(app, result, mq)
assert mq.enqueue.call_count == 2
Expand Down
6 changes: 4 additions & 2 deletions tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import pytest

from paperscout.models import Paper
from paperscout.models import MatchReason, Paper
from paperscout.storage import (
PaperCache,
ProbeState,
Expand Down Expand Up @@ -309,6 +309,8 @@ def test_matches_for_users_author_match(self, fake_pool):
assert "U1" in result
matched_papers = [p for p, _ in result["U1"].papers]
assert paper in matched_papers
_, reason = result["U1"].papers[0]
assert reason is MatchReason.AUTHOR

def test_matches_for_users_paper_match(self, fake_pool):
wl = UserWatchlist(fake_pool)
Expand Down Expand Up @@ -406,7 +408,7 @@ def test_matches_skips_bad_paper_row_author_match_still_works(self, fake_pool):
result = wl.matches_for_users([paper], [])
assert "U1" in result
reasons = [r for _, r in result["U1"].papers]
assert "author" in reasons
assert MatchReason.AUTHOR in reasons

def test_matches_paper_with_none_number_never_paper_matched(self, fake_pool):
wl = UserWatchlist(fake_pool)
Expand Down