Spaces:
Sleeping
Sleeping
| import json | |
| from pathlib import Path | |
| import httpx | |
| import pytest | |
| import respx | |
| import ops as ops_module | |
| from ops import OPSNotConfigured, _OPSTokenManager, _ops_get, ops_scrap_patent, ops_search | |
| FIXTURES = Path(__file__).parent / "fixtures" / "ops" | |
| TOKEN_URL = "https://ops.epo.org/3.2/auth/accesstoken" | |
| OPS_BASE = "https://ops.epo.org/3.2/rest-services" | |
| def load_json(name: str): | |
| return json.loads((FIXTURES / name).read_text()) | |
| def mock_token_endpoint(): | |
| return respx.post(TOKEN_URL).mock( | |
| return_value=httpx.Response(200, json=load_json("token_response.json")) | |
| ) | |
| def fresh_token_manager(monkeypatch): | |
| """A token manager with test credentials, swapped in for the module-level | |
| singleton so `_ops_get`/`ops_search`/etc. (which reach for the module | |
| global `token_manager` directly) pick it up. | |
| """ | |
| tm = _OPSTokenManager() | |
| tm._key = "test-key" | |
| tm._secret = "test-secret" | |
| monkeypatch.setattr(ops_module, "token_manager", tm) | |
| return tm | |
| class TestTokenManager: | |
| async def test_not_configured_raises(self): | |
| tm = _OPSTokenManager() | |
| tm._key = None | |
| tm._secret = None | |
| async with httpx.AsyncClient() as client: | |
| with pytest.raises(OPSNotConfigured): | |
| await tm.get_token(client) | |
| async def test_fetches_and_caches_the_token(self): | |
| tm = _OPSTokenManager() | |
| tm._key = "k" | |
| tm._secret = "s" | |
| with respx.mock: | |
| route = mock_token_endpoint() | |
| async with httpx.AsyncClient() as client: | |
| token1 = await tm.get_token(client) | |
| token2 = await tm.get_token(client) | |
| assert token1 == "test-access-token-123" | |
| assert token2 == "test-access-token-123" | |
| assert route.call_count == 1 | |
| async def test_force_refresh_refetches_even_if_cached(self): | |
| tm = _OPSTokenManager() | |
| tm._key = "k" | |
| tm._secret = "s" | |
| with respx.mock: | |
| route = mock_token_endpoint() | |
| async with httpx.AsyncClient() as client: | |
| await tm.get_token(client) | |
| await tm.get_token(client, force=True) | |
| assert route.call_count == 2 | |
| class TestOpsGet: | |
| async def test_retries_once_when_the_token_is_stale(self, fresh_token_manager): | |
| with respx.mock: | |
| mock_token_endpoint() | |
| get_route = respx.get(f"{OPS_BASE}/some/path").mock( | |
| side_effect=[ | |
| httpx.Response(401, text="Invalid access token."), | |
| httpx.Response(200, json={"ok": True}), | |
| ] | |
| ) | |
| async with httpx.AsyncClient() as client: | |
| data = await _ops_get(client, "/some/path") | |
| assert data == {"ok": True} | |
| assert get_route.call_count == 2 | |
| async def test_propagates_non_token_errors(self, fresh_token_manager): | |
| with respx.mock: | |
| mock_token_endpoint() | |
| respx.get(f"{OPS_BASE}/some/path").mock(return_value=httpx.Response(500, text="server error")) | |
| async with httpx.AsyncClient() as client: | |
| with pytest.raises(httpx.HTTPStatusError): | |
| await _ops_get(client, "/some/path") | |
| class TestOpsSearch: | |
| async def test_maps_ops_search_results_to_serp_dicts(self, fresh_token_manager): | |
| with respx.mock: | |
| mock_token_endpoint() | |
| respx.get(f"{OPS_BASE}/published-data/search/biblio").mock( | |
| return_value=httpx.Response(200, json=load_json("search_response.json")) | |
| ) | |
| async with httpx.AsyncClient() as client: | |
| results = await ops_search(client, "widget", 10) | |
| assert results == [ | |
| { | |
| "title": "Widget apparatus", | |
| "body": "An apparatus comprising a widget.", | |
| "href": "https://worldwide.espacenet.com/patent/search?q=US11930446", | |
| "id": "US11930446", | |
| }, | |
| { | |
| "title": "Gadget system", | |
| "body": "A system comprising a gadget.", | |
| "href": "https://worldwide.espacenet.com/patent/search?q=EP4760514", | |
| "id": "EP4760514", | |
| }, | |
| ] | |
| async def test_returns_empty_list_on_404_not_found(self, fresh_token_manager): | |
| with respx.mock: | |
| mock_token_endpoint() | |
| respx.get(f"{OPS_BASE}/published-data/search/biblio").mock( | |
| return_value=httpx.Response(404, json=load_json("search_not_found_response.json")) | |
| ) | |
| async with httpx.AsyncClient() as client: | |
| results = await ops_search(client, "nonexistent widget xyz", 10) | |
| assert results == [] | |
| class TestOpsScrapPatent: | |
| async def test_assembles_a_full_patent_from_three_endpoints(self, fresh_token_manager): | |
| number = "US11930446" | |
| with respx.mock: | |
| mock_token_endpoint() | |
| respx.get(f"{OPS_BASE}/published-data/publication/epodoc/{number}/biblio").mock( | |
| return_value=httpx.Response(200, json=load_json("biblio_response.json")) | |
| ) | |
| respx.get(f"{OPS_BASE}/published-data/publication/epodoc/{number}/claims").mock( | |
| return_value=httpx.Response(200, json=load_json("claims_response.json")) | |
| ) | |
| respx.get(f"{OPS_BASE}/published-data/publication/epodoc/{number}/description").mock( | |
| return_value=httpx.Response(200, json=load_json("description_response.json")) | |
| ) | |
| async with httpx.AsyncClient() as client: | |
| # Kind-code suffix ("B2") should be normalized away before | |
| # any of the three requests go out. | |
| result = await ops_scrap_patent(client, "US11930446B2") | |
| assert result.title == "Widget apparatus" | |
| assert result.abstract == "An apparatus comprising a widget." | |
| assert result.claims == "1. A widget comprising a gadget.\n2. The widget of claim 1, further comprising a mechanism." | |
| assert result.description == "This invention relates to widgets.\nThe widget comprises a gadget and a mechanism." | |
| assert {c.code for c in result.classifications} == {"G06F17/30", "H04L9/00"} | |
| async def test_missing_fulltext_degrades_to_none_instead_of_raising(self, fresh_token_manager): | |
| number = "US11930446" | |
| with respx.mock: | |
| mock_token_endpoint() | |
| respx.get(f"{OPS_BASE}/published-data/publication/epodoc/{number}/biblio").mock( | |
| return_value=httpx.Response(200, json=load_json("biblio_response.json")) | |
| ) | |
| respx.get(f"{OPS_BASE}/published-data/publication/epodoc/{number}/claims").mock( | |
| return_value=httpx.Response(404) | |
| ) | |
| respx.get(f"{OPS_BASE}/published-data/publication/epodoc/{number}/description").mock( | |
| return_value=httpx.Response(404) | |
| ) | |
| async with httpx.AsyncClient() as client: | |
| result = await ops_scrap_patent(client, number) | |
| assert result.title == "Widget apparatus" | |
| assert result.claims is None | |
| assert result.description is None | |