diff --git a/frontend/osa-chat-widget.js b/frontend/osa-chat-widget.js index 7065e82..a76045a 100644 --- a/frontend/osa-chat-widget.js +++ b/frontend/osa-chat-widget.js @@ -1938,6 +1938,7 @@ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ community_id: CONFIG.communityId, ...payload }), + signal: AbortSignal.timeout(10000), }); if (!response.ok) { console.warn('[OSA] Feedback submission returned', response.status); @@ -2069,12 +2070,25 @@ return; } - const ok = await postFeedback({ - feedback_type: 'general', - comment, - session_id: sessionId || null, - page_url: (typeof window !== 'undefined' && window.location) ? window.location.href : null, - }); + // Guard against a double-click submitting the comment twice while the POST + // is in flight (each would store a separate row). + const sendBtn = container.querySelector('.osa-feedback-send-btn'); + if (sendBtn) { + if (sendBtn.disabled) return; + sendBtn.disabled = true; + } + + let ok = false; + try { + ok = await postFeedback({ + feedback_type: 'general', + comment, + session_id: sessionId || null, + page_url: (typeof window !== 'undefined' && window.location) ? window.location.href : null, + }); + } finally { + if (sendBtn) sendBtn.disabled = false; + } if (ok) { const thanks = container.querySelector('.osa-feedback-modal-thanks'); diff --git a/pyproject.toml b/pyproject.toml index b45550c..410b4af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ server = [ "langchain-litellm>=0.2.0", # External APIs "pygithub>=2.8.0", - "pyalex>=0.19", # Database "psycopg[binary]>=3.3.0", # Utilities diff --git a/scripts/widget-sri.py b/scripts/widget-sri.py index 29b913c..274a7e3 100644 --- a/scripts/widget-sri.py +++ b/scripts/widget-sri.py @@ -46,7 +46,7 @@ def main() -> None: hash_value = sri(content) print(f"\nSRI hash ({label}):") - print(f" integrity=\"{hash_value}\"") + print(f' integrity="{hash_value}"') if tag: print(f"\nVersioned embed snippet for {tag}:") diff --git a/src/api/routers/feedback.py b/src/api/routers/feedback.py index 392f4e0..b4a0629 100644 --- a/src/api/routers/feedback.py +++ b/src/api/routers/feedback.py @@ -125,18 +125,23 @@ async def submit_feedback( body.community_id, ) - entry = FeedbackEntry( - feedback_id=str(uuid.uuid4()), - timestamp=now_iso(), - community_id=body.community_id, - feedback_type=body.feedback_type, - sentiment=body.sentiment, - request_id=body.request_id, - session_id=body.session_id, - message_index=body.message_index, - comment=body.comment, - page_url=body.page_url, - ) + try: + entry = FeedbackEntry( + feedback_id=str(uuid.uuid4()), + timestamp=now_iso(), + community_id=body.community_id, + feedback_type=body.feedback_type, + sentiment=body.sentiment, + request_id=body.request_id, + session_id=body.session_id, + message_index=body.message_index, + comment=body.comment, + page_url=body.page_url, + ) + except ValueError as e: + # FeedbackRequest already validates these invariants; this guards against + # the two validators drifting apart so a bad shape returns 422, not 500. + raise HTTPException(status_code=422, detail=str(e)) from e # write_feedback is best-effort: it logs and swallows storage errors (and # escalates after repeated failures) rather than failing the user's request, diff --git a/src/assistants/eeglab/config.yaml b/src/assistants/eeglab/config.yaml index 8e4f338..a51c072 100644 --- a/src/assistants/eeglab/config.yaml +++ b/src/assistants/eeglab/config.yaml @@ -414,6 +414,7 @@ github: # Paper/citation search configuration citations: + live_search: true # prompt already instructs the agent to ask before running it queries: - EEGLAB tutorial - EEGLAB plugin diff --git a/src/cli/sync.py b/src/cli/sync.py index e149686..ce4503a 100644 --- a/src/cli/sync.py +++ b/src/cli/sync.py @@ -190,7 +190,7 @@ def _safe_load_config() -> tuple[str | None, str | None]: if not pubmed_key: pubmed_key = os.environ.get("PUBMED_API_KEY") - # Configure OpenAlex from env vars (pyalex uses global config) + # Configure OpenAlex credentials from env vars (merged into the opencite Config) openalex_key = os.environ.get("OPENALEX_API_KEY") openalex_email = os.environ.get("OPENALEX_EMAIL") configure_openalex(api_key=openalex_key, email=openalex_email) diff --git a/src/core/config/community.py b/src/core/config/community.py index c7bce15..75d01b4 100644 --- a/src/core/config/community.py +++ b/src/core/config/community.py @@ -236,8 +236,12 @@ class CitationConfig(BaseModel): dois: list[str] = Field(default_factory=list) """Core paper DOIs to track citations for (format: '10.xxxx/yyyy').""" - live_search: bool = Field(default=True) - """Expose an on-demand live paper search tool (opencite) for recent literature.""" + live_search: bool = Field(default=False) + """Expose an on-demand live paper search tool (opencite) for recent literature. + + Off by default: the tool adds external-API latency to a turn and queries + OpenAlex anonymously. Communities opt in explicitly, and their prompt should + tell the agent to ask the user before running it.""" @field_validator("queries") @classmethod diff --git a/src/knowledge/papers_sync.py b/src/knowledge/papers_sync.py index 280af36..a83806b 100644 --- a/src/knowledge/papers_sync.py +++ b/src/knowledge/papers_sync.py @@ -16,8 +16,9 @@ import logging import os import threading -from collections.abc import Iterable +from collections.abc import Coroutine, Iterable from concurrent.futures import ThreadPoolExecutor +from typing import Any, TypeVar from opencite import Config, Paper from opencite.citations import CitationExplorer @@ -198,7 +199,10 @@ def _store_papers( return counts -def _run(coro): +_T = TypeVar("_T") + + +def _run(coro: Coroutine[Any, Any, _T]) -> _T: """Execute an async coroutine from synchronous code. OSA's sync callers (CLI command, scheduler thread) have no running event @@ -232,9 +236,15 @@ async def _search_queries( try: result = await searcher.search(query, max_results=max_results, sources=sources) out.append((query, result.papers)) - except Exception as e: + except (OpenCiteError, TimeoutError) as e: logger.warning("opencite search error for '%s': %s", query, e) out.append((query, [])) + except Exception: + # Unexpected (likely a bug, not an API failure): keep the batch + # going but log loudly with a traceback so it is not mistaken + # for a routine "no results" outcome. + logger.exception("unexpected error searching '%s'", query) + out.append((query, [])) return out @@ -250,9 +260,12 @@ async def _citing_for_dois( try: result = await explorer.citing_papers(doi, max_results=max_results) out.append((doi, result.papers)) - except Exception as e: + except (OpenCiteError, TimeoutError) as e: logger.warning("opencite citation error for DOI %s: %s", doi, e) out.append((doi, [])) + except Exception: + logger.exception("unexpected error fetching citations for DOI %s", doi) + out.append((doi, [])) return out @@ -357,10 +370,15 @@ def sync_all_papers( return results for query, papers in searched: - counts = _store_papers(papers, project) - for source, n in counts.items(): - results[source] = results.get(source, 0) + n - update_sync_metadata("papers", f"opencite:{query}", sum(counts.values()), project) + try: + counts = _store_papers(papers, project) + for source, n in counts.items(): + results[source] = results.get(source, 0) + n + update_sync_metadata("papers", f"opencite:{query}", sum(counts.values()), project) + except Exception: + # Isolate per-query: a DB failure on one query must not abort the + # whole batch or leave sync metadata inconsistent for the others. + logger.exception("failed to store papers for '%s' (%s)", query, project) total = sum(results.values()) logger.info("Total papers synced for %s: %d", project, total) @@ -401,11 +419,15 @@ def sync_citing_papers( total = 0 for doi, papers in cited: - counts = _store_papers(papers, project) - count = sum(counts.values()) - update_sync_metadata("papers", f"citing_{doi}", count, project) - logger.info("Synced %d papers citing %s", count, doi) - total += count + try: + counts = _store_papers(papers, project) + count = sum(counts.values()) + update_sync_metadata("papers", f"citing_{doi}", count, project) + logger.info("Synced %d papers citing %s", count, doi) + total += count + except Exception: + # Isolate per-DOI so one DB failure does not abort the batch. + logger.exception("failed to store citing papers for %s (%s)", doi, project) return total diff --git a/tests/test_api/test_feedback_endpoint.py b/tests/test_api/test_feedback_endpoint.py index b307cf8..38540b3 100644 --- a/tests/test_api/test_feedback_endpoint.py +++ b/tests/test_api/test_feedback_endpoint.py @@ -116,6 +116,15 @@ def test_general_without_comment_rejected(self, client): ) assert resp.status_code == 422 + def test_general_whitespace_comment_rejected(self, client): + # _normalize collapses a whitespace-only comment to None, and _check_shape + # then rejects general feedback that carries no real comment. + resp = client.post( + "/feedback", + json={"community_id": "hed", "feedback_type": "general", "comment": " "}, + ) + assert resp.status_code == 422 + def test_oversized_comment_rejected(self, client): resp = client.post( "/feedback", diff --git a/tests/test_core/test_config/test_community.py b/tests/test_core/test_config/test_community.py index 1e4ae5c..ab5f8f8 100644 --- a/tests/test_core/test_config/test_community.py +++ b/tests/test_core/test_config/test_community.py @@ -150,6 +150,11 @@ def test_empty_defaults(self) -> None: assert config.queries == [] assert config.dois == [] + def test_live_search_off_by_default(self) -> None: + """Live search is opt-in: a community must enable it explicitly.""" + assert CitationConfig().live_search is False + assert CitationConfig(live_search=True).live_search is True + def test_validates_doi_format(self) -> None: """Should validate DOI format.""" with pytest.raises(ValidationError, match="Invalid DOI format"): diff --git a/uv.lock b/uv.lock index 484cc1d..8824ba3 100644 --- a/uv.lock +++ b/uv.lock @@ -811,6 +811,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, @@ -818,6 +819,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, @@ -825,6 +827,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, @@ -832,6 +835,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, @@ -839,6 +843,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, @@ -2043,7 +2048,6 @@ dev = [ { name = "opencite" }, { name = "pre-commit" }, { name = "psycopg", extra = ["binary"] }, - { name = "pyalex" }, { name = "pydantic-settings" }, { name = "pygithub" }, { name = "pytest" }, @@ -2074,7 +2078,6 @@ server = [ { name = "markdownify" }, { name = "opencite" }, { name = "psycopg", extra = ["binary"] }, - { name = "pyalex" }, { name = "pydantic-settings" }, { name = "pygithub" }, { name = "python-dotenv" }, @@ -2120,8 +2123,6 @@ requires-dist = [ { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.5.0" }, { name = "psycopg", extras = ["binary"], marker = "extra == 'dev'", specifier = ">=3.3.0" }, { name = "psycopg", extras = ["binary"], marker = "extra == 'server'", specifier = ">=3.3.0" }, - { name = "pyalex", marker = "extra == 'dev'", specifier = ">=0.19" }, - { name = "pyalex", marker = "extra == 'server'", specifier = ">=0.19" }, { name = "pydantic", specifier = ">=2.12.0" }, { name = "pydantic-settings", marker = "extra == 'dev'", specifier = ">=2.12.0" }, { name = "pydantic-settings", marker = "extra == 'server'", specifier = ">=2.12.0" },