Initial release prep
This commit is contained in:
parent
c9b9d892f4
commit
92395b2b4b
100 changed files with 405 additions and 401 deletions
|
|
@ -9,13 +9,13 @@ from sqlalchemy import select
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models import ArxivQueryCacheEntry
|
||||
from app.services.domains.arxiv.cache import (
|
||||
from app.services.arxiv.cache import (
|
||||
build_query_fingerprint,
|
||||
get_cached_feed,
|
||||
run_with_inflight_dedupe,
|
||||
set_cached_feed,
|
||||
)
|
||||
from app.services.domains.arxiv.types import ArxivEntry, ArxivFeed, ArxivOpenSearchMeta
|
||||
from app.services.arxiv.types import ArxivEntry, ArxivFeed, ArxivOpenSearchMeta
|
||||
|
||||
|
||||
def _sample_feed(arxiv_id: str = "1234.5678") -> ArxivFeed:
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import httpx
|
|||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.services.domains.arxiv import client as arxiv_client_module
|
||||
from app.services.domains.arxiv.client import ArxivClient
|
||||
from app.services.domains.arxiv.errors import ArxivClientValidationError, ArxivRateLimitError
|
||||
from app.services.domains.arxiv.rate_limit import ArxivCooldownStatus
|
||||
from app.services.arxiv import client as arxiv_client_module
|
||||
from app.services.arxiv.client import ArxivClient
|
||||
from app.services.arxiv.errors import ArxivClientValidationError, ArxivRateLimitError
|
||||
from app.services.arxiv.rate_limit import ArxivCooldownStatus
|
||||
|
||||
_CLIENT_FEED_XML = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||
|
|
@ -160,7 +160,7 @@ async def test_client_logs_cache_hit_and_miss(
|
|||
def _capture_log(_msg: str, *args, **kwargs) -> None:
|
||||
logged.append({"event": _msg, **(kwargs.get("extra") or {})})
|
||||
|
||||
monkeypatch.setattr("app.services.domains.arxiv.client.logger.info", _capture_log)
|
||||
monkeypatch.setattr("app.services.arxiv.client.logger.info", _capture_log)
|
||||
client = ArxivClient(request_fn=_request_fn, cache_enabled=True)
|
||||
await client.search(query="ti:test-cache")
|
||||
await client.search(query="ti:test-cache")
|
||||
|
|
@ -196,7 +196,7 @@ async def test_request_feed_skips_live_call_when_global_cooldown_is_active(
|
|||
|
||||
monkeypatch.setattr(arxiv_client_module, "get_arxiv_cooldown_status", _cooldown_status)
|
||||
monkeypatch.setattr(arxiv_client_module, "run_with_global_arxiv_limit", _unexpected_limit_call)
|
||||
monkeypatch.setattr("app.services.domains.arxiv.client.logger.warning", _capture_warning)
|
||||
monkeypatch.setattr("app.services.arxiv.client.logger.warning", _capture_warning)
|
||||
|
||||
with pytest.raises(ArxivRateLimitError):
|
||||
await arxiv_client_module._request_arxiv_feed(
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ from types import SimpleNamespace
|
|||
|
||||
import pytest
|
||||
|
||||
from app.services.domains.arxiv import application as arxiv_application
|
||||
from app.services.domains.arxiv import gateway as arxiv_gateway
|
||||
from app.services.domains.arxiv.types import ArxivEntry, ArxivFeed, ArxivOpenSearchMeta
|
||||
from app.services.arxiv import application as arxiv_application
|
||||
from app.services.arxiv import gateway as arxiv_gateway
|
||||
from app.services.arxiv.types import ArxivEntry, ArxivFeed, ArxivOpenSearchMeta
|
||||
from app.settings import settings
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ from __future__ import annotations
|
|||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from app.services.domains.arxiv.guards import arxiv_skip_reason_for_item
|
||||
from app.services.domains.publication_identifiers.types import DisplayIdentifier
|
||||
from app.services.domains.publications.types import PublicationListItem
|
||||
from app.services.arxiv.guards import arxiv_skip_reason_for_item
|
||||
from app.services.publication_identifiers.types import DisplayIdentifier
|
||||
from app.services.publications.types import PublicationListItem
|
||||
|
||||
|
||||
def _item(
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ from __future__ import annotations
|
|||
|
||||
import pytest
|
||||
|
||||
from app.services.domains.arxiv.errors import ArxivParseError
|
||||
from app.services.domains.arxiv.parser import parse_arxiv_feed
|
||||
from app.services.arxiv.errors import ArxivParseError
|
||||
from app.services.arxiv.parser import parse_arxiv_feed
|
||||
|
||||
_VALID_FEED_XML = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ from sqlalchemy import select
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models import ArxivRuntimeState
|
||||
from app.services.domains.arxiv.constants import ARXIV_RUNTIME_STATE_KEY
|
||||
from app.services.domains.arxiv.errors import ArxivRateLimitError
|
||||
from app.services.domains.arxiv.rate_limit import get_arxiv_cooldown_status, run_with_global_arxiv_limit
|
||||
from app.services.arxiv.constants import ARXIV_RUNTIME_STATE_KEY
|
||||
from app.services.arxiv.errors import ArxivRateLimitError
|
||||
from app.services.arxiv.rate_limit import get_arxiv_cooldown_status, run_with_global_arxiv_limit
|
||||
from app.settings import settings
|
||||
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ async def test_arxiv_rate_limit_logs_request_scheduled_and_completed(
|
|||
def _capture_log(_msg: str, *args, **kwargs) -> None:
|
||||
logged.append({"event": _msg, **(kwargs.get("extra") or {})})
|
||||
|
||||
monkeypatch.setattr("app.services.domains.arxiv.rate_limit.logger.info", _capture_log)
|
||||
monkeypatch.setattr("app.services.arxiv.rate_limit.logger.info", _capture_log)
|
||||
await run_with_global_arxiv_limit(fetch=_fetch, source_path="search")
|
||||
|
||||
scheduled = [entry for entry in logged if entry.get("event") == "arxiv.request_scheduled"]
|
||||
|
|
@ -128,7 +128,7 @@ async def test_arxiv_rate_limit_logs_cooldown_activation(
|
|||
def _capture_warning(_msg: str, *args, **kwargs) -> None:
|
||||
logged_warning.append({"event": _msg, **(kwargs.get("extra") or {})})
|
||||
|
||||
monkeypatch.setattr("app.services.domains.arxiv.rate_limit.logger.warning", _capture_warning)
|
||||
monkeypatch.setattr("app.services.arxiv.rate_limit.logger.warning", _capture_warning)
|
||||
with pytest.raises(ArxivRateLimitError):
|
||||
await run_with_global_arxiv_limit(fetch=_fetch, source_path="lookup_ids")
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from app.services.domains.openalex.types import OpenAlexWork
|
||||
from app.services.openalex.types import OpenAlexWork
|
||||
|
||||
|
||||
def test_parse_openalex_work_from_api_dict() -> None:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from app.services.domains.openalex.matching import find_best_match
|
||||
from app.services.domains.openalex.types import OpenAlexWork
|
||||
from app.services.openalex.matching import find_best_match
|
||||
from app.services.openalex.types import OpenAlexWork
|
||||
|
||||
|
||||
def test_find_best_match_exact_title():
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||
import pytest
|
||||
|
||||
from app.db.models import ScholarPublication
|
||||
from app.services.domains.publications import dedup as dedup_service
|
||||
from app.services.domains.publications.dedup import (
|
||||
from app.services.publications import dedup as dedup_service
|
||||
from app.services.publications.dedup import (
|
||||
NearDuplicateCluster,
|
||||
NearDuplicateMember,
|
||||
find_identifier_duplicate_pairs,
|
||||
|
|
@ -87,15 +87,15 @@ async def test_merge_duplicate_publication_migrates_links_and_identifiers() -> N
|
|||
|
||||
with (
|
||||
patch(
|
||||
"app.services.domains.publications.dedup._load_publication",
|
||||
"app.services.publications.dedup._load_publication",
|
||||
new=AsyncMock(side_effect=[winner, dup]),
|
||||
),
|
||||
patch(
|
||||
"app.services.domains.publications.dedup._migrate_scholar_links",
|
||||
"app.services.publications.dedup._migrate_scholar_links",
|
||||
new=AsyncMock(),
|
||||
) as mock_links,
|
||||
patch(
|
||||
"app.services.domains.publications.dedup._migrate_identifiers",
|
||||
"app.services.publications.dedup._migrate_identifiers",
|
||||
new=AsyncMock(),
|
||||
) as mock_identifiers,
|
||||
):
|
||||
|
|
@ -112,7 +112,7 @@ async def test_merge_duplicate_publication_rejects_missing_publications() -> Non
|
|||
|
||||
with (
|
||||
patch(
|
||||
"app.services.domains.publications.dedup._load_publication",
|
||||
"app.services.publications.dedup._load_publication",
|
||||
new=AsyncMock(side_effect=[None, None]),
|
||||
),
|
||||
pytest.raises(ValueError),
|
||||
|
|
@ -160,7 +160,7 @@ async def test_migrate_scholar_links_drops_conflicts() -> None:
|
|||
@pytest.mark.asyncio
|
||||
async def test_sweep_returns_zero_when_no_pairs() -> None:
|
||||
with patch(
|
||||
"app.services.domains.publications.dedup.find_identifier_duplicate_pairs",
|
||||
"app.services.publications.dedup.find_identifier_duplicate_pairs",
|
||||
new=AsyncMock(return_value=[]),
|
||||
):
|
||||
session = AsyncMock()
|
||||
|
|
@ -174,11 +174,11 @@ async def test_sweep_returns_zero_when_no_pairs() -> None:
|
|||
async def test_sweep_returns_merge_count() -> None:
|
||||
with (
|
||||
patch(
|
||||
"app.services.domains.publications.dedup.find_identifier_duplicate_pairs",
|
||||
"app.services.publications.dedup.find_identifier_duplicate_pairs",
|
||||
new=AsyncMock(return_value=[(1, 2), (3, 4)]),
|
||||
),
|
||||
patch(
|
||||
"app.services.domains.publications.dedup.merge_duplicate_publication",
|
||||
"app.services.publications.dedup.merge_duplicate_publication",
|
||||
new=AsyncMock(),
|
||||
) as mock_merge,
|
||||
):
|
||||
|
|
@ -194,11 +194,11 @@ async def test_sweep_returns_merge_count() -> None:
|
|||
async def test_sweep_merges_each_dup_only_once() -> None:
|
||||
with (
|
||||
patch(
|
||||
"app.services.domains.publications.dedup.find_identifier_duplicate_pairs",
|
||||
"app.services.publications.dedup.find_identifier_duplicate_pairs",
|
||||
new=AsyncMock(return_value=[(1, 2), (1, 2)]),
|
||||
),
|
||||
patch(
|
||||
"app.services.domains.publications.dedup.merge_duplicate_publication",
|
||||
"app.services.publications.dedup.merge_duplicate_publication",
|
||||
new=AsyncMock(),
|
||||
) as mock_merge,
|
||||
):
|
||||
|
|
@ -227,7 +227,7 @@ async def test_find_near_duplicate_clusters_groups_similar_titles() -> None:
|
|||
assert second is not None
|
||||
|
||||
with patch(
|
||||
"app.services.domains.publications.dedup._load_near_duplicate_candidates",
|
||||
"app.services.publications.dedup._load_near_duplicate_candidates",
|
||||
new=AsyncMock(return_value=[first, second]),
|
||||
):
|
||||
clusters = await find_near_duplicate_clusters(AsyncMock())
|
||||
|
|
@ -255,7 +255,7 @@ async def test_find_near_duplicate_clusters_skips_unrelated_titles() -> None:
|
|||
assert second is not None
|
||||
|
||||
with patch(
|
||||
"app.services.domains.publications.dedup._load_near_duplicate_candidates",
|
||||
"app.services.publications.dedup._load_near_duplicate_candidates",
|
||||
new=AsyncMock(return_value=[first, second]),
|
||||
):
|
||||
clusters = await find_near_duplicate_clusters(AsyncMock())
|
||||
|
|
@ -277,7 +277,7 @@ async def test_merge_near_duplicate_cluster_merges_non_winner_members() -> None:
|
|||
)
|
||||
|
||||
with patch(
|
||||
"app.services.domains.publications.dedup.merge_duplicate_publication",
|
||||
"app.services.publications.dedup.merge_duplicate_publication",
|
||||
new=AsyncMock(),
|
||||
) as mock_merge:
|
||||
merged = await merge_near_duplicate_cluster(AsyncMock(), cluster=cluster)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue