From af6c9bd92f52d1630d488f4e21d00d5ad5d8e926 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Tue, 23 Jun 2026 12:26:25 -0400 Subject: [PATCH 01/34] QA-148 Full text checks --- qa/qa/checks/fulltext/text_checks.py | 52 +++++++++++++++++++++++++++ qa/tests/checks/fulltext/test_text.py | 40 +++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 qa/qa/checks/fulltext/text_checks.py create mode 100644 qa/tests/checks/fulltext/test_text.py diff --git a/qa/qa/checks/fulltext/text_checks.py b/qa/qa/checks/fulltext/text_checks.py new file mode 100644 index 00000000..0a582977 --- /dev/null +++ b/qa/qa/checks/fulltext/text_checks.py @@ -0,0 +1,52 @@ + +from qa.checks.base import BaseCheck + +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result + + +class MissingTextCheck(BaseCheck): + """Check for missing text (probably PDF invalid or password protected).""" + + name = "missing_text" + display_name = "Missing Text" + id = 14 + version = "1.0.0" + description = "Failed to extract text." + on_failure_policy = OnFailurePolicy.REJECT + failure_message = "Missing text (check PDF?)" + + required_inputs = {"fulltext"} + + def _run(self, data_registry: QaDataRegistry) -> Result: + fulltext = data_registry.fulltext + if fulltext is not None and fulltext != "": + passed = True + return self._result(passed) + else: + passed = False + return self._result(passed, message = self.failure_message) + + +class VeryShortTextCheck(BaseCheck): + """Check for very short text.""" + + name = "short_text" + display_name = "Short Text" + id = 15 + version = "1.0.0" + description = "The full text extracted is too short." + on_failure_policy = OnFailurePolicy.REJECT + failure_message = "Very short text (< 1400 words). Check full text." + required_inputs = {"fulltext"} + + def _run(self, data_registry: QaDataRegistry) -> Result: + fulltext = data_registry.fulltext + if len(fulltext) < 10000 and len(fulltext.split()) < 1400 : + passed = False + return self._result(passed, message = self.failure_message) + else: + passed = True + return self._result(passed) + + + diff --git a/qa/tests/checks/fulltext/test_text.py b/qa/tests/checks/fulltext/test_text.py new file mode 100644 index 00000000..0146e75b --- /dev/null +++ b/qa/tests/checks/fulltext/test_text.py @@ -0,0 +1,40 @@ +"""Tests for AbstractIsValid.""" + +import pytest + +from qa.checks.base import MissingDataError +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result +from qa.checks.fulltext.text_checks import ( + MissingTextCheck, + VeryShortTextCheck, +) + +class TestMissingText: + def test_pass_normal(self): + text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " + result = MissingTextCheck().run(QaDataRegistry(fulltext=text)) + assert result.passed + + def test_fail_on_none(self): + text = None + result = MissingTextCheck().run(QaDataRegistry(fulltext=text)) + assert not result.passed + + def test_fail_on_empty(self): + text = "" + result = MissingTextCheck().run(QaDataRegistry(fulltext=text)) + assert not result.passed + +class TestVeryShortText: + def test_pass_normal(self): + text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " * 140 + + result = VeryShortTextCheck().run(QaDataRegistry(fulltext=text)) + assert result.passed + + def test_fail_nospaces(self): + text = "Inthiswork, westudyaaa, bbb, andcccandconcludeddd. " * 100 + + result = VeryShortTextCheck().run(QaDataRegistry(fulltext=text)) + assert not result.passed + From 2b82c1ba81bb805aea15715c0dc33b6e88d4f91b Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Tue, 23 Jun 2026 12:41:25 -0400 Subject: [PATCH 02/34] QA-148 Fixed lint errors. --- qa/qa/checks/fulltext/text_checks.py | 3 ++- qa/tests/checks/fulltext/test_text.py | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/qa/qa/checks/fulltext/text_checks.py b/qa/qa/checks/fulltext/text_checks.py index 0a582977..7c001059 100644 --- a/qa/qa/checks/fulltext/text_checks.py +++ b/qa/qa/checks/fulltext/text_checks.py @@ -1,7 +1,7 @@ from qa.checks.base import BaseCheck -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result class MissingTextCheck(BaseCheck): @@ -37,6 +37,7 @@ class VeryShortTextCheck(BaseCheck): description = "The full text extracted is too short." on_failure_policy = OnFailurePolicy.REJECT failure_message = "Very short text (< 1400 words). Check full text." + required_inputs = {"fulltext"} def _run(self, data_registry: QaDataRegistry) -> Result: diff --git a/qa/tests/checks/fulltext/test_text.py b/qa/tests/checks/fulltext/test_text.py index 0146e75b..09bf8ce8 100644 --- a/qa/tests/checks/fulltext/test_text.py +++ b/qa/tests/checks/fulltext/test_text.py @@ -1,9 +1,6 @@ """Tests for AbstractIsValid.""" -import pytest - -from qa.checks.base import MissingDataError -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result +from qa.checks.models import QaDataRegistry from qa.checks.fulltext.text_checks import ( MissingTextCheck, VeryShortTextCheck, From ed123a098e8c5c24072fa7dc841e8fcd87e6c550 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Tue, 23 Jun 2026 12:44:54 -0400 Subject: [PATCH 03/34] QA-148 add new checks to list of checks. --- qa/qa/checks/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qa/qa/checks/__init__.py b/qa/qa/checks/__init__.py index bbcb73d8..4a9a3ec4 100644 --- a/qa/qa/checks/__init__.py +++ b/qa/qa/checks/__init__.py @@ -11,6 +11,8 @@ from qa.checks.metadata.report_num import ReportNumIsValid # noqa from qa.checks.metadata.title import TitleIsValid # noqa +from qa.checks.fulltext.text_checks import MissingTextCheck, VeryShortTextCheck # noqa + checks: list[BaseCheck] = [ TitleIsValid(), AuthorsAreValid(), @@ -21,4 +23,6 @@ DoiIsValid(), MscClassIsValid(), AcmClassIsValid(), + MissingTextCheck(), + VeryShortTextCheck(), ] From f812d864f5dc2716c288528d60cc8d28e408412d Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Tue, 23 Jun 2026 12:47:43 -0400 Subject: [PATCH 04/34] QA-148 linting --- qa/qa/checks/fulltext/text_checks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qa/qa/checks/fulltext/text_checks.py b/qa/qa/checks/fulltext/text_checks.py index 7c001059..d8312812 100644 --- a/qa/qa/checks/fulltext/text_checks.py +++ b/qa/qa/checks/fulltext/text_checks.py @@ -42,7 +42,11 @@ class VeryShortTextCheck(BaseCheck): def _run(self, data_registry: QaDataRegistry) -> Result: fulltext = data_registry.fulltext - if len(fulltext) < 10000 and len(fulltext.split()) < 1400 : + if fulltext is None or fulltext is "": + # Problem: we should only report the problem with missing texts + passed = True + return self._result(passed) + elif len(fulltext) < 10000 and len(fulltext.split()) < 1400 : passed = False return self._result(passed, message = self.failure_message) else: From ef51a46580272792f12d8e142e33a376b94416e3 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Tue, 23 Jun 2026 12:57:04 -0400 Subject: [PATCH 05/34] QA-148 linting --- qa/qa/checks/fulltext/text_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/qa/checks/fulltext/text_checks.py b/qa/qa/checks/fulltext/text_checks.py index d8312812..377b35f4 100644 --- a/qa/qa/checks/fulltext/text_checks.py +++ b/qa/qa/checks/fulltext/text_checks.py @@ -42,7 +42,7 @@ class VeryShortTextCheck(BaseCheck): def _run(self, data_registry: QaDataRegistry) -> Result: fulltext = data_registry.fulltext - if fulltext is None or fulltext is "": + if fulltext is None or fulltext == "": # Problem: we should only report the problem with missing texts passed = True return self._result(passed) From 5c9c4f0545ac322f2340094032df99adf7a24b32 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Tue, 23 Jun 2026 13:32:35 -0400 Subject: [PATCH 06/34] QA-148 add new checks to list of checks. --- qa/qa/checks/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/qa/checks/__init__.py b/qa/qa/checks/__init__.py index 4a9a3ec4..870789df 100644 --- a/qa/qa/checks/__init__.py +++ b/qa/qa/checks/__init__.py @@ -11,7 +11,7 @@ from qa.checks.metadata.report_num import ReportNumIsValid # noqa from qa.checks.metadata.title import TitleIsValid # noqa -from qa.checks.fulltext.text_checks import MissingTextCheck, VeryShortTextCheck # noqa +from qa.checks.fulltext.text_checks import MissingTextCheck, VeryShortTextCheck # noqa checks: list[BaseCheck] = [ TitleIsValid(), From b375c6eff45d3df5d79fd00181e580e22a9bd8b5 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Tue, 23 Jun 2026 17:47:23 -0400 Subject: [PATCH 07/34] QA-148 renumber ids, add unit test. Metadata => SubmissionMetadata. --- qa/qa/checks/generic/author_name.py | 14 ++-- qa/qa/checks/generic/text.py | 78 ++++++++++---------- qa/qa/checks/metadata/abstract.py | 6 +- qa/qa/checks/metadata/acm_class.py | 6 +- qa/qa/checks/metadata/authors.py | 6 +- qa/qa/checks/metadata/comments.py | 6 +- qa/qa/checks/metadata/doi.py | 6 +- qa/qa/checks/metadata/journal_ref.py | 6 +- qa/qa/checks/metadata/msc_class.py | 6 +- qa/qa/checks/metadata/report_num.py | 6 +- qa/qa/checks/metadata/title.py | 8 +- qa/qa/checks/models.py | 16 +++- qa/tests/checks/check_unique_ids.py | 12 +++ qa/tests/checks/generic/test_text.py | 4 +- qa/tests/checks/metadata/test_abstract.py | 6 +- qa/tests/checks/metadata/test_acm_class.py | 2 +- qa/tests/checks/metadata/test_authors.py | 6 +- qa/tests/checks/metadata/test_comments.py | 2 +- qa/tests/checks/metadata/test_doi.py | 2 +- qa/tests/checks/metadata/test_journal_ref.py | 2 +- qa/tests/checks/metadata/test_msc_class.py | 2 +- qa/tests/checks/metadata/test_report_num.py | 2 +- qa/tests/checks/metadata/test_title.py | 6 +- 23 files changed, 118 insertions(+), 92 deletions(-) create mode 100644 qa/tests/checks/check_unique_ids.py diff --git a/qa/qa/checks/generic/author_name.py b/qa/qa/checks/generic/author_name.py index 6b90443d..622a2c2b 100644 --- a/qa/qa/checks/generic/author_name.py +++ b/qa/qa/checks/generic/author_name.py @@ -8,6 +8,8 @@ from qa.checks.base import BaseGenericCheck from qa.checks.models import QaDataRegistry, OnFailurePolicy, Result +# Note: the ids in this file should be the metadata check id + 10000, +# to avoid collision with the check_ids previously used in the arxiv_checks table. _LLM_NAMES = { "llama", @@ -75,7 +77,7 @@ def _check_author(self, keyname: str, firstname: str, suffix: str) -> bool: class AuthorsDoNotContainLoneSurname(BaseAuthorCheck): name = "authors_do_not_contain_lone_surname" display_name = "Authors Do Not Contain Lone Surname" - id = 51 + id = 10051 version = "1.0.0" description = "No author has only a surname without a given name, unless it is a known collaboration or LLM name." failure_message = "Contains lone surname." @@ -112,7 +114,7 @@ def _check_author(self, keyname: str, firstname: str, suffix: str) -> bool: class AuthorsDoNotContainLlmAuthor(BaseAuthorCheck): name = "authors_do_not_contain_llm_author" display_name = "Authors Do Not Contain LLM Author" - id = 52 + id = 10052 version = "1.0.0" description = "No author's name appears to be an AI language model." failure_message = "Potential LLM author detected." @@ -148,7 +150,7 @@ def _check_author(self, keyname: str, firstname: str, suffix: str) -> bool: class AuthorNamesDoNotContainSemicolon(BaseAuthorPatternCheck): name = "author_names_do_not_contain_semicolon" display_name = "Author Names Do Not Contain Semicolon" - id = 53 + id = 10053 version = "1.0.0" description = "No parsed author name contains a semicolon." failure_message = "Contains semicolon(s) - use ',' or 'and' to separate authors." @@ -159,7 +161,7 @@ class AuthorNamesDoNotContainSemicolon(BaseAuthorPatternCheck): class AuthorNamesDoNotContainBrackets(BaseAuthorPatternCheck): name = "author_names_do_not_contain_brackets" display_name = "Author Names Do Not Contain Brackets" - id = 54 + id = 10054 version = "1.0.0" description = "No parsed author name contains square bracket characters." failure_message = "Unusual character detected." @@ -170,7 +172,7 @@ class AuthorNamesDoNotContainBrackets(BaseAuthorPatternCheck): class AuthorNamesDoNotContainNumbers(BaseAuthorPatternCheck): name = "author_names_do_not_contain_numbers" display_name = "Author Names Do Not Contain Numbers" - id = 55 + id = 10055 version = "1.0.0" description = "No parsed author name contains numeric digits." failure_message = "Contains a number." @@ -181,7 +183,7 @@ class AuthorNamesDoNotContainNumbers(BaseAuthorPatternCheck): class AuthorNamesDoNotContainAffiliation(BaseAuthorPatternCheck): name = "author_names_do_not_contain_affiliation" display_name = "Author Names Do Not Contain Affiliation" - id = 56 + id = 10056 version = "1.0.0" description = "No parsed author name contains institution or affiliation keywords." failure_message = "Contains a suffix that may be university affiliation or degree related." diff --git a/qa/qa/checks/generic/text.py b/qa/qa/checks/generic/text.py index b414d5f6..0d1af324 100644 --- a/qa/qa/checks/generic/text.py +++ b/qa/qa/checks/generic/text.py @@ -4,11 +4,13 @@ from qa.checks.base import BaseGenericCheck, BaseGenericPatternCheck from qa.checks.generic.all_caps_words import KNOWN_WORDS_IN_ALL_CAPS +# Note: the ids in this file should be the metadata check id + 10000, +# to avoid collision with the check_ids previously used in the arxiv_checks table. class DoesNotStartWithLowercase(BaseGenericPatternCheck): name = "does_not_start_with_lowercase" display_name = "Does Not Start With Lowercase" - id = 8 + id = 10008 version = "1.0.0" description = "The value does not start with a lowercase letter." failure_message = "Begins with a lowercase letter." @@ -19,7 +21,7 @@ class DoesNotStartWithLowercase(BaseGenericPatternCheck): class NoExcessiveCapitals(BaseGenericCheck): name = "no_excessive_capitals" display_name = "No Excessive Capitals" - id = 7 + id = 10007 version = "1.0.0" description = "The value does not contain excessive capitals." failure_message = "Likely excessive capitalization." @@ -39,7 +41,7 @@ def _run(self, data_registry: QaDataRegistry) -> Result: class NoUnapprovedLongCapsWords(BaseGenericPatternCheck): name = "no_unapproved_long_caps_words" display_name = "No Unapproved Long Caps Words" - id = 12 # NOTE: new + id = 10012 # NOTE: new version = "1.0.0" description = "The value does not contain two or more unapproved all caps words that are 6 or more characters long." failure_message = "Contains unapproved long caps words." @@ -68,7 +70,7 @@ def _run(self, data_registry: QaDataRegistry) -> Result: class NoBoundaryWhitespace(BaseGenericPatternCheck): name = "no_boundary_whitespace" display_name = "No Boundary Whitespace" - id = 16 + id = 10016 version = "1.0.0" description = "The value does not begin or end with whitespace." failure_message = "Leading or trailing whitespace." @@ -79,7 +81,7 @@ class NoBoundaryWhitespace(BaseGenericPatternCheck): class NoExtraWhitespace(BaseGenericPatternCheck): name = "no_extra_whitespace" display_name = "No Extra Whitespace" - id = 25 + id = 10025 version = "1.0.0" description = "The value does not contain multiple consecutive spaces, trailing whitespace before a newline, or irregular comma spacing." failure_message = "Excessive or irregular whitespace." @@ -90,7 +92,7 @@ class NoExtraWhitespace(BaseGenericPatternCheck): class NoUnnecessarySpaceInParens(BaseGenericPatternCheck): name = "no_unnecessary_space_in_parens" display_name = "No Unnecessary Space in Parens" - id = 33 + id = 10033 version = "1.0.0" description = "The value does not contain leading or trailing spaces immediately inside parentheses." failure_message = "Unnecessary space inside parentheses." @@ -101,7 +103,7 @@ class NoUnnecessarySpaceInParens(BaseGenericPatternCheck): class NoHtmlElements(BaseGenericPatternCheck): name = "no_html_elements" display_name = "No HTML Elements" - id = 11 + id = 10011 version = "1.0.0" description = "The value does not contain raw HTML elements." failure_message = "Contains HTML." @@ -131,7 +133,7 @@ class NoHtmlElements(BaseGenericPatternCheck): class AllBracketsBalanced(BaseGenericCheck): name = "all_brackets_balanced" display_name = "All Brackets Balanced" - id = 13 + id = 10013 version = "1.0.0" description = "All parentheses, square brackets, and curly braces are properly closed." failure_message = "Unbalanced brackets." @@ -170,7 +172,7 @@ def _run(self, data_registry: QaDataRegistry) -> Result: class NotTooLong(BaseGenericCheck): name = "not_too_long" display_name = "Not Too Long" - id = 36 + id = 10036 version = "1.0.0" description = "The value does not exceed the maximum character length." failure_message = "Too long." @@ -206,7 +208,7 @@ def _run(self, data_registry: QaDataRegistry) -> Result: class NotTooShort(BaseGenericCheck): name = "not_too_short" display_name = "Not Too Short" - id = 2 + id = 10002 version = "1.0.0" description = "The value meets or exceeds the minimum character length." failure_message = "Text likely too short." @@ -242,7 +244,7 @@ def _run(self, data_registry: QaDataRegistry) -> Result: class DoesNotBeginWithTitle(BaseGenericPatternCheck): name = "does_not_begin_with_title" display_name = "Does Not Begin With Title" - id = 3 + id = 10003 version = "1.0.0" description = "The value does not begin with the literal prefix 'title:'." failure_message = "Begins with 'title'." @@ -253,7 +255,7 @@ class DoesNotBeginWithTitle(BaseGenericPatternCheck): class DoesNotContainLinebreak(BaseGenericPatternCheck): name = "does_not_contain_linebreak" display_name = "Does Not Contain Linebreak" - id = 6 + id = 10006 version = "1.0.0" description = "The value does not contain LaTeX-style or escaped linebreaks." failure_message = "Contains a line break." @@ -264,7 +266,7 @@ class DoesNotContainLinebreak(BaseGenericPatternCheck): class DoesNotContainUnnecessaryEscape(BaseGenericPatternCheck): name = "does_not_contain_unnecessary_escape" display_name = "Does Not Contain Unnecessary Escape" - id = 10 + id = 10010 version = "1.0.0" description = "The value does not contain unnecessary escape characters preceding hash or percent symbols." failure_message = "Contains unnecessary escape." @@ -275,7 +277,7 @@ class DoesNotContainUnnecessaryEscape(BaseGenericPatternCheck): class DoesNotContainTex(BaseGenericPatternCheck): name = "does_not_contain_tex" display_name = "Does Not Contain TeX" - id = 9 + id = 10009 version = "1.0.0" description = "The value does not contain href or url raw TeX commands." failure_message = "Contains TeX." @@ -286,7 +288,7 @@ class DoesNotContainTex(BaseGenericPatternCheck): class DoesNotContainControlChars(BaseGenericPatternCheck): name = "does_not_contain_control_chars" display_name = "Does Not Contain Control Chars" - id = 26 + id = 10026 version = "1.0.0" description = "The value does not contain control characters including newlines, tabs, and backspaces." failure_message = "Contains control characters: newlines, tabs, or backspaces." @@ -297,7 +299,7 @@ class DoesNotContainControlChars(BaseGenericPatternCheck): class DoesNotContainControlCharsAllowNewlines(BaseGenericPatternCheck): name = "does_not_contain_control_chars_allow_newlines" display_name = "Does Not Contain Control Chars (Allow Newlines)" - id = 18 + id = 10018 version = "1.0.0" description = "The value does not contain control characters, but newlines (\\n) are permitted." failure_message = "Contains control characters." @@ -308,7 +310,7 @@ class DoesNotContainControlCharsAllowNewlines(BaseGenericPatternCheck): class NoUtf8DecodingErrors(BaseGenericPatternCheck): name = "no_utf8_decoding_errors" display_name = "No UTF-8 Decoding Errors" - id = 14 + id = 10014 version = "1.0.0" description = "The value does not contain malformed Unicode sequences." failure_message = "Bad Unicode encoding." @@ -320,7 +322,7 @@ class NoUtf8DecodingErrors(BaseGenericPatternCheck): class NoAnnotationSymbols(BaseGenericPatternCheck): name = "no_annotation_symbols" display_name = "No Annotation Symbols" - id = 15 + id = 10015 version = "1.0.0" description = "The value does not contain invalid characters such as *, #, ^, or @." failure_message = "Unusual character detected." @@ -331,7 +333,7 @@ class NoAnnotationSymbols(BaseGenericPatternCheck): class DoesNotContainAnonymous(BaseGenericPatternCheck): name = "does_not_contain_anonymous" display_name = "Does Not Contain Anonymous" - id = 19 + id = 10019 version = "1.0.0" description = "The value does not contain the word 'anonymous'." failure_message = "Contains 'anonymous'." @@ -342,7 +344,7 @@ class DoesNotContainAnonymous(BaseGenericPatternCheck): class DoesNotContainCorresponding(BaseGenericPatternCheck): name = "does_not_contain_corresponding" display_name = "Does Not Contain Corresponding" - id = 20 + id = 10020 version = "1.0.0" description = "The value does not contain the word 'corresponding'." failure_message = "Contains 'corresponding'." @@ -353,7 +355,7 @@ class DoesNotContainCorresponding(BaseGenericPatternCheck): class DoesNotContainTexDagger(BaseGenericPatternCheck): name = "does_not_contain_tex_dagger" display_name = "Does Not Contain TeX Dagger" - id = 21 + id = 10021 version = "1.0.0" description = "The value does not contain TeX dagger symbols (\\dag, \\ddag, etc.)." failure_message = "Contains a dagger symbol." @@ -364,7 +366,7 @@ class DoesNotContainTexDagger(BaseGenericPatternCheck): class DoesNotBeginWithAuthor(BaseGenericPatternCheck): name = "does_not_begin_with_author" display_name = "Does Not Begin With Author" - id = 4 + id = 10004 version = "1.0.0" description = "The value does not begin with the prefix 'author' or 'authors'." failure_message = "Begins with 'author'." @@ -375,7 +377,7 @@ class DoesNotBeginWithAuthor(BaseGenericPatternCheck): class DoesNotContainTildeAsHardSpace(BaseGenericPatternCheck): name = "does_not_contain_tilde_as_hard_space" display_name = "Does Not Contain Tilde As Hard Space" - id = 32 + id = 10032 version = "1.0.0" description = "The value does not contain an unescaped tilde used as a hard space." failure_message = "Tilde as hard space." @@ -386,7 +388,7 @@ class DoesNotContainTildeAsHardSpace(BaseGenericPatternCheck): class DoesNotBeginWithAbstract(BaseGenericPatternCheck): name = "does_not_begin_with_abstract" display_name = "Does Not Begin With Abstract" - id = 5 + id = 10005 version = "1.0.0" description = "The value does not begin with the literal prefix 'abstract'." failure_message = "Begins with 'abstract'." @@ -397,7 +399,7 @@ class DoesNotBeginWithAbstract(BaseGenericPatternCheck): class DoesNotContainTexBeginEnv(BaseGenericPatternCheck): name = "does_not_contain_tex_begin_env" display_name = "Does Not Contain TeX Begin Env" - id = 17 + id = 10017 version = "1.0.0" description = "The value does not contain a tex begin command that is not followed by a curly brace." failure_message = "Contains TeX." @@ -408,7 +410,7 @@ class DoesNotContainTexBeginEnv(BaseGenericPatternCheck): class DoesNotEndWithPunctuation(BaseGenericPatternCheck): name = "does_not_end_with_punctuation" display_name = "Does Not End With Punctuation" - id = 29 + id = 10029 version = "1.0.0" description = "The value does not end with punctuation (trailing 'et al.' is permitted)." failure_message = "Ends with punctuation." @@ -419,7 +421,7 @@ class DoesNotEndWithPunctuation(BaseGenericPatternCheck): class DoesNotContainUrl(BaseGenericPatternCheck): name = "does_not_contain_url" display_name = "Does Not Contain URL" - id = 39 + id = 10039 version = "1.0.0" description = "The value does not contain a URL." failure_message = "Contains a URL." @@ -430,7 +432,7 @@ class DoesNotContainUrl(BaseGenericPatternCheck): class DoesNotContainDoi(BaseGenericPatternCheck): name = "does_not_contain_doi" display_name = "Does Not Contain DOI" - id = 45 + id = 10045 version = "1.0.0" description = "The value does not contain the word 'DOI'." failure_message = "Contains 'DOI'." @@ -441,7 +443,7 @@ class DoesNotContainDoi(BaseGenericPatternCheck): class DoesNotContainBareDoi(BaseGenericPatternCheck): name = "does_not_contain_bare_doi" display_name = "Does Not Contain Bare DOI" - id = 40 + id = 10040 version = "1.0.0" description = "The value does not contain a bare DOI number (e.g. 10.1234/abc)." failure_message = "Contains a DOI." @@ -452,7 +454,7 @@ class DoesNotContainBareDoi(BaseGenericPatternCheck): class ContainsLetters(BaseGenericPatternCheck): name = "contains_letters" display_name = "Contains Letters" - id = 38 + id = 10038 version = "1.0.0" description = "The value contains at least one letter." failure_message = "No letters found." @@ -463,7 +465,7 @@ class ContainsLetters(BaseGenericPatternCheck): class ContainsDigits(BaseGenericPatternCheck): name = "contains_digits" display_name = "Contains Digits" - id = 37 + id = 10037 version = "1.0.0" description = "The value contains at least one digit." failure_message = "No digits found." @@ -474,7 +476,7 @@ class ContainsDigits(BaseGenericPatternCheck): class DoesNotContainSemicolon(BaseGenericPatternCheck): # TODO remove? name = "does_not_contain_semicolon" display_name = "Does Not Contain Semicolon" - id = 22 + id = 10022 version = "1.0.0" description = "The value does not contain a semicolon." failure_message = "Contains semicolon(s) - use ',' or 'and' to separate authors." @@ -485,7 +487,7 @@ class DoesNotContainSemicolon(BaseGenericPatternCheck): # TODO remove? class DoesNotContainAccepted(BaseGenericPatternCheck): name = "does_not_contain_accepted" display_name = "Does Not Contain Accepted" - id = 41 + id = 10041 version = "1.0.0" description = "The value does not contain the word 'accepted'." failure_message = "Contains 'accepted'." @@ -496,7 +498,7 @@ class DoesNotContainAccepted(BaseGenericPatternCheck): class DoesNotContainSubmitted(BaseGenericPatternCheck): name = "does_not_contain_submitted" display_name = "Does Not Contain Submitted" - id = 42 + id = 10042 version = "1.0.0" description = "The value does not contain the word 'submitted'." failure_message = "Contains 'submitted'." @@ -507,7 +509,7 @@ class DoesNotContainSubmitted(BaseGenericPatternCheck): class DoesNotContainBibtex(BaseGenericPatternCheck): name = "does_not_contain_bibtex" display_name = "Does Not Contain BibTeX" - id = 44 + id = 10044 version = "1.0.0" description = "The value does not contain BibTeX field assignments." failure_message = "Contains bibtex." @@ -518,7 +520,7 @@ class DoesNotContainBibtex(BaseGenericPatternCheck): class DoesNotContainBadDoiPrefix(BaseGenericPatternCheck): name = "does_not_contain_bad_doi_prefix" display_name = "Does Not Contain Bad DOI Prefix" - id = 47 + id = 10047 version = "1.0.0" description = "The value does not begin with 'doi:', 'https://doi.org/', or similar URL prefixes." failure_message = "Contains unnecessary prefix." @@ -529,10 +531,10 @@ class DoesNotContainBadDoiPrefix(BaseGenericPatternCheck): class DoiHasValidFormat(BaseGenericPatternCheck): name = "doi_has_valid_format" display_name = "DOI Has Valid Format" - id = 50 + id = 10050 version = "1.0.0" description = "Each space-separated DOI in the value matches the expected DOI format." - failure_message = "Invalid DOI." + failure_message = "Invaleeid DOI." _pattern = r"(?i)^(?![0-9][0-9]*\.[0-9][0-9]*/[A-Za-z0-9():;._/-]*$)" diff --git a/qa/qa/checks/metadata/abstract.py b/qa/qa/checks/metadata/abstract.py index 0e1fd7c9..1a8b753b 100644 --- a/qa/qa/checks/metadata/abstract.py +++ b/qa/qa/checks/metadata/abstract.py @@ -1,7 +1,7 @@ """Abstract metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result from qa.checks.generic.text import ( AllBracketsBalanced, DoesNotBeginWithAbstract, @@ -27,7 +27,7 @@ class AbstractIsValid(BaseAggregateCheck): name = "abstract_is_valid" display_name = "Abstract Is Valid" - id = 520 + id = 300 version = "1.0.0" description = "The metadata abstract field is valid." on_failure_policy = OnFailurePolicy.REJECT @@ -37,7 +37,7 @@ class AbstractIsValid(BaseAggregateCheck): @classmethod def check(cls, abstract: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=Metadata(abstract=abstract))) + return cls().run(QaDataRegistry(metadata=SubmissionMetadata(abstract=abstract))) _checks = ( NotTooShort(5, on_failure_policy=OnFailurePolicy.WARN, data="metadata", field="abstract"), diff --git a/qa/qa/checks/metadata/acm_class.py b/qa/qa/checks/metadata/acm_class.py index 123a4d4f..ca1de6b1 100644 --- a/qa/qa/checks/metadata/acm_class.py +++ b/qa/qa/checks/metadata/acm_class.py @@ -1,7 +1,7 @@ """ACM class metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result from qa.checks.generic.text import ( NotTooLong, DoesNotContainUrl, @@ -19,7 +19,7 @@ class AcmClassIsValid(BaseAggregateCheck): name = "acm_class_is_valid" display_name = "ACM Class Is Valid" - id = 590 + id = 900 version = "1.0.0" description = "The metadata acm_class field is valid." on_failure_policy = OnFailurePolicy.REJECT @@ -29,7 +29,7 @@ class AcmClassIsValid(BaseAggregateCheck): @classmethod def check(cls, acm_class: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=Metadata(acm_class=acm_class))) + return cls().run(QaDataRegistry(metadata=SubmissionMetadata(acm_class=acm_class))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/authors.py b/qa/qa/checks/metadata/authors.py index 695fc93a..1dfc075c 100644 --- a/qa/qa/checks/metadata/authors.py +++ b/qa/qa/checks/metadata/authors.py @@ -1,7 +1,7 @@ """Author metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result from qa.checks.generic.text import ( AllBracketsBalanced, DoesNotBeginWithAuthor, @@ -36,7 +36,7 @@ class AuthorsAreValid(BaseAggregateCheck): name = "authors_are_valid" display_name = "Authors Are Valid" - id = 510 + id = 200 version = "1.0.0" description = "The metadata authors field is valid." on_failure_policy = OnFailurePolicy.REJECT @@ -46,7 +46,7 @@ class AuthorsAreValid(BaseAggregateCheck): @classmethod def check(cls, authors: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=Metadata(authors=authors))) + return cls().run(QaDataRegistry(metadata=SubmissionMetadata(authors=authors))) _checks = ( NotTooShort(4, on_failure_policy=OnFailurePolicy.WARN, data="metadata", field="authors"), diff --git a/qa/qa/checks/metadata/comments.py b/qa/qa/checks/metadata/comments.py index 1236232a..17c21001 100644 --- a/qa/qa/checks/metadata/comments.py +++ b/qa/qa/checks/metadata/comments.py @@ -1,7 +1,7 @@ """Comments metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result from qa.checks.generic.text import ( NotTooLong, DoesNotContainLinebreak, @@ -22,7 +22,7 @@ class CommentsAreValid(BaseAggregateCheck): name = "comments_are_valid" display_name = "Comments Are Valid" - id = 530 + id = 400 version = "1.0.0" description = "The metadata comments field is valid." on_failure_policy = OnFailurePolicy.REJECT @@ -32,7 +32,7 @@ class CommentsAreValid(BaseAggregateCheck): @classmethod def check(cls, comments: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=Metadata(comments=comments))) + return cls().run(QaDataRegistry(metadata=SubmissionMetadata(comments=comments))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/doi.py b/qa/qa/checks/metadata/doi.py index 4720a26f..7e4e3cd0 100644 --- a/qa/qa/checks/metadata/doi.py +++ b/qa/qa/checks/metadata/doi.py @@ -1,7 +1,7 @@ """DOI metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result from qa.checks.generic.text import ( NotTooShort, NotTooLong, @@ -22,7 +22,7 @@ class DoiIsValid(BaseAggregateCheck): name = "doi_is_valid" display_name = "DOI Is Valid" - id = 570 + id = 700 version = "1.0.0" description = "The metadata doi field is valid." on_failure_policy = OnFailurePolicy.REJECT @@ -32,7 +32,7 @@ class DoiIsValid(BaseAggregateCheck): @classmethod def check(cls, doi: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=Metadata(doi=doi))) + return cls().run(QaDataRegistry(metadata=SubmissionMetadata(doi=doi))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/journal_ref.py b/qa/qa/checks/metadata/journal_ref.py index 9c747186..75d3dcd6 100644 --- a/qa/qa/checks/metadata/journal_ref.py +++ b/qa/qa/checks/metadata/journal_ref.py @@ -1,7 +1,7 @@ """Journal reference metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result from qa.checks.generic.text import ( NotTooShort, NotTooLong, @@ -24,7 +24,7 @@ class JournalRefIsValid(BaseAggregateCheck): name = "journal_ref_is_valid" display_name = "Journal Reference Is Valid" - id = 560 + id = 600 version = "1.0.0" description = "The metadata journal_ref field is valid." on_failure_policy = OnFailurePolicy.REJECT @@ -34,7 +34,7 @@ class JournalRefIsValid(BaseAggregateCheck): @classmethod def check(cls, journal_ref: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=Metadata(journal_ref=journal_ref))) + return cls().run(QaDataRegistry(metadata=SubmissionMetadata(journal_ref=journal_ref))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/msc_class.py b/qa/qa/checks/metadata/msc_class.py index c65d6b82..3ae68e26 100644 --- a/qa/qa/checks/metadata/msc_class.py +++ b/qa/qa/checks/metadata/msc_class.py @@ -1,7 +1,7 @@ """MSC class metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result from qa.checks.generic.text import ( NotTooLong, DoesNotContainUrl, @@ -20,7 +20,7 @@ class MscClassIsValid(BaseAggregateCheck): name = "msc_class_is_valid" display_name = "MSC Class Is Valid" - id = 580 + id = 800 version = "1.0.0" description = "The metadata msc_class field is valid." on_failure_policy = OnFailurePolicy.REJECT @@ -30,7 +30,7 @@ class MscClassIsValid(BaseAggregateCheck): @classmethod def check(cls, msc_class: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=Metadata(msc_class=msc_class))) + return cls().run(QaDataRegistry(metadata=SubmissionMetadata(msc_class=msc_class))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/report_num.py b/qa/qa/checks/metadata/report_num.py index d717743d..7906f7ca 100644 --- a/qa/qa/checks/metadata/report_num.py +++ b/qa/qa/checks/metadata/report_num.py @@ -1,7 +1,7 @@ """Report number metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result from qa.checks.generic.text import ( NotTooShort, NotTooLong, @@ -22,7 +22,7 @@ class ReportNumIsValid(BaseAggregateCheck): name = "report_num_is_valid" display_name = "Report Number Is Valid" - id = 550 + id = 500 version = "1.0.0" description = "The metadata report_num field is valid." on_failure_policy = OnFailurePolicy.REJECT @@ -32,7 +32,7 @@ class ReportNumIsValid(BaseAggregateCheck): @classmethod def check(cls, report_num: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=Metadata(report_num=report_num))) + return cls().run(QaDataRegistry(metadata=SubmissionMetadata(report_num=report_num))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/title.py b/qa/qa/checks/metadata/title.py index beac44ed..7cadbb0c 100644 --- a/qa/qa/checks/metadata/title.py +++ b/qa/qa/checks/metadata/title.py @@ -1,5 +1,7 @@ +"""Title metadata checks.""" + from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result from qa.checks.generic.text import ( NotTooShort, NotTooLong, @@ -25,7 +27,7 @@ class TitleIsValid(BaseAggregateCheck): name = "title_is_valid" display_name = "Title Is Valid" - id = 500 + id = 100 version = "1.0.0" description = "The metadata title field is valid." on_failure_policy = OnFailurePolicy.REJECT @@ -35,7 +37,7 @@ class TitleIsValid(BaseAggregateCheck): @classmethod def check(cls, title: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=Metadata(title=title))) + return cls().run(QaDataRegistry(metadata=SubmissionMetadata(title=title))) _checks = ( NotTooShort(5, on_failure_policy=OnFailurePolicy.WARN, data="metadata", field="title"), diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index 9cfe6ec5..18bbcc4a 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -52,9 +52,13 @@ class Result(BaseModel): results: list["Result"] | None = None -class Metadata(BaseModel): - """A concrete implementation of MetadataProtocol for use in checks.""" +class SubmissionMetadata(BaseModel): + """ + Information about a submission. + This should be a concrete implementation of MetadataProtocol for use in checks. + """ + # MetadataProtocol fields title: str | None = None authors: str | None = None abstract: str | None = None @@ -64,7 +68,11 @@ class Metadata(BaseModel): doi: str | None = None msc_class: str | None = None acm_class: str | None = None - + # End MetadataProtocol fields + type: str | None = None # one of: "new", "rep", "wdr", "jref", or "cross" + is_overlap: bool = False + data_version: int = 0 + metadata_version: int = 0 @runtime_checkable class MetadataProtocol(Protocol): @@ -93,4 +101,4 @@ class QaDataRegistry(BaseModel): author_report: str | None = None flagged_terms_report: str | None = None tex_report: str | None = None - metadata: Metadata | None = None + metadata: SubmissionMetadata | None = None diff --git a/qa/tests/checks/check_unique_ids.py b/qa/tests/checks/check_unique_ids.py new file mode 100644 index 00000000..8082ddcf --- /dev/null +++ b/qa/tests/checks/check_unique_ids.py @@ -0,0 +1,12 @@ +"""Some consistency checks on all_checks.""" + +from qa.checks import checks as all_checks + +class TestCheckIds: + def test_all_check_ids_unique(self): + assert len(all_checks) == len(set([check.id for check in all_checks])) + +class TestCheckNames: + def test_all_check_names_unique(self): + assert len(all_checks) == len(set([check.name.lower() for check in all_checks])) + diff --git a/qa/tests/checks/generic/test_text.py b/qa/tests/checks/generic/test_text.py index e67dfb09..89d3f2a9 100644 --- a/qa/tests/checks/generic/test_text.py +++ b/qa/tests/checks/generic/test_text.py @@ -3,7 +3,7 @@ import pytest from qa.checks.base import EmptyFieldError, MissingDataError -from qa.checks.models import QaDataRegistry, Metadata, OnFailurePolicy +from qa.checks.models import QaDataRegistry, SubmissionMetadata, OnFailurePolicy from qa.checks.generic.text import ( AllBracketsBalanced, DoesNotBeginWithTitle, @@ -25,7 +25,7 @@ def inputs(title: str | None) -> QaDataRegistry: - return QaDataRegistry(metadata=Metadata(title=title)) + return QaDataRegistry(metadata=SubmissionMetadata(title=title)) def make(cls, **kwargs): diff --git a/qa/tests/checks/metadata/test_abstract.py b/qa/tests/checks/metadata/test_abstract.py index 20f67b12..d9fb00e6 100644 --- a/qa/tests/checks/metadata/test_abstract.py +++ b/qa/tests/checks/metadata/test_abstract.py @@ -3,7 +3,7 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result +from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result from qa.checks.metadata.abstract import AbstractIsValid @@ -87,7 +87,7 @@ def test_warn_tex_begin_no_brace(self): assert not sub_result(result, "does_not_contain_tex_begin_env").passed def test_none_field_short_circuits(self): - result = AbstractIsValid().run(QaDataRegistry(metadata=Metadata(abstract=None))) + result = AbstractIsValid().run(QaDataRegistry(metadata=SubmissionMetadata(abstract=None))) assert not result.passed assert result.results == [] assert result.message == "Abstract is invalid or empty." @@ -99,7 +99,7 @@ def test_missing_metadata_raises(self): def test_result_has_check_metadata(self): result = AbstractIsValid.check("A fine abstract with enough text.") assert result.check_config["name"] == "abstract_is_valid" - assert result.check_config["id"] == 520 + assert result.check_config["id"] == 300 assert result.check_config["version"] == "1.0.0" assert result.check_config["on_failure_policy"] == OnFailurePolicy.REJECT diff --git a/qa/tests/checks/metadata/test_acm_class.py b/qa/tests/checks/metadata/test_acm_class.py index 46dd6555..67a20b5e 100644 --- a/qa/tests/checks/metadata/test_acm_class.py +++ b/qa/tests/checks/metadata/test_acm_class.py @@ -56,6 +56,6 @@ def test_missing_metadata_raises(self): def test_result_has_check_metadata(self): result = AcmClassIsValid.check("F.2.2") assert result.check_config["name"] == "acm_class_is_valid" - assert result.check_config["id"] == 590 + assert result.check_config["id"] == 900 assert result.check_config["version"] == "1.0.0" assert result.check_config["on_failure_policy"] == OnFailurePolicy.REJECT diff --git a/qa/tests/checks/metadata/test_authors.py b/qa/tests/checks/metadata/test_authors.py index 2f399816..ee4cafb3 100644 --- a/qa/tests/checks/metadata/test_authors.py +++ b/qa/tests/checks/metadata/test_authors.py @@ -3,7 +3,7 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result +from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result from qa.checks.metadata.authors import AuthorsAreValid @@ -267,7 +267,7 @@ def test_pass_and_separated(self): ).passed def test_none_field_short_circuits(self): - result = AuthorsAreValid().run(QaDataRegistry(metadata=Metadata(authors=None))) + result = AuthorsAreValid().run(QaDataRegistry(metadata=SubmissionMetadata(authors=None))) assert not result.passed assert result.results == [] assert result.message == "Authors are invalid or empty." @@ -279,7 +279,7 @@ def test_missing_metadata_raises(self): def test_result_has_check_metadata(self): result = AuthorsAreValid.check("Fred Smith") assert result.check_config["name"] == "authors_are_valid" - assert result.check_config["id"] == 510 + assert result.check_config["id"] == 200 assert result.check_config["version"] == "1.0.0" assert result.check_config["on_failure_policy"] == OnFailurePolicy.REJECT diff --git a/qa/tests/checks/metadata/test_comments.py b/qa/tests/checks/metadata/test_comments.py index bdbc64c9..577b1f44 100644 --- a/qa/tests/checks/metadata/test_comments.py +++ b/qa/tests/checks/metadata/test_comments.py @@ -53,6 +53,6 @@ def test_missing_metadata_raises(self): def test_result_has_check_metadata(self): result = CommentsAreValid.check("12 pages, 3 figures") assert result.check_config["name"] == "comments_are_valid" - assert result.check_config["id"] == 530 + assert result.check_config["id"] == 400 assert result.check_config["version"] == "1.0.0" assert result.check_config["on_failure_policy"] == OnFailurePolicy.REJECT diff --git a/qa/tests/checks/metadata/test_doi.py b/qa/tests/checks/metadata/test_doi.py index c2cb0a0b..056e9de9 100644 --- a/qa/tests/checks/metadata/test_doi.py +++ b/qa/tests/checks/metadata/test_doi.py @@ -87,6 +87,6 @@ def test_missing_metadata_raises(self): def test_result_has_check_metadata(self): result = DoiIsValid.check("10.1103/PhysRevLett.132.011001") assert result.check_config["name"] == "doi_is_valid" - assert result.check_config["id"] == 570 + assert result.check_config["id"] == 700 assert result.check_config["version"] == "1.0.0" assert result.check_config["on_failure_policy"] == OnFailurePolicy.REJECT diff --git a/qa/tests/checks/metadata/test_journal_ref.py b/qa/tests/checks/metadata/test_journal_ref.py index 54e8b023..73bd0740 100644 --- a/qa/tests/checks/metadata/test_journal_ref.py +++ b/qa/tests/checks/metadata/test_journal_ref.py @@ -78,6 +78,6 @@ def test_missing_metadata_raises(self): def test_result_has_check_metadata(self): result = JournalRefIsValid.check("Phys. Rev. Lett. 132, 011001 (2024)") assert result.check_config["name"] == "journal_ref_is_valid" - assert result.check_config["id"] == 560 + assert result.check_config["id"] == 600 assert result.check_config["version"] == "1.0.0" assert result.check_config["on_failure_policy"] == OnFailurePolicy.REJECT diff --git a/qa/tests/checks/metadata/test_msc_class.py b/qa/tests/checks/metadata/test_msc_class.py index 387e7d0b..1bf85d8e 100644 --- a/qa/tests/checks/metadata/test_msc_class.py +++ b/qa/tests/checks/metadata/test_msc_class.py @@ -64,6 +64,6 @@ def test_missing_metadata_raises(self): def test_result_has_check_metadata(self): result = MscClassIsValid.check("35K55") assert result.check_config["name"] == "msc_class_is_valid" - assert result.check_config["id"] == 580 + assert result.check_config["id"] == 800 assert result.check_config["version"] == "1.0.0" assert result.check_config["on_failure_policy"] == OnFailurePolicy.REJECT diff --git a/qa/tests/checks/metadata/test_report_num.py b/qa/tests/checks/metadata/test_report_num.py index c79f2d73..958dbd54 100644 --- a/qa/tests/checks/metadata/test_report_num.py +++ b/qa/tests/checks/metadata/test_report_num.py @@ -96,6 +96,6 @@ def test_missing_metadata_raises(self): def test_result_has_check_metadata(self): result = ReportNumIsValid.check("CERN-EP-2024-001") assert result.check_config["name"] == "report_num_is_valid" - assert result.check_config["id"] == 550 + assert result.check_config["id"] == 500 assert result.check_config["version"] == "1.0.0" assert result.check_config["on_failure_policy"] == OnFailurePolicy.REJECT diff --git a/qa/tests/checks/metadata/test_title.py b/qa/tests/checks/metadata/test_title.py index 1ace02c7..678caec5 100644 --- a/qa/tests/checks/metadata/test_title.py +++ b/qa/tests/checks/metadata/test_title.py @@ -3,7 +3,7 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result +from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result from qa.checks.metadata.title import TitleIsValid @@ -71,7 +71,7 @@ def test_pass_long_greek(self): assert result.passed def test_none_field_short_circuits(self): - result = TitleIsValid().run(QaDataRegistry(metadata=Metadata(title=None))) + result = TitleIsValid().run(QaDataRegistry(metadata=SubmissionMetadata(title=None))) assert not result.passed assert result.results == [] assert result.message == "Title is invalid or empty." @@ -83,7 +83,7 @@ def test_missing_metadata_raises(self): def test_result_has_check_metadata(self): result = TitleIsValid.check("A fine title") assert result.check_config["name"] == "title_is_valid" - assert result.check_config["id"] == 500 + assert result.check_config["id"] == 100 assert result.check_config["version"] == "1.0.0" assert result.check_config["on_failure_policy"] == OnFailurePolicy.REJECT From f9d3c3390d84adc39732f322bfe66a6c4731b926 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Wed, 24 Jun 2026 11:31:33 -0400 Subject: [PATCH 08/34] QA-148 changes from CR feedback. --- qa/qa/checks/__init__.py | 8 +++++--- qa/qa/checks/fulltext/text_checks.py | 17 +++++++++-------- qa/tests/checks/fulltext/test_text.py | 18 +++++++++--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/qa/qa/checks/__init__.py b/qa/qa/checks/__init__.py index 870789df..407b078b 100644 --- a/qa/qa/checks/__init__.py +++ b/qa/qa/checks/__init__.py @@ -11,7 +11,9 @@ from qa.checks.metadata.report_num import ReportNumIsValid # noqa from qa.checks.metadata.title import TitleIsValid # noqa -from qa.checks.fulltext.text_checks import MissingTextCheck, VeryShortTextCheck # noqa +from qa.checks.fulltext.text_checks import ( + FulltextExtractedCheck, FulltextNotTooShortCheck +) # noqa checks: list[BaseCheck] = [ TitleIsValid(), @@ -23,6 +25,6 @@ DoiIsValid(), MscClassIsValid(), AcmClassIsValid(), - MissingTextCheck(), - VeryShortTextCheck(), + FulltextExtractedCheck(), + FulltextNotTooShortCheck(), ] diff --git a/qa/qa/checks/fulltext/text_checks.py b/qa/qa/checks/fulltext/text_checks.py index 377b35f4..707af46d 100644 --- a/qa/qa/checks/fulltext/text_checks.py +++ b/qa/qa/checks/fulltext/text_checks.py @@ -4,14 +4,14 @@ from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result -class MissingTextCheck(BaseCheck): +class FulltextExtractedCheck(BaseCheck): """Check for missing text (probably PDF invalid or password protected).""" - name = "missing_text" - display_name = "Missing Text" + name = "text_extraction_successful" + display_name = "Text Extraction Successful" id = 14 version = "1.0.0" - description = "Failed to extract text." + description = "Text extraction was successful." on_failure_policy = OnFailurePolicy.REJECT failure_message = "Missing text (check PDF?)" @@ -19,6 +19,7 @@ class MissingTextCheck(BaseCheck): def _run(self, data_registry: QaDataRegistry) -> Result: fulltext = data_registry.fulltext + # fulltext should never be None here if fulltext is not None and fulltext != "": passed = True return self._result(passed) @@ -27,14 +28,14 @@ def _run(self, data_registry: QaDataRegistry) -> Result: return self._result(passed, message = self.failure_message) -class VeryShortTextCheck(BaseCheck): +class FulltextNotTooShortCheck(BaseCheck): """Check for very short text.""" - name = "short_text" - display_name = "Short Text" + name = "fulltext_not_too_short" + display_name = "Fulltext Not Too Short" id = 15 version = "1.0.0" - description = "The full text extracted is too short." + description = "The full text extracted is not too short." on_failure_policy = OnFailurePolicy.REJECT failure_message = "Very short text (< 1400 words). Check full text." diff --git a/qa/tests/checks/fulltext/test_text.py b/qa/tests/checks/fulltext/test_text.py index 09bf8ce8..e946d5a5 100644 --- a/qa/tests/checks/fulltext/test_text.py +++ b/qa/tests/checks/fulltext/test_text.py @@ -2,36 +2,36 @@ from qa.checks.models import QaDataRegistry from qa.checks.fulltext.text_checks import ( - MissingTextCheck, - VeryShortTextCheck, + FulltextExtractedCheck, + FulltextNotTooShortCheck, ) -class TestMissingText: +class TestFulltextExtractedCheck: def test_pass_normal(self): text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " - result = MissingTextCheck().run(QaDataRegistry(fulltext=text)) + result = FulltextExtractedCheck().run(QaDataRegistry(fulltext=text)) assert result.passed def test_fail_on_none(self): text = None - result = MissingTextCheck().run(QaDataRegistry(fulltext=text)) + result = FulltextExtractedCheck().run(QaDataRegistry(fulltext=text)) assert not result.passed def test_fail_on_empty(self): text = "" - result = MissingTextCheck().run(QaDataRegistry(fulltext=text)) + result = FulltextExtractedCheck().run(QaDataRegistry(fulltext=text)) assert not result.passed -class TestVeryShortText: +class TestFulltextNotTooShortCheck: def test_pass_normal(self): text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " * 140 - result = VeryShortTextCheck().run(QaDataRegistry(fulltext=text)) + result = FulltextNotTooShortCheck().run(QaDataRegistry(fulltext=text)) assert result.passed def test_fail_nospaces(self): text = "Inthiswork, westudyaaa, bbb, andcccandconcludeddd. " * 100 - result = VeryShortTextCheck().run(QaDataRegistry(fulltext=text)) + result = FulltextNotTooShortCheck().run(QaDataRegistry(fulltext=text)) assert not result.passed From 2f350d4e07a08525629c955ab370b9df7632c765 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Wed, 24 Jun 2026 23:58:03 -0400 Subject: [PATCH 09/34] QA-173 and QA-174 add checks for oversize and withdrawals. Checks fail when an issue is detected, and are named after the issue they detect. --- qa/qa/checks/metadata/oversize.py | 25 ++++++++ qa/qa/checks/metadata/withdrawal.py | 30 ++++++++++ qa/tests/checks/metadata/test_oversize.py | 60 ++++++++++++++++++++ qa/tests/checks/metadata/test_withdrawals.py | 31 ++++++++++ 4 files changed, 146 insertions(+) create mode 100644 qa/qa/checks/metadata/oversize.py create mode 100644 qa/qa/checks/metadata/withdrawal.py create mode 100644 qa/tests/checks/metadata/test_oversize.py create mode 100644 qa/tests/checks/metadata/test_withdrawals.py diff --git a/qa/qa/checks/metadata/oversize.py b/qa/qa/checks/metadata/oversize.py new file mode 100644 index 00000000..3f258235 --- /dev/null +++ b/qa/qa/checks/metadata/oversize.py @@ -0,0 +1,25 @@ + +from qa.checks.base import BaseCheck +from qa.checks. models import OnFailurePolicy, QaDataRegistry, Result + + +class OversizeCheck(BaseCheck): + name = "oversize" + display_name = "Oversize" + id = 48 + version = "0.1.0" + description = "The oversize check fails on submissions which are too large." + on_failure_policy = OnFailurePolicy.REJECT + failure_message = "Submission is oversize ." + + required_inputs = {"metadata"} + + def _run(self, data_registry: QaDataRegistry) -> Result: + if data_registry.metadata.is_oversize: + passed = False + message = self.failure_message + return self._result(passed=passed, message=message) + else: + return self._result(passed=True, message="") + + diff --git a/qa/qa/checks/metadata/withdrawal.py b/qa/qa/checks/metadata/withdrawal.py new file mode 100644 index 00000000..39650f34 --- /dev/null +++ b/qa/qa/checks/metadata/withdrawal.py @@ -0,0 +1,30 @@ + +from qa.checks.base import BaseCheck + +from qa.checks. models import OnFailurePolicy, QaDataRegistry, Result + + +class WithdrawalCheck(BaseCheck): + """Check for withdrawals, which require staff approval.""" + + name = "withdrawal" + display_name = "Withdrawal" + id = 14 + version = "1.0.0" + description = "Review all withdrawals." + on_failure_policy = OnFailurePolicy.REJECT + failure_message = " Result: + _type = data_registry.metadata.type + + if _type == "wdr": + passed = False + return self._result(passed, message = self.failure_message) + else: + passed = True + return self._result(passed) + + diff --git a/qa/tests/checks/metadata/test_oversize.py b/qa/tests/checks/metadata/test_oversize.py new file mode 100644 index 00000000..1ab2e813 --- /dev/null +++ b/qa/tests/checks/metadata/test_oversize.py @@ -0,0 +1,60 @@ +"""Tests for AbstractIsValid.""" + +import pytest + +from qa.checks.base import MissingDataError +from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result +from qa.checks.metadata.oversize import OversizeCheck + + +class TestOversizeCheck: + def test_new_pass(self): + check = OversizeCheck() + metadata = SubmissionMetadata( + type="new", + title="Test Title", + is_oversize=False) + registry = QaDataRegistry(metadata=metadata) + result = check._run(registry) + assert result.passed + + def test_rep_pass(self): + check = OversizeCheck() + metadata = SubmissionMetadata( + type="rep", + title="Test Title", + is_oversize=False) + registry = QaDataRegistry(metadata=metadata) + result = check._run(registry) + assert result.passed + + def test_cross_pass(self): + check = OversizeCheck() + metadata = SubmissionMetadata( + type="cross", + title="Test Title", + is_oversize=False) + registry = QaDataRegistry(metadata=metadata) + result = check._run(registry) + assert result.passed + + def test_oversize_fail(self): + check = OversizeCheck() + metadata = SubmissionMetadata( + type="new", + title="Test Title", + is_oversize=True) + registry = QaDataRegistry(metadata=metadata) + result = check._run(registry) + assert not result.passed + + def test_oversize_rep_fail(self): + check = OversizeCheck() + metadata = SubmissionMetadata( + type="rep", + title="Test Title", + is_oversize=True) + registry = QaDataRegistry(metadata=metadata) + result = check._run(registry) + assert not result.passed + diff --git a/qa/tests/checks/metadata/test_withdrawals.py b/qa/tests/checks/metadata/test_withdrawals.py new file mode 100644 index 00000000..f04b4253 --- /dev/null +++ b/qa/tests/checks/metadata/test_withdrawals.py @@ -0,0 +1,31 @@ +"""Tests for AbstractIsValid.""" + +import pytest + +from qa.checks.base import MissingDataError +from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result +from qa.checks.metadata.withdrawal import WithdrawalCheck + + +class TestWithdrawalCheck: + def test_new_pass(self): + check = WithdrawalCheck() + metadata = SubmissionMetadata(type="new", title="Test Title") + registry = QaDataRegistry(metadata=metadata) + result = check._run(registry) + assert result.passed + + def test_rep_pass(self): + check = WithdrawalCheck() + metadata = SubmissionMetadata(type="rep", title="Test Title") + registry = QaDataRegistry(metadata=metadata) + result = check._run(registry) + assert result.passed + + def test_wdr_fail(self): + check = WithdrawalCheck() + metadata = SubmissionMetadata(type="wdr", title="Test Title") + registry = QaDataRegistry(metadata=metadata) + result = check._run(registry) + assert not result.passed + From 94664a2bbe6df180a9bc7c24e8282f31d78a7d2c Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Thu, 25 Jun 2026 00:02:20 -0400 Subject: [PATCH 10/34] QA-174 check for withdrawals QA-173 check for oversize --- qa/qa/checks/__init__.py | 5 +++++ qa/qa/checks/models.py | 2 +- qa/tests/checks/metadata/test_oversize.py | 5 +---- qa/tests/checks/metadata/test_withdrawals.py | 5 +---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/qa/qa/checks/__init__.py b/qa/qa/checks/__init__.py index 407b078b..d19d09a6 100644 --- a/qa/qa/checks/__init__.py +++ b/qa/qa/checks/__init__.py @@ -11,6 +11,9 @@ from qa.checks.metadata.report_num import ReportNumIsValid # noqa from qa.checks.metadata.title import TitleIsValid # noqa +from qa.checks.metadata.oversize import OversizeCheck # noqa +from qa.checks.metadata.withdrawal import WithdrawalCheck # noqa + from qa.checks.fulltext.text_checks import ( FulltextExtractedCheck, FulltextNotTooShortCheck ) # noqa @@ -25,6 +28,8 @@ DoiIsValid(), MscClassIsValid(), AcmClassIsValid(), + OversizeCheck(), + WithdrawalCheck(), FulltextExtractedCheck(), FulltextNotTooShortCheck(), ] diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index 18bbcc4a..51a663e0 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -70,7 +70,7 @@ class SubmissionMetadata(BaseModel): acm_class: str | None = None # End MetadataProtocol fields type: str | None = None # one of: "new", "rep", "wdr", "jref", or "cross" - is_overlap: bool = False + is_oversize: bool = False data_version: int = 0 metadata_version: int = 0 diff --git a/qa/tests/checks/metadata/test_oversize.py b/qa/tests/checks/metadata/test_oversize.py index 1ab2e813..de228f3b 100644 --- a/qa/tests/checks/metadata/test_oversize.py +++ b/qa/tests/checks/metadata/test_oversize.py @@ -1,9 +1,6 @@ """Tests for AbstractIsValid.""" -import pytest - -from qa.checks.base import MissingDataError -from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, SubmissionMetadata from qa.checks.metadata.oversize import OversizeCheck diff --git a/qa/tests/checks/metadata/test_withdrawals.py b/qa/tests/checks/metadata/test_withdrawals.py index f04b4253..e247407f 100644 --- a/qa/tests/checks/metadata/test_withdrawals.py +++ b/qa/tests/checks/metadata/test_withdrawals.py @@ -1,9 +1,6 @@ """Tests for AbstractIsValid.""" -import pytest - -from qa.checks.base import MissingDataError -from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, SubmissionMetadata from qa.checks.metadata.withdrawal import WithdrawalCheck From 6b785163e316d0ba8e64d867bbe616561e62c037 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Thu, 25 Jun 2026 08:32:52 -0400 Subject: [PATCH 11/34] QA-174 check for withdrawals QA-173 check for oversize (typechecker fixes). --- qa/qa/checks/metadata/oversize.py | 1 + qa/qa/checks/metadata/withdrawal.py | 1 + 2 files changed, 2 insertions(+) diff --git a/qa/qa/checks/metadata/oversize.py b/qa/qa/checks/metadata/oversize.py index 3f258235..e98a7ee8 100644 --- a/qa/qa/checks/metadata/oversize.py +++ b/qa/qa/checks/metadata/oversize.py @@ -15,6 +15,7 @@ class OversizeCheck(BaseCheck): required_inputs = {"metadata"} def _run(self, data_registry: QaDataRegistry) -> Result: + assert data_registry.metadata is not None if data_registry.metadata.is_oversize: passed = False message = self.failure_message diff --git a/qa/qa/checks/metadata/withdrawal.py b/qa/qa/checks/metadata/withdrawal.py index 39650f34..a5eee698 100644 --- a/qa/qa/checks/metadata/withdrawal.py +++ b/qa/qa/checks/metadata/withdrawal.py @@ -18,6 +18,7 @@ class WithdrawalCheck(BaseCheck): required_inputs = {"metadata"} def _run(self, data_registry: QaDataRegistry) -> Result: + assert data_registry.metadata is not None _type = data_registry.metadata.type if _type == "wdr": From 9beb7d3b4f7c71050d2e48f6f6eb44b993a66857 Mon Sep 17 00:00:00 2001 From: Jake Weiskoff Date: Mon, 6 Jul 2026 17:35:33 -0500 Subject: [PATCH 12/34] ARXIVCE-4313: update copyright notice to arXiv, Inc. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 2ea9b089..bfdad633 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017-2025 Cornell University +Copyright (c) arXiv, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From e8b26ea2511cb1c15f57e14b5b0f86e494db44b3 Mon Sep 17 00:00:00 2001 From: Jake Weiskoff Date: Wed, 8 Jul 2026 16:02:07 -0500 Subject: [PATCH 13/34] ARXIVCE-4313: dual arXiv/Cornell copyright attribution --- LICENSE | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index bfdad633..a006ee00 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,28 @@ -Copyright (c) arXiv, Inc. +Copyright (c) 2026 arXiv, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Contributions made on or before June 30, 2026 are: + +Copyright (c) 2017-2025 Cornell University Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From e45d933449604295d1a05fea615e3e219b02a05e Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Wed, 15 Jul 2026 12:21:16 -0400 Subject: [PATCH 14/34] QA-241 drop FK relationship from arXiv_check_results to arXiv_checks table. --- arxiv/db/arxiv_db_schema.sql | 1 - arxiv/db/models.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/arxiv/db/arxiv_db_schema.sql b/arxiv/db/arxiv_db_schema.sql index 33b80957..29c92bee 100644 --- a/arxiv/db/arxiv_db_schema.sql +++ b/arxiv/db/arxiv_db_schema.sql @@ -365,7 +365,6 @@ CREATE TABLE `arXiv_check_results` ( KEY `check_results_submission_index` (`submission_id`), KEY `check_results_check_index` (`check_id`), KEY `check_results_user_index` (`user_id`), - CONSTRAINT `check_results_checks_fk` FOREIGN KEY (`check_id`) REFERENCES `arXiv_checks` (`check_id`), CONSTRAINT `check_results_sub_fk` FOREIGN KEY (`submission_id`) REFERENCES `arXiv_submissions` (`submission_id`), CONSTRAINT `check_results_user_fk` FOREIGN KEY (`user_id`) REFERENCES `tapir_users` (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; diff --git a/arxiv/db/models.py b/arxiv/db/models.py index 23c6b172..e56a318f 100644 --- a/arxiv/db/models.py +++ b/arxiv/db/models.py @@ -2261,7 +2261,7 @@ class CheckTargets(Base): def __repr__(self): return f"{ type(self).__name__ }/{ self.check_target_id }:{self.name}" - +# This class is no longer used class Checks(Base): __tablename__ = "arXiv_checks" __table_args__ = {"mysql_charset": "latin1"} @@ -2305,7 +2305,6 @@ class CheckResults(Base): data: Mapped[Optional[str]] = mapped_column(Text) submission: Mapped["Submission"] = relationship("Submission", back_populates="arXiv_check_results") - check: Mapped["Checks"] = relationship("Checks", back_populates="arXiv_check_results") user: Mapped["TapirUser"] = relationship("TapirUser", back_populates="arXiv_check_results") check_responses: Mapped[List["CheckResponses"]] = relationship("CheckResponses", back_populates="check_result") """ From 874b5da8bac8b04c54f8bde07d6136c88fdcaed4 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Wed, 15 Jul 2026 12:36:12 -0400 Subject: [PATCH 15/34] QA-241 Remove check_id mapping. --- arxiv/db/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arxiv/db/models.py b/arxiv/db/models.py index e56a318f..fe8e1386 100644 --- a/arxiv/db/models.py +++ b/arxiv/db/models.py @@ -2297,7 +2297,6 @@ class CheckResults(Base): submission_id: Mapped[int] = mapped_column(ForeignKey("arXiv_submissions.submission_id"), nullable=False, index=True) data_version: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("'0'")) metadata_version: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("'0'")) - check_id: Mapped[int] = mapped_column(ForeignKey("arXiv_checks.check_id"), nullable=False, index=True) user_id: Mapped[int] = mapped_column(ForeignKey("tapir_users.user_id"), nullable=False, index=True) ok: Mapped[int] = mapped_column(Integer, nullable=False) created: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now()) From 1409b94f7838e004d01171ced2742dbab9c9b8cb Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Wed, 15 Jul 2026 13:07:40 -0400 Subject: [PATCH 16/34] QA-241 drop FK relationship from arXiv_check_results to arXiv_checks table. --- arxiv/db/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arxiv/db/models.py b/arxiv/db/models.py index fe8e1386..e9c7175a 100644 --- a/arxiv/db/models.py +++ b/arxiv/db/models.py @@ -2283,7 +2283,6 @@ class Checks(Base): role: Mapped["CheckRoles"] = relationship("CheckRoles") view: Mapped["CheckResultViews"] = relationship("CheckResultViews") - arXiv_check_results: Mapped[List["CheckResults"]] = relationship("CheckResults", back_populates="check") def __repr__(self): return f"{ type(self).__name__ }/{ self.check_id }:{self.name};{'enabled' if self.enable_check else ''}" From 0826f3ffdb82866dd96537ff5a058815af00aef9 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Wed, 15 Jul 2026 13:16:15 -0400 Subject: [PATCH 17/34] QA-241 drop FK relationship from arXiv_check_results to arXiv_checks table. --- arxiv/db/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arxiv/db/models.py b/arxiv/db/models.py index e9c7175a..8598e431 100644 --- a/arxiv/db/models.py +++ b/arxiv/db/models.py @@ -2296,6 +2296,7 @@ class CheckResults(Base): submission_id: Mapped[int] = mapped_column(ForeignKey("arXiv_submissions.submission_id"), nullable=False, index=True) data_version: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("'0'")) metadata_version: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("'0'")) + check_id: Mapped[int] = mapped_column(Integer, nullable=True) user_id: Mapped[int] = mapped_column(ForeignKey("tapir_users.user_id"), nullable=False, index=True) ok: Mapped[int] = mapped_column(Integer, nullable=False) created: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now()) @@ -2303,6 +2304,7 @@ class CheckResults(Base): data: Mapped[Optional[str]] = mapped_column(Text) submission: Mapped["Submission"] = relationship("Submission", back_populates="arXiv_check_results") + check: Mapped["Checks"] = relationship("Checks", back_populates="arXiv_check_results") user: Mapped["TapirUser"] = relationship("TapirUser", back_populates="arXiv_check_results") check_responses: Mapped[List["CheckResponses"]] = relationship("CheckResponses", back_populates="check_result") """ From 431aba2ad88411e89f365b4346c9cd269a75c5b2 Mon Sep 17 00:00:00 2001 From: Jonathan Young Date: Wed, 15 Jul 2026 13:19:56 -0400 Subject: [PATCH 18/34] QA-241 drop FK relationship from arXiv_check_results to arXiv_checks table. --- arxiv/db/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arxiv/db/models.py b/arxiv/db/models.py index 8598e431..c6d45370 100644 --- a/arxiv/db/models.py +++ b/arxiv/db/models.py @@ -2304,7 +2304,6 @@ class CheckResults(Base): data: Mapped[Optional[str]] = mapped_column(Text) submission: Mapped["Submission"] = relationship("Submission", back_populates="arXiv_check_results") - check: Mapped["Checks"] = relationship("Checks", back_populates="arXiv_check_results") user: Mapped["TapirUser"] = relationship("TapirUser", back_populates="arXiv_check_results") check_responses: Mapped[List["CheckResponses"]] = relationship("CheckResponses", back_populates="check_result") """ From 853a1510aa7e37890b08a7d8174ade3be3311d2c Mon Sep 17 00:00:00 2001 From: carly Date: Mon, 20 Jul 2026 15:51:53 -0400 Subject: [PATCH 19/34] fix dependency override so base sub-deps are not installed --- qa/pyproject.toml | 1 + qa/uv.lock | 1144 +-------------------------------------------- 2 files changed, 2 insertions(+), 1143 deletions(-) diff --git a/qa/pyproject.toml b/qa/pyproject.toml index 32578d7c..39376fce 100644 --- a/qa/pyproject.toml +++ b/qa/pyproject.toml @@ -25,6 +25,7 @@ arxiv-base = { git = "https://github.com/arXiv/arxiv-base", rev = "8135d213c8409 # do not install arxiv-base deps [[tool.uv.dependency-metadata]] name = "arxiv-base" +version = "1.0.1" # required requires-dist = [] [tool.ruff] diff --git a/qa/uv.lock b/qa/uv.lock index 81db603a..5988bbca 100644 --- a/qa/uv.lock +++ b/qa/uv.lock @@ -10,6 +10,7 @@ resolution-markers = [ [[manifest.dependency-metadata]] name = "arxiv-base" +version = "1.0.1" [[package]] name = "annotated-types" @@ -24,209 +25,6 @@ wheels = [ name = "arxiv-base" version = "1.0.1" source = { git = "https://github.com/arXiv/arxiv-base?rev=8135d213c8409297e807f1d9a73709799b9750ef#8135d213c8409297e807f1d9a73709799b9750ef" } -dependencies = [ - { name = "bleach" }, - { name = "fastly" }, - { name = "fire" }, - { name = "flask" }, - { name = "flask-s3" }, - { name = "google-auth" }, - { name = "google-cloud-logging" }, - { name = "google-cloud-monitoring" }, - { name = "google-cloud-pubsub" }, - { name = "google-cloud-storage" }, - { name = "jwcrypto" }, - { name = "markupsafe" }, - { name = "mysqlclient" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt" }, - { name = "pytz" }, - { name = "redis" }, - { name = "redis-py-cluster" }, - { name = "retry" }, - { name = "ruamel-yaml" }, - { name = "setuptools" }, - { name = "sqlalchemy" }, - { name = "typing-extensions" }, - { name = "validators" }, - { name = "wtforms" }, -] - -[[package]] -name = "bleach" -version = "6.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, -] - -[[package]] -name = "blinker" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, -] - -[[package]] -name = "boto3" -version = "1.43.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, - { name = "jmespath" }, - { name = "s3transfer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/cc/42d798fc5305e4636170b50cdfb305ff0a81f470e35131f4a0d2641976ae/boto3-1.43.9.tar.gz", hash = "sha256:37dac72f2921095378c0200caf07918d5e10a82b7c1f611abb70e44f69d0b962", size = 113135, upload-time = "2026-05-15T19:28:31.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/dc/51286e9551f7852a79ce5d2a57468d9d905c30d32bcace55204551db202d/boto3-1.43.9-py3-none-any.whl", hash = "sha256:5e967292d361482793471bd80fad1e714515b7401f65a0d5b4aa6ef9d009c030", size = 140523, upload-time = "2026-05-15T19:28:28.948Z" }, -] - -[[package]] -name = "botocore" -version = "1.43.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jmespath" }, - { name = "python-dateutil" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/e8/f696c80982685a4cdb3df5f0781919afa50262f40e1aac7066c9c2520deb/botocore-1.43.9.tar.gz", hash = "sha256:93e91c7160678182860f5902ee4cfe6d643cac0d9ee84d3eb65becc9f4c00228", size = 15357963, upload-time = "2026-05-15T19:28:19.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c9/a1b51a74d476f5cb2f555ce8274f0f6b9fb21d75cc3f57b87dd0632ee17a/botocore-1.43.9-py3-none-any.whl", hash = "sha256:b9bdcd9c87fc552aad30006f00167d9ebb3480e1b06f1902bac5b2c41014fdab", size = 15039827, upload-time = "2026-05-15T19:28:14.543Z" }, -] - -[[package]] -name = "certifi" -version = "2026.4.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, -] - -[[package]] -name = "click" -version = "8.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, -] [[package]] name = "colorama" @@ -311,446 +109,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "cryptography" -version = "48.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, -] - -[[package]] -name = "decorator" -version = "5.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, -] - -[[package]] -name = "fastly" -version = "13.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/cc/0f866bc299564d09600217ef47697b7f6a5f45511969f099b9079b37451c/fastly-13.1.0.tar.gz", hash = "sha256:2afa5140ac20058dee38e002bafa36becec2767d930f3de98b8aae94513cd5da", size = 915157, upload-time = "2026-03-31T05:24:57.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/db/5775206cf35fd47153464e0eb57047097f6ef80cffed4c2aff2693830901/fastly-13.1.0-py3-none-any.whl", hash = "sha256:f1e24761702bb44dca558db27bf23e7e1e90b9da3c680794d5fabf8c484b8c38", size = 3188588, upload-time = "2026-03-31T05:24:55.509Z" }, -] - -[[package]] -name = "fire" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, - { name = "termcolor" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1b/1b/84c63f592ecdfbb3d77d22a8d93c9b92791e4fa35677ad71a7d6449100f8/fire-0.6.0.tar.gz", hash = "sha256:54ec5b996ecdd3c0309c800324a0703d6da512241bc73b553db959d98de0aa66", size = 88439, upload-time = "2024-03-11T19:52:00.293Z" } - -[[package]] -name = "flask" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "blinker" }, - { name = "click" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/e1/d104c83026f8d35dfd2c261df7d64738341067526406b40190bc063e829a/flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842", size = 676315, upload-time = "2024-04-07T19:26:11.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", size = 101735, upload-time = "2024-04-07T19:26:08.569Z" }, -] - -[[package]] -name = "flask-s3" -version = "0.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "boto3" }, - { name = "flask" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/db/3f/fdc16869d4140df0096af7cdd4c6beee4a66629d80d15d444f5eca0fe6a5/Flask-S3-0.3.3.tar.gz", hash = "sha256:1d49061d4b78759df763358a901f4ed32bb43f672c9f8e1ec7226793f6ae0fd2", size = 7788, upload-time = "2017-02-23T20:44:59.09Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/554bf192eed00cef7fa08a82724415c272cf9832810e9112cdc587edacd2/Flask_S3-0.3.3-py3-none-any.whl", hash = "sha256:23cbbb1db4c29c313455dbe16f25be078d6318f0a11abcbb610f99e116945b62", size = 8248, upload-time = "2017-02-23T20:44:58.23Z" }, -] - -[[package]] -name = "google-api-core" -version = "2.30.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" }, -] - -[package.optional-dependencies] -grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, -] - -[[package]] -name = "google-auth" -version = "2.52.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyasn1-modules" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d4/f8/80d2493cbedece1c623dc3e3cb1883300871af0dcdae254409522985ac23/google_auth-2.52.0.tar.gz", hash = "sha256:01f30e1a9e3638698d89464f5e603ce29d18e1c0e63ec31ac570aba4e164aaf5", size = 335027, upload-time = "2026-05-07T19:45:24.033Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/fc/2cdc74252746f547f81ff3f02d4d4234a3f411b5de5b61af97e633a060b9/google_auth-2.52.0-py3-none-any.whl", hash = "sha256:aee92803ba0ff93a70a3b8a35c7b4797837751cd6380b63ff38372b98f3ed627", size = 245614, upload-time = "2026-05-07T19:45:21.914Z" }, -] - -[[package]] -name = "google-cloud-appengine-logging" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpcio" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/02/800897064ca6f1a26835cdf23939c4b93e38a30f3fb5c7cec7c01ae2edc2/google_cloud_appengine_logging-1.9.0.tar.gz", hash = "sha256:ff397f0bbc1485f979ab45767c38e0f676c9598c97c384f7412216e6ea22f805", size = 17963, upload-time = "2026-03-30T22:51:33.556Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/4a/304d42664ab2afbe7be39559c9eb3f81dd06e7ac9284f9f36f726f15939d/google_cloud_appengine_logging-1.9.0-py3-none-any.whl", hash = "sha256:bbf3a7e4dc171678f7f481259d1f68c3ae7d337530f1f2361f8a0b214dbcfe36", size = 18333, upload-time = "2026-03-30T22:49:39.045Z" }, -] - -[[package]] -name = "google-cloud-audit-log" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/9f/3aedb3ce1d58c58ec7dd06b3964836eabfd17a16a95b60c8f609c0afff7f/google_cloud_audit_log-0.5.0.tar.gz", hash = "sha256:3b32d5e77db634c46fbd6c5e01f5bda836f420dfbb21d730501c75e9fab4e4a4", size = 44670, upload-time = "2026-03-30T22:50:42.295Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/40/79fa535b6e3321d5e07b2a9ab4bb63860d3fea12230c765837881348003c/google_cloud_audit_log-0.5.0-py3-none-any.whl", hash = "sha256:3f4632f25bf67446fa9085c52868f3cb42fb1afbab9489ba8978e30991afc79f", size = 44862, upload-time = "2026-03-30T22:47:57.533Z" }, -] - -[[package]] -name = "google-cloud-core" -version = "2.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "google-auth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/dd/1eef226e470369b26824a505c34482c0b493bc35fe8e0c6b003b5feca21a/google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83", size = 36001, upload-time = "2026-05-07T08:04:04.124Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, -] - -[[package]] -name = "google-cloud-logging" -version = "3.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "google-cloud-appengine-logging" }, - { name = "google-cloud-audit-log" }, - { name = "google-cloud-core" }, - { name = "grpc-google-iam-v1" }, - { name = "grpcio" }, - { name = "opentelemetry-api" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/06/253e9795a5877f35183a7175977ca47a17255fe0c8487155f48b86c83f3e/google_cloud_logging-3.15.0.tar.gz", hash = "sha256:72168a1e98bbfc27c75f0b8f630a7f5d786065f3f1f7e9e53d2d787a03693a4a", size = 294881, upload-time = "2026-03-26T22:18:36.947Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/fc1a0c57f95d21559ed13e381d9024e9ee9d521489707573fd10af856545/google_cloud_logging-3.15.0-py3-none-any.whl", hash = "sha256:7dcc67434c4e7181510c133d5ac8fd4ce60c23fa4158661f67e54bf440c32450", size = 234212, upload-time = "2026-03-26T22:15:16.404Z" }, -] - -[[package]] -name = "google-cloud-monitoring" -version = "2.30.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpcio" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/3f/7bc306ebb006114f58fb9143aec91e1b014a11577350d8bbd6bbc38389f9/google_cloud_monitoring-2.30.0.tar.gz", hash = "sha256:a9530aa9aa246c490810dfa7be32d67e8340d19108acc99cbc02d1ed494fba76", size = 407108, upload-time = "2026-03-26T22:17:10.365Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/c8/666c21c470b9d6fd62ac9ee74dc265419975228f9b16f8ad72ec22e8d98b/google_cloud_monitoring-2.30.0-py3-none-any.whl", hash = "sha256:2729f3b88a4798b7757b1d9d31b6cb562bb3544e8173765e4e5cd44d8685b1ed", size = 391367, upload-time = "2026-03-26T22:15:04.088Z" }, -] - -[[package]] -name = "google-cloud-pubsub" -version = "2.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpc-google-iam-v1" }, - { name = "grpcio" }, - { name = "grpcio-status" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/a5/0c7b27493ad6e744d175999e93bc51141ae70b23184d0bdbcb13fc9a4b29/google_cloud_pubsub-2.38.0.tar.gz", hash = "sha256:9212309f8d6cfaefb577bca52492b13464b56e584505408685d63e69346c56cf", size = 402783, upload-time = "2026-05-07T08:04:29.024Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/ea/d000ce0663c8989651f80b02e2ea8bb700d29140ade580f275b4ec4c9687/google_cloud_pubsub-2.38.0-py3-none-any.whl", hash = "sha256:fed2c40cfb77d58f6dced563a8146a8c34319c7dfbbb4d045b6c9c101e043db9", size = 324839, upload-time = "2026-05-07T08:03:04.923Z" }, -] - -[[package]] -name = "google-cloud-storage" -version = "3.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-crc32c" }, - { name = "google-resumable-media" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/ff/ca9ab2417fa913d75aae38bf40bf856bb2749a604b2e0f701b37cfcd23cc/google_cloud_storage-3.10.1-py3-none-any.whl", hash = "sha256:a72f656759b7b99bda700f901adcb3425a828d4a29f911bc26b3ea79c5b1217f", size = 324453, upload-time = "2026-03-23T09:35:21.368Z" }, -] - -[[package]] -name = "google-crc32c" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, - { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, - { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, - { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, - { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, - { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, - { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, -] - -[[package]] -name = "google-resumable-media" -version = "2.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-crc32c" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/4b/0b235beccc310d0a48adbc7246b719d173cca6c88c572dfa4b090e39143c/google_resumable_media-2.9.0.tar.gz", hash = "sha256:f7cfb224846a9dd444d125115dfbe8ef02a2b893e78f087762fe716a255a734b", size = 2164534, upload-time = "2026-05-07T08:04:44.236Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/73/3518e63deb1667c5409a4579e28daf5e84479a87a72c547e0487f7883dcd/google_resumable_media-2.9.0-py3-none-any.whl", hash = "sha256:c8901e88e389af8bed64d9696c74d8bad961865eb2236e13e0bfca9bb0a65ca3", size = 81507, upload-time = "2026-05-07T08:03:23.809Z" }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.75.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, -] - -[package.optional-dependencies] -grpc = [ - { name = "grpcio" }, -] - -[[package]] -name = "greenlet" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ab/192090c4a5b30df148c22bf4b8895457d739a7c7c5a7b9c41e5dd7f537f2/greenlet-3.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564", size = 623976, upload-time = "2026-04-27T13:02:37.363Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" }, - { url = "https://files.pythonhosted.org/packages/24/11/05eb2b9b188c6df7d68a89c99134d644a7af616a40b9808e8e6ced315d5d/greenlet-3.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", size = 418379, upload-time = "2026-04-27T13:05:12.755Z" }, - { url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" }, - { url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" }, - { url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" }, - { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, - { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, - { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e9/4eeadf8cb3403ac274245ba75f07844abc7fa5f6787583fc9156ba741e0f/greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", size = 620610, upload-time = "2026-04-27T13:02:39.194Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ef/f913b3c0eb7d26d86a2401c5e1546c9d46b657efee724b06f6f4ac5d8824/greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", size = 422775, upload-time = "2026-04-27T13:05:14.261Z" }, - { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, - { url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, - { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" }, - { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, - { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" }, - { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, -] - -[[package]] -name = "grpc-google-iam-v1" -version = "0.14.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos", extra = ["grpc"] }, - { name = "grpcio" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/4f/d098419ad0bfc06c9ce440575f05aa22d8973b6c276e86ac7890093d3c37/grpc_google_iam_v1-0.14.4.tar.gz", hash = "sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038", size = 23706, upload-time = "2026-04-01T01:57:49.813Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl", hash = "sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964", size = 32675, upload-time = "2026-04-01T01:57:47.69Z" }, -] - -[[package]] -name = "grpcio" -version = "1.80.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, - { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, - { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, - { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, - { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, - { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, - { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, - { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, - { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, - { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, - { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, - { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, - { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, - { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, -] - -[[package]] -name = "grpcio-status" -version = "1.80.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "grpcio" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/ed/105f619bdd00cb47a49aa2feea6232ea2bbb04199d52a22cc6a7d603b5cb/grpcio_status-1.80.0.tar.gz", hash = "sha256:df73802a4c89a3ea88aa2aff971e886fccce162bc2e6511408b3d67a144381cd", size = 13901, upload-time = "2026-03-30T08:54:34.784Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/80/58cd2dfc19a07d022abe44bde7c365627f6c7cb6f692ada6c65ca437d09a/grpcio_status-1.80.0-py3-none-any.whl", hash = "sha256:4b56990363af50dbf2c2ebb80f1967185c07d87aa25aa2bea45ddb75fc181dbe", size = 14638, upload-time = "2026-03-30T08:54:01.569Z" }, -] - -[[package]] -name = "idna" -version = "3.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -760,152 +118,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "jmespath" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, -] - -[[package]] -name = "jwcrypto" -version = "1.5.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/90/f065668004d22715c1940d6e88e4c3afc8ee16d5664e4478d2c8fd23a250/jwcrypto-1.5.7.tar.gz", hash = "sha256:70204d7cca406eda8c82352e3c41ba2d946610dafd19e54403f0a1f4f18633c6", size = 89535, upload-time = "2026-04-07T00:35:36.116Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/24/fb7da4d6613de7001feaf540d4b5969c6b5a1c42839043b0196cb13aa057/jwcrypto-1.5.7-py3-none-any.whl", hash = "sha256:729463fefe28b6de5cf1ebfda3e94f1a1b41d2799148ef98a01cb9678ebe2bb0", size = 94799, upload-time = "2026-04-07T00:35:35.085Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, -] - -[[package]] -name = "mysqlclient" -version = "2.2.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/b0/9df076488cb2e536d40ce6dbd4273c1f20a386e31ffe6e7cb613902b3c2a/mysqlclient-2.2.8.tar.gz", hash = "sha256:8ed20c5615a915da451bb308c7d0306648a4fd9a2809ba95c992690006306199", size = 92287, upload-time = "2026-02-10T10:58:37.405Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/f6/6253d116c024aeb11fb193ad14e2bae2bd3380690e885028abf798b1b0b4/mysqlclient-2.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:60c9ed339dc09e3d5380cc2a9f42e86754fee25a661ced77a02df77990664c92", size = 206875, upload-time = "2026-02-10T10:58:42.223Z" }, - { url = "https://files.pythonhosted.org/packages/db/1f/7d6b3fbfc8a317805fdb8bbfbc23af99f2090497090e82513daf491a29ce/mysqlclient-2.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:000c7ec3d11e7c411db832e4cfcd7f05db47464326381f5d5ae991b4bb572f93", size = 207171, upload-time = "2026-02-10T10:58:39.827Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d5/76e369b0fdccd2eb9ed7d890e4e3e23aa1344fea62f0180d7f1574285e54/mysqlclient-2.2.8-cp313-cp313-win_amd64.whl", hash = "sha256:a81f5e12f8d05439709cb02fba97f9f76d1a6c528164f2260d8798fec969e300", size = 207158, upload-time = "2026-02-10T10:58:38.663Z" }, -] - -[[package]] -name = "opentelemetry-api" -version = "1.41.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.41.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.62b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, -] - [[package]] name = "packaging" version = "26.0" @@ -924,72 +136,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "proto-plus" -version = "1.28.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, -] - -[[package]] -name = "protobuf" -version = "6.33.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, - { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, - { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, - { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, -] - -[[package]] -name = "py" -version = "1.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, -] - -[[package]] -name = "pyasn1" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - [[package]] name = "pydantic" version = "2.12.5" @@ -1074,20 +220,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] -[[package]] -name = "pydantic-settings" -version = "2.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1097,15 +229,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] -[[package]] -name = "pyjwt" -version = "2.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, -] - [[package]] name = "pytest" version = "8.4.2" @@ -1136,36 +259,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - -[[package]] -name = "pytz" -version = "2026.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, -] - [[package]] name = "qa" version = "0.1.0" @@ -1197,102 +290,6 @@ dev = [ { name = "ty" }, ] -[[package]] -name = "redis" -version = "2.10.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/8d/6d34b75326bf96d4139a2ddd8e74b80840f800a0a79f9294399e212cb9a7/redis-2.10.6.tar.gz", hash = "sha256:a22ca993cea2962dbb588f9f30d0015ac4afcc45bee27d3978c0dbe9e97c6c0f", size = 97299, upload-time = "2017-08-16T23:37:35.011Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/f6/7a76333cf0b9251ecf49efff635015171843d9b977e4ffcf59f9c4428052/redis-2.10.6-py2.py3-none-any.whl", hash = "sha256:8a1900a9f2a0a44ecf6e8b5eb3e967a9909dfed219ad66df094f27f7d6f330fb", size = 64960, upload-time = "2017-08-16T23:37:33.302Z" }, -] - -[[package]] -name = "redis-py-cluster" -version = "1.3.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "redis" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/02/b2458f900496d1e573ada7ffd882efe62aeee992eab1222411fe08aa5f75/redis-py-cluster-1.3.6.tar.gz", hash = "sha256:7db54b1de60bd34da3806676b112f07fc9afae556d8260ac02c3335d574ee42c", size = 33854, upload-time = "2018-11-16T14:55:22.977Z" } - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, -] - -[[package]] -name = "retry" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "decorator" }, - { name = "py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/72/75d0b85443fbc8d9f38d08d2b1b67cc184ce35280e4a3813cda2f445f3a4/retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4", size = 6448, upload-time = "2016-05-11T13:58:51.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/0d/53aea75710af4528a25ed6837d71d117602b01946b307a3912cb3cfcbcba/retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", size = 7986, upload-time = "2016-05-11T13:58:39.925Z" }, -] - -[[package]] -name = "ruamel-yaml" -version = "0.18.17" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/2b/7a1f1ebcd6b3f14febdc003e658778d81e76b40df2267904ee6b13f0c5c6/ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c", size = 149602, upload-time = "2025-12-17T20:02:55.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d", size = 121594, upload-time = "2025-12-17T20:02:07.657Z" }, -] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, - { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, - { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, - { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, - { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, - { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, - { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, - { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, - { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, - { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, - { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, - { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, - { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, - { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, - { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, - { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, - { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, - { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, - { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, -] - [[package]] name = "ruff" version = "0.13.3" @@ -1319,85 +316,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" }, ] -[[package]] -name = "s3transfer" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/ec/7c692cde9125b77e84b307354d4fb705f98b8ccad59a036d5957ca75bfc3/s3transfer-0.17.0.tar.gz", hash = "sha256:9edeb6d1c3c2f89d6050348548834ad8289610d886e5bf7b7207728bd43ce33a", size = 155337, upload-time = "2026-04-29T22:07:36.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/72/c6c32d2b657fa3dad1de340254e14390b1e334ce38268b7ad51abda3c8c2/s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20", size = 86811, upload-time = "2026-04-29T22:07:34.966Z" }, -] - -[[package]] -name = "setuptools" -version = "78.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/9c/42314ee079a3e9c24b27515f9fbc7a3c1d29992c33451779011c74488375/setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d", size = 1368163, upload-time = "2025-04-19T18:23:36.68Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561", size = 1256462, upload-time = "2025-04-19T18:23:34.525Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.49" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, - { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, - { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, - { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, - { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, - { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, - { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, - { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, - { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, - { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, - { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, - { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, - { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, - { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, - { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, - { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, -] - -[[package]] -name = "termcolor" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, -] - [[package]] name = "tomli" version = "2.4.1" @@ -1478,63 +396,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, -] - -[[package]] -name = "validators" -version = "0.35.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/66/a435d9ae49850b2f071f7ebd8119dd4e84872b01630d6736761e6e7fd847/validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a", size = 73399, upload-time = "2025-05-01T05:42:06.7Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" }, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, -] - -[[package]] -name = "werkzeug" -version = "3.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, -] - -[[package]] -name = "wtforms" -version = "3.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/91/ed9b517da898e3fb747566aa3c12a734bd64ea7449a0d25ec74ce8f8b8eb/wtforms-3.2.2.tar.gz", hash = "sha256:7b00c73f8670f35d4edb0293dcd81b980528bee72fd662b182aaba27ae570b93", size = 139583, upload-time = "2026-05-03T05:53:44.147Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/76/bb225c8300f3a0ba28e01df51419c6c9574a297c43d71b29048e03b65deb/wtforms-3.2.2-py3-none-any.whl", hash = "sha256:72b90d5d921bd3119252069cf0301e9c13915f9e52792652bc91c5dda4b79e56", size = 158656, upload-time = "2026-05-03T05:53:46.072Z" }, -] - -[[package]] -name = "zipp" -version = "3.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, -] From 5ece56422c340ba07a75bf95ed9bee97d6079826 Mon Sep 17 00:00:00 2001 From: carly Date: Mon, 20 Jul 2026 15:52:03 -0400 Subject: [PATCH 20/34] format --- qa/qa/checks/__init__.py | 4 +-- qa/qa/checks/fulltext/text_checks.py | 14 ++++----- qa/qa/checks/generic/text.py | 1 + qa/qa/checks/metadata/oversize.py | 5 +--- qa/qa/checks/metadata/withdrawal.py | 7 ++--- qa/qa/checks/models.py | 5 ++-- qa/tests/checks/check_unique_ids.py | 3 +- qa/tests/checks/fulltext/test_text.py | 9 +++--- qa/tests/checks/metadata/test_oversize.py | 30 +++++--------------- qa/tests/checks/metadata/test_withdrawals.py | 3 +- 10 files changed, 28 insertions(+), 53 deletions(-) diff --git a/qa/qa/checks/__init__.py b/qa/qa/checks/__init__.py index d19d09a6..1a49c4a8 100644 --- a/qa/qa/checks/__init__.py +++ b/qa/qa/checks/__init__.py @@ -14,9 +14,7 @@ from qa.checks.metadata.oversize import OversizeCheck # noqa from qa.checks.metadata.withdrawal import WithdrawalCheck # noqa -from qa.checks.fulltext.text_checks import ( - FulltextExtractedCheck, FulltextNotTooShortCheck -) # noqa +from qa.checks.fulltext.text_checks import FulltextExtractedCheck, FulltextNotTooShortCheck # noqa checks: list[BaseCheck] = [ TitleIsValid(), diff --git a/qa/qa/checks/fulltext/text_checks.py b/qa/qa/checks/fulltext/text_checks.py index 707af46d..7356dda2 100644 --- a/qa/qa/checks/fulltext/text_checks.py +++ b/qa/qa/checks/fulltext/text_checks.py @@ -1,4 +1,3 @@ - from qa.checks.base import BaseCheck from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result @@ -25,8 +24,8 @@ def _run(self, data_registry: QaDataRegistry) -> Result: return self._result(passed) else: passed = False - return self._result(passed, message = self.failure_message) - + return self._result(passed, message=self.failure_message) + class FulltextNotTooShortCheck(BaseCheck): """Check for very short text.""" @@ -38,7 +37,7 @@ class FulltextNotTooShortCheck(BaseCheck): description = "The full text extracted is not too short." on_failure_policy = OnFailurePolicy.REJECT failure_message = "Very short text (< 1400 words). Check full text." - + required_inputs = {"fulltext"} def _run(self, data_registry: QaDataRegistry) -> Result: @@ -47,12 +46,9 @@ def _run(self, data_registry: QaDataRegistry) -> Result: # Problem: we should only report the problem with missing texts passed = True return self._result(passed) - elif len(fulltext) < 10000 and len(fulltext.split()) < 1400 : + elif len(fulltext) < 10000 and len(fulltext.split()) < 1400: passed = False - return self._result(passed, message = self.failure_message) + return self._result(passed, message=self.failure_message) else: passed = True return self._result(passed) - - - diff --git a/qa/qa/checks/generic/text.py b/qa/qa/checks/generic/text.py index 0d1af324..9b238592 100644 --- a/qa/qa/checks/generic/text.py +++ b/qa/qa/checks/generic/text.py @@ -7,6 +7,7 @@ # Note: the ids in this file should be the metadata check id + 10000, # to avoid collision with the check_ids previously used in the arxiv_checks table. + class DoesNotStartWithLowercase(BaseGenericPatternCheck): name = "does_not_start_with_lowercase" display_name = "Does Not Start With Lowercase" diff --git a/qa/qa/checks/metadata/oversize.py b/qa/qa/checks/metadata/oversize.py index e98a7ee8..53e2afef 100644 --- a/qa/qa/checks/metadata/oversize.py +++ b/qa/qa/checks/metadata/oversize.py @@ -1,6 +1,5 @@ - from qa.checks.base import BaseCheck -from qa.checks. models import OnFailurePolicy, QaDataRegistry, Result +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result class OversizeCheck(BaseCheck): @@ -22,5 +21,3 @@ def _run(self, data_registry: QaDataRegistry) -> Result: return self._result(passed=passed, message=message) else: return self._result(passed=True, message="") - - diff --git a/qa/qa/checks/metadata/withdrawal.py b/qa/qa/checks/metadata/withdrawal.py index a5eee698..1a26af7a 100644 --- a/qa/qa/checks/metadata/withdrawal.py +++ b/qa/qa/checks/metadata/withdrawal.py @@ -1,7 +1,6 @@ - from qa.checks.base import BaseCheck -from qa.checks. models import OnFailurePolicy, QaDataRegistry, Result +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result class WithdrawalCheck(BaseCheck): @@ -23,9 +22,7 @@ def _run(self, data_registry: QaDataRegistry) -> Result: if _type == "wdr": passed = False - return self._result(passed, message = self.failure_message) + return self._result(passed, message=self.failure_message) else: passed = True return self._result(passed) - - diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index 51a663e0..fc3928ea 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -58,7 +58,7 @@ class SubmissionMetadata(BaseModel): This should be a concrete implementation of MetadataProtocol for use in checks. """ - # MetadataProtocol fields + # MetadataProtocol fields title: str | None = None authors: str | None = None abstract: str | None = None @@ -69,11 +69,12 @@ class SubmissionMetadata(BaseModel): msc_class: str | None = None acm_class: str | None = None # End MetadataProtocol fields - type: str | None = None # one of: "new", "rep", "wdr", "jref", or "cross" + type: str | None = None # one of: "new", "rep", "wdr", "jref", or "cross" is_oversize: bool = False data_version: int = 0 metadata_version: int = 0 + @runtime_checkable class MetadataProtocol(Protocol): """ diff --git a/qa/tests/checks/check_unique_ids.py b/qa/tests/checks/check_unique_ids.py index 8082ddcf..357e00de 100644 --- a/qa/tests/checks/check_unique_ids.py +++ b/qa/tests/checks/check_unique_ids.py @@ -2,11 +2,12 @@ from qa.checks import checks as all_checks + class TestCheckIds: def test_all_check_ids_unique(self): assert len(all_checks) == len(set([check.id for check in all_checks])) + class TestCheckNames: def test_all_check_names_unique(self): assert len(all_checks) == len(set([check.name.lower() for check in all_checks])) - diff --git a/qa/tests/checks/fulltext/test_text.py b/qa/tests/checks/fulltext/test_text.py index e946d5a5..e1379b02 100644 --- a/qa/tests/checks/fulltext/test_text.py +++ b/qa/tests/checks/fulltext/test_text.py @@ -6,12 +6,13 @@ FulltextNotTooShortCheck, ) + class TestFulltextExtractedCheck: def test_pass_normal(self): text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " result = FulltextExtractedCheck().run(QaDataRegistry(fulltext=text)) assert result.passed - + def test_fail_on_none(self): text = None result = FulltextExtractedCheck().run(QaDataRegistry(fulltext=text)) @@ -22,16 +23,16 @@ def test_fail_on_empty(self): result = FulltextExtractedCheck().run(QaDataRegistry(fulltext=text)) assert not result.passed + class TestFulltextNotTooShortCheck: def test_pass_normal(self): text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " * 140 result = FulltextNotTooShortCheck().run(QaDataRegistry(fulltext=text)) assert result.passed - + def test_fail_nospaces(self): text = "Inthiswork, westudyaaa, bbb, andcccandconcludeddd. " * 100 - + result = FulltextNotTooShortCheck().run(QaDataRegistry(fulltext=text)) assert not result.passed - diff --git a/qa/tests/checks/metadata/test_oversize.py b/qa/tests/checks/metadata/test_oversize.py index de228f3b..41a0cbe1 100644 --- a/qa/tests/checks/metadata/test_oversize.py +++ b/qa/tests/checks/metadata/test_oversize.py @@ -7,51 +7,35 @@ class TestOversizeCheck: def test_new_pass(self): check = OversizeCheck() - metadata = SubmissionMetadata( - type="new", - title="Test Title", - is_oversize=False) + metadata = SubmissionMetadata(type="new", title="Test Title", is_oversize=False) registry = QaDataRegistry(metadata=metadata) result = check._run(registry) assert result.passed def test_rep_pass(self): check = OversizeCheck() - metadata = SubmissionMetadata( - type="rep", - title="Test Title", - is_oversize=False) + metadata = SubmissionMetadata(type="rep", title="Test Title", is_oversize=False) registry = QaDataRegistry(metadata=metadata) result = check._run(registry) assert result.passed - + def test_cross_pass(self): check = OversizeCheck() - metadata = SubmissionMetadata( - type="cross", - title="Test Title", - is_oversize=False) + metadata = SubmissionMetadata(type="cross", title="Test Title", is_oversize=False) registry = QaDataRegistry(metadata=metadata) result = check._run(registry) assert result.passed - + def test_oversize_fail(self): check = OversizeCheck() - metadata = SubmissionMetadata( - type="new", - title="Test Title", - is_oversize=True) + metadata = SubmissionMetadata(type="new", title="Test Title", is_oversize=True) registry = QaDataRegistry(metadata=metadata) result = check._run(registry) assert not result.passed def test_oversize_rep_fail(self): check = OversizeCheck() - metadata = SubmissionMetadata( - type="rep", - title="Test Title", - is_oversize=True) + metadata = SubmissionMetadata(type="rep", title="Test Title", is_oversize=True) registry = QaDataRegistry(metadata=metadata) result = check._run(registry) assert not result.passed - diff --git a/qa/tests/checks/metadata/test_withdrawals.py b/qa/tests/checks/metadata/test_withdrawals.py index e247407f..4a3e9849 100644 --- a/qa/tests/checks/metadata/test_withdrawals.py +++ b/qa/tests/checks/metadata/test_withdrawals.py @@ -18,11 +18,10 @@ def test_rep_pass(self): registry = QaDataRegistry(metadata=metadata) result = check._run(registry) assert result.passed - + def test_wdr_fail(self): check = WithdrawalCheck() metadata = SubmissionMetadata(type="wdr", title="Test Title") registry = QaDataRegistry(metadata=metadata) result = check._run(registry) assert not result.passed - From ba5b8e1b930e80b8b384da8caeb737a2ff4bfc55 Mon Sep 17 00:00:00 2001 From: carly Date: Mon, 20 Jul 2026 16:00:00 -0400 Subject: [PATCH 21/34] enforce formatting --- .github/workflows/lint-test-package.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/lint-test-package.yml b/.github/workflows/lint-test-package.yml index 0105151f..d91faee7 100644 --- a/.github/workflows/lint-test-package.yml +++ b/.github/workflows/lint-test-package.yml @@ -47,6 +47,10 @@ jobs: run: uv run ruff check --output-format=github . working-directory: ${{ inputs.package_name }} + - name: Check formatting with Ruff + run: uv run ruff format --check + working-directory: ${{ inputs.package_name }} + - name: Type with ty run: uv run ty check working-directory: ${{ inputs.package_name }} From 9d346a2c5971a2650b9e9e5c149254018ff713f8 Mon Sep 17 00:00:00 2001 From: carly Date: Tue, 21 Jul 2026 15:59:20 -0400 Subject: [PATCH 22/34] move models and remove reports directory --- qa/qa/checks/models.py | 44 ++++++++++++++++++- qa/qa/reports/__init__.py | 0 qa/qa/reports/models/__init__.py | 4 -- qa/qa/reports/models/base.py | 37 ---------------- qa/qa/reports/models/fulltext.py | 11 ----- .../test_models.py} | 3 +- 6 files changed, 43 insertions(+), 56 deletions(-) delete mode 100644 qa/qa/reports/__init__.py delete mode 100644 qa/qa/reports/models/__init__.py delete mode 100644 qa/qa/reports/models/base.py delete mode 100644 qa/qa/reports/models/fulltext.py rename qa/tests/{reports/test_report_models.py => checks/test_models.py} (94%) diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index fc3928ea..41a25e42 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -1,6 +1,9 @@ -from pydantic import BaseModel -from typing import Protocol, runtime_checkable +from pydantic import BaseModel, Field +from typing import Literal, Protocol, runtime_checkable from enum import StrEnum +from datetime import datetime, timezone + +kebab_case = "^[a-z0-9]+(-[a-z0-9]+)*$" class OnFailurePolicy(StrEnum): @@ -103,3 +106,40 @@ class QaDataRegistry(BaseModel): flagged_terms_report: str | None = None tex_report: str | None = None metadata: SubmissionMetadata | None = None + + +class Flag(BaseModel): + id: str = Field( + description="TODO the id of the individual flag or of the category of flag?", + examples=["tex-created-flag"], + pattern=kebab_case, + ) + description: str | None + + +class BaseReport(BaseModel): + name: str = Field(description="The full name of the report.") + key_name: str = Field( + description="The abbreviated name of the report.", + examples=["arxiv-example-report"], + pattern=kebab_case, + ) + version: str = Field(description="The semantic version of the report schema.") + submission_id: int = Field(gt=0) + created: str = Field( + description="The timestamp representing when the report was created.", + default_factory=lambda: datetime.now(timezone.utc).isoformat(), + ) + flags: list[Flag] = [] + qa_exec_time_sec: int | None = Field( + default=None, + description="Time it took to process and generate the report, in seconds.", + ge=0, + ) + data: dict + + +class FulltextReport(BaseReport): + name: str = "arXiv Fulltext Report" + key_name: str = "fulltext" + version: Literal["1.0"] = "1.0" diff --git a/qa/qa/reports/__init__.py b/qa/qa/reports/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/qa/qa/reports/models/__init__.py b/qa/qa/reports/models/__init__.py deleted file mode 100644 index 8090153b..00000000 --- a/qa/qa/reports/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""QA report models.""" - -from qa.reports.models.base import Flag # noqa -from qa.reports.models.fulltext import FulltextReport # noqa diff --git a/qa/qa/reports/models/base.py b/qa/qa/reports/models/base.py deleted file mode 100644 index 7c254847..00000000 --- a/qa/qa/reports/models/base.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Base model for QA reports persisted to storage.""" - -from pydantic import BaseModel, Field -from datetime import datetime, timezone - -kebab_case = "^[a-z0-9]+(-[a-z0-9]+)*$" - - -class Flag(BaseModel): - id: str = Field( - description="TODO the id of the individual flag or of the category of flag?", - examples=["tex-created-flag"], - pattern=kebab_case, - ) - description: str | None - - -class BaseReport(BaseModel): - name: str = Field(description="The full name of the report.") - key_name: str = Field( - description="The abbreviated name of the report.", - examples=["arxiv-example-report"], - pattern=kebab_case, - ) - version: str = Field(description="The semantic version of the report schema.") - submission_id: int = Field(gt=0) - created: str = Field( - description="The timestamp representing when the report was created.", - default_factory=lambda: datetime.now(timezone.utc).isoformat(), - ) - flags: list[Flag] = [] - qa_exec_time_sec: int | None = Field( - default=None, - description="Time it took to process and generate the report, in seconds.", - ge=0, - ) - data: dict diff --git a/qa/qa/reports/models/fulltext.py b/qa/qa/reports/models/fulltext.py deleted file mode 100644 index 899b1d84..00000000 --- a/qa/qa/reports/models/fulltext.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Model for fulltext QA reports persisted to GCS.""" - -from typing import Literal - -from qa.reports.models.base import BaseReport - - -class FulltextReport(BaseReport): - name: str = "arXiv Fulltext Report" - key_name: str = "fulltext" - version: Literal["1.0"] = "1.0" diff --git a/qa/tests/reports/test_report_models.py b/qa/tests/checks/test_models.py similarity index 94% rename from qa/tests/reports/test_report_models.py rename to qa/tests/checks/test_models.py index dcb63859..84049e46 100644 --- a/qa/tests/reports/test_report_models.py +++ b/qa/tests/checks/test_models.py @@ -3,8 +3,7 @@ from pydantic import ValidationError from unittest import TestCase -from qa.reports.models.base import BaseReport, Flag -from qa.reports.models.fulltext import FulltextReport +from qa.checks.models import BaseReport, Flag, FulltextReport def base_report( From 5f21951b72686ae691b2500c7dba10c9dcfa3fe8 Mon Sep 17 00:00:00 2001 From: carly Date: Tue, 21 Jul 2026 16:17:26 -0400 Subject: [PATCH 23/34] clean up --- qa/qa/checks/models.py | 61 +++++++++++++--------------------- qa/tests/checks/test_models.py | 14 +------- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index 41a25e42..7779c493 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field -from typing import Literal, Protocol, runtime_checkable +from typing import Protocol, runtime_checkable from enum import StrEnum from datetime import datetime, timezone @@ -55,6 +55,28 @@ class Result(BaseModel): results: list["Result"] | None = None +class Flag(BaseModel): + id: str = Field(pattern=kebab_case) + description: str | None + + +class BaseReport(BaseModel): + name: str + key_name: str = Field(pattern=kebab_case) + version: str + submission_id: int = Field(gt=0) + created: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat()) + flags: list[Flag] = [] + qa_exec_time_sec: int | None = Field(default=None, ge=0) + data: dict + + +class FulltextReport(BaseReport): + name: str = "arXiv Fulltext Report" + key_name: str = "fulltext" + version: str = "1.0" + + class SubmissionMetadata(BaseModel): """ Information about a submission. @@ -106,40 +128,3 @@ class QaDataRegistry(BaseModel): flagged_terms_report: str | None = None tex_report: str | None = None metadata: SubmissionMetadata | None = None - - -class Flag(BaseModel): - id: str = Field( - description="TODO the id of the individual flag or of the category of flag?", - examples=["tex-created-flag"], - pattern=kebab_case, - ) - description: str | None - - -class BaseReport(BaseModel): - name: str = Field(description="The full name of the report.") - key_name: str = Field( - description="The abbreviated name of the report.", - examples=["arxiv-example-report"], - pattern=kebab_case, - ) - version: str = Field(description="The semantic version of the report schema.") - submission_id: int = Field(gt=0) - created: str = Field( - description="The timestamp representing when the report was created.", - default_factory=lambda: datetime.now(timezone.utc).isoformat(), - ) - flags: list[Flag] = [] - qa_exec_time_sec: int | None = Field( - default=None, - description="Time it took to process and generate the report, in seconds.", - ge=0, - ) - data: dict - - -class FulltextReport(BaseReport): - name: str = "arXiv Fulltext Report" - key_name: str = "fulltext" - version: Literal["1.0"] = "1.0" diff --git a/qa/tests/checks/test_models.py b/qa/tests/checks/test_models.py index 84049e46..416cd02c 100644 --- a/qa/tests/checks/test_models.py +++ b/qa/tests/checks/test_models.py @@ -3,7 +3,7 @@ from pydantic import ValidationError from unittest import TestCase -from qa.checks.models import BaseReport, Flag, FulltextReport +from qa.checks.models import BaseReport, Flag def base_report( @@ -46,15 +46,3 @@ def test_id_rejects_underscores(self): def test_id_accepts_kebab(self): Flag(id="tex-created-flag", description="a flag") - - -class TestFulltextReport(TestCase): - def test_defaults(self): - r = FulltextReport(submission_id=1, data={}) - self.assertEqual(r.name, "arXiv Fulltext Report") - self.assertEqual(r.key_name, "fulltext") - self.assertEqual(r.version, "1.0") - - def test_version_is_pinned(self): - with self.assertRaises(ValidationError): - FulltextReport(submission_id=1, data={}, version="2.0") # type: ignore From 416d79c8eb1844cf101ee192f4b58beab83b4e6e Mon Sep 17 00:00:00 2001 From: carly Date: Tue, 21 Jul 2026 16:22:51 -0400 Subject: [PATCH 24/34] update type in data registry --- qa/qa/checks/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index 7779c493..e4ddae3b 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -83,7 +83,6 @@ class SubmissionMetadata(BaseModel): This should be a concrete implementation of MetadataProtocol for use in checks. """ - # MetadataProtocol fields title: str | None = None authors: str | None = None abstract: str | None = None @@ -93,7 +92,6 @@ class SubmissionMetadata(BaseModel): doi: str | None = None msc_class: str | None = None acm_class: str | None = None - # End MetadataProtocol fields type: str | None = None # one of: "new", "rep", "wdr", "jref", or "cross" is_oversize: bool = False data_version: int = 0 @@ -123,7 +121,7 @@ class QaDataRegistry(BaseModel): """Data dependencies for checks.""" fulltext: str | None = None - fulltext_report: str | None = None + fulltext_report: FulltextReport | None = None author_report: str | None = None flagged_terms_report: str | None = None tex_report: str | None = None From 2a988d7dac198c8550499ff3179672e6a494b56a Mon Sep 17 00:00:00 2001 From: carly Date: Tue, 21 Jul 2026 16:49:56 -0400 Subject: [PATCH 25/34] rename and refactor fulltext checks, update extraction failure check, return config, add tests --- qa/qa/checks/__init__.py | 7 ++- qa/qa/checks/fulltext/extraction.py | 34 +++++++++++ qa/qa/checks/fulltext/structure.py | 38 ++++++++++++ qa/qa/checks/fulltext/text_checks.py | 54 ----------------- qa/tests/checks/fulltext/test_extraction.py | 40 +++++++++++++ qa/tests/checks/fulltext/test_text.py | 64 +++++++++------------ 6 files changed, 142 insertions(+), 95 deletions(-) create mode 100644 qa/qa/checks/fulltext/extraction.py create mode 100644 qa/qa/checks/fulltext/structure.py delete mode 100644 qa/qa/checks/fulltext/text_checks.py create mode 100644 qa/tests/checks/fulltext/test_extraction.py diff --git a/qa/qa/checks/__init__.py b/qa/qa/checks/__init__.py index 1a49c4a8..f5b24885 100644 --- a/qa/qa/checks/__init__.py +++ b/qa/qa/checks/__init__.py @@ -14,7 +14,8 @@ from qa.checks.metadata.oversize import OversizeCheck # noqa from qa.checks.metadata.withdrawal import WithdrawalCheck # noqa -from qa.checks.fulltext.text_checks import FulltextExtractedCheck, FulltextNotTooShortCheck # noqa +from qa.checks.fulltext.extraction import TextExtractionSuccessful # noqa +from qa.checks.fulltext.structure import FulltextNotTooShort # noqa checks: list[BaseCheck] = [ TitleIsValid(), @@ -28,6 +29,6 @@ AcmClassIsValid(), OversizeCheck(), WithdrawalCheck(), - FulltextExtractedCheck(), - FulltextNotTooShortCheck(), + TextExtractionSuccessful(), + FulltextNotTooShort(), ] diff --git a/qa/qa/checks/fulltext/extraction.py b/qa/qa/checks/fulltext/extraction.py new file mode 100644 index 00000000..09e8deb8 --- /dev/null +++ b/qa/qa/checks/fulltext/extraction.py @@ -0,0 +1,34 @@ +from qa.checks.base import BaseCheck + +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result + + +class TextExtractionSuccessful(BaseCheck): + name = "text_extraction_successful" + display_name = "Text Extraction Successful" + id = 14 + version = "1.0.0" + description = "Text extraction was successful." + on_failure_policy = OnFailurePolicy.REJECT + failure_message = "Text extraction failed." + + required_data = {"fulltext_report"} + + failure_flag_id = "text-extraction-failed" + + @property + def config(self) -> dict: + return { + **super().config, + "failure_flag_id": self.failure_flag_id, + } + + def _run(self, data_registry: QaDataRegistry) -> Result: + fulltext_report = data_registry.fulltext_report + assert fulltext_report is not None + + extraction_failed = any(flag.id == self.failure_flag_id for flag in fulltext_report.flags) + + if extraction_failed: + return self._result(passed=False, message=self.failure_message) + return self._result(passed=True) diff --git a/qa/qa/checks/fulltext/structure.py b/qa/qa/checks/fulltext/structure.py new file mode 100644 index 00000000..d28bea17 --- /dev/null +++ b/qa/qa/checks/fulltext/structure.py @@ -0,0 +1,38 @@ +from qa.checks.base import BaseCheck + +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result + + +class FulltextNotTooShort(BaseCheck): + name = "fulltext_not_too_short" + display_name = "Fulltext Not Too Short" + id = 15 + version = "1.0.0" + description = "The full text extracted is not too short." + on_failure_policy = OnFailurePolicy.REJECT + failure_message = "Text too short." + + required_inputs = {"fulltext"} + + min_chars = 10000 + min_words = 1400 + + @property + def config(self) -> dict: + return { + **super().config, + "min_chars": self.min_chars, + "min_words": self.min_words, + } + + def _run(self, data_registry: QaDataRegistry) -> Result: + fulltext = data_registry.fulltext + if fulltext is None or fulltext == "": + passed = True + return self._result(passed) + elif len(fulltext) < self.min_chars and len(fulltext.split()) < self.min_words: + passed = False + return self._result(passed, message=self.failure_message) + else: + passed = True + return self._result(passed) diff --git a/qa/qa/checks/fulltext/text_checks.py b/qa/qa/checks/fulltext/text_checks.py deleted file mode 100644 index 7356dda2..00000000 --- a/qa/qa/checks/fulltext/text_checks.py +++ /dev/null @@ -1,54 +0,0 @@ -from qa.checks.base import BaseCheck - -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result - - -class FulltextExtractedCheck(BaseCheck): - """Check for missing text (probably PDF invalid or password protected).""" - - name = "text_extraction_successful" - display_name = "Text Extraction Successful" - id = 14 - version = "1.0.0" - description = "Text extraction was successful." - on_failure_policy = OnFailurePolicy.REJECT - failure_message = "Missing text (check PDF?)" - - required_inputs = {"fulltext"} - - def _run(self, data_registry: QaDataRegistry) -> Result: - fulltext = data_registry.fulltext - # fulltext should never be None here - if fulltext is not None and fulltext != "": - passed = True - return self._result(passed) - else: - passed = False - return self._result(passed, message=self.failure_message) - - -class FulltextNotTooShortCheck(BaseCheck): - """Check for very short text.""" - - name = "fulltext_not_too_short" - display_name = "Fulltext Not Too Short" - id = 15 - version = "1.0.0" - description = "The full text extracted is not too short." - on_failure_policy = OnFailurePolicy.REJECT - failure_message = "Very short text (< 1400 words). Check full text." - - required_inputs = {"fulltext"} - - def _run(self, data_registry: QaDataRegistry) -> Result: - fulltext = data_registry.fulltext - if fulltext is None or fulltext == "": - # Problem: we should only report the problem with missing texts - passed = True - return self._result(passed) - elif len(fulltext) < 10000 and len(fulltext.split()) < 1400: - passed = False - return self._result(passed, message=self.failure_message) - else: - passed = True - return self._result(passed) diff --git a/qa/tests/checks/fulltext/test_extraction.py b/qa/tests/checks/fulltext/test_extraction.py new file mode 100644 index 00000000..d7504695 --- /dev/null +++ b/qa/tests/checks/fulltext/test_extraction.py @@ -0,0 +1,40 @@ +"""Tests for TextExtractionSuccessful.""" + +import pytest + +from qa.checks.base import MissingDataError +from qa.checks.fulltext.extraction import TextExtractionSuccessful +from qa.checks.models import Flag, FulltextReport, QaDataRegistry + + +def fulltext_report(flags: list[Flag] | None = None) -> FulltextReport: + return FulltextReport(submission_id=1, data={}, flags=flags or []) + + +class TestTextExtractionSuccessful: + def test_pass_when_no_flags(self): + report = fulltext_report() + result = TextExtractionSuccessful().run(QaDataRegistry(fulltext_report=report)) + assert result.passed + + def test_fail_on_extraction_failed_flag(self): + report = fulltext_report( + flags=[ + Flag( + id="text-extraction-failed", + description="Text extraction failed: All extraction methods failed", + ) + ], + ) + result = TextExtractionSuccessful().run(QaDataRegistry(fulltext_report=report)) + assert not result.passed + assert result.message == "Text extraction failed." + + def test_pass_with_unrelated_flags(self): + report = fulltext_report(flags=[Flag(id="some-other-flag", description="unrelated")]) + result = TextExtractionSuccessful().run(QaDataRegistry(fulltext_report=report)) + assert result.passed + + def test_missing_fulltext_report_raises(self): + with pytest.raises(MissingDataError): + TextExtractionSuccessful().run(QaDataRegistry()) diff --git a/qa/tests/checks/fulltext/test_text.py b/qa/tests/checks/fulltext/test_text.py index e1379b02..148ab10e 100644 --- a/qa/tests/checks/fulltext/test_text.py +++ b/qa/tests/checks/fulltext/test_text.py @@ -1,38 +1,26 @@ -"""Tests for AbstractIsValid.""" - -from qa.checks.models import QaDataRegistry -from qa.checks.fulltext.text_checks import ( - FulltextExtractedCheck, - FulltextNotTooShortCheck, -) - - -class TestFulltextExtractedCheck: - def test_pass_normal(self): - text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " - result = FulltextExtractedCheck().run(QaDataRegistry(fulltext=text)) - assert result.passed - - def test_fail_on_none(self): - text = None - result = FulltextExtractedCheck().run(QaDataRegistry(fulltext=text)) - assert not result.passed - - def test_fail_on_empty(self): - text = "" - result = FulltextExtractedCheck().run(QaDataRegistry(fulltext=text)) - assert not result.passed - - -class TestFulltextNotTooShortCheck: - def test_pass_normal(self): - text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " * 140 - - result = FulltextNotTooShortCheck().run(QaDataRegistry(fulltext=text)) - assert result.passed - - def test_fail_nospaces(self): - text = "Inthiswork, westudyaaa, bbb, andcccandconcludeddd. " * 100 - - result = FulltextNotTooShortCheck().run(QaDataRegistry(fulltext=text)) - assert not result.passed +"""Tests for fulltext structure checks.""" + +from qa.checks.models import QaDataRegistry +from qa.checks.fulltext.structure import FulltextNotTooShort + + +class TestFulltextNotTooShort: + def test_pass_normal(self): + text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " * 140 + + result = FulltextNotTooShort().run(QaDataRegistry(fulltext=text)) + assert result.passed + + def test_fail_nospaces(self): + text = "Inthiswork, westudyaaa, bbb, andcccandconcludeddd. " * 100 + + result = FulltextNotTooShort().run(QaDataRegistry(fulltext=text)) + assert not result.passed + + def test_pass_on_none(self): + result = FulltextNotTooShort().run(QaDataRegistry(fulltext=None)) + assert result.passed + + def test_pass_on_empty(self): + result = FulltextNotTooShort().run(QaDataRegistry(fulltext="")) + assert result.passed From 95dcc0eacd52978219d40ef8290b435b980de339 Mon Sep 17 00:00:00 2001 From: carly Date: Tue, 21 Jul 2026 17:09:19 -0400 Subject: [PATCH 26/34] add convenience methods, fulltext and fulltext_report should never be None --- qa/qa/checks/fulltext/extraction.py | 8 ++++++-- qa/qa/checks/fulltext/structure.py | 19 ++++++++++--------- qa/tests/checks/fulltext/test_extraction.py | 16 ++++------------ qa/tests/checks/fulltext/test_text.py | 15 +++++---------- 4 files changed, 25 insertions(+), 33 deletions(-) diff --git a/qa/qa/checks/fulltext/extraction.py b/qa/qa/checks/fulltext/extraction.py index 09e8deb8..ee0f554e 100644 --- a/qa/qa/checks/fulltext/extraction.py +++ b/qa/qa/checks/fulltext/extraction.py @@ -1,6 +1,6 @@ from qa.checks.base import BaseCheck -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result +from qa.checks.models import FulltextReport, OnFailurePolicy, QaDataRegistry, Result class TextExtractionSuccessful(BaseCheck): @@ -9,13 +9,17 @@ class TextExtractionSuccessful(BaseCheck): id = 14 version = "1.0.0" description = "Text extraction was successful." - on_failure_policy = OnFailurePolicy.REJECT + on_failure_policy = OnFailurePolicy.WARN failure_message = "Text extraction failed." required_data = {"fulltext_report"} failure_flag_id = "text-extraction-failed" + @classmethod + def check(cls, fulltext_report: FulltextReport) -> Result: + return cls().run(QaDataRegistry(fulltext_report=fulltext_report)) + @property def config(self) -> dict: return { diff --git a/qa/qa/checks/fulltext/structure.py b/qa/qa/checks/fulltext/structure.py index d28bea17..c34defcc 100644 --- a/qa/qa/checks/fulltext/structure.py +++ b/qa/qa/checks/fulltext/structure.py @@ -9,7 +9,7 @@ class FulltextNotTooShort(BaseCheck): id = 15 version = "1.0.0" description = "The full text extracted is not too short." - on_failure_policy = OnFailurePolicy.REJECT + on_failure_policy = OnFailurePolicy.WARN failure_message = "Text too short." required_inputs = {"fulltext"} @@ -17,6 +17,10 @@ class FulltextNotTooShort(BaseCheck): min_chars = 10000 min_words = 1400 + @classmethod + def check(cls, fulltext: str) -> Result: + return cls().run(QaDataRegistry(fulltext=fulltext)) + @property def config(self) -> dict: return { @@ -27,12 +31,9 @@ def config(self) -> dict: def _run(self, data_registry: QaDataRegistry) -> Result: fulltext = data_registry.fulltext - if fulltext is None or fulltext == "": - passed = True - return self._result(passed) - elif len(fulltext) < self.min_chars and len(fulltext.split()) < self.min_words: - passed = False - return self._result(passed, message=self.failure_message) + assert fulltext is not None + + if len(fulltext) < self.min_chars and len(fulltext.split()) < self.min_words: + return self._result(passed=False, message=self.failure_message) else: - passed = True - return self._result(passed) + return self._result(passed=True) diff --git a/qa/tests/checks/fulltext/test_extraction.py b/qa/tests/checks/fulltext/test_extraction.py index d7504695..ed686903 100644 --- a/qa/tests/checks/fulltext/test_extraction.py +++ b/qa/tests/checks/fulltext/test_extraction.py @@ -1,10 +1,7 @@ """Tests for TextExtractionSuccessful.""" -import pytest - -from qa.checks.base import MissingDataError from qa.checks.fulltext.extraction import TextExtractionSuccessful -from qa.checks.models import Flag, FulltextReport, QaDataRegistry +from qa.checks.models import Flag, FulltextReport def fulltext_report(flags: list[Flag] | None = None) -> FulltextReport: @@ -13,8 +10,7 @@ def fulltext_report(flags: list[Flag] | None = None) -> FulltextReport: class TestTextExtractionSuccessful: def test_pass_when_no_flags(self): - report = fulltext_report() - result = TextExtractionSuccessful().run(QaDataRegistry(fulltext_report=report)) + result = TextExtractionSuccessful.check(fulltext_report()) assert result.passed def test_fail_on_extraction_failed_flag(self): @@ -26,15 +22,11 @@ def test_fail_on_extraction_failed_flag(self): ) ], ) - result = TextExtractionSuccessful().run(QaDataRegistry(fulltext_report=report)) + result = TextExtractionSuccessful.check(report) assert not result.passed assert result.message == "Text extraction failed." def test_pass_with_unrelated_flags(self): report = fulltext_report(flags=[Flag(id="some-other-flag", description="unrelated")]) - result = TextExtractionSuccessful().run(QaDataRegistry(fulltext_report=report)) + result = TextExtractionSuccessful.check(report) assert result.passed - - def test_missing_fulltext_report_raises(self): - with pytest.raises(MissingDataError): - TextExtractionSuccessful().run(QaDataRegistry()) diff --git a/qa/tests/checks/fulltext/test_text.py b/qa/tests/checks/fulltext/test_text.py index 148ab10e..61d65b4c 100644 --- a/qa/tests/checks/fulltext/test_text.py +++ b/qa/tests/checks/fulltext/test_text.py @@ -1,6 +1,5 @@ """Tests for fulltext structure checks.""" -from qa.checks.models import QaDataRegistry from qa.checks.fulltext.structure import FulltextNotTooShort @@ -8,19 +7,15 @@ class TestFulltextNotTooShort: def test_pass_normal(self): text = "In this work, we study aaa, bbb, and ccc and conclude ddd. " * 140 - result = FulltextNotTooShort().run(QaDataRegistry(fulltext=text)) + result = FulltextNotTooShort.check(text) assert result.passed def test_fail_nospaces(self): text = "Inthiswork, westudyaaa, bbb, andcccandconcludeddd. " * 100 - result = FulltextNotTooShort().run(QaDataRegistry(fulltext=text)) + result = FulltextNotTooShort.check(text) assert not result.passed - def test_pass_on_none(self): - result = FulltextNotTooShort().run(QaDataRegistry(fulltext=None)) - assert result.passed - - def test_pass_on_empty(self): - result = FulltextNotTooShort().run(QaDataRegistry(fulltext="")) - assert result.passed + def test_fail_on_empty(self): + result = FulltextNotTooShort.check("") + assert not result.passed From e811745a0e4a2a9889a419eb1b8fd2d4ef79833b Mon Sep 17 00:00:00 2001 From: carly Date: Tue, 21 Jul 2026 17:14:33 -0400 Subject: [PATCH 27/34] rename test file, add tests for None case --- qa/tests/checks/fulltext/test_extraction.py | 8 +++++++- .../checks/fulltext/{test_text.py => test_structure.py} | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) rename qa/tests/checks/fulltext/{test_text.py => test_structure.py} (74%) diff --git a/qa/tests/checks/fulltext/test_extraction.py b/qa/tests/checks/fulltext/test_extraction.py index ed686903..0194244b 100644 --- a/qa/tests/checks/fulltext/test_extraction.py +++ b/qa/tests/checks/fulltext/test_extraction.py @@ -1,7 +1,9 @@ """Tests for TextExtractionSuccessful.""" +import pytest + from qa.checks.fulltext.extraction import TextExtractionSuccessful -from qa.checks.models import Flag, FulltextReport +from qa.checks.models import Flag, FulltextReport, QaDataRegistry def fulltext_report(flags: list[Flag] | None = None) -> FulltextReport: @@ -30,3 +32,7 @@ def test_pass_with_unrelated_flags(self): report = fulltext_report(flags=[Flag(id="some-other-flag", description="unrelated")]) result = TextExtractionSuccessful.check(report) assert result.passed + + def test_none_fulltext_report_raises(self): + with pytest.raises(AssertionError): + TextExtractionSuccessful()._run(QaDataRegistry(fulltext_report=None)) diff --git a/qa/tests/checks/fulltext/test_text.py b/qa/tests/checks/fulltext/test_structure.py similarity index 74% rename from qa/tests/checks/fulltext/test_text.py rename to qa/tests/checks/fulltext/test_structure.py index 61d65b4c..010f89c0 100644 --- a/qa/tests/checks/fulltext/test_text.py +++ b/qa/tests/checks/fulltext/test_structure.py @@ -1,6 +1,9 @@ """Tests for fulltext structure checks.""" +import pytest + from qa.checks.fulltext.structure import FulltextNotTooShort +from qa.checks.models import QaDataRegistry class TestFulltextNotTooShort: @@ -19,3 +22,7 @@ def test_fail_nospaces(self): def test_fail_on_empty(self): result = FulltextNotTooShort.check("") assert not result.passed + + def test_none_fulltext_raises(self): + with pytest.raises(AssertionError): + FulltextNotTooShort()._run(QaDataRegistry(fulltext=None)) From c358d9f098bb6092f3fcf0d26750d02300e45461 Mon Sep 17 00:00:00 2001 From: carly Date: Wed, 22 Jul 2026 17:12:24 -0400 Subject: [PATCH 28/34] move and rename submission checks, fix unit tests, fix unique id test --- qa/qa/checks/__init__.py | 8 ++-- qa/qa/checks/metadata/oversize.py | 23 ----------- qa/qa/checks/metadata/withdrawal.py | 28 ------------- qa/qa/checks/submission/files.py | 28 +++++++++++++ qa/qa/checks/submission/type.py | 26 +++++++++++++ qa/qa/checks/submission/user.py | 5 +++ qa/tests/checks/check_unique_ids.py | 13 ------- qa/tests/checks/metadata/test_oversize.py | 41 -------------------- qa/tests/checks/metadata/test_withdrawals.py | 27 ------------- qa/tests/checks/submission/test_files.py | 18 +++++++++ qa/tests/checks/submission/test_type.py | 18 +++++++++ qa/tests/checks/test_unique_ids.py | 37 ++++++++++++++++++ 12 files changed, 136 insertions(+), 136 deletions(-) delete mode 100644 qa/qa/checks/metadata/oversize.py delete mode 100644 qa/qa/checks/metadata/withdrawal.py create mode 100644 qa/qa/checks/submission/files.py create mode 100644 qa/qa/checks/submission/type.py create mode 100644 qa/qa/checks/submission/user.py delete mode 100644 qa/tests/checks/check_unique_ids.py delete mode 100644 qa/tests/checks/metadata/test_oversize.py delete mode 100644 qa/tests/checks/metadata/test_withdrawals.py create mode 100644 qa/tests/checks/submission/test_files.py create mode 100644 qa/tests/checks/submission/test_type.py create mode 100644 qa/tests/checks/test_unique_ids.py diff --git a/qa/qa/checks/__init__.py b/qa/qa/checks/__init__.py index 1a49c4a8..ac2e2e51 100644 --- a/qa/qa/checks/__init__.py +++ b/qa/qa/checks/__init__.py @@ -11,8 +11,8 @@ from qa.checks.metadata.report_num import ReportNumIsValid # noqa from qa.checks.metadata.title import TitleIsValid # noqa -from qa.checks.metadata.oversize import OversizeCheck # noqa -from qa.checks.metadata.withdrawal import WithdrawalCheck # noqa +from qa.checks.submission.files import DoesNotExceedTheFileSizeLimit # noqa +from qa.checks.submission.type import IsNotAWithdrawal # noqa from qa.checks.fulltext.text_checks import FulltextExtractedCheck, FulltextNotTooShortCheck # noqa @@ -26,8 +26,8 @@ DoiIsValid(), MscClassIsValid(), AcmClassIsValid(), - OversizeCheck(), - WithdrawalCheck(), + DoesNotExceedTheFileSizeLimit(), + IsNotAWithdrawal(), FulltextExtractedCheck(), FulltextNotTooShortCheck(), ] diff --git a/qa/qa/checks/metadata/oversize.py b/qa/qa/checks/metadata/oversize.py deleted file mode 100644 index 53e2afef..00000000 --- a/qa/qa/checks/metadata/oversize.py +++ /dev/null @@ -1,23 +0,0 @@ -from qa.checks.base import BaseCheck -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result - - -class OversizeCheck(BaseCheck): - name = "oversize" - display_name = "Oversize" - id = 48 - version = "0.1.0" - description = "The oversize check fails on submissions which are too large." - on_failure_policy = OnFailurePolicy.REJECT - failure_message = "Submission is oversize ." - - required_inputs = {"metadata"} - - def _run(self, data_registry: QaDataRegistry) -> Result: - assert data_registry.metadata is not None - if data_registry.metadata.is_oversize: - passed = False - message = self.failure_message - return self._result(passed=passed, message=message) - else: - return self._result(passed=True, message="") diff --git a/qa/qa/checks/metadata/withdrawal.py b/qa/qa/checks/metadata/withdrawal.py deleted file mode 100644 index 1a26af7a..00000000 --- a/qa/qa/checks/metadata/withdrawal.py +++ /dev/null @@ -1,28 +0,0 @@ -from qa.checks.base import BaseCheck - -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result - - -class WithdrawalCheck(BaseCheck): - """Check for withdrawals, which require staff approval.""" - - name = "withdrawal" - display_name = "Withdrawal" - id = 14 - version = "1.0.0" - description = "Review all withdrawals." - on_failure_policy = OnFailurePolicy.REJECT - failure_message = " Result: - assert data_registry.metadata is not None - _type = data_registry.metadata.type - - if _type == "wdr": - passed = False - return self._result(passed, message=self.failure_message) - else: - passed = True - return self._result(passed) diff --git a/qa/qa/checks/submission/files.py b/qa/qa/checks/submission/files.py new file mode 100644 index 00000000..6d92377d --- /dev/null +++ b/qa/qa/checks/submission/files.py @@ -0,0 +1,28 @@ +"""Submission file checks.""" + +from qa.checks.base import BaseCheck +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result + + +class DoesNotExceedTheFileSizeLimit(BaseCheck): + name = "does_not_exceed_the_file_size_limit" + display_name = "Does Not Exceed the File Size Limit" + id = 48 + version = "1.0.0" + description = "The submission does not exceed the file size limit." + on_failure_policy = OnFailurePolicy.WARN + failure_message = "Submission exceeds the file size limit." + + required_inputs = {"metadata"} + + def _run(self, data_registry: QaDataRegistry) -> Result: + assert data_registry.metadata is not None + + if data_registry.metadata.is_oversize: + return self._result(passed=False, message=self.failure_message) + else: + return self._result(passed=True) + + +# TODO: add HTML file type check +# TODO: add TeX processing flag checks (post-exCITe) diff --git a/qa/qa/checks/submission/type.py b/qa/qa/checks/submission/type.py new file mode 100644 index 00000000..0100acb1 --- /dev/null +++ b/qa/qa/checks/submission/type.py @@ -0,0 +1,26 @@ +"""Submission type checks.""" + +from qa.checks.base import BaseCheck +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result + + +class IsNotAWithdrawal(BaseCheck): + name = "is_not_a_withdrawal" + display_name = "Is Not A Withdrawal" + id = 49 + version = "1.0.0" + description = "The submission is not a withdrawal." + on_failure_policy = OnFailurePolicy.WARN + failure_message = "The submission is a withdrawal which requires staff approval." + + required_inputs = {"metadata"} + + def _run(self, data_registry: QaDataRegistry) -> Result: + assert data_registry.metadata is not None + + _type = data_registry.metadata.type + + if _type == "wdr": + return self._result(passed=False, message=self.failure_message) + else: + return self._result(passed=True) diff --git a/qa/qa/checks/submission/user.py b/qa/qa/checks/submission/user.py new file mode 100644 index 00000000..25c443d3 --- /dev/null +++ b/qa/qa/checks/submission/user.py @@ -0,0 +1,5 @@ +"""Submission user checks.""" + +# TODO add flagged user check +# TODO add proxy check +# TODO add proxy of proxy check diff --git a/qa/tests/checks/check_unique_ids.py b/qa/tests/checks/check_unique_ids.py deleted file mode 100644 index 357e00de..00000000 --- a/qa/tests/checks/check_unique_ids.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Some consistency checks on all_checks.""" - -from qa.checks import checks as all_checks - - -class TestCheckIds: - def test_all_check_ids_unique(self): - assert len(all_checks) == len(set([check.id for check in all_checks])) - - -class TestCheckNames: - def test_all_check_names_unique(self): - assert len(all_checks) == len(set([check.name.lower() for check in all_checks])) diff --git a/qa/tests/checks/metadata/test_oversize.py b/qa/tests/checks/metadata/test_oversize.py deleted file mode 100644 index 41a0cbe1..00000000 --- a/qa/tests/checks/metadata/test_oversize.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Tests for AbstractIsValid.""" - -from qa.checks.models import QaDataRegistry, SubmissionMetadata -from qa.checks.metadata.oversize import OversizeCheck - - -class TestOversizeCheck: - def test_new_pass(self): - check = OversizeCheck() - metadata = SubmissionMetadata(type="new", title="Test Title", is_oversize=False) - registry = QaDataRegistry(metadata=metadata) - result = check._run(registry) - assert result.passed - - def test_rep_pass(self): - check = OversizeCheck() - metadata = SubmissionMetadata(type="rep", title="Test Title", is_oversize=False) - registry = QaDataRegistry(metadata=metadata) - result = check._run(registry) - assert result.passed - - def test_cross_pass(self): - check = OversizeCheck() - metadata = SubmissionMetadata(type="cross", title="Test Title", is_oversize=False) - registry = QaDataRegistry(metadata=metadata) - result = check._run(registry) - assert result.passed - - def test_oversize_fail(self): - check = OversizeCheck() - metadata = SubmissionMetadata(type="new", title="Test Title", is_oversize=True) - registry = QaDataRegistry(metadata=metadata) - result = check._run(registry) - assert not result.passed - - def test_oversize_rep_fail(self): - check = OversizeCheck() - metadata = SubmissionMetadata(type="rep", title="Test Title", is_oversize=True) - registry = QaDataRegistry(metadata=metadata) - result = check._run(registry) - assert not result.passed diff --git a/qa/tests/checks/metadata/test_withdrawals.py b/qa/tests/checks/metadata/test_withdrawals.py deleted file mode 100644 index 4a3e9849..00000000 --- a/qa/tests/checks/metadata/test_withdrawals.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Tests for AbstractIsValid.""" - -from qa.checks.models import QaDataRegistry, SubmissionMetadata -from qa.checks.metadata.withdrawal import WithdrawalCheck - - -class TestWithdrawalCheck: - def test_new_pass(self): - check = WithdrawalCheck() - metadata = SubmissionMetadata(type="new", title="Test Title") - registry = QaDataRegistry(metadata=metadata) - result = check._run(registry) - assert result.passed - - def test_rep_pass(self): - check = WithdrawalCheck() - metadata = SubmissionMetadata(type="rep", title="Test Title") - registry = QaDataRegistry(metadata=metadata) - result = check._run(registry) - assert result.passed - - def test_wdr_fail(self): - check = WithdrawalCheck() - metadata = SubmissionMetadata(type="wdr", title="Test Title") - registry = QaDataRegistry(metadata=metadata) - result = check._run(registry) - assert not result.passed diff --git a/qa/tests/checks/submission/test_files.py b/qa/tests/checks/submission/test_files.py new file mode 100644 index 00000000..40bc8816 --- /dev/null +++ b/qa/tests/checks/submission/test_files.py @@ -0,0 +1,18 @@ +"""Tests for submission file checks.""" + +from qa.checks.models import QaDataRegistry, SubmissionMetadata +from qa.checks.submission.files import DoesNotExceedTheFileSizeLimit + + +class TestOversizeCheck: + def test_oversize_pass(self): + metadata = SubmissionMetadata(type="new", title="Test Title", is_oversize=False) + result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(metadata=metadata)) + + assert result.passed + + def test_oversize_fail(self): + metadata = SubmissionMetadata(type="new", title="Test Title", is_oversize=True) + result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(metadata=metadata)) + + assert not result.passed diff --git a/qa/tests/checks/submission/test_type.py b/qa/tests/checks/submission/test_type.py new file mode 100644 index 00000000..cb66fafe --- /dev/null +++ b/qa/tests/checks/submission/test_type.py @@ -0,0 +1,18 @@ +"""Tests for submission type checks.""" + +from qa.checks.models import QaDataRegistry, SubmissionMetadata +from qa.checks.submission.type import IsNotAWithdrawal + + +class TestWithdrawalCheck: + def test_wdr_pass(self): + metadata = SubmissionMetadata(type="new", title="Test Title") + result = IsNotAWithdrawal().run(QaDataRegistry(metadata=metadata)) + + assert result.passed + + def test_wdr_fail(self): + metadata = SubmissionMetadata(type="wdr", title="Test Title") + result = IsNotAWithdrawal().run(QaDataRegistry(metadata=metadata)) + + assert not result.passed diff --git a/qa/tests/checks/test_unique_ids.py b/qa/tests/checks/test_unique_ids.py new file mode 100644 index 00000000..42b083cf --- /dev/null +++ b/qa/tests/checks/test_unique_ids.py @@ -0,0 +1,37 @@ +"""Consistency checks across every implemented check class, not just the registered ones.""" + +import qa.checks # noqa: F401 imports every check module so all subclasses are defined +from qa.checks.base import BaseCheck + + +def _all_check_classes() -> list[type[BaseCheck]]: + """Recursively collect every concrete check class (one that declares its own id/name).""" + classes: list[type[BaseCheck]] = [] + seen: set[type[BaseCheck]] = set() + stack = [BaseCheck] + + while stack: + cls = stack.pop() + for sub in cls.__subclasses__(): + if sub in seen: + continue + seen.add(sub) + stack.append(sub) + if "id" in vars(sub): + classes.append(sub) + + return classes + + +class TestCheckIds: + def test_all_check_ids_unique(self): + classes = _all_check_classes() + ids = [cls.id for cls in classes] + assert len(classes) == len(set(ids)) + + +class TestCheckNames: + def test_all_check_names_unique(self): + classes = _all_check_classes() + names = [cls.name.lower() for cls in classes] + assert len(classes) == len(set(names)) From 5f635357b9f4014b9f6383ec623ae2ab95591414 Mon Sep 17 00:00:00 2001 From: carly Date: Wed, 22 Jul 2026 17:43:46 -0400 Subject: [PATCH 29/34] bugfix required_data, update tests, separate metadata from submission metadata --- qa/qa/checks/metadata/abstract.py | 4 +-- qa/qa/checks/metadata/acm_class.py | 4 +-- qa/qa/checks/metadata/authors.py | 4 +-- qa/qa/checks/metadata/comments.py | 4 +-- qa/qa/checks/metadata/doi.py | 4 +-- qa/qa/checks/metadata/journal_ref.py | 4 +-- qa/qa/checks/metadata/msc_class.py | 4 +-- qa/qa/checks/metadata/report_num.py | 4 +-- qa/qa/checks/metadata/title.py | 4 +-- qa/qa/checks/models.py | 37 +++++++++++++---------- qa/qa/checks/submission/files.py | 6 ++-- qa/qa/checks/submission/type.py | 6 ++-- qa/tests/checks/generic/test_text.py | 4 +-- qa/tests/checks/metadata/test_abstract.py | 4 +-- qa/tests/checks/metadata/test_authors.py | 4 +-- qa/tests/checks/metadata/test_title.py | 4 +-- qa/tests/checks/submission/test_files.py | 15 ++++++--- qa/tests/checks/submission/test_type.py | 15 ++++++--- 18 files changed, 75 insertions(+), 56 deletions(-) diff --git a/qa/qa/checks/metadata/abstract.py b/qa/qa/checks/metadata/abstract.py index 1a8b753b..fac6ff00 100644 --- a/qa/qa/checks/metadata/abstract.py +++ b/qa/qa/checks/metadata/abstract.py @@ -1,7 +1,7 @@ """Abstract metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result from qa.checks.generic.text import ( AllBracketsBalanced, DoesNotBeginWithAbstract, @@ -37,7 +37,7 @@ class AbstractIsValid(BaseAggregateCheck): @classmethod def check(cls, abstract: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=SubmissionMetadata(abstract=abstract))) + return cls().run(QaDataRegistry(metadata=Metadata(abstract=abstract))) _checks = ( NotTooShort(5, on_failure_policy=OnFailurePolicy.WARN, data="metadata", field="abstract"), diff --git a/qa/qa/checks/metadata/acm_class.py b/qa/qa/checks/metadata/acm_class.py index ca1de6b1..98f6e648 100644 --- a/qa/qa/checks/metadata/acm_class.py +++ b/qa/qa/checks/metadata/acm_class.py @@ -1,7 +1,7 @@ """ACM class metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result from qa.checks.generic.text import ( NotTooLong, DoesNotContainUrl, @@ -29,7 +29,7 @@ class AcmClassIsValid(BaseAggregateCheck): @classmethod def check(cls, acm_class: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=SubmissionMetadata(acm_class=acm_class))) + return cls().run(QaDataRegistry(metadata=Metadata(acm_class=acm_class))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/authors.py b/qa/qa/checks/metadata/authors.py index 1dfc075c..c35f52a8 100644 --- a/qa/qa/checks/metadata/authors.py +++ b/qa/qa/checks/metadata/authors.py @@ -1,7 +1,7 @@ """Author metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result from qa.checks.generic.text import ( AllBracketsBalanced, DoesNotBeginWithAuthor, @@ -46,7 +46,7 @@ class AuthorsAreValid(BaseAggregateCheck): @classmethod def check(cls, authors: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=SubmissionMetadata(authors=authors))) + return cls().run(QaDataRegistry(metadata=Metadata(authors=authors))) _checks = ( NotTooShort(4, on_failure_policy=OnFailurePolicy.WARN, data="metadata", field="authors"), diff --git a/qa/qa/checks/metadata/comments.py b/qa/qa/checks/metadata/comments.py index 17c21001..4c253653 100644 --- a/qa/qa/checks/metadata/comments.py +++ b/qa/qa/checks/metadata/comments.py @@ -1,7 +1,7 @@ """Comments metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result from qa.checks.generic.text import ( NotTooLong, DoesNotContainLinebreak, @@ -32,7 +32,7 @@ class CommentsAreValid(BaseAggregateCheck): @classmethod def check(cls, comments: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=SubmissionMetadata(comments=comments))) + return cls().run(QaDataRegistry(metadata=Metadata(comments=comments))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/doi.py b/qa/qa/checks/metadata/doi.py index 7e4e3cd0..96af8102 100644 --- a/qa/qa/checks/metadata/doi.py +++ b/qa/qa/checks/metadata/doi.py @@ -1,7 +1,7 @@ """DOI metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result from qa.checks.generic.text import ( NotTooShort, NotTooLong, @@ -32,7 +32,7 @@ class DoiIsValid(BaseAggregateCheck): @classmethod def check(cls, doi: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=SubmissionMetadata(doi=doi))) + return cls().run(QaDataRegistry(metadata=Metadata(doi=doi))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/journal_ref.py b/qa/qa/checks/metadata/journal_ref.py index 75d3dcd6..121203c5 100644 --- a/qa/qa/checks/metadata/journal_ref.py +++ b/qa/qa/checks/metadata/journal_ref.py @@ -1,7 +1,7 @@ """Journal reference metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result from qa.checks.generic.text import ( NotTooShort, NotTooLong, @@ -34,7 +34,7 @@ class JournalRefIsValid(BaseAggregateCheck): @classmethod def check(cls, journal_ref: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=SubmissionMetadata(journal_ref=journal_ref))) + return cls().run(QaDataRegistry(metadata=Metadata(journal_ref=journal_ref))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/msc_class.py b/qa/qa/checks/metadata/msc_class.py index 3ae68e26..c1055a9a 100644 --- a/qa/qa/checks/metadata/msc_class.py +++ b/qa/qa/checks/metadata/msc_class.py @@ -1,7 +1,7 @@ """MSC class metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result from qa.checks.generic.text import ( NotTooLong, DoesNotContainUrl, @@ -30,7 +30,7 @@ class MscClassIsValid(BaseAggregateCheck): @classmethod def check(cls, msc_class: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=SubmissionMetadata(msc_class=msc_class))) + return cls().run(QaDataRegistry(metadata=Metadata(msc_class=msc_class))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/report_num.py b/qa/qa/checks/metadata/report_num.py index 7906f7ca..bf85fbe1 100644 --- a/qa/qa/checks/metadata/report_num.py +++ b/qa/qa/checks/metadata/report_num.py @@ -1,7 +1,7 @@ """Report number metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result from qa.checks.generic.text import ( NotTooShort, NotTooLong, @@ -32,7 +32,7 @@ class ReportNumIsValid(BaseAggregateCheck): @classmethod def check(cls, report_num: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=SubmissionMetadata(report_num=report_num))) + return cls().run(QaDataRegistry(metadata=Metadata(report_num=report_num))) def _run(self, data_registry: QaDataRegistry) -> Result: """Both None and empty string are valid and should pass without running sub-checks.""" diff --git a/qa/qa/checks/metadata/title.py b/qa/qa/checks/metadata/title.py index 7cadbb0c..af878e5f 100644 --- a/qa/qa/checks/metadata/title.py +++ b/qa/qa/checks/metadata/title.py @@ -1,7 +1,7 @@ """Title metadata checks.""" from qa.checks.base import BaseAggregateCheck -from qa.checks.models import QaDataRegistry, OnFailurePolicy, SubmissionMetadata, Result +from qa.checks.models import QaDataRegistry, OnFailurePolicy, Metadata, Result from qa.checks.generic.text import ( NotTooShort, NotTooLong, @@ -37,7 +37,7 @@ class TitleIsValid(BaseAggregateCheck): @classmethod def check(cls, title: str | None) -> Result: - return cls().run(QaDataRegistry(metadata=SubmissionMetadata(title=title))) + return cls().run(QaDataRegistry(metadata=Metadata(title=title))) _checks = ( NotTooShort(5, on_failure_policy=OnFailurePolicy.WARN, data="metadata", field="title"), diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index fc3928ea..8eefed0d 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -1,16 +1,17 @@ from pydantic import BaseModel -from typing import Protocol, runtime_checkable +from typing import Literal, Protocol, runtime_checkable from enum import StrEnum class OnFailurePolicy(StrEnum): """ - The on failure policy describes how to handle a failure (non-passing result) from a particular check. + The on failure policy is an attribute of a check. It is part of that check's configuration. + It describes how to handle a failure (non-passing result) from that check. Each instance of a check should be configured with only one on failure policy. IGNORE - failure should be ignored - WARN - failure should elicit a non-blocking warning - REJECT - failure should be a blocking error + WARN - failure should not block but prompt a warning or review + REJECT - failure should block further progression """ IGNORE = "ignore" @@ -20,9 +21,10 @@ class OnFailurePolicy(StrEnum): class Disposition(StrEnum): """ - The disposition represents the end state of running a check. - It rationalizes a passing/non-passing result against that check's on failure policy. + A disposition is an attribute of a check result. It represents the end state of running a check on a particular input. + It is the rationalization of the result (passing/non-passing) against that check's on failure policy. All passing check results will provide a disposition of "ok". + The disposition should be used by consumers of check results to guide next steps. """ OK = "ok" @@ -52,13 +54,20 @@ class Result(BaseModel): results: list["Result"] | None = None -class SubmissionMetadata(BaseModel): +class SubmissionMetadata(BaseModel): # check which fields are always provided by the snapshot and which are optional + """Metadata about a submission.""" + + type: Literal["new", "rep", "wdr", "jref", "cross"] + is_oversize: bool + data_version: int + metadata_version: int + + +class Metadata(BaseModel): """ - Information about a submission. - This should be a concrete implementation of MetadataProtocol for use in checks. + Paper metadata. """ - # MetadataProtocol fields title: str | None = None authors: str | None = None abstract: str | None = None @@ -68,11 +77,6 @@ class SubmissionMetadata(BaseModel): doi: str | None = None msc_class: str | None = None acm_class: str | None = None - # End MetadataProtocol fields - type: str | None = None # one of: "new", "rep", "wdr", "jref", or "cross" - is_oversize: bool = False - data_version: int = 0 - metadata_version: int = 0 @runtime_checkable @@ -102,4 +106,5 @@ class QaDataRegistry(BaseModel): author_report: str | None = None flagged_terms_report: str | None = None tex_report: str | None = None - metadata: SubmissionMetadata | None = None + metadata: Metadata | None = None + submission_metadata: SubmissionMetadata | None = None diff --git a/qa/qa/checks/submission/files.py b/qa/qa/checks/submission/files.py index 6d92377d..56108667 100644 --- a/qa/qa/checks/submission/files.py +++ b/qa/qa/checks/submission/files.py @@ -13,12 +13,12 @@ class DoesNotExceedTheFileSizeLimit(BaseCheck): on_failure_policy = OnFailurePolicy.WARN failure_message = "Submission exceeds the file size limit." - required_inputs = {"metadata"} + required_data = {"submission_metadata"} def _run(self, data_registry: QaDataRegistry) -> Result: - assert data_registry.metadata is not None + assert data_registry.submission_metadata is not None - if data_registry.metadata.is_oversize: + if data_registry.submission_metadata.is_oversize: return self._result(passed=False, message=self.failure_message) else: return self._result(passed=True) diff --git a/qa/qa/checks/submission/type.py b/qa/qa/checks/submission/type.py index 0100acb1..b33ec474 100644 --- a/qa/qa/checks/submission/type.py +++ b/qa/qa/checks/submission/type.py @@ -13,12 +13,12 @@ class IsNotAWithdrawal(BaseCheck): on_failure_policy = OnFailurePolicy.WARN failure_message = "The submission is a withdrawal which requires staff approval." - required_inputs = {"metadata"} + required_data = {"submission_metadata"} def _run(self, data_registry: QaDataRegistry) -> Result: - assert data_registry.metadata is not None + assert data_registry.submission_metadata is not None - _type = data_registry.metadata.type + _type = data_registry.submission_metadata.type if _type == "wdr": return self._result(passed=False, message=self.failure_message) diff --git a/qa/tests/checks/generic/test_text.py b/qa/tests/checks/generic/test_text.py index 89d3f2a9..e67dfb09 100644 --- a/qa/tests/checks/generic/test_text.py +++ b/qa/tests/checks/generic/test_text.py @@ -3,7 +3,7 @@ import pytest from qa.checks.base import EmptyFieldError, MissingDataError -from qa.checks.models import QaDataRegistry, SubmissionMetadata, OnFailurePolicy +from qa.checks.models import QaDataRegistry, Metadata, OnFailurePolicy from qa.checks.generic.text import ( AllBracketsBalanced, DoesNotBeginWithTitle, @@ -25,7 +25,7 @@ def inputs(title: str | None) -> QaDataRegistry: - return QaDataRegistry(metadata=SubmissionMetadata(title=title)) + return QaDataRegistry(metadata=Metadata(title=title)) def make(cls, **kwargs): diff --git a/qa/tests/checks/metadata/test_abstract.py b/qa/tests/checks/metadata/test_abstract.py index d9fb00e6..8227b16b 100644 --- a/qa/tests/checks/metadata/test_abstract.py +++ b/qa/tests/checks/metadata/test_abstract.py @@ -3,7 +3,7 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result from qa.checks.metadata.abstract import AbstractIsValid @@ -87,7 +87,7 @@ def test_warn_tex_begin_no_brace(self): assert not sub_result(result, "does_not_contain_tex_begin_env").passed def test_none_field_short_circuits(self): - result = AbstractIsValid().run(QaDataRegistry(metadata=SubmissionMetadata(abstract=None))) + result = AbstractIsValid().run(QaDataRegistry(metadata=Metadata(abstract=None))) assert not result.passed assert result.results == [] assert result.message == "Abstract is invalid or empty." diff --git a/qa/tests/checks/metadata/test_authors.py b/qa/tests/checks/metadata/test_authors.py index ee4cafb3..80f2753a 100644 --- a/qa/tests/checks/metadata/test_authors.py +++ b/qa/tests/checks/metadata/test_authors.py @@ -3,7 +3,7 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result from qa.checks.metadata.authors import AuthorsAreValid @@ -267,7 +267,7 @@ def test_pass_and_separated(self): ).passed def test_none_field_short_circuits(self): - result = AuthorsAreValid().run(QaDataRegistry(metadata=SubmissionMetadata(authors=None))) + result = AuthorsAreValid().run(QaDataRegistry(metadata=Metadata(authors=None))) assert not result.passed assert result.results == [] assert result.message == "Authors are invalid or empty." diff --git a/qa/tests/checks/metadata/test_title.py b/qa/tests/checks/metadata/test_title.py index 678caec5..7a6c3161 100644 --- a/qa/tests/checks/metadata/test_title.py +++ b/qa/tests/checks/metadata/test_title.py @@ -3,7 +3,7 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import OnFailurePolicy, QaDataRegistry, SubmissionMetadata, Result +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Metadata, Result from qa.checks.metadata.title import TitleIsValid @@ -71,7 +71,7 @@ def test_pass_long_greek(self): assert result.passed def test_none_field_short_circuits(self): - result = TitleIsValid().run(QaDataRegistry(metadata=SubmissionMetadata(title=None))) + result = TitleIsValid().run(QaDataRegistry(metadata=Metadata(title=None))) assert not result.passed assert result.results == [] assert result.message == "Title is invalid or empty." diff --git a/qa/tests/checks/submission/test_files.py b/qa/tests/checks/submission/test_files.py index 40bc8816..28027920 100644 --- a/qa/tests/checks/submission/test_files.py +++ b/qa/tests/checks/submission/test_files.py @@ -1,18 +1,25 @@ """Tests for submission file checks.""" +import pytest + +from qa.checks.base import MissingDataError from qa.checks.models import QaDataRegistry, SubmissionMetadata from qa.checks.submission.files import DoesNotExceedTheFileSizeLimit class TestOversizeCheck: def test_oversize_pass(self): - metadata = SubmissionMetadata(type="new", title="Test Title", is_oversize=False) - result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(metadata=metadata)) + sub_metadata = SubmissionMetadata(type="new", is_oversize=False, data_version=1, metadata_version=1) + result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submission_metadata=sub_metadata)) assert result.passed def test_oversize_fail(self): - metadata = SubmissionMetadata(type="new", title="Test Title", is_oversize=True) - result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(metadata=metadata)) + sub_metadata = SubmissionMetadata(type="new", is_oversize=True, data_version=1, metadata_version=1) + result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submission_metadata=sub_metadata)) assert not result.passed + + def test_missing_submission_metadata_raises(self): + with pytest.raises(MissingDataError): + DoesNotExceedTheFileSizeLimit().run(QaDataRegistry()) diff --git a/qa/tests/checks/submission/test_type.py b/qa/tests/checks/submission/test_type.py index cb66fafe..1970e647 100644 --- a/qa/tests/checks/submission/test_type.py +++ b/qa/tests/checks/submission/test_type.py @@ -1,18 +1,25 @@ """Tests for submission type checks.""" +import pytest + +from qa.checks.base import MissingDataError from qa.checks.models import QaDataRegistry, SubmissionMetadata from qa.checks.submission.type import IsNotAWithdrawal class TestWithdrawalCheck: def test_wdr_pass(self): - metadata = SubmissionMetadata(type="new", title="Test Title") - result = IsNotAWithdrawal().run(QaDataRegistry(metadata=metadata)) + sub_metadata = SubmissionMetadata(type="new", is_oversize=False, data_version=1, metadata_version=1) + result = IsNotAWithdrawal().run(QaDataRegistry(submission_metadata=sub_metadata)) assert result.passed def test_wdr_fail(self): - metadata = SubmissionMetadata(type="wdr", title="Test Title") - result = IsNotAWithdrawal().run(QaDataRegistry(metadata=metadata)) + sub_metadata = SubmissionMetadata(type="wdr", is_oversize=False, data_version=1, metadata_version=1) + result = IsNotAWithdrawal().run(QaDataRegistry(submission_metadata=sub_metadata)) assert not result.passed + + def test_missing_submission_metadata_raises(self): + with pytest.raises(MissingDataError): + IsNotAWithdrawal().run(QaDataRegistry()) From 48a9c0ad0f91e9c978b4ef6d1a7057d9f02976c1 Mon Sep 17 00:00:00 2001 From: carly Date: Thu, 23 Jul 2026 11:00:19 -0400 Subject: [PATCH 30/34] rename SubmissionMetadata --- qa/qa/checks/models.py | 4 +- qa/qa/checks/submission/files.py | 56 ++++++++++++------------ qa/qa/checks/submission/type.py | 52 +++++++++++----------- qa/tests/checks/submission/test_files.py | 12 ++--- qa/tests/checks/submission/test_type.py | 12 ++--- 5 files changed, 68 insertions(+), 68 deletions(-) diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index 8eefed0d..296a2bf6 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -54,7 +54,7 @@ class Result(BaseModel): results: list["Result"] | None = None -class SubmissionMetadata(BaseModel): # check which fields are always provided by the snapshot and which are optional +class SubmissionPubsubInfo(BaseModel): # check which fields are always provided by the snapshot and which are optional """Metadata about a submission.""" type: Literal["new", "rep", "wdr", "jref", "cross"] @@ -107,4 +107,4 @@ class QaDataRegistry(BaseModel): flagged_terms_report: str | None = None tex_report: str | None = None metadata: Metadata | None = None - submission_metadata: SubmissionMetadata | None = None + submission_pubsub_info: SubmissionPubsubInfo | None = None diff --git a/qa/qa/checks/submission/files.py b/qa/qa/checks/submission/files.py index 56108667..0e765ece 100644 --- a/qa/qa/checks/submission/files.py +++ b/qa/qa/checks/submission/files.py @@ -1,28 +1,28 @@ -"""Submission file checks.""" - -from qa.checks.base import BaseCheck -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result - - -class DoesNotExceedTheFileSizeLimit(BaseCheck): - name = "does_not_exceed_the_file_size_limit" - display_name = "Does Not Exceed the File Size Limit" - id = 48 - version = "1.0.0" - description = "The submission does not exceed the file size limit." - on_failure_policy = OnFailurePolicy.WARN - failure_message = "Submission exceeds the file size limit." - - required_data = {"submission_metadata"} - - def _run(self, data_registry: QaDataRegistry) -> Result: - assert data_registry.submission_metadata is not None - - if data_registry.submission_metadata.is_oversize: - return self._result(passed=False, message=self.failure_message) - else: - return self._result(passed=True) - - -# TODO: add HTML file type check -# TODO: add TeX processing flag checks (post-exCITe) +"""Submission file checks.""" + +from qa.checks.base import BaseCheck +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result + + +class DoesNotExceedTheFileSizeLimit(BaseCheck): + name = "does_not_exceed_the_file_size_limit" + display_name = "Does Not Exceed the File Size Limit" + id = 48 + version = "1.0.0" + description = "The submission does not exceed the file size limit." + on_failure_policy = OnFailurePolicy.WARN + failure_message = "Submission exceeds the file size limit." + + required_data = {"submission_pubsub_info"} + + def _run(self, data_registry: QaDataRegistry) -> Result: + assert data_registry.submission_pubsub_info is not None + + if data_registry.submission_pubsub_info.is_oversize: + return self._result(passed=False, message=self.failure_message) + else: + return self._result(passed=True) + + +# TODO: add HTML file type check +# TODO: add TeX processing flag checks (post-exCITe) diff --git a/qa/qa/checks/submission/type.py b/qa/qa/checks/submission/type.py index b33ec474..94757442 100644 --- a/qa/qa/checks/submission/type.py +++ b/qa/qa/checks/submission/type.py @@ -1,26 +1,26 @@ -"""Submission type checks.""" - -from qa.checks.base import BaseCheck -from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result - - -class IsNotAWithdrawal(BaseCheck): - name = "is_not_a_withdrawal" - display_name = "Is Not A Withdrawal" - id = 49 - version = "1.0.0" - description = "The submission is not a withdrawal." - on_failure_policy = OnFailurePolicy.WARN - failure_message = "The submission is a withdrawal which requires staff approval." - - required_data = {"submission_metadata"} - - def _run(self, data_registry: QaDataRegistry) -> Result: - assert data_registry.submission_metadata is not None - - _type = data_registry.submission_metadata.type - - if _type == "wdr": - return self._result(passed=False, message=self.failure_message) - else: - return self._result(passed=True) +"""Submission type checks.""" + +from qa.checks.base import BaseCheck +from qa.checks.models import OnFailurePolicy, QaDataRegistry, Result + + +class IsNotAWithdrawal(BaseCheck): + name = "is_not_a_withdrawal" + display_name = "Is Not A Withdrawal" + id = 49 + version = "1.0.0" + description = "The submission is not a withdrawal." + on_failure_policy = OnFailurePolicy.WARN + failure_message = "The submission is a withdrawal which requires staff approval." + + required_data = {"submission_pubsub_info"} + + def _run(self, data_registry: QaDataRegistry) -> Result: + assert data_registry.submission_pubsub_info is not None + + _type = data_registry.submission_pubsub_info.type + + if _type == "wdr": + return self._result(passed=False, message=self.failure_message) + else: + return self._result(passed=True) diff --git a/qa/tests/checks/submission/test_files.py b/qa/tests/checks/submission/test_files.py index 28027920..90ef218e 100644 --- a/qa/tests/checks/submission/test_files.py +++ b/qa/tests/checks/submission/test_files.py @@ -3,23 +3,23 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import QaDataRegistry, SubmissionMetadata +from qa.checks.models import QaDataRegistry, SubmissionPubsubInfo from qa.checks.submission.files import DoesNotExceedTheFileSizeLimit class TestOversizeCheck: def test_oversize_pass(self): - sub_metadata = SubmissionMetadata(type="new", is_oversize=False, data_version=1, metadata_version=1) - result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submission_metadata=sub_metadata)) + sub_metadata = SubmissionPubsubInfo(type="new", is_oversize=False, data_version=1, metadata_version=1) + result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submission_pubsub_info=sub_metadata)) assert result.passed def test_oversize_fail(self): - sub_metadata = SubmissionMetadata(type="new", is_oversize=True, data_version=1, metadata_version=1) - result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submission_metadata=sub_metadata)) + sub_metadata = SubmissionPubsubInfo(type="new", is_oversize=True, data_version=1, metadata_version=1) + result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submission_pubsub_info=sub_metadata)) assert not result.passed - def test_missing_submission_metadata_raises(self): + def test_missing_submission_pubsub_info_raises(self): with pytest.raises(MissingDataError): DoesNotExceedTheFileSizeLimit().run(QaDataRegistry()) diff --git a/qa/tests/checks/submission/test_type.py b/qa/tests/checks/submission/test_type.py index 1970e647..4aaf8060 100644 --- a/qa/tests/checks/submission/test_type.py +++ b/qa/tests/checks/submission/test_type.py @@ -3,23 +3,23 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import QaDataRegistry, SubmissionMetadata +from qa.checks.models import QaDataRegistry, SubmissionPubsubInfo from qa.checks.submission.type import IsNotAWithdrawal class TestWithdrawalCheck: def test_wdr_pass(self): - sub_metadata = SubmissionMetadata(type="new", is_oversize=False, data_version=1, metadata_version=1) - result = IsNotAWithdrawal().run(QaDataRegistry(submission_metadata=sub_metadata)) + sub_metadata = SubmissionPubsubInfo(type="new", is_oversize=False, data_version=1, metadata_version=1) + result = IsNotAWithdrawal().run(QaDataRegistry(submission_pubsub_info=sub_metadata)) assert result.passed def test_wdr_fail(self): - sub_metadata = SubmissionMetadata(type="wdr", is_oversize=False, data_version=1, metadata_version=1) - result = IsNotAWithdrawal().run(QaDataRegistry(submission_metadata=sub_metadata)) + sub_metadata = SubmissionPubsubInfo(type="wdr", is_oversize=False, data_version=1, metadata_version=1) + result = IsNotAWithdrawal().run(QaDataRegistry(submission_pubsub_info=sub_metadata)) assert not result.passed - def test_missing_submission_metadata_raises(self): + def test_missing_submission_pubsub_info_raises(self): with pytest.raises(MissingDataError): IsNotAWithdrawal().run(QaDataRegistry()) From e401bb7d0f268fe04ab2211fc6b85ef8a1a1d0bf Mon Sep 17 00:00:00 2001 From: carly Date: Thu, 23 Jul 2026 11:01:05 -0400 Subject: [PATCH 31/34] complete rename --- qa/qa/checks/models.py | 4 ++-- qa/qa/checks/submission/files.py | 6 +++--- qa/qa/checks/submission/type.py | 6 +++--- qa/tests/checks/submission/test_files.py | 12 ++++++------ qa/tests/checks/submission/test_type.py | 12 ++++++------ 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index 296a2bf6..c8fe494f 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -54,7 +54,7 @@ class Result(BaseModel): results: list["Result"] | None = None -class SubmissionPubsubInfo(BaseModel): # check which fields are always provided by the snapshot and which are optional +class SubmitEventInfo(BaseModel): # check which fields are always provided by the snapshot and which are optional """Metadata about a submission.""" type: Literal["new", "rep", "wdr", "jref", "cross"] @@ -107,4 +107,4 @@ class QaDataRegistry(BaseModel): flagged_terms_report: str | None = None tex_report: str | None = None metadata: Metadata | None = None - submission_pubsub_info: SubmissionPubsubInfo | None = None + submit_event_info: SubmitEventInfo | None = None diff --git a/qa/qa/checks/submission/files.py b/qa/qa/checks/submission/files.py index 0e765ece..fc6528f4 100644 --- a/qa/qa/checks/submission/files.py +++ b/qa/qa/checks/submission/files.py @@ -13,12 +13,12 @@ class DoesNotExceedTheFileSizeLimit(BaseCheck): on_failure_policy = OnFailurePolicy.WARN failure_message = "Submission exceeds the file size limit." - required_data = {"submission_pubsub_info"} + required_data = {"submit_event_info"} def _run(self, data_registry: QaDataRegistry) -> Result: - assert data_registry.submission_pubsub_info is not None + assert data_registry.submit_event_info is not None - if data_registry.submission_pubsub_info.is_oversize: + if data_registry.submit_event_info.is_oversize: return self._result(passed=False, message=self.failure_message) else: return self._result(passed=True) diff --git a/qa/qa/checks/submission/type.py b/qa/qa/checks/submission/type.py index 94757442..9502030f 100644 --- a/qa/qa/checks/submission/type.py +++ b/qa/qa/checks/submission/type.py @@ -13,12 +13,12 @@ class IsNotAWithdrawal(BaseCheck): on_failure_policy = OnFailurePolicy.WARN failure_message = "The submission is a withdrawal which requires staff approval." - required_data = {"submission_pubsub_info"} + required_data = {"submit_event_info"} def _run(self, data_registry: QaDataRegistry) -> Result: - assert data_registry.submission_pubsub_info is not None + assert data_registry.submit_event_info is not None - _type = data_registry.submission_pubsub_info.type + _type = data_registry.submit_event_info.type if _type == "wdr": return self._result(passed=False, message=self.failure_message) diff --git a/qa/tests/checks/submission/test_files.py b/qa/tests/checks/submission/test_files.py index 90ef218e..1e5bac48 100644 --- a/qa/tests/checks/submission/test_files.py +++ b/qa/tests/checks/submission/test_files.py @@ -3,23 +3,23 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import QaDataRegistry, SubmissionPubsubInfo +from qa.checks.models import QaDataRegistry, SubmitEventInfo from qa.checks.submission.files import DoesNotExceedTheFileSizeLimit class TestOversizeCheck: def test_oversize_pass(self): - sub_metadata = SubmissionPubsubInfo(type="new", is_oversize=False, data_version=1, metadata_version=1) - result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submission_pubsub_info=sub_metadata)) + sub_metadata = SubmitEventInfo(type="new", is_oversize=False, data_version=1, metadata_version=1) + result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submit_event_info=sub_metadata)) assert result.passed def test_oversize_fail(self): - sub_metadata = SubmissionPubsubInfo(type="new", is_oversize=True, data_version=1, metadata_version=1) - result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submission_pubsub_info=sub_metadata)) + sub_metadata = SubmitEventInfo(type="new", is_oversize=True, data_version=1, metadata_version=1) + result = DoesNotExceedTheFileSizeLimit().run(QaDataRegistry(submit_event_info=sub_metadata)) assert not result.passed - def test_missing_submission_pubsub_info_raises(self): + def test_missing_submit_event_info_raises(self): with pytest.raises(MissingDataError): DoesNotExceedTheFileSizeLimit().run(QaDataRegistry()) diff --git a/qa/tests/checks/submission/test_type.py b/qa/tests/checks/submission/test_type.py index 4aaf8060..370cdb26 100644 --- a/qa/tests/checks/submission/test_type.py +++ b/qa/tests/checks/submission/test_type.py @@ -3,23 +3,23 @@ import pytest from qa.checks.base import MissingDataError -from qa.checks.models import QaDataRegistry, SubmissionPubsubInfo +from qa.checks.models import QaDataRegistry, SubmitEventInfo from qa.checks.submission.type import IsNotAWithdrawal class TestWithdrawalCheck: def test_wdr_pass(self): - sub_metadata = SubmissionPubsubInfo(type="new", is_oversize=False, data_version=1, metadata_version=1) - result = IsNotAWithdrawal().run(QaDataRegistry(submission_pubsub_info=sub_metadata)) + sub_metadata = SubmitEventInfo(type="new", is_oversize=False, data_version=1, metadata_version=1) + result = IsNotAWithdrawal().run(QaDataRegistry(submit_event_info=sub_metadata)) assert result.passed def test_wdr_fail(self): - sub_metadata = SubmissionPubsubInfo(type="wdr", is_oversize=False, data_version=1, metadata_version=1) - result = IsNotAWithdrawal().run(QaDataRegistry(submission_pubsub_info=sub_metadata)) + sub_metadata = SubmitEventInfo(type="wdr", is_oversize=False, data_version=1, metadata_version=1) + result = IsNotAWithdrawal().run(QaDataRegistry(submit_event_info=sub_metadata)) assert not result.passed - def test_missing_submission_pubsub_info_raises(self): + def test_missing_submit_event_info_raises(self): with pytest.raises(MissingDataError): IsNotAWithdrawal().run(QaDataRegistry()) From 8f62838425c91863484e7e174fb2dae82e2e3794 Mon Sep 17 00:00:00 2001 From: carly Date: Thu, 23 Jul 2026 11:46:03 -0400 Subject: [PATCH 32/34] fix SubmitEventInfo duplicating Metadata fields after develop merge The merge of develop (which still had the pre-split combined SubmissionMetadata class) reintroduced all nine paper-metadata fields onto SubmitEventInfo, duplicating what now lives on Metadata. SubmitEventInfo should only carry submission-level fields. Co-Authored-By: Claude Sonnet 5 --- qa/qa/checks/models.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index fba02903..4e903636 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -82,15 +82,6 @@ class FulltextReport(BaseReport): class SubmitEventInfo(BaseModel): # check which fields are always provided by the snapshot and which are optional """Information about the submission provided by the submit event.""" - title: str | None = None - authors: str | None = None - abstract: str | None = None - comments: str | None = None - report_num: str | None = None - journal_ref: str | None = None - doi: str | None = None - msc_class: str | None = None - acm_class: str | None = None type: Literal["new", "rep", "wdr", "jref", "cross"] is_oversize: bool data_version: int From 1f7e0edfa19a893d3c1fad02eab72006fc19ab88 Mon Sep 17 00:00:00 2001 From: carly Date: Thu, 23 Jul 2026 15:42:00 -0400 Subject: [PATCH 33/34] update comment --- qa/qa/checks/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/qa/checks/models.py b/qa/qa/checks/models.py index 4e903636..14194466 100644 --- a/qa/qa/checks/models.py +++ b/qa/qa/checks/models.py @@ -79,7 +79,7 @@ class FulltextReport(BaseReport): version: str = "1.0" -class SubmitEventInfo(BaseModel): # check which fields are always provided by the snapshot and which are optional +class SubmitEventInfo(BaseModel): # TODO: check which fields are always provided by the snapshot and which are optional """Information about the submission provided by the submit event.""" type: Literal["new", "rep", "wdr", "jref", "cross"] From e3f1e61de379ae05703d0b02c8a29374c6151a7a Mon Sep 17 00:00:00 2001 From: carly Date: Fri, 24 Jul 2026 14:08:57 -0400 Subject: [PATCH 34/34] separate lists of checks --- qa/qa/checks/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qa/qa/checks/__init__.py b/qa/qa/checks/__init__.py index d7ad4362..65a3ead6 100644 --- a/qa/qa/checks/__init__.py +++ b/qa/qa/checks/__init__.py @@ -17,7 +17,7 @@ from qa.checks.fulltext.extraction import TextExtractionSuccessful # noqa from qa.checks.fulltext.structure import FulltextNotTooShort # noqa -checks: list[BaseCheck] = [ +submit_event_checks: list[BaseCheck] = [ TitleIsValid(), AuthorsAreValid(), AbstractIsValid(), @@ -29,6 +29,11 @@ AcmClassIsValid(), DoesNotExceedTheFileSizeLimit(), IsNotAWithdrawal(), +] + +fulltext_checks: list[BaseCheck] = [ TextExtractionSuccessful(), FulltextNotTooShort(), ] + +checks: list[BaseCheck] = submit_event_checks + fulltext_checks