From 4628ead61d84e173b154c9af8ab312ba1122f65d Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Fri, 26 Jun 2026 01:02:02 -0400 Subject: [PATCH 1/2] Add GitHub issue parsing (sources/github) GitHubIssueTest / GitHubIssuesTestCases pull BabelTest assertions embedded in GitHub issue bodies (wiki {{BabelTest|...}} markers or fenced YAML blocks), resolve each to an assertion handler from the assertions package, and evaluate it against NodeNorm/NameRes. Add the dependencies this needs: pygithub, pyyaml, tqdm and python-dotenv. Co-Authored-By: Claude Opus 4.8 --- pyproject.toml | 8 +- .../sources/github/__init__.py | 0 .../github/github_issues_test_cases.py | 310 ++++++++++++++++++ uv.lock | 294 ++++++++++++++++- 4 files changed, 610 insertions(+), 2 deletions(-) create mode 100644 src/babel_validation/sources/github/__init__.py create mode 100644 src/babel_validation/sources/github/github_issues_test_cases.py diff --git a/pyproject.toml b/pyproject.toml index 80f4ba02..9452bca2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,12 +7,18 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "black>=25.9.0", + "pyyaml>=6.0", "requests>=2.32.5", + "tqdm>=4.67.1", "filelock", "deepdiff>=8.6.1", + "python-dotenv>=0.9.9", "openapi-spec-validator>=0.7.2", - "pytest>=8.4.2", + "pygithub>=2.8.1", + "pytest>=9.0.2", "pytest-timeout>=2.4.0", + "pytest-xdist[psutil]", + "pytest-subtests", ] [project.urls] diff --git a/src/babel_validation/sources/github/__init__.py b/src/babel_validation/sources/github/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/babel_validation/sources/github/github_issues_test_cases.py b/src/babel_validation/sources/github/github_issues_test_cases.py new file mode 100644 index 00000000..fa33dab2 --- /dev/null +++ b/src/babel_validation/sources/github/github_issues_test_cases.py @@ -0,0 +1,310 @@ +""" +Parse and evaluate BabelTest assertions embedded in GitHub issue bodies. + +Terminology +----------- +assertion — The name of the test type, e.g. "Resolves" or "ResolvesWith". + Case-insensitive. Maps to a key in ASSERTION_HANDLERS. + +param_set — One set of parameters for a single invocation of an assertion. + Represented as a list of strings. Each Wiki-syntax line produces + exactly one param_set; each entry under a YAML assertion key + produces one param_set. + + Often the first element is "special" (e.g. an expected label or + Biolink type) and the remaining elements are CURIEs to test, + but the interpretation is assertion-specific. + Example: ["CHEBI:15365", "PUBCHEM.COMPOUND:1"] + +param_sets — The full list of param_sets for one assertion in one issue. + A list of lists (list[list[str]]). YAML syntax allows many + param_sets for one assertion type in a single block. + Example: [["CHEBI:15365", "PUBCHEM.COMPOUND:1"], + ["MONDO:0005015", "DOID:9351"]] +""" + +import json +import logging +import re +from typing import Iterator + +import yaml + +from github import Github, Auth, Issue +from tqdm import tqdm + +from src.babel_validation.assertions import ASSERTION_HANDLERS +from src.babel_validation.core.testrow import TestResult +from src.babel_validation.services.nameres import CachedNameRes +from src.babel_validation.services.nodenorm import CachedNodeNorm + +_logger = logging.getLogger(__name__) + + +def _to_list(value, context: str) -> list: + """Normalize a YAML value that may be a bare string or a list; raise on anything else.""" + if isinstance(value, str): + return [value] + if isinstance(value, list): + return value + raise ValueError(f"{context}: expected str or list, got {type(value).__name__}") + + +class GitHubIssueTest: + """Represents one assertion extracted from a GitHub issue body — an assertion name paired with a list of param_sets to evaluate.""" + + def __init__(self, github_issue_id: str, github_issue: Issue.Issue, assertion: str, param_sets: list[list[str]] = None): + """ + :param github_issue_id: Human-readable issue identifier, e.g. "org/repo#42". + :param github_issue: The PyGitHub Issue object this test was extracted from. + :param assertion: The assertion name (case-insensitive), e.g. "Resolves" or "HasLabel". + Must match a key in ASSERTION_HANDLERS. + :param param_sets: A list of param_sets (list[list[str]]) to evaluate for this assertion. + Each inner list is one param_set — see module docstring for details. + """ + if not isinstance(param_sets, list) and param_sets is not None: + raise ValueError(f"param_sets must be a list when creating a GitHubIssueTest({github_issue}, {assertion}, {param_sets})") + self.github_issue = github_issue + self.assertion = assertion + self.param_sets = param_sets if param_sets is not None else [] + self.github_issue_id = github_issue_id + + _logger.info("Creating GitHubIssueTest for %s %s(%s)", github_issue.html_url, assertion, param_sets) + + def __str__(self): + return f"{self.github_issue_id}: {self.assertion}({len(self.param_sets)} param sets: {json.dumps(self.param_sets)})" + + def _get_handler(self): + handler = ASSERTION_HANDLERS.get(self.assertion.lower()) + if handler is None: + raise ValueError(f"Unknown assertion type for {self}: {self.assertion}") + return handler + + def test_with_nodenorm(self, nodenorm: CachedNodeNorm) -> Iterator[TestResult]: + return self._get_handler().test_with_nodenorm(self.param_sets, nodenorm, label=str(self)) + + def test_with_nameres(self, nodenorm: CachedNodeNorm, nameres: CachedNameRes, pass_if_found_in_top=5) -> Iterator[TestResult]: + return self._get_handler().test_with_nameres(self.param_sets, nodenorm, nameres, pass_if_found_in_top, label=str(self)) + + +class GitHubIssuesTestCases: + """ + The idea here is to allow test cases to be efficiently embedded within GitHub issues, to test them + regularly, and to provide a list of cases where either: + - An open issue has test cases that are now passing (and so should be updated or maybe even closed). + - A closed issue has test cases that are now failing (and so should be reopened). + """ + + # Case-insensitive, matching the case-insensitivity of assertion names. + # Group 1 captures everything between '{{BabelTest|' and '}}'. + _BABELTEST_RE = re.compile(r'{{BabelTest\|(.*?)}}', re.IGNORECASE) + _BABELTEST_YAML_RE = re.compile(r'```yaml\s+babel_tests:\s+.*?\s+```', re.DOTALL) + + def __init__(self, github_token: str, github_repositories): + """ + Create a GitHubIssuesTestCase object. + + Requires a GitHub authentication token. You can generate a personal authentication token + at https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#about-personal-access-tokens, + or you can read the GITHUB_TOKEN during a GitHub Action (https://docs.github.com/en/actions/tutorials/authenticate-with-github_token). + + :param github_token: A GitHub authentication to use for making these queries. + :param github_repositories: A list of GitHub repositories to pull issues from, specified as 'organization/repo'. + """ + self.github_token = github_token + if not self.github_token or self.github_token.strip() == '': + raise ValueError("No GitHub authentication token provided.") + + self.github = Github(auth=Auth.Token(self.github_token)) + self.logger = logging.getLogger(self.__class__.__name__) + + if not github_repositories: + raise ValueError("No GitHub repositories specified in `github_repositories`.") + self.github_repositories = github_repositories + self.logger.info("Configured GitHub repositories: %s", self.github_repositories) + + def get_test_issues_from_issue(self, github_issue: Issue.Issue) -> list[GitHubIssueTest]: + """ + Extract test rows from a single GitHub issue. + + Two syntaxes are supported: + - Wiki syntax: {{BabelTest|AssertionType|param1|param2|...}} + - YAML syntax: + + ```yaml + babel_tests: + assertion: + - param1 + - ['param1', 'param2'] + ``` + + For the full list of supported assertion types and their parameters, see + src/babel_validation/assertions/README.md or inspect ASSERTION_HANDLERS.keys(). + + :param github_issue: A single GitHub issue to extract test cases from. + :return: A list of GitHubIssueTest objects found in the issue body. + """ + + github_issue_id = f"{github_issue.repository.full_name}#{github_issue.number}" + self.logger.debug("Looking for tests in issue %s: %s (%s, %s)", + github_issue_id, github_issue.title, github_issue.state, github_issue.html_url) + + # Is there an issue body at all? + if not github_issue.body or github_issue.body.strip() == '': + return [] + + # Look for BabelTest syntax. + testrows = [] + + for babeltest_match in self._BABELTEST_RE.finditer(github_issue.body): + match = babeltest_match.group(0) + self.logger.info("Found BabelTest in issue %s: %s", github_issue_id, match) + + # Figure out parameters. + test_string = babeltest_match.group(1) + params = test_string.split("|") + if not params or not params[0]: + raise ValueError(f"Missing assertion name in BabelTest in issue {github_issue_id}: {match}") + # Wiki syntax: params[0] is the assertion name; params[1:] form a single + # param_set (may be empty for assertions like Needed), so param_sets is a + # one-element list: [params[1:]]. + testrows.append(GitHubIssueTest(github_issue_id, github_issue, params[0], [params[1:]])) + + babeltest_yaml_matches = re.findall(self._BABELTEST_YAML_RE, github_issue.body) + if babeltest_yaml_matches: + for match in babeltest_yaml_matches: + self.logger.info("Found BabelTest YAML in issue %s: %s", github_issue_id, match) + + # Parse string as YAML. + yaml_dict = yaml.safe_load(match.removeprefix("```yaml").removesuffix("```")) + + babel_tests = yaml_dict.get('babel_tests') if isinstance(yaml_dict, dict) else None + if babel_tests is None: + raise ValueError( + f"YAML block in issue {github_issue_id} matched the detection pattern " + f"but contains no 'babel_tests' top-level key: {match!r}" + ) + if not isinstance(babel_tests, dict): + raise ValueError( + f"YAML block in issue {github_issue_id}: 'babel_tests' must be a mapping of " + f"assertion name to param sets, but got {type(babel_tests).__name__}: {babel_tests!r}" + ) + + for assertion, original_param_sets in babel_tests.items(): + # YAML syntax: each entry under an assertion key becomes one param_set. + # A bare string becomes a single-element param_set; a list is used as-is. + if not isinstance(assertion, str): + raise ValueError( + f"YAML block in issue {github_issue_id}: assertion name must be a string, " + f"but got {type(assertion).__name__}: {assertion!r}" + ) + if original_param_sets is None: + raise ValueError( + f"YAML block in issue {github_issue_id}: assertion '{assertion}' has a null " + f"param list — use an empty list [] or remove the entry" + ) + normalized = _to_list( + original_param_sets, + f"YAML block in issue {github_issue_id}: assertion '{assertion}'" + ) + param_sets = [ + _to_list(ps, f"YAML block in issue {github_issue_id}: assertion '{assertion}' param_set") + for ps in normalized + ] + testrows.append(GitHubIssueTest(github_issue_id, github_issue, assertion, param_sets)) + + return testrows + + def issue_has_tests(self, issue: Issue.Issue) -> bool: + """Quick regex check to see if an issue body contains any BabelTest syntax.""" + if not issue.body or issue.body.strip() == '': + return False + return bool(self._BABELTEST_RE.search(issue.body) or + self._BABELTEST_YAML_RE.search(issue.body)) + + def get_issues_by_ids(self, issue_ids: list[str]) -> list[Issue.Issue]: + """ + Fetch specific GitHub issues by their ID strings, supporting three formats: + - 'org/repo#N' → direct fetch from that repo + - 'repo#N' → search self.github_repositories for matching repo name + - 'N' → fetch #N from all configured repositories + """ + from github import UnknownObjectException + issues = [] + for issue_id in issue_ids: + found = False + if m := re.match(r'^([^/]+)/([^#]+)#(\d+)$', issue_id): + # org/repo#N + issue = self.github.get_repo(f"{m.group(1)}/{m.group(2)}").get_issue(int(m.group(3))) + issues.append(issue) + found = True + elif m := re.match(r'^([^/#]+)#(\d+)$', issue_id): + # repo#N — find repo in configured list + repo_name, num = m.group(1), int(m.group(2)) + for full_repo in self.github_repositories: + parts = full_repo.split('/') + if len(parts) >= 2 and parts[1] == repo_name: + issues.append(self.github.get_repo(full_repo).get_issue(num)) + found = True + break + elif m := re.match(r'^(\d+)$', issue_id): + # N — try all configured repos; skip repos that don't have this issue number. + num = int(m.group(1)) + for full_repo in self.github_repositories: + try: + issues.append(self.github.get_repo(full_repo).get_issue(num)) + found = True + except UnknownObjectException: + pass + if not found: + raise ValueError( + f"Could not resolve issue ID {issue_id!r} in configured repositories " + f"{self.github_repositories}. Use 'org/repo#N', 'repo#N', or 'N'." + ) + return issues + + def get_issues_with_tests(self, github_repositories=None) -> Iterator[Issue.Issue]: + """Use GitHub search API to find only issues containing BabelTest syntax. + + This is much faster than get_all_issues() + issue_has_tests() filtering because + it only fetches issues that match the search query rather than paginating through + every issue in each repository. + + Note: GitHub's search index has a ~60-second lag for very recent edits. For + immediate testing of freshly-edited issues, use --issue which calls + get_issues_by_ids() directly. + """ + if github_repositories is None: + github_repositories = self.github_repositories + for repo_id in github_repositories: + seen_numbers = set() + for keyword in ['{{BabelTest', 'babel_tests:']: + query = f'"{keyword}" is:issue in:body repo:{repo_id}' + self.logger.info("Searching GitHub issues with query: %s", query) + for issue in self.github.search_issues(query): + if issue.number not in seen_numbers: + seen_numbers.add(issue.number) + if self.issue_has_tests(issue): + yield issue + + def get_all_issues(self, github_repositories=None) -> Iterator[Issue.Issue]: + """ + Get a list of test rows from one or more repositories. + + :param github_repositories: A list of GitHub repositories to search for test cases. If none is provided, + we default to the list specified when creating this GitHubIssuesTestCases class. + :return: A list of TestRows to process. + """ + if github_repositories is None: + github_repositories = self.github_repositories + + for repo_id in github_repositories: + self.logger.info("Looking up issues in GitHub repository %s", repo_id) + repo = self.github.get_repo(repo_id, lazy=True) + + issue_count = 0 + for issue in tqdm(repo.get_issues(state='all', sort='updated'), desc=f"Processing issues in {repo_id}"): + issue_count += 1 + yield issue + + self.logger.info("Found %d issues in GitHub repository %s", issue_count, repo_id) diff --git a/uv.lock b/uv.lock index 18f74c62..41e28623 100644 --- a/uv.lock +++ b/uv.lock @@ -29,9 +29,15 @@ dependencies = [ { name = "deepdiff" }, { name = "filelock" }, { name = "openapi-spec-validator" }, + { name = "pygithub" }, { name = "pytest" }, + { name = "pytest-subtests" }, { name = "pytest-timeout" }, + { name = "pytest-xdist", extra = ["psutil"] }, + { name = "python-dotenv" }, + { name = "pyyaml" }, { name = "requests" }, + { name = "tqdm" }, ] [package.metadata] @@ -40,9 +46,15 @@ requires-dist = [ { name = "deepdiff", specifier = ">=8.6.1" }, { name = "filelock" }, { name = "openapi-spec-validator", specifier = ">=0.7.2" }, - { name = "pytest", specifier = ">=8.4.2" }, + { name = "pygithub", specifier = ">=2.8.1" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-subtests" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, + { name = "pytest-xdist", extras = ["psutil"] }, + { name = "python-dotenv", specifier = ">=0.9.9" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "requests", specifier = ">=2.32.5" }, + { name = "tqdm", specifier = ">=4.67.1" }, ] [[package]] @@ -91,6 +103,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] +[[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" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -185,6 +267,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +] + [[package]] name = "deepdiff" version = "8.6.1" @@ -197,6 +335,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b", size = 91378, upload-time = "2025-09-03T19:40:39.679Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "filelock" version = "3.29.4" @@ -401,6 +548,43 @@ 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 = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[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" @@ -527,6 +711,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] +[[package]] +name = "pygithub" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjwt", extra = ["crypto"] }, + { name = "pynacl" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/c3/8465a311197e16cf5ab68789fe689535e90f6b61ab524cc32a39e67237ae/pygithub-2.9.1.tar.gz", hash = "sha256:59771d7ff63d54d427be2e7d0dad2208dfffc2b0a045fec959263787739b611c", size = 2594989, upload-time = "2026-04-14T07:26:13.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/aa/81a5506f089a26338bff17535e4339b3b22049ebd1bcdeff756c4d7a7559/pygithub-2.9.1-py3-none-any.whl", hash = "sha256:2ec78fca30092d51a42d76f4ddb02131b6f0c666a35dfdf364cf302cdda115b9", size = 449710, upload-time = "2026-04-14T07:26:12.382Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -536,6 +736,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -552,6 +801,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-subtests" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/d9/20097971a8d315e011e055d512fa120fd6be3bdb8f4b3aa3e3c6bf77bebc/pytest_subtests-0.15.0.tar.gz", hash = "sha256:cb495bde05551b784b8f0b8adfaa27edb4131469a27c339b80fd8d6ba33f887c", size = 18525, upload-time = "2025-10-20T16:26:18.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/64/bba465299b37448b4c1b84c7a04178399ac22d47b3dc5db1874fe55a2bd3/pytest_subtests-0.15.0-py3-none-any.whl", hash = "sha256:da2d0ce348e1f8d831d5a40d81e3aeac439fec50bd5251cbb7791402696a9493", size = 9185, upload-time = "2025-10-20T16:26:17.239Z" }, +] + [[package]] name = "pytest-timeout" version = "2.4.0" @@ -564,6 +826,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[package.optional-dependencies] +psutil = [ + { name = "psutil" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -820,6 +1100,18 @@ 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 = "tqdm" +version = "4.68.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" From 1dcea2f2bc01af2d63031528903d346ede0a8cf8 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Fri, 26 Jun 2026 01:02:20 -0400 Subject: [PATCH 2/2] Add GitHub issue test harness Dynamically generate one pytest per GitHub issue (across the repos listed in targets.ini [DEFAULT] Repositories), each running the issue's BabelTest assertions as independent subtests. Open-issue failures are reported as xfail so they don't block CI. Add a --issue option (and selected_github_issues fixture) to restrict a run to specific issues, wipe the issues cache at session start, parallelize with pytest-xdist, and add a PR workflow that runs the offline `-m unit` suite. tests/github_issues/ test_system.py covers the parsing/dispatch logic with mocked GitHub data. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/tests.yaml | 17 ++ .gitignore | 3 + tests/conftest.py | 19 +- tests/github_issues/__init__.py | 0 tests/github_issues/conftest.py | 145 ++++++++++ tests/github_issues/test_github_issues.py | 90 +++++++ tests/github_issues/test_system.py | 314 ++++++++++++++++++++++ tests/targets.ini | 17 +- 8 files changed, 600 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/tests.yaml create mode 100644 tests/github_issues/__init__.py create mode 100644 tests/github_issues/conftest.py create mode 100644 tests/github_issues/test_github_issues.py create mode 100644 tests/github_issues/test_system.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..22fd8eef --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,17 @@ +name: Tests + +on: + pull_request: + +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v6 + + - name: Run unit tests + env: + GITHUB_TOKEN: ${{ github.token }} + run: uv run pytest -m unit -v diff --git a/.gitignore b/.gitignore index a1480ec8..4bb67d43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Ignore the root .env file. +/.env + # Ignore all data files. data/ diff --git a/tests/conftest.py b/tests/conftest.py index 96e75f72..5f292053 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -43,6 +43,9 @@ def pytest_configure(config): for f in glob.glob(os.path.join(tempfile.gettempdir(), 'babel_validation_gsheet_*.csv')): _silent_unlink(f) _silent_unlink(f.removesuffix('.csv') + '.lock') + tmpdir = tempfile.gettempdir() + for name in ('babel_validation_issues_cache.json', 'babel_validation_issues_cache.lock'): + _silent_unlink(os.path.join(tmpdir, name)) def pytest_addoption(parser): @@ -67,6 +70,14 @@ def pytest_addoption(parser): help="The categories of tests to exclude." ) + # Only test particular GitHub issues. + parser.addoption( + '--issue', + default=[], + action='append', + help="One or more GitHub issues to test. Should be specified as either 'organization/repo#110', 'repo#110' or '110'" + ) + def read_targets(config_path): cp = configparser.ConfigParser() @@ -139,4 +150,10 @@ def category_test(cat): return False return True - return category_test \ No newline at end of file + return category_test + + +# --issue is consumed by tests/github_issues/conftest.py; this fixture exposes it to any test that wants it. +@pytest.fixture +def selected_github_issues(pytestconfig): + return pytestconfig.getoption('issue') \ No newline at end of file diff --git a/tests/github_issues/__init__.py b/tests/github_issues/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/github_issues/conftest.py b/tests/github_issues/conftest.py new file mode 100644 index 00000000..ec1bdb21 --- /dev/null +++ b/tests/github_issues/conftest.py @@ -0,0 +1,145 @@ +import configparser +import json +import os +import tempfile +from pathlib import Path + +import dotenv +import pytest +from filelock import FileLock +from github import GithubException, Issue + +from src.babel_validation.sources.github.github_issues_test_cases import GitHubIssuesTestCases +from tests._pytest_helpers import deselected_by_markexpr + +_github_token = None +_github_auth_error: str | None = None + +_AUTH_ERROR_ID = "github-auth-error" +_AUTH_HELP = ( + "GitHub token is expired or invalid. Generate a new one at " + "https://github.com/settings/tokens and set it as the GITHUB_TOKEN " + "environment variable (or in a .env file), then delete the cache file " + f"at {Path(tempfile.gettempdir()) / 'babel_validation_issues_cache.json'} " + "if it exists." +) + +_targets_config = configparser.ConfigParser() +_targets_config.read(Path(__file__).parent.parent / 'targets.ini') +_repos = [ + r.strip() + for r in _targets_config['DEFAULT']['Repositories'].splitlines() + if r.strip() +] +_github_issues_test_cases = None + +_CACHE_FILE = Path(tempfile.gettempdir()) / "babel_validation_issues_cache.json" +_LOCK_FILE = _CACHE_FILE.with_suffix(".lock") + +# Module-level cache to avoid re-fetching issues already retrieved during collection. +_fetched_issues_cache: dict[str, Issue.Issue] = {} + + +def _get_github_issues_test_cases() -> GitHubIssuesTestCases: + """Lazily construct GitHubIssuesTestCases, skipping tests if no token is available.""" + global _github_issues_test_cases, _github_token + if _github_issues_test_cases is not None: + return _github_issues_test_cases + if _github_token is None: + dotenv.load_dotenv() + _github_token = os.getenv('GITHUB_TOKEN') or '' + if not _github_token: + pytest.skip( + "GITHUB_TOKEN environment variable not set; skipping GitHub issues tests.", + allow_module_level=True, + ) + _github_issues_test_cases = GitHubIssuesTestCases(_github_token, _repos) + return _github_issues_test_cases + + +def _issue_id(issue: Issue.Issue) -> str: + """Derive a test ID from an issue without making extra API calls.""" + return f"{issue.repository.full_name}#{issue.number}" + + +def _record_auth_error(e: GithubException) -> None: + global _github_auth_error + _github_auth_error = f"{_AUTH_HELP}\n\nOriginal error: {e}" + + +_cached_ids: list[str] | None = None + + +def _get_all_test_issue_ids() -> list[str]: + """Return IDs of all issues that contain tests, using a file-based cache.""" + global _cached_ids + if _cached_ids is not None: + return _cached_ids + with FileLock(_LOCK_FILE): + if _CACHE_FILE.exists(): + try: + _cached_ids = json.loads(_CACHE_FILE.read_text()) + return _cached_ids + except (json.JSONDecodeError, OSError): + _CACHE_FILE.unlink(missing_ok=True) + try: + issues = list(_get_github_issues_test_cases().get_issues_with_tests()) + except GithubException as e: + if e.status == 401: + _record_auth_error(e) + _cached_ids = [_AUTH_ERROR_ID] + return _cached_ids + raise + ids = [_issue_id(i) for i in issues] + for issue, id_ in zip(issues, ids): + _fetched_issues_cache[id_] = issue + _CACHE_FILE.write_text(json.dumps(ids)) + _cached_ids = ids + return ids + + +def pytest_generate_tests(metafunc): + if "github_issue_id" not in metafunc.fixturenames: + return + if deselected_by_markexpr(metafunc): + # Parametrizing fetches issues from GitHub at collection time. Skip the + # network round trip when a -m filter (e.g. `pytest -m unit`) would + # deselect this test anyway. + metafunc.parametrize("github_issue_id", [], ids=[]) + return + issue_id_filter = metafunc.config.getoption("issue", default=[]) + if issue_id_filter: + try: + issues = _get_github_issues_test_cases().get_issues_by_ids(issue_id_filter) + except GithubException as e: + if e.status == 401: + _record_auth_error(e) + metafunc.parametrize("github_issue_id", [_AUTH_ERROR_ID], ids=[_AUTH_ERROR_ID]) + return + raise + ids = [_issue_id(i) for i in issues] + for issue, id_ in zip(issues, ids): + _fetched_issues_cache[id_] = issue + else: + ids = _get_all_test_issue_ids() + metafunc.parametrize("github_issue_id", ids, ids=ids) + + +@pytest.fixture +def github_issue(github_issue_id): + """Hydrate a GitHub Issue object from its string ID.""" + if github_issue_id == _AUTH_ERROR_ID: + pytest.fail(_github_auth_error or _AUTH_HELP) + if github_issue_id in _fetched_issues_cache: + return _fetched_issues_cache[github_issue_id] + try: + return _get_github_issues_test_cases().get_issues_by_ids([github_issue_id])[0] + except GithubException as e: + if e.status == 401: + pytest.fail(f"{_AUTH_HELP}\n\nOriginal error: {e}") + raise + + +@pytest.fixture(scope="session") +def github_issues_test_cases(): + return _get_github_issues_test_cases() diff --git a/tests/github_issues/test_github_issues.py b/tests/github_issues/test_github_issues.py new file mode 100644 index 00000000..32aa5790 --- /dev/null +++ b/tests/github_issues/test_github_issues.py @@ -0,0 +1,90 @@ +import itertools +import json + +import pytest + +from src.babel_validation.assertions import ASSERTION_HANDLERS +from src.babel_validation.services.nameres import CachedNameRes +from src.babel_validation.services.nodenorm import CachedNodeNorm +from src.babel_validation.core.testrow import TestResult, TestStatus + + +def test_github_issue(request, target_info, github_issue_id, github_issue, github_issues_test_cases, subtests): + nodenorm = CachedNodeNorm.from_url(target_info['NodeNormURL']) + nameres = CachedNameRes.from_url(target_info['NameResURL']) + # NameRes assertions pass if the expected CURIE is in the top N results; N + # comes from targets.ini (NameResXFailIfInTop) rather than being hardcoded. + pass_if_found_in_top = int(target_info.get('NameResXFailIfInTop', 5)) + tests = github_issues_test_cases.get_test_issues_from_issue(github_issue) + if not tests: + pytest.skip(f"No tests found in issue {github_issue}") + return + + is_open = github_issue.state == "open" + + # Unknown assertion types must fail hard, not XFAIL. + unknown = [f"'{t.assertion}'" for t in tests + if t.assertion.lower() not in ASSERTION_HANDLERS] + if unknown: + pytest.fail( + f"Issue {github_issue_id} uses unknown assertion type(s): " + f"{', '.join(unknown)} with param sets {json.dumps([t.param_sets for t in tests])}. " + f"Valid types (case-insensitive): {sorted(ASSERTION_HANDLERS.keys())}" + ) + + # Open issues are assumed to fail, so we set an xfail marker (but we set it to strict so + # that XPASSes are reported loudly). + if is_open: + request.node.add_marker(pytest.mark.xfail( + reason=f"Issue {github_issue.html_url} is expected to fail because the issue is open.", + strict=True, + )) + + count_subtests = 0 + failed_messages = [] + for test_issue in tests: + results_nodenorm = test_issue.test_with_nodenorm(nodenorm) + results_nameres = test_issue.test_with_nameres(nodenorm, nameres, pass_if_found_in_top) + + for result in itertools.chain(results_nodenorm, results_nameres): + count_subtests += 1 + + if is_open: + # Don't assert inside subtests for open issues: subtest failures + # don't respect the xfail marker on the parent test, so they would + # be reported as real failures. Collect them and xfail below instead. + if result.status == TestStatus.Failed: + failed_messages.append(f"{github_issue_id} ({github_issue.state}): {result.message}") + continue + + with subtests.test(msg=github_issue_id): + match result: + case TestResult(status=TestStatus.Passed, message=message): + assert True, f"{github_issue_id} ({github_issue.state}): {message}" + + case TestResult(status=TestStatus.Failed, message=message): + assert False, f"{github_issue_id} ({github_issue.state}): {message}" + + case TestResult(status=TestStatus.Skipped, message=message): + pytest.skip(f"{github_issue_id} ({github_issue.state}): {message}") + + case _: + assert False, f"Unknown result from {github_issue_id}: {result}" + + # For open issues: xfail so the result stays in the xfail family. + # - Some assertions failed → xfail with a count summary (expected outcome). + # - All assertions passed → the xfail marker added above makes this XPASS, + # which (strict=True) reports as a failure and signals the issue is closeable. + # - No assertions ran → xfail as a configuration error. + if is_open: + if count_subtests == 0: + pytest.xfail(f"Open issue {github_issue_id} produced no test results — check assertion configuration") + elif failed_messages: + pct = len(failed_messages) / count_subtests + details = "\n".join(failed_messages[:5]) + if len(failed_messages) > 5: + details += f"\n... and {len(failed_messages) - 5} more" + pytest.xfail( + f"Open issue {github_issue_id} has {len(failed_messages):,} failing assertions " + f"out of {count_subtests:,} ({pct:.0%}):\n{details}" + ) diff --git a/tests/github_issues/test_system.py b/tests/github_issues/test_system.py new file mode 100644 index 00000000..dafc93ed --- /dev/null +++ b/tests/github_issues/test_system.py @@ -0,0 +1,314 @@ +"""System tests for BabelTest trigger detection in GitHub issue bodies.""" + +from unittest.mock import MagicMock, patch +import pytest +import yaml + +from src.babel_validation.core.testrow import TestStatus +from src.babel_validation.sources.github.github_issues_test_cases import GitHubIssuesTestCases + +pytestmark = pytest.mark.unit + +INVALID_NAME = "NotARealAssertion" + + +@pytest.fixture +def github_issues_test_cases(): + """Override the conftest fixture: these unit tests never hit the GitHub API, + so build the parser with a dummy token rather than requiring GITHUB_TOKEN.""" + return GitHubIssuesTestCases("unit-test-dummy-token", ["test-org/test-repo"]) + + +def _mock_issue(body: str, number: int = 999) -> MagicMock: + """Minimal mock GitHub Issue for get_test_issues_from_issue().""" + issue = MagicMock() + issue.body = body + issue.number = number + issue.repository.full_name = "test-org/test-repo" + return issue + + +class TestInvalidAssertionNameDetection: + """Invalid assertion names are parsed but raise ValueError at execution time.""" + + def _wiki_issue(self): + return _mock_issue(f"{{{{BabelTest|{INVALID_NAME}|CHEBI:90926}}}}") + + def _yaml_issue(self): + return _mock_issue( + f"```yaml\nbabel_tests:\n {INVALID_NAME}:\n - CHEBI:90926\n```" + ) + + # --- parsing: invalid names are extracted, not rejected --- + + def test_wiki_syntax_parses_invalid_name(self, github_issues_test_cases): + tests = github_issues_test_cases.get_test_issues_from_issue(self._wiki_issue()) + assert len(tests) == 1 + assert tests[0].assertion == INVALID_NAME + + def test_yaml_syntax_parses_invalid_name(self, github_issues_test_cases): + tests = github_issues_test_cases.get_test_issues_from_issue(self._yaml_issue()) + assert len(tests) == 1 + assert tests[0].assertion == INVALID_NAME + + # --- execution: invalid names raise ValueError before any service call --- + + def test_wiki_invalid_name_raises_on_nodenorm(self, github_issues_test_cases): + tests = github_issues_test_cases.get_test_issues_from_issue(self._wiki_issue()) + with pytest.raises(ValueError, match="Unknown assertion type"): + list(tests[0].test_with_nodenorm(None)) + + def test_wiki_invalid_name_raises_on_nameres(self, github_issues_test_cases): + tests = github_issues_test_cases.get_test_issues_from_issue(self._wiki_issue()) + with pytest.raises(ValueError, match="Unknown assertion type"): + list(tests[0].test_with_nameres(None, None)) + + def test_yaml_invalid_name_raises_on_nodenorm(self, github_issues_test_cases): + tests = github_issues_test_cases.get_test_issues_from_issue(self._yaml_issue()) + with pytest.raises(ValueError, match="Unknown assertion type"): + list(tests[0].test_with_nodenorm(None)) + + def test_yaml_invalid_name_raises_on_nameres(self, github_issues_test_cases): + tests = github_issues_test_cases.get_test_issues_from_issue(self._yaml_issue()) + with pytest.raises(ValueError, match="Unknown assertion type"): + list(tests[0].test_with_nameres(None, None)) + + +@pytest.mark.unit +class TestTooManyParams: + """Extra params for fixed-arity assertions should yield a failed result, not silently pass.""" + + def _results(self, fixture, wiki_syntax, service="nodenorm"): + mock = _mock_issue(wiki_syntax) + tests = fixture.get_test_issues_from_issue(mock) + assert len(tests) == 1 + if service == "nodenorm": + return list(tests[0].test_with_nodenorm(MagicMock())) + else: + return list(tests[0].test_with_nameres(None, None)) + + def test_haslabel_too_many_params(self, github_issues_test_cases): + results = self._results( + github_issues_test_cases, + "{{BabelTest|HasLabel|CHEBI:15365|aspirin|unexpected}}" + ) + assert len(results) == 1 + assert results[0].status == TestStatus.Failed + assert "exactly two" in results[0].message + + def test_searchbyname_too_many_params(self, github_issues_test_cases): + results = self._results( + github_issues_test_cases, + "{{BabelTest|SearchByName|water|CHEBI:15377|unexpected}}", + service="nameres" + ) + assert len(results) == 1 + assert results[0].status == TestStatus.Failed + assert "exactly two" in results[0].message + + +@pytest.mark.unit +class TestTooFewParams: + """Comparison assertions require 2+ CURIEs; a single CURIE must fail loudly, + not pass (ResolvesWith) or fail (DoesNotResolveWith) vacuously.""" + + def _results(self, fixture, wiki_syntax): + mock = _mock_issue(wiki_syntax) + tests = fixture.get_test_issues_from_issue(mock) + assert len(tests) == 1 + return list(tests[0].test_with_nodenorm(MagicMock())) + + def test_resolveswith_single_curie_fails(self, github_issues_test_cases): + results = self._results( + github_issues_test_cases, "{{BabelTest|ResolvesWith|CHEBI:15365}}" + ) + assert len(results) == 1 + assert results[0].status == TestStatus.Failed + assert "at least two" in results[0].message + + def test_doesnotresolvewith_single_curie_fails(self, github_issues_test_cases): + results = self._results( + github_issues_test_cases, "{{BabelTest|DoesNotResolveWith|CHEBI:15365}}" + ) + assert len(results) == 1 + assert results[0].status == TestStatus.Failed + assert "at least two" in results[0].message + + +@pytest.mark.unit +class TestMalformedYaml: + """A YAML block that matches the detection regex but is not valid YAML raises yaml.YAMLError.""" + + MALFORMED_BODY = "```yaml\nbabel_tests:\n Resolves:\n - [unclosed bracket\n```" + + def test_malformed_yaml_raises(self, github_issues_test_cases): + mock = _mock_issue(self.MALFORMED_BODY) + with pytest.raises(yaml.YAMLError): + github_issues_test_cases.get_test_issues_from_issue(mock) + + +@pytest.mark.unit +class TestEmptyOrNullBabelTests: + """Documents behaviour when issue bodies contain empty or null babel test content.""" + + # --- empty/null body (already handled gracefully) --- + + def test_none_body_returns_empty(self, github_issues_test_cases): + mock = _mock_issue(None) + assert github_issues_test_cases.get_test_issues_from_issue(mock) == [] + + def test_whitespace_body_returns_empty(self, github_issues_test_cases): + mock = _mock_issue(" ") + assert github_issues_test_cases.get_test_issues_from_issue(mock) == [] + + # --- wiki syntax: assertion name only, no curie params --- + + def test_wiki_no_curie_params_parsed(self, github_issues_test_cases): + # {{BabelTest|Resolves}} with no params parses to an empty param_set, not a parse error. + mock = _mock_issue("{{BabelTest|Resolves}}") + tests = github_issues_test_cases.get_test_issues_from_issue(mock) + assert len(tests) == 1 + assert tests[0].assertion == "Resolves" + assert tests[0].param_sets == [[]] + + def test_wiki_needed_no_params(self, github_issues_test_cases): + # {{BabelTest|Needed}} with no extra params is valid per the documented wiki syntax. + mock = _mock_issue("{{BabelTest|Needed}}") + tests = github_issues_test_cases.get_test_issues_from_issue(mock) + assert len(tests) == 1 + assert tests[0].assertion == "Needed" + assert tests[0].param_sets == [[]] + + # --- YAML syntax: null / empty values --- + + def test_yaml_null_babel_tests_raises(self, github_issues_test_cases): + # babel_tests: null → ValueError with a clear message (null yaml value has no .items()) + mock = _mock_issue("```yaml\nbabel_tests:\n\n```") + with pytest.raises(ValueError, match="no 'babel_tests' top-level key"): + github_issues_test_cases.get_test_issues_from_issue(mock) + + def test_yaml_null_assertion_params_raises(self, github_issues_test_cases): + # Resolves: null → ValueError with a clear message (null param list is a config error) + mock = _mock_issue("```yaml\nbabel_tests:\n Resolves:\n```") + with pytest.raises(ValueError, match="null param list"): + github_issues_test_cases.get_test_issues_from_issue(mock) + + def test_yaml_empty_assertion_params(self, github_issues_test_cases): + # Resolves: [] → GitHubIssueTest with empty param_sets (no crash) + mock = _mock_issue("```yaml\nbabel_tests:\n Resolves: []\n```") + tests = github_issues_test_cases.get_test_issues_from_issue(mock) + assert len(tests) == 1 + assert tests[0].param_sets == [] + + def test_yaml_scalar_assertion_params_treated_as_single_param_set(self, github_issues_test_cases): + # Resolves: CHEBI:15365 (bare scalar) → wrapped as a single one-element param_set + mock = _mock_issue("```yaml\nbabel_tests:\n Resolves: CHEBI:15365\n```") + tests = github_issues_test_cases.get_test_issues_from_issue(mock) + assert len(tests) == 1 + assert tests[0].param_sets == [["CHEBI:15365"]] + + def test_yaml_babel_tests_as_list_raises(self, github_issues_test_cases): + # babel_tests as a list (not a mapping) → clear ValueError, not an AttributeError on .items(). + mock = _mock_issue("```yaml\nbabel_tests:\n - Resolves\n```") + with pytest.raises(ValueError, match="must be a mapping"): + github_issues_test_cases.get_test_issues_from_issue(mock) + + def test_yaml_non_string_assertion_name_raises(self, github_issues_test_cases): + # A non-string assertion key (e.g. an integer) → clear ValueError, not a later + # AttributeError when .lower() is called on the assertion name. + mock = _mock_issue("```yaml\nbabel_tests:\n 123:\n - CHEBI:15365\n```") + with pytest.raises(ValueError, match="assertion name must be a string"): + github_issues_test_cases.get_test_issues_from_issue(mock) + + +@pytest.mark.unit +class TestWikiMarkerCaseInsensitivity: + """The {{BabelTest|...}} marker is case-insensitive, like assertion names.""" + + @pytest.mark.parametrize("marker", ["BabelTest", "babeltest", "BABELTEST", "Babeltest"]) + def test_wiki_marker_any_case_parsed(self, github_issues_test_cases, marker): + mock = _mock_issue(f"{{{{{marker}|Resolves|CHEBI:15365}}}}") + tests = github_issues_test_cases.get_test_issues_from_issue(mock) + assert len(tests) == 1 + assert tests[0].assertion == "Resolves" + assert tests[0].param_sets == [["CHEBI:15365"]] + + def test_wiki_marker_any_case_detected(self, github_issues_test_cases): + assert github_issues_test_cases.issue_has_tests( + _mock_issue("{{babeltest|Resolves|CHEBI:12345}}") + ) is True + + +@pytest.mark.unit +class TestIssueHasTests: + """Documents issue_has_tests() behaviour for various body contents.""" + + def test_none_body_returns_false(self, github_issues_test_cases): + assert github_issues_test_cases.issue_has_tests(_mock_issue(None)) is False + + def test_whitespace_body_returns_false(self, github_issues_test_cases): + assert github_issues_test_cases.issue_has_tests(_mock_issue(" ")) is False + + def test_wiki_syntax_detected(self, github_issues_test_cases): + assert github_issues_test_cases.issue_has_tests( + _mock_issue("{{BabelTest|Resolves|CHEBI:12345}}") + ) is True + + def test_yaml_syntax_detected(self, github_issues_test_cases): + assert github_issues_test_cases.issue_has_tests( + _mock_issue("```yaml\nbabel_tests:\n Resolves:\n - CHEBI:12345\n```") + ) is True + + def test_plain_text_not_detected(self, github_issues_test_cases): + assert github_issues_test_cases.issue_has_tests( + _mock_issue("Just some text without babel tests.") + ) is False + + +@pytest.mark.unit +class TestGetIssuesWithTests: + """Documents the search-API path of get_issues_with_tests().""" + + _REPOS = ["test-org/test-repo"] + + def test_no_results_yields_nothing(self, github_issues_test_cases): + with patch.object(github_issues_test_cases.github, "search_issues", + return_value=[]): + results = list( + github_issues_test_cases.get_issues_with_tests(self._REPOS) + ) + assert results == [] + + def test_matching_issue_is_yielded(self, github_issues_test_cases): + mock = _mock_issue("{{BabelTest|Resolves|CHEBI:12345}}", number=1) + # Two keyword searches per repo; first returns our issue, second returns nothing. + with patch.object(github_issues_test_cases.github, "search_issues", + side_effect=[[mock], []]): + results = list( + github_issues_test_cases.get_issues_with_tests(self._REPOS) + ) + assert results == [mock] + + def test_duplicate_across_keywords_deduplicated(self, github_issues_test_cases): + # Issue contains both syntaxes → appears in both keyword searches → yielded once. + body = ( + "{{BabelTest|Resolves|CHEBI:12345}}\n" + "```yaml\nbabel_tests:\n Resolves:\n - CHEBI:12345\n```" + ) + mock = _mock_issue(body, number=42) + with patch.object(github_issues_test_cases.github, "search_issues", + side_effect=[[mock], [mock]]): + results = list( + github_issues_test_cases.get_issues_with_tests(self._REPOS) + ) + assert results == [mock] + + def test_search_false_positive_filtered(self, github_issues_test_cases): + # GitHub returns an issue that mentions the keyword in prose (no real BabelTest block). + mock = _mock_issue("This issue discusses babel_tests: in passing.", number=99) + with patch.object(github_issues_test_cases.github, "search_issues", + side_effect=[[mock], []]): + results = list( + github_issues_test_cases.get_issues_with_tests(self._REPOS) + ) + assert results == [] diff --git a/tests/targets.ini b/tests/targets.ini index d942212e..1484ce09 100644 --- a/tests/targets.ini +++ b/tests/targets.ini @@ -14,10 +14,15 @@ [DEFAULT] NameResLimit = 20 NameResXFailIfInTop = 5 - -[ci-es] -NodeNormURL = https://biothings.ci.transltr.io/nodenorm/ -NameResURL = https://name-lookup.ci.transltr.io/ +# GitHub repositories to scan for embedded BabelTest assertions. +# One "org/repo" per line. Read from this DEFAULT section only — the GitHub +# issue tests do not support per-environment overrides. +Repositories = + NCATSTranslator/Babel + NCATSTranslator/NodeNormalization + NCATSTranslator/NameResolution + TranslatorSRI/babel-validation + TranslatorSRI/babel-explorer [prod] NodeNormURL = https://nodenorm.transltr.io/ @@ -33,6 +38,10 @@ NameResURL = https://name-lookup.ci.transltr.io/ [ci] NodeNormURL = https://nodenorm-es.ci.transltr.io/ +NameResURL = https://name-lookup.ci.transltr.io/ + +[ci-es] +NodeNormURL = https://nodenorm-es.ci.transltr.io/ NameResURL = https://namelookup-es.ci.transltr.io/ [dev]