From 61a5cebd8a249ccb0c49fb1affc784c26e72dada Mon Sep 17 00:00:00 2001 From: Stephan Merker Date: Wed, 12 Nov 2025 09:15:08 +0100 Subject: [PATCH 1/2] Use modern Python tooling for org automation - uv for package mgmt - ruff and basedpyright for linting - pyproject.toml for dependencies and configuration - moved sources into a org_management module to comply with std python project setup - updated dependencies - fixed most severe linting errors - ignored typing warnings for now --- .../org-inactive-user-management.yml | 17 +- .../workflows/org-management-check-prs.yml | 17 +- .github/workflows/org-management-ci.yml | 38 +-- .github/workflows/org-management.yml | 34 ++- .gitignore | 6 +- .vscode/settings.json | 21 ++ orgs/.flake8 | 3 - orgs/org_management/__init__.py | 0 orgs/org_management/__main__.py | 23 ++ orgs/{ => org_management}/org_management.py | 66 ++--- .../org_user_management.py | 31 +- .../test_org_management.py | 6 +- orgs/pyproject.toml | 99 ++++++- orgs/readme.md | 31 +- orgs/requirements-dev.txt | 4 - orgs/requirements.txt | 3 - orgs/uv.lock | 265 ++++++++++++++++++ 17 files changed, 535 insertions(+), 129 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 orgs/.flake8 create mode 100644 orgs/org_management/__init__.py create mode 100644 orgs/org_management/__main__.py rename orgs/{ => org_management}/org_management.py (93%) rename orgs/{ => org_management}/org_user_management.py (94%) rename orgs/{ => org_management}/test_org_management.py (99%) delete mode 100644 orgs/requirements-dev.txt delete mode 100644 orgs/requirements.txt create mode 100644 orgs/uv.lock diff --git a/.github/workflows/org-inactive-user-management.yml b/.github/workflows/org-inactive-user-management.yml index 452991631..6e5aa8cd9 100644 --- a/.github/workflows/org-inactive-user-management.yml +++ b/.github/workflows/org-inactive-user-management.yml @@ -12,18 +12,21 @@ jobs: org-config-generation-check: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v5 - with: - python-version: 3.13 - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: path: community + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: "community/orgs/pyproject.toml" + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true - name: Clean inactive github org users id: uds + working-directory: ./community/orgs run: | - python -m pip install --upgrade pip - pip install -r community/orgs/requirements.txt - python community/orgs/org_user_management.py + uv run --no-dev python -m org_management.org_user_management env: GH_TOKEN: ${{ secrets.GH_TOKEN }} INACTIVE_USER_MANAGEMENT_TAG_USERS: ${{ secrets.INACTIVE_USER_MANAGEMENT_TAG_USERS }} diff --git a/.github/workflows/org-management-check-prs.yml b/.github/workflows/org-management-check-prs.yml index 90a8cdc1e..29653b523 100644 --- a/.github/workflows/org-management-check-prs.yml +++ b/.github/workflows/org-management-check-prs.yml @@ -10,14 +10,17 @@ jobs: org-config-generation-check: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v5 - with: - python-version: 3.13 - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: path: community + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: "community/orgs/pyproject.toml" + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true - name: Generate github org configuration + working-directory: ./community/orgs run: | - python -m pip install --upgrade pip - pip install -r community/orgs/requirements.txt - python community/orgs/org_management.py -o orgs.out.yml -b branchprotection.out.yml + uv run --no-dev python -m org_management -o orgs.out.yml -b branchprotection.out.yml diff --git a/.github/workflows/org-management-ci.yml b/.github/workflows/org-management-ci.yml index 2ac1affe9..ee907470a 100644 --- a/.github/workflows/org-management-ci.yml +++ b/.github/workflows/org-management-ci.yml @@ -2,30 +2,32 @@ name: 'Org Automation CI' on: pull_request: paths: - - 'orgs/*.py' - - 'orgs/requirements*' + - 'orgs/org_management/*.py' - 'orgs/pyproject.toml' - '.github/workflows/org-management-ci.yml' jobs: org-automation-tests: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v5 - with: - python-version: 3.13 - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: path: community - - name: pip install - run: | - python -m pip install --upgrade pip - pip install -r community/orgs/requirements.txt - pip install -r community/orgs/requirements-dev.txt - - name: flake8 and black - run: | - cd community/orgs - python -m flake8 - - name: unit tests + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: "community/orgs/pyproject.toml" + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + - name: Install Python dependencies + working-directory: ./community/orgs + run: uv sync --all-extras --dev --locked + - name: Lint Python + working-directory: ./community/orgs run: | - cd community/orgs - python -m unittest discover -s . + uv run ruff check + uv run ruff format --check + uv run basedpyright + - name: Run Python tests + working-directory: ./community/orgs + run: uv run -m unittest discover -s . \ No newline at end of file diff --git a/.github/workflows/org-management.yml b/.github/workflows/org-management.yml index 721f81bac..f6fa13a64 100644 --- a/.github/workflows/org-management.yml +++ b/.github/workflows/org-management.yml @@ -34,17 +34,20 @@ jobs: key: ghproxy-cache-${{ github.run_number }} restore-keys: | ghproxy-cache- - - uses: actions/setup-python@v5 - with: - python-version: 3.13 - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: path: community + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: "community/orgs/pyproject.toml" + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true - name: Generate github org configuration + working-directory: ./community/orgs run: | - python -m pip install --upgrade pip - pip install -r community/orgs/requirements.txt - python community/orgs/org_management.py -o orgs.out.yml -b branchprotection.out.yml + uv run --no-dev python -m org_management -o orgs.out.yml -b branchprotection.out.yml - name: write github private key run: | echo "${GH_PRIVATE_KEY}" > private_key @@ -96,17 +99,20 @@ jobs: key: ghproxy-cache-${{ github.run_number }} restore-keys: | ghproxy-cache- - - uses: actions/setup-python@v5 - with: - python-version: 3.13 - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: path: community + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: "community/orgs/pyproject.toml" + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true - name: Generate github org configuration + working-directory: ./community/orgs run: | - python -m pip install --upgrade pip - pip install -r community/orgs/requirements.txt - python community/orgs/org_management.py -o orgs.out.yml -b branchprotection.out.yml + uv run --no-dev python -m org_management -o orgs.out.yml -b branchprotection.out.yml - name: write github private key run: | echo "${GH_PRIVATE_KEY}" > private_key diff --git a/.gitignore b/.gitignore index 3072c1ffe..db78a1bff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ toc/elections/2021/private.csv /.secrets -.vscode __pycache__ orgs.out.yml branchprotection.out.yml -/.idea \ No newline at end of file +/.idea +.venv +.ruff_cache +.DS_Store \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..6dd4ecf59 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + "./orgs", + "-p", + "test_*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true, + "python-envs.defaultEnvManager": "ms-python.python:venv", + "python-envs.pythonProjects": [ + { + "path": "orgs", + "envManager": "ms-python.python:venv", + "packageManager": "ms-python.python:pip" + } + ], + "python.analysis.typeCheckingMode": "off", + "basedpyright.analysis.configFilePath": "${workspaceFolder}/orgs" +} \ No newline at end of file diff --git a/orgs/.flake8 b/orgs/.flake8 deleted file mode 100644 index 0e92d2a45..000000000 --- a/orgs/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -max-line-length = 140 -extend-ignore = E203 \ No newline at end of file diff --git a/orgs/org_management/__init__.py b/orgs/org_management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/orgs/org_management/__main__.py b/orgs/org_management/__main__.py new file mode 100644 index 000000000..1573d143e --- /dev/null +++ b/orgs/org_management/__main__.py @@ -0,0 +1,23 @@ +import argparse + +from .org_management import OrgGenerator + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="CFF Managed Github Orgs Generator") + parser.add_argument("-o", "--out", default="orgs.out.yml", help="output file for generated org configuration") + parser.add_argument( + "-b", "--branchprotection", default="branchprotection.out.yml", help="output file for generated branch protection rules" + ) + args = parser.parse_args() + + print("Generating CFF Managed Github Org configuration.") + generator = OrgGenerator() + generator.load_from_project() + if not generator.validate_repo_ownership(): + print("ERROR: Repository ownership is invalid. Refer to RFC-0007.") + exit(1) + generator.generate_org_members() + generator.generate_teams() + generator.generate_branch_protection() + generator.write_org_config(args.out) + generator.write_branch_protection(args.branchprotection) diff --git a/orgs/org_management.py b/orgs/org_management/org_management.py similarity index 93% rename from orgs/org_management.py rename to orgs/org_management/org_management.py index fcb2fede9..263872d61 100644 --- a/orgs/org_management.py +++ b/orgs/org_management/org_management.py @@ -7,19 +7,21 @@ # See readme.md import glob -import yaml -import re import os -import argparse +import re +from pathlib import Path +from typing import Any, final, override + import jsonschema -from typing import Any, Dict, Set, List, Optional, Tuple +import yaml -_SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +_SCRIPT_PATH = Path(__file__).parent.parent.resolve() # pyyaml silently ignores duplicate keys but they shall be rejected # https://yaml.org/spec/1.2.2/ requires unique keys class UniqueKeyLoader(yaml.SafeLoader): + @override def construct_mapping(self, node, deep=False): mapping = set() for key_node, _ in node.value: @@ -30,6 +32,7 @@ def construct_mapping(self, node, deep=False): return super().construct_mapping(node, deep) +@final class OrgGenerator: # list of managed orgs, should match ./ORGS.md _MANAGED_ORGS = ["cloudfoundry"] @@ -38,11 +41,11 @@ class OrgGenerator: # parameters intended for testing only, all params are yaml docs def __init__( self, - static_org_cfg: Optional[str] = None, - contributors: Optional[str] = None, - toc: Optional[str] = None, - working_groups: Optional[List[str]] = None, - branch_protection: Optional[str] = None, + static_org_cfg: str | None = None, + contributors: str | None = None, + toc: str | None = None, + working_groups: list[str] | None = None, + branch_protection: str | None = None, ): self.org_cfg = OrgGenerator._validate_github_org_cfg(OrgGenerator._yaml_load(static_org_cfg)) if static_org_cfg else {"orgs": {}} self.contributors = dict[str, set[str]]() @@ -131,10 +134,10 @@ def validate_repo_ownership(self) -> bool: repo_owners[repo] = wg_name return valid - def get_contributors(self, org: str) -> Set[str]: + def get_contributors(self, org: str) -> set[str]: return set(self.contributors[org]) if org in self.contributors else set() - def get_community_members_with_role_by_wg(self, org: str) -> Dict[str, Set[str]]: + def get_community_members_with_role_by_wg(self, org: str) -> dict[str, set[str]]: # TOC is always added result = {"toc": set(OrgGenerator._wg_github_users(self.toc))} for wg in self.working_groups[org]: @@ -221,13 +224,13 @@ def _yaml_load(stream) -> dict[str, Any]: @staticmethod def _read_yml_file(path: str): - with open(path, "r") as stream: + with open(path) as stream: return OrgGenerator._yaml_load(stream) @staticmethod def _read_wg_charter(path: str): print(f"Reading WG from {path}") - with open(path, "r") as stream: + with open(path) as stream: wg_charter = stream.read() wg = OrgGenerator._extract_wg_config(wg_charter) if not wg: @@ -255,7 +258,7 @@ def _empty_wg_config(name: str): } @staticmethod - def _wg_github_users(wg) -> Set[str]: + def _wg_github_users(wg) -> set[str]: users = {u["github"] for u in wg["execution_leads"]} users |= {u["github"] for u in wg["technical_leads"]} users |= {u["github"] for u in wg["bots"]} @@ -268,7 +271,7 @@ def _wg_github_users(wg) -> Set[str]: return users @staticmethod - def _wg_github_users_leads(wg) -> Set[str]: + def _wg_github_users_leads(wg) -> set[str]: users = {u["github"] for u in wg["execution_leads"]} users |= {u["github"] for u in wg["technical_leads"]} return users @@ -417,7 +420,7 @@ def _validate_branch_protection(cfg): # https://github.com/cloudfoundry/community/blob/main/toc/rfc/rfc-0005-github-teams-and-access.md @staticmethod - def _generate_wg_teams(wg) -> Tuple[str, Dict[str, Any]]: + def _generate_wg_teams(wg) -> tuple[str, dict[str, Any]]: org = wg["org"] org_prefix = org + "/" org_prefix_len = len(org_prefix) @@ -427,7 +430,7 @@ def _generate_wg_teams(wg) -> Tuple[str, Dict[str, Any]]: approvers = {u["github"] for a in wg["areas"] for u in a["approvers"]} repositories = {r[org_prefix_len:] for a in wg["areas"] for r in a["repositories"] if r.startswith(org_prefix)} # WG team and teams for WG areas - team = { + team: dict[str, Any] = { "description": f"Leads and approvers for {wg['name']} WG", "privacy": "closed", "maintainers": sorted(maintainers), @@ -486,7 +489,7 @@ def _generate_wg_teams(wg) -> Tuple[str, Dict[str, Any]]: return (name, team) @staticmethod - def _generate_toc_team(wg) -> Tuple[str, Dict[str, Any]]: + def _generate_toc_team(wg) -> tuple[str, dict[str, Any]]: org = wg["org"] org_prefix = org + "/" org_prefix_len = len(org_prefix) @@ -501,7 +504,7 @@ def _generate_toc_team(wg) -> Tuple[str, Dict[str, Any]]: return ("toc", team) @staticmethod - def _generate_wg_leads_team(wgs: List[Any]) -> Tuple[str, Dict[str, Any]]: + def _generate_wg_leads_team(wgs: list[Any]) -> tuple[str, dict[str, Any]]: members = {u for wg in wgs for u in OrgGenerator._wg_github_users_leads(wg)} team = { "description": "Technical and Execution Leads for all WGs", @@ -512,7 +515,7 @@ def _generate_wg_leads_team(wgs: List[Any]) -> Tuple[str, Dict[str, Any]]: # https://github.com/cloudfoundry/community/blob/main/toc/rfc/rfc-0015-branch-protection.md # returns hash with branch protection rules per repo - def _generate_wg_branch_protection(self, wg) -> Dict[str, Any]: + def _generate_wg_branch_protection(self, wg) -> dict[str, Any]: org = wg["org"] org_prefix = org + "/" org_prefix_len = len(org_prefix) @@ -563,24 +566,3 @@ def _kebab_case(name: str) -> str: # kebab case = lower case and all special chars replaced by dash # no leading, trailing or double dashes return OrgGenerator._KEBAB_CASE_RE.sub("-", name.lower()).strip("-") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="CFF Managed Github Orgs Generator") - parser.add_argument("-o", "--out", default="orgs.out.yml", help="output file for generated org configuration") - parser.add_argument( - "-b", "--branchprotection", default="branchprotection.out.yml", help="output file for generated branch protection rules" - ) - args = parser.parse_args() - - print("Generating CFF Managed Github Org configuration.") - generator = OrgGenerator() - generator.load_from_project() - if not generator.validate_repo_ownership(): - print("ERROR: Repository ownership is invalid. Refer to RFC-0007.") - exit(1) - generator.generate_org_members() - generator.generate_teams() - generator.generate_branch_protection() - generator.write_org_config(args.out) - generator.write_branch_protection(args.branchprotection) diff --git a/orgs/org_user_management.py b/orgs/org_management/org_user_management.py similarity index 94% rename from orgs/org_user_management.py rename to orgs/org_management/org_user_management.py index 6efce4000..fc6988c31 100644 --- a/orgs/org_user_management.py +++ b/orgs/org_management/org_user_management.py @@ -1,15 +1,19 @@ -import requests import argparse import datetime -import yaml import os import uuid +from pathlib import Path +from typing import final + +import requests +import yaml -from org_management import OrgGenerator +from .org_management import OrgGenerator -_SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +_SCRIPT_PATH = Path(__file__).parent.parent.resolve() +@final class InactiveUserHandler: def __init__( self, @@ -37,8 +41,9 @@ def _execute_query(self, query): return self._process_request_result(request) def _build_query(self, after_cursor_value=None): - after_cursor = '"{}"'.format(after_cursor_value) if after_cursor_value else "null" - query = """ + after_cursor = f'"{after_cursor_value}"' if after_cursor_value else "null" + query = ( + """ { organization(login: \"%s\") { membersWithRole(first: 20, after:%s) { @@ -55,11 +60,13 @@ def _build_query(self, after_cursor_value=None): } } } - """ % ( - self.github_org, - after_cursor, - self.github_org_id, - self.activity_date, + """ # noqa: UP031 + % ( + self.github_org, + after_cursor, + self.github_org_id, + self.activity_date, + ) ) return query @@ -83,7 +90,7 @@ def get_inactive_users(self): return inactive_users def _load_yaml_file(self, path): - with open(path, "r") as stream: + with open(path) as stream: return yaml.safe_load(stream) def _write_yaml_file(self, path, data): diff --git a/orgs/test_org_management.py b/orgs/org_management/test_org_management.py similarity index 99% rename from orgs/test_org_management.py rename to orgs/org_management/test_org_management.py index 1a74c1df3..685b3d33e 100644 --- a/orgs/test_org_management.py +++ b/orgs/org_management/test_org_management.py @@ -1,7 +1,9 @@ import unittest -import yaml + import jsonschema -from org_management import OrgGenerator +import yaml + +from .org_management import OrgGenerator org_cfg = """ --- diff --git a/orgs/pyproject.toml b/orgs/pyproject.toml index 3435b3263..5131b7566 100644 --- a/orgs/pyproject.toml +++ b/orgs/pyproject.toml @@ -1,2 +1,97 @@ -[tool.black] -line-length = 140 \ No newline at end of file +[project] +name = "org_management" +version = "0.1.0" +description = "Automation for GitHub orgs managed by the Cloud Foundry Foundation" +readme = "readme.md" +requires-python = ">=3.14" +license-files = ["LICENSE"] +classifiers = ["Private :: Do Not Upload"] +dependencies = [ + "pyyaml", + "jsonschema", + "requests", +] + +[dependency-groups] +dev = [ + "ruff", + "basedpyright", +] + +[build-system] +requires = ["uv_build>=0.9.0,<0.10.0"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-root = "" + +[tool.ruff] +line-length = 140 + +[tool.ruff.lint] +select = [ + # See: https://docs.astral.sh/ruff/rules/ + # Basic list from: https://docs.astral.sh/ruff/linter/#rule-selection + "E", # https://docs.astral.sh/ruff/rules/#error-e + "F", # https://docs.astral.sh/ruff/rules/#pyflakes-f + "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "B", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "I", # https://docs.astral.sh/ruff/rules/#isort-i + # Other possibilities: + # "D" # https://docs.astral.sh/ruff/rules/#pydocstyle-d + # "Q" # https://docs.astral.sh/ruff/rules/#flake8-quotes-q + # "COM" # https://docs.astral.sh/ruff/rules/#flake8-commas-com + # "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + +] +ignore = [ + # Disable some rules that are overly pedantic. Add/remove as desired: + "E501", # https://docs.astral.sh/ruff/rules/line-too-long/ + "E402", # https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/ + "E731", # https://docs.astral.sh/ruff/rules/lambda-assignment/ + # We use both ruff formatter and linter so some rules should always be disabled. + # See: https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", # https://docs.astral.sh/ruff/rules/tab-indentation/ + "E111", # https://docs.astral.sh/ruff/rules/indentation-with-invalid-multiple/ + "E114", # https://docs.astral.sh/ruff/rules/indentation-with-invalid-multiple-comment/ + "E117", # https://docs.astral.sh/ruff/rules/over-indented/ + "D206", # https://docs.astral.sh/ruff/rules/docstring-tab-indentation/ + "D300", # https://docs.astral.sh/ruff/rules/triple-single-quotes/ + "Q000", # https://docs.astral.sh/ruff/rules/bad-quotes-inline-string/ + "Q001", # https://docs.astral.sh/ruff/rules/bad-quotes-multiline-string/ + "Q002", # https://docs.astral.sh/ruff/rules/bad-quotes-docstring/ + "Q003", # https://docs.astral.sh/ruff/rules/avoidable-escaped-quote/ + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ + "COM819", # https://docs.astral.sh/ruff/rules/prohibited-trailing-comma/ + "ISC002", # https://docs.astral.sh/ruff/rules/multi-line-implicit-string-concatenation/ +] + + +[tool.basedpyright] +# https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright +# https://docs.basedpyright.com/latest/configuration/config-files/#sample-pyprojecttoml-file +include = ["org_management"] +# By default BasedPyright is very strict, so you almost certainly want to disable +# some of the rules. +# First, these turn off warnings about (yes) how you ignore warnings: +failOnWarnings = false +enableTypeIgnoreComments = true +# reportIgnoreCommentWithoutRule = false +# reportUnnecessaryTypeIgnoreComment = false +# A few typically noisy warnings are next. +# How many you enable is up to you. The first few are off by default, but you can +# comment/uncomment these as desired: +reportMissingTypeStubs = false +reportUnusedCallResult = false +reportAny = false +reportExplicitAny = false +# reportImplicitStringConcatenation = false +# reportUnreachable = false +# reportImplicitOverride = false +# reportPrivateImportUsage = false +# reportPrivateLocalImportUsage = false +# reportMissingImports = false +# reportUnnecessaryIsInstance = false +# reportUnknownVariableType = false +# reportUnknownArgumentType = false +# reportUnknownMemberType = false diff --git a/orgs/readme.md b/orgs/readme.md index 2a14f8f18..1e467ff30 100644 --- a/orgs/readme.md +++ b/orgs/readme.md @@ -83,21 +83,21 @@ Inactive users according to the criteria defined in ## Development -Requires Python 3.13. +Requires [uv](https://docs.astral.sh/uv/getting-started/installation/). + +Recommended IDE: VSCode with Python, Ruff and BasedPyright extensions. How to run locally: ``` cd ./orgs -python -m venv -source /bin/activate -pip install -r requirements.txt -python -m org_management --help -python -m org_user_management --help +uv sync +uv run python -m org_management --help +uv run python -m org_management.org_user_management --help ``` Usage: ``` -$ python -m org_management --help +$ uv run python -m org_management --help usage: org_management.py [-h] [-o OUT] [-b BRANCHPROTECTION] Cloud Foundry Org Generator @@ -110,7 +110,7 @@ optional arguments: ``` ``` -python -m org_user_management --help +$ uv run python -m org_management.org_user_management --help usage: org_user_management.py [-h] [-goid GITHUBORGID] [-go GITHUBORG] [-sd SINCEDATE] [-gt GITHUBTOKEN] [-dr DRYRUN] [-tu TAGUSERS] Cloud Foundry Org Inactive User Handler @@ -134,9 +134,14 @@ options: How to run tests: ``` cd ./org -python -m venv -source /bin/activate -pip install -r requirements-dev.txt -python -m flake8 -python -m unittest discover -s . +uv run ruff check +uv run ruff format --check +uv run basedpyright +uv run -m unittest discover -s . +``` + +How to update dependencies (there is no renovate or dependabot configured): +``` +cd ./org +uv sync --upgrade ``` \ No newline at end of file diff --git a/orgs/requirements-dev.txt b/orgs/requirements-dev.txt deleted file mode 100644 index 0c04ea99e..000000000 --- a/orgs/requirements-dev.txt +++ /dev/null @@ -1,4 +0,0 @@ -black -flake8 -flake8-bugbear -flake8-black \ No newline at end of file diff --git a/orgs/requirements.txt b/orgs/requirements.txt deleted file mode 100644 index dd4cca626..000000000 --- a/orgs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pyyaml -jsonschema -requests \ No newline at end of file diff --git a/orgs/uv.lock b/orgs/uv.lock new file mode 100644 index 000000000..b69bfde75 --- /dev/null +++ b/orgs/uv.lock @@ -0,0 +1,265 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "basedpyright" +version = "1.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/c1/d62811ef22ec66bd3112ee2a8c18ff39ea87f3d8040cf3a93574024b17ea/basedpyright-1.33.0.tar.gz", hash = "sha256:a7e4d6d2285b93d8c0c91c75490d5bc00adbe7ef96ba1f1d4c010d9a731e243a", size = 22786957, upload-time = "2025-11-07T11:49:32.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/87/d95da7ef033c26d71a7153623e7b14fd9cc2c8676e03cf4e9199320cbaf5/basedpyright-1.33.0-py3-none-any.whl", hash = "sha256:1ab813683e28184aa60fd4000731f6fe9dbd083c9bd489420bb20f3e5dbb6ab8", size = 11859149, upload-time = "2025-11-07T11:49:28.694Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "nodejs-wheel-binaries" +version = "22.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/54/02f58c8119e2f1984e2572cc77a7b469dbaf4f8d171ad376e305749ef48e/nodejs_wheel_binaries-22.20.0.tar.gz", hash = "sha256:a62d47c9fd9c32191dff65bbe60261504f26992a0a19fe8b4d523256a84bd351", size = 8058, upload-time = "2025-09-26T09:48:00.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/6d/333e5458422f12318e3c3e6e7f194353aa68b0d633217c7e89833427ca01/nodejs_wheel_binaries-22.20.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:455add5ac4f01c9c830ab6771dbfad0fdf373f9b040d3aabe8cca9b6c56654fb", size = 53246314, upload-time = "2025-09-26T09:47:32.536Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/dcd6879d286a35b3c4c8f9e5e0e1bcf4f9e25fe35310fc77ecf97f915a23/nodejs_wheel_binaries-22.20.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:5d8c12f97eea7028b34a84446eb5ca81829d0c428dfb4e647e09ac617f4e21fa", size = 53644391, upload-time = "2025-09-26T09:47:36.093Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/c7b2e7aa3bb281d380a1c531f84d0ccfe225832dfc3bed1ca171753b9630/nodejs_wheel_binaries-22.20.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a2b0989194148f66e9295d8f11bc463bde02cbe276517f4d20a310fb84780ae", size = 60282516, upload-time = "2025-09-26T09:47:39.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c5/8befacf4190e03babbae54cb0809fb1a76e1600ec3967ab8ee9f8fc85b65/nodejs_wheel_binaries-22.20.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5c500aa4dc046333ecb0a80f183e069e5c30ce637f1c1a37166b2c0b642dc21", size = 60347290, upload-time = "2025-09-26T09:47:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/cfffd1e334277afa0714962c6ec432b5fe339340a6bca2e5fa8e678e7590/nodejs_wheel_binaries-22.20.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3279eb1b99521f0d20a850bbfc0159a658e0e85b843b3cf31b090d7da9f10dfc", size = 62178798, upload-time = "2025-09-26T09:47:47.752Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/10b83a9c02faac985b3e9f5e65d63a34fc0f46b48d8a2c3e4caa3e1e7318/nodejs_wheel_binaries-22.20.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d29705797b33bade62d79d8f106c2453c8a26442a9b2a5576610c0f7e7c351ed", size = 62772957, upload-time = "2025-09-26T09:47:51.266Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a9/c6a480259aa0d6b270aac2c6ba73a97444b9267adde983a5b7e34f17e45a/nodejs_wheel_binaries-22.20.0-py2.py3-none-win_amd64.whl", hash = "sha256:4bd658962f24958503541963e5a6f2cc512a8cb301e48a69dc03c879f40a28ae", size = 40120431, upload-time = "2025-09-26T09:47:54.363Z" }, + { url = "https://files.pythonhosted.org/packages/42/b1/6a4eb2c6e9efa028074b0001b61008c9d202b6b46caee9e5d1b18c088216/nodejs_wheel_binaries-22.20.0-py2.py3-none-win_arm64.whl", hash = "sha256:1fccac931faa210d22b6962bcdbc99269d16221d831b9a118bbb80fe434a60b8", size = 38844133, upload-time = "2025-09-26T09:47:57.357Z" }, +] + +[[package]] +name = "org-management" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "jsonschema" }, + { name = "pyyaml" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "basedpyright" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "jsonschema" }, + { name = "pyyaml" }, + { name = "requests" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "basedpyright" }, + { name = "ruff" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" }, + { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" }, + { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" }, + { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" }, + { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" }, + { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" }, + { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" }, + { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" }, + { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" }, + { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" }, + { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" }, + { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" }, + { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" }, + { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" }, + { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" }, + { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/55/cccfca45157a2031dcbb5a462a67f7cf27f8b37d4b3b1cd7438f0f5c1df6/ruff-0.14.4.tar.gz", hash = "sha256:f459a49fe1085a749f15414ca76f61595f1a2cc8778ed7c279b6ca2e1fd19df3", size = 5587844, upload-time = "2025-11-06T22:07:45.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/b9/67240254166ae1eaa38dec32265e9153ac53645a6c6670ed36ad00722af8/ruff-0.14.4-py3-none-linux_armv6l.whl", hash = "sha256:e6604613ffbcf2297cd5dcba0e0ac9bd0c11dc026442dfbb614504e87c349518", size = 12606781, upload-time = "2025-11-06T22:07:01.841Z" }, + { url = "https://files.pythonhosted.org/packages/46/c8/09b3ab245d8652eafe5256ab59718641429f68681ee713ff06c5c549f156/ruff-0.14.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d99c0b52b6f0598acede45ee78288e5e9b4409d1ce7f661f0fa36d4cbeadf9a4", size = 12946765, upload-time = "2025-11-06T22:07:05.858Z" }, + { url = "https://files.pythonhosted.org/packages/14/bb/1564b000219144bf5eed2359edc94c3590dd49d510751dad26202c18a17d/ruff-0.14.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9358d490ec030f1b51d048a7fd6ead418ed0826daf6149e95e30aa67c168af33", size = 11928120, upload-time = "2025-11-06T22:07:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/a3/92/d5f1770e9988cc0742fefaa351e840d9aef04ec24ae1be36f333f96d5704/ruff-0.14.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b40d27924f1f02dfa827b9c0712a13c0e4b108421665322218fc38caf615c2", size = 12370877, upload-time = "2025-11-06T22:07:10.015Z" }, + { url = "https://files.pythonhosted.org/packages/e2/29/e9282efa55f1973d109faf839a63235575519c8ad278cc87a182a366810e/ruff-0.14.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f5e649052a294fe00818650712083cddc6cc02744afaf37202c65df9ea52efa5", size = 12408538, upload-time = "2025-11-06T22:07:13.085Z" }, + { url = "https://files.pythonhosted.org/packages/8e/01/930ed6ecfce130144b32d77d8d69f5c610e6d23e6857927150adf5d7379a/ruff-0.14.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa082a8f878deeba955531f975881828fd6afd90dfa757c2b0808aadb437136e", size = 13141942, upload-time = "2025-11-06T22:07:15.386Z" }, + { url = "https://files.pythonhosted.org/packages/6a/46/a9c89b42b231a9f487233f17a89cbef9d5acd538d9488687a02ad288fa6b/ruff-0.14.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1043c6811c2419e39011890f14d0a30470f19d47d197c4858b2787dfa698f6c8", size = 14544306, upload-time = "2025-11-06T22:07:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/9c6cf86491f2a6d52758b830b89b78c2ae61e8ca66b86bf5a20af73d20e6/ruff-0.14.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f3a936ac27fb7c2a93e4f4b943a662775879ac579a433291a6f69428722649", size = 14210427, upload-time = "2025-11-06T22:07:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/71/f4/0666fe7769a54f63e66404e8ff698de1dcde733e12e2fd1c9c6efb689cb5/ruff-0.14.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95643ffd209ce78bc113266b88fba3d39e0461f0cbc8b55fb92505030fb4a850", size = 13658488, upload-time = "2025-11-06T22:07:22.32Z" }, + { url = "https://files.pythonhosted.org/packages/ee/79/6ad4dda2cfd55e41ac9ed6d73ef9ab9475b1eef69f3a85957210c74ba12c/ruff-0.14.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456daa2fa1021bc86ca857f43fe29d5d8b3f0e55e9f90c58c317c1dcc2afc7b5", size = 13354908, upload-time = "2025-11-06T22:07:24.347Z" }, + { url = "https://files.pythonhosted.org/packages/b5/60/f0b6990f740bb15c1588601d19d21bcc1bd5de4330a07222041678a8e04f/ruff-0.14.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f911bba769e4a9f51af6e70037bb72b70b45a16db5ce73e1f72aefe6f6d62132", size = 13587803, upload-time = "2025-11-06T22:07:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/c9/da/eaaada586f80068728338e0ef7f29ab3e4a08a692f92eb901a4f06bbff24/ruff-0.14.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76158a7369b3979fa878612c623a7e5430c18b2fd1c73b214945c2d06337db67", size = 12279654, upload-time = "2025-11-06T22:07:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/b1d0e82cf9bf8aed10a6d45be47b3f402730aa2c438164424783ac88c0ed/ruff-0.14.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f3b8f3b442d2b14c246e7aeca2e75915159e06a3540e2f4bed9f50d062d24469", size = 12357520, upload-time = "2025-11-06T22:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/04/f4/53e2b42cc82804617e5c7950b7079d79996c27e99c4652131c6a1100657f/ruff-0.14.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c62da9a06779deecf4d17ed04939ae8b31b517643b26370c3be1d26f3ef7dbde", size = 12719431, upload-time = "2025-11-06T22:07:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/a2/94/80e3d74ed9a72d64e94a7b7706b1c1ebaa315ef2076fd33581f6a1cd2f95/ruff-0.14.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a443a83a1506c684e98acb8cb55abaf3ef725078be40237463dae4463366349", size = 13464394, upload-time = "2025-11-06T22:07:35.905Z" }, + { url = "https://files.pythonhosted.org/packages/54/1a/a49f071f04c42345c793d22f6cf5e0920095e286119ee53a64a3a3004825/ruff-0.14.4-py3-none-win32.whl", hash = "sha256:643b69cb63cd996f1fc7229da726d07ac307eae442dd8974dbc7cf22c1e18fff", size = 12493429, upload-time = "2025-11-06T22:07:38.43Z" }, + { url = "https://files.pythonhosted.org/packages/bc/22/e58c43e641145a2b670328fb98bc384e20679b5774258b1e540207580266/ruff-0.14.4-py3-none-win_amd64.whl", hash = "sha256:26673da283b96fe35fa0c939bf8411abec47111644aa9f7cfbd3c573fb125d2c", size = 13635380, upload-time = "2025-11-06T22:07:40.496Z" }, + { url = "https://files.pythonhosted.org/packages/30/bd/4168a751ddbbf43e86544b4de8b5c3b7be8d7167a2a5cb977d274e04f0a1/ruff-0.14.4-py3-none-win_arm64.whl", hash = "sha256:dd09c292479596b0e6fec8cd95c65c3a6dc68e9ad17b8f2382130f87ff6a75bb", size = 12663065, upload-time = "2025-11-06T22:07:42.603Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] From 591970445d8cafe55d7e7ab137babecf52ca0b8c Mon Sep 17 00:00:00 2001 From: Stephan Merker Date: Wed, 12 Nov 2025 11:14:49 +0100 Subject: [PATCH 2/2] Fix typing warnings - and enable failOnWarnings for basedpyright --- orgs/org_management/org_management.py | 40 ++++++++++---------- orgs/org_management/org_user_management.py | 44 +++++++++++----------- orgs/org_management/test_org_management.py | 5 +++ orgs/pyproject.toml | 2 +- 4 files changed, 50 insertions(+), 41 deletions(-) diff --git a/orgs/org_management/org_management.py b/orgs/org_management/org_management.py index 263872d61..6a39e3071 100644 --- a/orgs/org_management/org_management.py +++ b/orgs/org_management/org_management.py @@ -10,7 +10,7 @@ import os import re from pathlib import Path -from typing import Any, final, override +from typing import Any, TextIO, final, override import jsonschema import yaml @@ -22,10 +22,10 @@ # https://yaml.org/spec/1.2.2/ requires unique keys class UniqueKeyLoader(yaml.SafeLoader): @override - def construct_mapping(self, node, deep=False): - mapping = set() + def construct_mapping(self, node: Any, deep: bool = False): + mapping = set[str]() for key_node, _ in node.value: - key = self.construct_object(key_node, deep=deep) + key: str = str(self.construct_object(key_node, deep=deep)) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] if key in mapping: raise yaml.MarkedYAMLError(f"Duplicate key {key!r} found.", key_node.start_mark) mapping.add(key) @@ -47,10 +47,12 @@ def __init__( working_groups: list[str] | None = None, branch_protection: str | None = None, ): - self.org_cfg = OrgGenerator._validate_github_org_cfg(OrgGenerator._yaml_load(static_org_cfg)) if static_org_cfg else {"orgs": {}} + self.org_cfg: dict[str, Any] = ( + OrgGenerator._validate_github_org_cfg(OrgGenerator._yaml_load(static_org_cfg)) if static_org_cfg else {"orgs": {}} + ) self.contributors = dict[str, set[str]]() - self.working_groups = {} - self.branch_protection = ( + self.working_groups: dict[str, Any] = {} + self.branch_protection: dict[str, Any] = ( OrgGenerator._validate_branch_protection(OrgGenerator._yaml_load(branch_protection)) if branch_protection else {"branch-protection": {"orgs": {}}} @@ -218,7 +220,7 @@ def write_branch_protection(self, path: str): return yaml.safe_dump(self.branch_protection, stream) @staticmethod - def _yaml_load(stream) -> dict[str, Any]: + def _yaml_load(stream: TextIO | str) -> dict[str, Any]: # safe_load + reject unique keys return yaml.load(stream, UniqueKeyLoader) @@ -247,7 +249,7 @@ def _extract_wg_config(wg_charter: str): return OrgGenerator._validate_wg(OrgGenerator._yaml_load(match.group(1))) if match else None @staticmethod - def _empty_wg_config(name: str): + def _empty_wg_config(name: str) -> dict[str, Any]: return { "name": name, "org": OrgGenerator._DEFAULT_ORG, @@ -258,7 +260,7 @@ def _empty_wg_config(name: str): } @staticmethod - def _wg_github_users(wg) -> set[str]: + def _wg_github_users(wg: dict[str, Any]) -> set[str]: users = {u["github"] for u in wg["execution_leads"]} users |= {u["github"] for u in wg["technical_leads"]} users |= {u["github"] for u in wg["bots"]} @@ -271,7 +273,7 @@ def _wg_github_users(wg) -> set[str]: return users @staticmethod - def _wg_github_users_leads(wg) -> set[str]: + def _wg_github_users_leads(wg: dict[str, Any]) -> set[str]: users = {u["github"] for u in wg["execution_leads"]} users |= {u["github"] for u in wg["technical_leads"]} return users @@ -297,7 +299,7 @@ def _wg_github_users_leads(wg) -> set[str]: } @staticmethod - def _validate_contributors(contributors) -> dict[str, Any]: + def _validate_contributors(contributors: dict[str, Any]) -> dict[str, Any]: jsonschema.validate(contributors, OrgGenerator._CONTRIBUTORS_SCHEMA) # check that orgs are in _ORGS for org in contributors["orgs"]: @@ -343,7 +345,7 @@ def _validate_contributors(contributors) -> dict[str, Any]: } @staticmethod - def _validate_wg(wg) -> dict[str, Any]: + def _validate_wg(wg: dict[str, Any]) -> dict[str, Any]: jsonschema.validate(wg, OrgGenerator._WG_SCHEMA) # validate org and use 'cloudfoundry' if missing if "org" not in wg: @@ -375,7 +377,7 @@ def _validate_wg(wg) -> dict[str, Any]: } @staticmethod - def _validate_github_org_cfg(cfg): + def _validate_github_org_cfg(cfg: dict[str, Any]) -> dict[str, Any]: jsonschema.validate(cfg, OrgGenerator._GITHUB_ORG_CFG_SCHEMA) # check that orgs are in _ORGS for org in cfg["orgs"]: @@ -410,7 +412,7 @@ def _validate_github_org_cfg(cfg): } @staticmethod - def _validate_branch_protection(cfg): + def _validate_branch_protection(cfg: dict[str, Any]) -> dict[str, Any]: jsonschema.validate(cfg, OrgGenerator._BRANCH_PROTECTION_SCHEMA) # check that orgs are in _ORGS for org in cfg["branch-protection"]["orgs"]: @@ -420,7 +422,7 @@ def _validate_branch_protection(cfg): # https://github.com/cloudfoundry/community/blob/main/toc/rfc/rfc-0005-github-teams-and-access.md @staticmethod - def _generate_wg_teams(wg) -> tuple[str, dict[str, Any]]: + def _generate_wg_teams(wg: dict[str, Any]) -> tuple[str, dict[str, Any]]: org = wg["org"] org_prefix = org + "/" org_prefix_len = len(org_prefix) @@ -489,7 +491,7 @@ def _generate_wg_teams(wg) -> tuple[str, dict[str, Any]]: return (name, team) @staticmethod - def _generate_toc_team(wg) -> tuple[str, dict[str, Any]]: + def _generate_toc_team(wg: dict[str, Any]) -> tuple[str, dict[str, Any]]: org = wg["org"] org_prefix = org + "/" org_prefix_len = len(org_prefix) @@ -504,7 +506,7 @@ def _generate_toc_team(wg) -> tuple[str, dict[str, Any]]: return ("toc", team) @staticmethod - def _generate_wg_leads_team(wgs: list[Any]) -> tuple[str, dict[str, Any]]: + def _generate_wg_leads_team(wgs: list[dict[str, Any]]) -> tuple[str, dict[str, Any]]: members = {u for wg in wgs for u in OrgGenerator._wg_github_users_leads(wg)} team = { "description": "Technical and Execution Leads for all WGs", @@ -515,7 +517,7 @@ def _generate_wg_leads_team(wgs: list[Any]) -> tuple[str, dict[str, Any]]: # https://github.com/cloudfoundry/community/blob/main/toc/rfc/rfc-0015-branch-protection.md # returns hash with branch protection rules per repo - def _generate_wg_branch_protection(self, wg) -> dict[str, Any]: + def _generate_wg_branch_protection(self, wg: dict[str, Any]) -> dict[str, Any]: org = wg["org"] org_prefix = org + "/" org_prefix_len = len(org_prefix) diff --git a/orgs/org_management/org_user_management.py b/orgs/org_management/org_user_management.py index fc6988c31..ab41b2367 100644 --- a/orgs/org_management/org_user_management.py +++ b/orgs/org_management/org_user_management.py @@ -3,7 +3,7 @@ import os import uuid from pathlib import Path -from typing import final +from typing import Any, final import requests import yaml @@ -30,17 +30,17 @@ def __init__( def _get_request_headrs(self): return {"Authorization": f"Bearer {self.github_token}"} - def _process_request_result(self, request): + def _process_request_result(self, request: requests.Response): if request.status_code == 200 or request.status_code == 201: return request.json() else: raise Exception(f"Request execution failed with status code of {request.status_code}. {request.status_code}") - def _execute_query(self, query): + def _execute_query(self, query: str): request = requests.post("https://api.github.com/graphql", json={"query": query}, headers=self._get_request_headrs()) return self._process_request_result(request) - def _build_query(self, after_cursor_value=None): + def _build_query(self, after_cursor_value: Any): after_cursor = f'"{after_cursor_value}"' if after_cursor_value else "null" query = ( """ @@ -70,10 +70,10 @@ def _build_query(self, after_cursor_value=None): ) return query - def get_inactive_users(self): - inactive_users = set() + def get_inactive_users(self) -> set[str]: + inactive_users = set[str]() has_next_page = True - after_cursor_value = None + after_cursor_value: Any = None while has_next_page: result = self._execute_query(self._build_query(after_cursor_value)) for user_node in result["data"]["organization"]["membersWithRole"]["nodes"]: @@ -89,31 +89,31 @@ def get_inactive_users(self): return inactive_users - def _load_yaml_file(self, path): + def _load_yaml_file(self, path: Path) -> dict[str, Any]: with open(path) as stream: return yaml.safe_load(stream) - def _write_yaml_file(self, path, data): + def _write_yaml_file(self, path: Path, data: dict[str, Any]): with open(path, "w") as f: yaml.dump(data, f) - def _get_inactive_users_msg_for_wgs(self, inactive_users_by_wg, user_tagging_prefix): + def _get_inactive_users_msg_for_wgs(self, inactive_users_by_wg: dict[str, set[str]], user_tagging_prefix: str) -> str: result = "\n\nWarning:\n" if inactive_users_by_wg else "" for wg, users in inactive_users_by_wg.items(): wg_users_as_list = "\n".join(str(user_tagging_prefix + s) for s in users) result += f'Inactive users of Working Group "{wg}" are: \n{wg_users_as_list}\n' return result - def delete_inactive_contributors(self, users_to_delete): - path = f"{_SCRIPT_PATH}/contributors.yml" + def delete_inactive_contributors(self, users_to_delete: set[str]): + path = _SCRIPT_PATH / "contributors.yml" contributors_yaml = self._load_yaml_file(path) - users_to_delete_lower = [user.lower() for user in users_to_delete] + users_to_delete_lower = {user.lower() for user in users_to_delete} contributors_yaml["orgs"][self.github_org]["contributors"] = [ c for c in contributors_yaml["orgs"][self.github_org]["contributors"] if c.lower() not in users_to_delete_lower ] self._write_yaml_file(path, contributors_yaml) - def get_inactive_users_msg(self, users_to_delete, inactive_users_by_wg, tagusers): + def get_inactive_users_msg(self, users_to_delete: set[str], inactive_users_by_wg: dict[str, set[str]], tagusers: bool) -> str: rfc = ( "https://github.com/cloudfoundry/community/blob/main/toc/rfc/" "rfc-0025-define-criteria-and-removal-process-for-inactive-members.md" @@ -137,8 +137,10 @@ def get_inactive_users_msg(self, users_to_delete, inactive_users_by_wg, tagusers f"{self._get_inactive_users_msg_for_wgs(inactive_users_by_wg, user_tagging_prefix)}" ) - def get_inactive_users_by_wg(self, inactive_users, community_members_with_role_by_wg): - result = dict() + def get_inactive_users_by_wg( + self, inactive_users: set[str], community_members_with_role_by_wg: dict[str, set[str]] + ) -> dict[str, set[str]]: + result = dict[str, set[str]]() for wg, members in community_members_with_role_by_wg.items(): wg_inactive_members = inactive_users.intersection(members) if len(wg_inactive_members) != 0 and wg != "Admin": @@ -146,8 +148,8 @@ def get_inactive_users_by_wg(self, inactive_users, community_members_with_role_b return result @staticmethod - def _get_bool_env_var(env_var_name, default): - return os.getenv(env_var_name, default).lower() == "true" + def get_bool_env_var(env_var_name: str, default: bool) -> bool: + return os.getenv(env_var_name, str(default)).lower() == "true" if __name__ == "__main__": @@ -178,7 +180,7 @@ def _get_bool_env_var(env_var_name, default): generator = OrgGenerator() generator.load_from_project() community_members_with_role_by_wg = generator.get_community_members_with_role_by_wg(args.githuborg) - community_members_with_role = set() + community_members_with_role = set[str]() for members in community_members_with_role_by_wg.values(): community_members_with_role |= set(members) @@ -188,11 +190,11 @@ def _get_bool_env_var(env_var_name, default): print(f"Inactive users length is {len(inactive_users)} and inactive users are {inactive_users}") users_to_delete = inactive_users - community_members_with_role - tagusers = args.tagusers or InactiveUserHandler._get_bool_env_var("INACTIVE_USER_MANAGEMENT_TAG_USERS", "False") + tagusers = args.tagusers or InactiveUserHandler.get_bool_env_var("INACTIVE_USER_MANAGEMENT_TAG_USERS", False) inactive_users_by_wg = userHandler.get_inactive_users_by_wg(inactive_users, community_members_with_role_by_wg) inactive_users_msg = userHandler.get_inactive_users_msg(users_to_delete, inactive_users_by_wg, tagusers) print(f"Inactive users by wg are {inactive_users_by_wg}") - if args.dryrun or InactiveUserHandler._get_bool_env_var("INACTIVE_USER_MANAGEMENT_DRY_RUN", "False"): + if args.dryrun or InactiveUserHandler.get_bool_env_var("INACTIVE_USER_MANAGEMENT_DRY_RUN", False): print(f"Dry-run mode.\nInactive_users_msg is: {inactive_users_msg}") print(f"Following users will be deleted: {inactive_users}") elif users_to_delete: diff --git a/orgs/org_management/test_org_management.py b/orgs/org_management/test_org_management.py index 685b3d33e..74d8469f4 100644 --- a/orgs/org_management/test_org_management.py +++ b/orgs/org_management/test_org_management.py @@ -1,10 +1,13 @@ import unittest +from typing import final, override import jsonschema import yaml from .org_management import OrgGenerator +# pyright: reportPrivateUsage=false + org_cfg = """ --- orgs: @@ -278,7 +281,9 @@ """ +@final class TestOrgGenerator(unittest.TestCase): + @override def setUp(self) -> None: OrgGenerator._MANAGED_ORGS = ["cloudfoundry"] diff --git a/orgs/pyproject.toml b/orgs/pyproject.toml index 5131b7566..923260e4d 100644 --- a/orgs/pyproject.toml +++ b/orgs/pyproject.toml @@ -74,7 +74,7 @@ include = ["org_management"] # By default BasedPyright is very strict, so you almost certainly want to disable # some of the rules. # First, these turn off warnings about (yes) how you ignore warnings: -failOnWarnings = false +# failOnWarnings = false enableTypeIgnoreComments = true # reportIgnoreCommentWithoutRule = false # reportUnnecessaryTypeIgnoreComment = false