diff --git a/.github/workflows/codeql-analysis.yml.disabled b/.github/workflows/codeql-analysis.yml.disabled deleted file mode 100644 index d6616f9d..00000000 --- a/.github/workflows/codeql-analysis.yml.disabled +++ /dev/null @@ -1,70 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '22 4 * * 5' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 009501bf..2af5e57e 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -12,11 +12,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: extractions/setup-just@v3 - name: Install uv uses: astral-sh/setup-uv@v7 with: activate-environment: "true" github-token: ${{ github.token }} + - uses: extractions/setup-just@v3 - name: Running type checking run: | - make types + just lint diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 7e1cc3a2..b135c33d 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -12,11 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: extractions/setup-just@v3 - name: Install uv uses: astral-sh/setup-uv@v7 with: activate-environment: "true" github-token: ${{ github.token }} - - name: Running ruff + - name: Running ruff checks run: | - uv run ruff check $(basename $(pwd)) tests + just format diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 2eebadf1..cdbb03bf 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -12,6 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: extractions/setup-just@v3 - name: Install uv uses: astral-sh/setup-uv@v7 with: @@ -19,4 +20,4 @@ jobs: github-token: ${{ github.token }} - name: Running pytest run: | - uv run python -m pytest -m 'not network' + just test -m 'not\ network' diff --git a/CLAUDE.md b/AGENTS.md similarity index 95% rename from CLAUDE.md rename to AGENTS.md index 5e32d88e..0eebe440 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -6,14 +6,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co github_linter is a Python tool for auditing GitHub repositories at scale. It scans repositories for common configuration issues, missing files, and standardization opportunities across multiple repos. +**MANDATORY** You are not finished with a task until running `just check` passes without warnings or errors. + ## Development Commands ### Testing and Linting -- Run all precommit checks: `make precommit` -- Run linting: `make ruff` -- Run type checking: `make types` -- Run tests: `make test` +- Run all precommit checks: `just check` +- Run linting: `just ruff` +- Run type checking: `just lint` +- Run tests: `just test` - Run single test: `uv run pytest tests/test_.py::` ### Running the CLI @@ -26,13 +28,12 @@ github_linter is a Python tool for auditing GitHub repositories at scale. It sca ### Web Interface -- Start web server: `uv run github-linter-web` -- Or use the script: `./run_web.sh` +- Start web server: `./run_web.sh` ### Docker -- Build container: `make docker_build` -- Run web server in container: `make docker_run` +- Build container: `just docker_build` +- Run web server in container: `just docker_run` ## Architecture diff --git a/Makefile b/Makefile deleted file mode 100644 index 09099f6d..00000000 --- a/Makefile +++ /dev/null @@ -1,43 +0,0 @@ -FULLNAME ?= kanidm/kanidm - - -.DEFAULT: precommit - -.PHONY: precommit -precommit: ruff types test - -.PHONY: jslint -jslint: - find github_linter -name '*.js' -or -name '*.css' -not -name 'pico.min.css' | xargs biome check --json-formatter-enabled=false - -.PHONY: ruff -ruff: - uv run ruff check github_linter tests - -.PHONY: types -types: - uv run ty check - -.PHONY: test -test: - uv run pytest github_linter tests - -.PHONY: docker_build -docker_build: - docker build -t 'ghcr.io/yaleman/github_linter:latest' \ - . - -.PHONY: docker_run -docker_run: docker_build - docker run --rm -it \ - -p 8000:8000 \ - --env-file .envrc -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ - 'ghcr.io/yaleman/github_linter:latest' \ - python -m github_linter.web - -.PHONY: container/workflow_stats -container/workflow_stats: - docker run --rm -it \ - --env-file .envrc -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ - 'ghcr.io/yaleman/github_linter:latest' \ - python -m github_linter.workflow_stats -f $(FULLNAME) \ No newline at end of file diff --git a/README.md b/README.md index 236e39d2..aa22ab9a 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,9 @@ docker run --rm -it \ ghcr.io/yaleman/github_linter:latest \ python -m github_linter.web ``` + +## Thanks + +* [Vue.js](http://vuejs.org) used in the UI +* [Pico](https://picocss.com) CSS framework. + \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..0f53fa4f --- /dev/null +++ b/biome.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "files": { + "includes": [ + "github_linter/**/*.js", + "github_linter/**/*.css", + "!!.venv/", + "!!**/pico.min.css" + ] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "javascript": { + "formatter": { + "lineWidth": 80 + } + }, + "overrides": [ + { + "includes": ["*.vue"], + "linter": { + "enabled": true + }, + "formatter": { + "enabled": true + } + } + ] +} diff --git a/github_linter/__init__.py b/github_linter/__init__.py index 8f89a575..f5e0585e 100644 --- a/github_linter/__init__.py +++ b/github_linter/__init__.py @@ -282,7 +282,7 @@ def filter_by_repo(repo_list: List[Repository], repo_filters: List[str]) -> List return retval -class RepoSearchString(pydantic.BaseModel): # pylint: disable=no-member +class RepoSearchString(pydantic.BaseModel): """Result of running generate_repo_search_string""" needs_post_filtering: bool diff --git a/github_linter/__main__.py b/github_linter/__main__.py index 468b4614..05e50bdb 100644 --- a/github_linter/__main__.py +++ b/github_linter/__main__.py @@ -33,7 +33,6 @@ @click.option("--check", "-k", multiple=True, help="Filter by check name, eg check_example") @click.option("--list-repos", is_flag=True, default=False, help="List repos and exit") @click.option("--debug", "-d", is_flag=True, default=False, help="Enable debug logging") -# pylint: disable=too-many-arguments,too-many-locals def cli( repo: Optional[Tuple[str]] = None, owner: Optional[Tuple[str]] = None, diff --git a/github_linter/custom_types.py b/github_linter/custom_types.py index e7809fbd..aab1f954 100644 --- a/github_linter/custom_types.py +++ b/github_linter/custom_types.py @@ -1,4 +1,4 @@ -""" Custom types """ +"""Custom types""" from typing import Dict, List diff --git a/github_linter/defaults.py b/github_linter/defaults.py index 23f4d64c..77fd5329 100644 --- a/github_linter/defaults.py +++ b/github_linter/defaults.py @@ -1,4 +1,4 @@ -""" default linter configuration goes here """ +"""default linter configuration goes here""" from typing import Dict, List, Optional, TypedDict diff --git a/github_linter/fixes/github_actions/__init__.py b/github_linter/fixes/github_actions/__init__.py index a37c6cb3..9a59e322 100644 --- a/github_linter/fixes/github_actions/__init__.py +++ b/github_linter/fixes/github_actions/__init__.py @@ -30,7 +30,6 @@ def get_repo_default_workflow_permissions( # https://docs.github.com/en/rest/actions/permissions?apiVersion=2022-11-28#set-default-workflow-permissions-for-a-repository - # pylint: disable=protected-access resp = repo.repository3._get( f"https://api.github.com/repos/{repo.repository3.owner}/{repo.repository3.name}/actions/permissions/workflow", ) @@ -51,16 +50,13 @@ def set_repo_default_workflow_permissions( # https://docs.github.com/en/rest/actions/permissions?apiVersion=2022-11-28#set-default-workflow-permissions-for-a-repository if default_workflow_permissions not in VALID_DEFAULT_WORKFLOW_PERMISSIONS: - raise ValueError( - f"Invalid default_workflow_permissions: {default_workflow_permissions}. Valid values are: {VALID_DEFAULT_WORKFLOW_PERMISSIONS}" - ) + raise ValueError(f"Invalid default_workflow_permissions: {default_workflow_permissions}. Valid values are: {VALID_DEFAULT_WORKFLOW_PERMISSIONS}") payload = { "default_workflow_permissions": default_workflow_permissions, "can_approve_pull_request_reviews": can_approve_pull_request_reviews, } - # pylint: disable=protected-access res: Response = repo.repository3._put( f"https://api.github.com/repos/{repo.repository3.owner}/{repo.repository3.name}/actions/permissions/workflow", data=json.dumps(payload), diff --git a/github_linter/loaders.py b/github_linter/loaders.py index 8eecd890..0b219034 100644 --- a/github_linter/loaders.py +++ b/github_linter/loaders.py @@ -18,11 +18,9 @@ def load_yaml_file( if not fileresult: return {} try: - filecontents: Dict[Any, Any] = YAML(pure=True).load( - fileresult.decoded_content.decode("utf-8") - ) + filecontents: Dict[Any, Any] = YAML(pure=True).load(fileresult.decoded_content.decode("utf-8")) return filecontents - except Exception as error_message: # pylint: disable=broad-except + except Exception as error_message: logger.error("Failed to parse yaml file {}: {}", filename, error_message) # TODO: Catch a better exception in loaders.load_yaml_file return None diff --git a/github_linter/repolinter.py b/github_linter/repolinter.py index 9d203a5f..e061dfed 100644 --- a/github_linter/repolinter.py +++ b/github_linter/repolinter.py @@ -136,7 +136,6 @@ def cached_get_file(self, filepath: str, clear_cache: bool = False) -> Optional[ return None return self.filecache[filepath] - # pylint: disable=too-many-branches def create_or_update_file( self, filepath: str, diff --git a/github_linter/tests/__init__.py b/github_linter/tests/__init__.py index 28608b6a..c848c92e 100644 --- a/github_linter/tests/__init__.py +++ b/github_linter/tests/__init__.py @@ -17,7 +17,6 @@ homebrew, # noqa: F401 issues, # noqa: F401 mkdocs, # noqa: F401 - # pylintrc, pyproject, # noqa: F401 security_md, # noqa: F401 terraform, # noqa: F401 diff --git a/github_linter/tests/dependabot/__init__.py b/github_linter/tests/dependabot/__init__.py index c9921bd9..a406ace5 100644 --- a/github_linter/tests/dependabot/__init__.py +++ b/github_linter/tests/dependabot/__init__.py @@ -104,7 +104,6 @@ def generate_expected_update_config( return config_file -# pylint: disable=too-many-branches def check_updates_for_languages(repo: RepoLinter) -> None: """ensures that for every known language/package ecosystem, there's a configured update task""" diff --git a/github_linter/tests/dependabot/constants.py b/github_linter/tests/dependabot/constants.py index 0c488eb8..0baeac1e 100644 --- a/github_linter/tests/dependabot/constants.py +++ b/github_linter/tests/dependabot/constants.py @@ -1,4 +1,4 @@ -""" github_linter dependabot tests constants """ +"""github_linter dependabot tests constants""" from typing import Dict, List diff --git a/github_linter/tests/dependabot/types.py b/github_linter/tests/dependabot/types.py index 9f3eed8d..705a4060 100644 --- a/github_linter/tests/dependabot/types.py +++ b/github_linter/tests/dependabot/types.py @@ -1,4 +1,4 @@ -""" types for gihtub_linter dependabot tests """ +"""types for gihtub_linter dependabot tests""" from typing import Any, Dict, List, Optional, TypedDict, Union @@ -32,9 +32,7 @@ def validate_interval(cls, value: str) -> str: # TODO: write tests for this @pydantic.field_validator("timezone") - def validate_timezone( - cls, value: Optional[DoubleQuotedScalarString] - ) -> Optional[DoubleQuotedScalarString]: + def validate_timezone(cls, value: Optional[DoubleQuotedScalarString]) -> Optional[DoubleQuotedScalarString]: """validator""" if value not in pytz.all_timezones: raise ValueError(f"Invalid timezone: {value}") @@ -42,9 +40,7 @@ def validate_timezone( # TODO: write tests for this @pydantic.field_validator("time") - def validate_time( - cls, value: Optional[DoubleQuotedScalarString] - ) -> Optional[DoubleQuotedScalarString]: + def validate_time(cls, value: Optional[DoubleQuotedScalarString]) -> Optional[DoubleQuotedScalarString]: """validator""" if value is not None: return DoubleQuotedScalarString(value) @@ -122,9 +118,7 @@ class DependabotUpdateConfig(pydantic.BaseModel): None # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#allow ) assignees: Optional[List[str]] = None - commit_message: Optional[DependabotCommitMessage] = pydantic.Field( - None, alias="commit-message" - ) + commit_message: Optional[DependabotCommitMessage] = pydantic.Field(None, alias="commit-message") ignore: Optional[List[str]] = None insecure_external_code_execution: Optional[str] = pydantic.Field( alias="insecure-external-code-execution", @@ -136,20 +130,15 @@ class DependabotUpdateConfig(pydantic.BaseModel): alias="open-pull-requests-limit", default=None, ) - # noqa: E501 pylint: disable=line-too-long # TODO: this needs to be a thing - https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#pull-request-branch-nameseparator # pull-request-branch-name.separator - rebase_strategy: Optional[str] = pydantic.Field( - alias="rebase-strategy", default=None - ) + rebase_strategy: Optional[str] = pydantic.Field(alias="rebase-strategy", default=None) # TODO: registries typing for DependabotUpdateConfig registries: Optional[Any] = None reviewers: Optional[List[str]] = None target_branch: Optional[str] = pydantic.Field(alias="target-branch", default=None) vendor: Optional[bool] = None - versioning_strategy: Optional[str] = pydantic.Field( - alias="versioning-strategy", default=None - ) + versioning_strategy: Optional[str] = pydantic.Field(alias="versioning-strategy", default=None) # TODO: write tests for this @pydantic.field_validator("package_ecosystem") @@ -172,9 +161,7 @@ def validate_rebase_strategy(cls, value: str) -> str: def validate_execution_permissions(cls, value: str) -> str: """validates you're getting the right value""" if value not in ["deny", "allow"]: - raise ValueError( - "insecure-external-code-execution needs to be either 'allow' or 'deny'." - ) + raise ValueError("insecure-external-code-execution needs to be either 'allow' or 'deny'.") return value diff --git a/github_linter/tests/dependabot/utils.py b/github_linter/tests/dependabot/utils.py index 401a24fd..c6921bf0 100644 --- a/github_linter/tests/dependabot/utils.py +++ b/github_linter/tests/dependabot/utils.py @@ -53,7 +53,7 @@ def load_dependabot_config_file( for update in retval.updates: logger.debug("Package: {}", update.package_ecosystem) return retval - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: logger.error("Failed to parse dependabot config: {}", exc) repo.error(category, f"Failed to parse dependabot config: {exc}") return None diff --git a/github_linter/tests/docs.py b/github_linter/tests/docs.py index 3b9bf3b4..4f825e1e 100644 --- a/github_linter/tests/docs.py +++ b/github_linter/tests/docs.py @@ -67,10 +67,7 @@ def fix_contributing_exists(repo: RepoLinter) -> None: oldfile = repo.cached_get_file(filepath) - if ( - oldfile is not None - and oldfile.decoded_content.decode("utf-8") == new_filecontents - ): + if oldfile is not None and oldfile.decoded_content.decode("utf-8") == new_filecontents: logger.debug("Don't need to update {}", filepath) return diff --git a/github_linter/tests/generic.py b/github_linter/tests/generic.py index b2a7ee69..fb756bcc 100644 --- a/github_linter/tests/generic.py +++ b/github_linter/tests/generic.py @@ -92,7 +92,6 @@ def generate_funding_file(input_data: FundingDict) -> str: yaml.dump(output_data, outputio) outputio.seek(0) - # pylint: disable=line-too-long # doc_line = "# Documentation for this file format is here: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository" result = outputio.read() return result diff --git a/github_linter/tests/issues.py b/github_linter/tests/issues.py index 28e45568..c77899a8 100644 --- a/github_linter/tests/issues.py +++ b/github_linter/tests/issues.py @@ -25,7 +25,6 @@ class DefaultConfig(TypedDict): } -# pylint: disable=unused-argument def check_open_issues( repo: RepoLinter, ) -> None: @@ -37,7 +36,6 @@ def check_open_issues( ) -# pylint: disable=unused-argument def check_open_prs( repo: RepoLinter, ) -> None: diff --git a/github_linter/tests/mkdocs/__init__.py b/github_linter/tests/mkdocs/__init__.py index 67f61f8c..cc495eff 100644 --- a/github_linter/tests/mkdocs/__init__.py +++ b/github_linter/tests/mkdocs/__init__.py @@ -40,9 +40,7 @@ def needs_mkdocs_workflow(repo: RepoLinter) -> bool: def check_mkdocs_workflow_exists(repo: RepoLinter) -> None: """checks that the mkdocs github actions workflow exists""" if needs_mkdocs_workflow(repo): - if not repo.cached_get_file( - repo.config[CATEGORY]["workflow_filepath"], clear_cache=True - ): + if not repo.cached_get_file(repo.config[CATEGORY]["workflow_filepath"], clear_cache=True): repo.error(CATEGORY, "MKDocs github actions configuration missing.") # TODO: check if the file differs from expected. @@ -58,9 +56,7 @@ def fix_missing_mkdocs_workflow(repo: RepoLinter) -> None: newfile=get_fix_file_path(CATEGORY, "mkdocs.yml"), message="github-linter.mkdocs created MKDocs github actions configuration", ) - repo.fix( - CATEGORY, f"Created MKDocs github actions configuration: {commit_url}" - ) + repo.fix(CATEGORY, f"Created MKDocs github actions configuration: {commit_url}") else: fix_file = get_fix_file_path(CATEGORY, "mkdocs.yml") @@ -74,9 +70,7 @@ def fix_missing_mkdocs_workflow(repo: RepoLinter) -> None: oldfile=workflow_file, message="github-linter.mkdocs updated MKDocs github actions configuration", ) - repo.fix( - CATEGORY, f"Updated MKDocs github actions configuration: {commit_url}" - ) + repo.fix(CATEGORY, f"Updated MKDocs github actions configuration: {commit_url}") def generate_expected_config(repo: RepoLinter) -> Tuple[str, bytes]: @@ -127,14 +121,10 @@ def check_github_metadata(repo: RepoLinter) -> bool: current_file = repo.cached_get_file(current_filename) if current_file is None: - raise ValueError( - f"Somehow you got an empty file from {current_filename}, the file may have gone missing while the script was running?" - ) + raise ValueError(f"Somehow you got an empty file from {current_filename}, the file may have gone missing while the script was running?") if current_file.decoded_content == expected_config: - logger.debug( - "Config is up to date, no action required from check_github_metadata" - ) + logger.debug("Config is up to date, no action required from check_github_metadata") return True repo.error(CATEGORY, "mkdocs needs updating for github metadata") @@ -152,13 +142,9 @@ def fix_github_metadata(repo: RepoLinter) -> None: current_file = repo.cached_get_file(current_filename) if current_file is None or current_file.content is None: - raise ValueError( - f"Somehow you got an empty file from {current_filename}, the file may have gone missing while the script was running?" - ) + raise ValueError(f"Somehow you got an empty file from {current_filename}, the file may have gone missing while the script was running?") if current_file.decoded_content == expected_config: - logger.debug( - "Config is up to date, no action required from fix_github_metadata" - ) + logger.debug("Config is up to date, no action required from fix_github_metadata") raise NoChangeNeeded # generate a diff for debugging purposes diff --git a/github_linter/tests/pyproject.py b/github_linter/tests/pyproject.py index 8136185d..98e68738 100644 --- a/github_linter/tests/pyproject.py +++ b/github_linter/tests/pyproject.py @@ -29,8 +29,8 @@ DEFAULT_CONFIG: DefaultConfig = { # TODO: FIX THIS TO USE UV/HATCHLING "build-system": [ - # "flit_core.buildapi", # flit - # "poetry.core.masonry.api", # poetry + # "flit_core.buildapi", # flit + # "poetry.core.masonry.api", # poetry ], "readme": "README.md", } @@ -55,7 +55,6 @@ def validate_pyproject_authors( repo.warning(CATEGORY, f"Check author is expected: {author}") -# pylint: disable=unused-argument def validate_project_name( repo: RepoLinter, project_object: Dict[str, Any], diff --git a/github_linter/tests/terraform.py b/github_linter/tests/terraform.py index 196f9ac9..0832c719 100644 --- a/github_linter/tests/terraform.py +++ b/github_linter/tests/terraform.py @@ -104,7 +104,7 @@ def check_providers_for_modules( found_files.append(filename) - # # make a list of the providers + # make a list of the providers for provider in required_providers: for provider_name in provider: provider_list.append(provider_name) diff --git a/github_linter/utils/pages.py b/github_linter/utils/pages.py index 337d5f12..ae93cf3b 100644 --- a/github_linter/utils/pages.py +++ b/github_linter/utils/pages.py @@ -40,19 +40,17 @@ def get_repo_pages_data(repo: RepoLinter) -> PagesData: github = GithubLinter() github.do_login() url = f"/repos/{repo.repository.full_name}/pages" - # pylint: disable=protected-access - pagesdata = github.github._Github__requester.requestJson(verb="GET", url=url) # type: ignore + if hasattr(github.github, "_Github__requester") and github.github._Github__requester is not None: + pagesdata = github.github._Github__requester.requestJson(verb="GET", url=url) # type: ignore[possibly-missing-attribute] + else: + raise ValueError("Github object doesn't have a requester, can't get pages data.") if len(pagesdata) != 3: - raise ValueError( - f"Got {len(pagesdata)} from requesting the repo pages endpoint ({url})." - ) + raise ValueError(f"Got {len(pagesdata)} from requesting the repo pages endpoint ({url}).") pages: PagesData = json.loads(pagesdata[2]) if pages is None: - raise ValueError( - f"Invalid data returned from requesting the repo pages endpoint ({url})." - ) + raise ValueError(f"Invalid data returned from requesting the repo pages endpoint ({url}).") logger.debug( json.dumps( diff --git a/github_linter/web/__init__.py b/github_linter/web/__init__.py index cfd60a10..f3e1f947 100644 --- a/github_linter/web/__init__.py +++ b/github_linter/web/__init__.py @@ -1,11 +1,13 @@ """web interface for the project that outgrew its intention""" +from contextlib import asynccontextmanager from pathlib import Path from time import time from typing import Any, AsyncGenerator, Generator, List, Optional, Union, Tuple from fastapi import BackgroundTasks, FastAPI, Depends, HTTPException from fastapi.responses import HTMLResponse, FileResponse, Response +from fastapi.middleware.gzip import GZipMiddleware from github.Repository import Repository from jinja2 import Environment, PackageLoader, select_autoescape from loguru import logger @@ -33,10 +35,33 @@ async_session_maker = async_sessionmaker(engine, expire_on_commit=False) Base = declarative_base() -app = FastAPI() + +async def create_db() -> None: + """do the initial DB creation""" + logger.info("Synchronising database on startup...") + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + logger.info("Done!") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Handle app startup and shutdown""" + # Startup + try: + await create_db() + except Exception as error_message: + logger.critical(f"Failed to create DB, shutting down: {error_message}") + raise + yield + # Shutdown + await engine.dispose() + + +app = FastAPI(lifespan=lifespan) +app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=9) # type: ignore[argument-type] -# pylint: disable=too-few-public-methods class SQLRepos(Base): """sqlrepos""" @@ -56,7 +81,6 @@ class SQLRepos(Base): parent = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) -# pylint: disable=too-few-public-methods class SQLMetadata(Base): """metadata""" @@ -73,13 +97,6 @@ class MetaData(BaseModel): model_config = ConfigDict(from_attributes=True) -async def create_db() -> None: - """do the initial DB creation""" - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - # logger.info("Result of creating DB: {}", result) - - async def get_async_session() -> AsyncGenerator[AsyncSession, None]: """session factory""" async with async_session_maker() as session: @@ -105,6 +122,19 @@ class RepoData(BaseModel): model_config = ConfigDict(from_attributes=True) +class RepoDataSimple(BaseModel): + """because the full data is chonky, cuts it by 50%""" + + full_name: str + archived: bool + fork: bool + open_issues: int + open_prs: int + last_updated: float + + model_config = ConfigDict(from_attributes=True, extra="ignore") + + def githublinter_factory() -> Generator[GithubLinter, None, None]: """githublinter factory""" githublinter = GithubLinter() @@ -216,6 +246,15 @@ async def favicon() -> Union[Response, FileResponse]: return Response(status_code=404) +@app.get("/images/{filename}", response_model=None) +async def images(filename: str) -> Union[Response, FileResponse]: + """return an image""" + icon_file = Path(Path(__file__).resolve().parent.as_posix() + f"/images/{filename}") + if icon_file.exists(): + return FileResponse(icon_file) + return Response(status_code=404) + + @app.get("/css/{filename:str}", response_model=None) async def css_file(filename: str) -> Union[Response, FileResponse]: """css returner""" @@ -263,13 +302,11 @@ async def db_update_running() -> bool: ) await set_db_update_running(False) return False - # pylint: disable=broad-except except Exception as error_message: logger.warning(f"Failed to pull update_running: {error_message}") try: await set_db_update_running(False) logger.success("Set it to False instead") - # pylint: disable=broad-except except Exception as error: logger.error( "Tried to set update_running to False but THAT went wrong too! {}", @@ -320,14 +357,12 @@ async def db_updated() -> int: data = MetaData.model_validate(row) if "." in data.value: return int(data.value.split(".")[0]) - # pylint: disable=broad-except except Exception as error_message: logger.warning(f"Failed to pull last_updated: {error_message}") try: await set_update_time(-1, conn) await conn.commit() logger.success("Set it to -1 instead") - # pylint: disable=broad-except except Exception as error: logger.error("Tried to set it to -1 but THAT went wrong too! {}", error) return -1 @@ -371,13 +406,13 @@ async def get_health( @app.get("/repos") async def get_repos( session: AsyncSession = Depends(get_async_session), -) -> List[RepoData]: +) -> List[RepoDataSimple]: """endpoint to provide the cached repo list""" try: stmt = sqlalchemy.select(SQLRepos) result = await session.execute(stmt) - retval = [RepoData.model_validate(element.SQLRepos) for element in result.fetchall()] + retval = [RepoDataSimple.model_validate(element.SQLRepos) for element in result.fetchall()] except OperationalError as operational_error: logger.warning("Failed to pull repos from DB: {}", operational_error) return [] @@ -389,10 +424,6 @@ async def root( background_tasks: BackgroundTasks, ) -> Union[Response, HTMLResponse]: """homepage""" - logger.info("Creating background task to create DB.") - background_tasks.add_task( - create_db, - ) env = Environment( loader=PackageLoader( package_name="github_linter.web.templates", @@ -400,6 +431,6 @@ async def root( ), autoescape=select_autoescape(), ) - template = env.get_template("index.html") + template = env.get_template("index.vue") return HTMLResponse(template.render()) diff --git a/github_linter/web/css/github-linter.css b/github_linter/web/css/github-linter.css new file mode 100644 index 00000000..0fe8b6c1 --- /dev/null +++ b/github_linter/web/css/github-linter.css @@ -0,0 +1,30 @@ +[data-theme="light"], +:root:not([data-theme="dark"]) { + --primary: #8e24aa; + --primary-hover: #5f1972; +} + +main { + margin-top: 0.8rem; +} +h1 { + margin-bottom: 1rem; +} + +.buttonbar { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; + justify-content: space-between; +} + +[role="button"] { + width: 20vw; +} + +.open { + background-image: url("/images/open-black.svg"); + width: 1rem; + height: 1rem; + background-repeat: no-repeat; +} diff --git a/github_linter/web/github_linter.js b/github_linter/web/github_linter.js index 14c7fc1d..9f25bec4 100644 --- a/github_linter/web/github_linter.js +++ b/github_linter/web/github_linter.js @@ -1,120 +1,116 @@ // axios get from here: https://reactgo.com/vue-fetch-data/ -// Vue.component('repo-item', { -// props: ['repo'], -// template: '{{ repo.full_name }}{{repo.archived }}' -// }) - const _repo_app = new Vue({ - delimiters: ["|", "|"], // because we're using it alongside jinja2 - el: "#repos", - data: { - repos: [{ full_name: "Loading" }], - repo_filter: "", - hide_archived: false, - show_has_issues: true, - show_has_prs: false, - last_updated: null, - waiting_for_update: false, // used when waiting for repos to update - // total_issues: -1, - }, - created() { - this.updateRepos(); - this.getLastUpdated(); - this.timer = setInterval(this.getLastUpdated, 5000); - this.timer = setInterval(this.updateRepos, 5000); - this.timer = setInterval(this.checkIsWaiting, 1000); - }, - computed: { - filteredRows() { - return this.repos.filter((repo) => { - const full_name = repo.full_name.toString().toLowerCase(); - let owner = ""; - if (typeof repo.owner !== "undefined" && repo.owner != null) { - owner = repo.owner.toString().toLowerCase(); - } - // const department = repo.department.toLowerCase(); - const searchTerm = this.repo_filter.toLowerCase(); - let result = - full_name.includes(searchTerm) || owner.includes(searchTerm); + delimiters: ["|", "|"], // because we're using it alongside jinja2 + el: "#repos", + data: { + repos: [{ full_name: "Loading" }], + repo_filter: "", + hide_archived: true, + show_has_issues: true, + show_has_prs: true, + last_updated: null, + waiting_for_update: false, // used when waiting for repos to update + }, + created() { + this.updateRepos(); + this.getLastUpdated(); + this.timers = { + lastUpdated: setInterval(this.getLastUpdated, 5000), + updateRepos: setInterval(this.updateRepos, 5000), + checkWaiting: setInterval(this.checkIsWaiting, 1500), + }; + }, + computed: { + filteredRows() { + return this.repos.filter((repo) => { + const full_name = repo.full_name.toString().toLowerCase(); + let owner = ""; + if (typeof repo.owner !== "undefined" && repo.owner != null) { + owner = repo.owner.toString().toLowerCase(); + } + // const department = repo.department.toLowerCase(); + const searchTerm = this.repo_filter.toLowerCase(); + let result = + full_name.includes(searchTerm) || owner.includes(searchTerm); - // filter for show_has_issues - if (this.show_has_issues && result) { - if (repo.open_issues === 0) { - result = false; - // console.log("hiding " + repo.full_name + " because it doesn't have issues"); - } - } - // filter for show_has_prs - if (this.show_has_prs && result) { - if (repo.open_prs === 0) { - result = false; - } else { - result = true; - } - } - // filter for hide_archived - if (this.hide_archived && result) { - if (repo.archived === true) { - // console.log("skipping archived" + repo.full_name); - result = false; - } - } + // filter for show_has_issues + if (this.show_has_issues && result) { + if (repo.open_issues === 0) { + result = false; + // console.log("hiding " + repo.full_name + " because it doesn't have issues"); + } + } + // filter for show_has_prs + if (this.show_has_prs && result) { + if (repo.open_prs === 0) { + result = false; + } else { + result = true; + } + } + // filter for hide_archived + if (this.hide_archived && result) { + if (repo.archived === true) { + // console.log("skipping archived" + repo.full_name); + result = false; + } + } - return result; - }); - }, - totalRepos() { - // this calculates the current view's number of total open issues - return this.filteredRows.length; - }, - totalFilteredOpenIssues() { - // this calculates the current view's number of total open issues - tmp_count = 0; - for (const issue of this.filteredRows) { - tmp_count += issue.open_issues; - // console.log("issues: "+issue.open_issues) - } - return tmp_count; - }, - totalFilteredPRs() { - // this calculates the current view's number of total open issues - tmp_count = 0; - for (const issue of this.filteredRows) { - tmp_count += issue.open_prs; - } - return tmp_count; - }, - }, - methods: { - updateRepos: function () { - const headers = { crossDomain: true }; - axios.get("/repos", headers).then((res) => { - this.repos = res.data; - }); - // TODO: maybe block the update repos button for 30 seconds? - }, - getLastUpdated: function () { - // get the "last updated" data - axios.get("/db/updated").then((res) => { - if (res.data > this.last_updated) { - this.waiting_for_update = false; - this.last_updated = res.data; - } else { - console.log( - `result: ${res.data} less than or equal to ${this.last_updated}`, - ); - } - }); - }, - updateReposBackend: function () { - this.waiting_for_update = true; - axios.get("/repos/update"); - }, - checkIsWaiting: function () { - axios.get("/db/updating").then((res) => { - this.waiting_for_update = res.data; - }); - }, - }, + return result; + }); + }, + totalRepos() { + // this calculates the current view's number of total open issues + return this.filteredRows.length; + }, + totalFilteredOpenIssues() { + // this calculates the current view's number of total open issues + tmp_count = 0; + for (const issue of this.filteredRows) { + tmp_count += issue.open_issues; + // console.log("issues: "+issue.open_issues) + } + return tmp_count; + }, + totalFilteredPRs() { + // this calculates the current view's number of total open issues + tmp_count = 0; + for (const issue of this.filteredRows) { + tmp_count += issue.open_prs; + } + return tmp_count; + }, + }, + methods: { + updateRepos: function () { + const headers = { crossDomain: true }; + axios.get("/repos", headers).then((res) => { + this.repos = res.data; + }); + // TODO: maybe block the update repos button for 30 seconds? + }, + getLastUpdated: function () { + // get the "last updated" data + axios.get("/db/updated").then((res) => { + if (res.data > this.last_updated) { + this.waiting_for_update = false; + this.last_updated = res.data; + } else { + console.log( + `result: ${res.data} less than or equal to ${this.last_updated}`, + ); + } + }); + }, + updateReposBackend: function () { + this.waiting_for_update = true; + axios.get("/repos/update"); + }, + checkIsWaiting: function () { + axios.get("/db/updating").then((res) => { + this.waiting_for_update = res.data; + }); + }, + }, }); diff --git a/github_linter/web/images/open-black.svg b/github_linter/web/images/open-black.svg new file mode 100644 index 00000000..43ab5c91 --- /dev/null +++ b/github_linter/web/images/open-black.svg @@ -0,0 +1,11 @@ + + + ic_fluent_open_24_filled + + + + + + + + \ No newline at end of file diff --git a/github_linter/web/images/open-white.svg b/github_linter/web/images/open-white.svg new file mode 100644 index 00000000..8fd86071 --- /dev/null +++ b/github_linter/web/images/open-white.svg @@ -0,0 +1,11 @@ + + + ic_fluent_open_24_filled + + + + + + + + \ No newline at end of file diff --git a/github_linter/web/templates/basetemplate.html b/github_linter/web/templates/basetemplate.html index 7613a215..ab0e0255 100644 --- a/github_linter/web/templates/basetemplate.html +++ b/github_linter/web/templates/basetemplate.html @@ -1,32 +1,24 @@ - - Github Linter + + Github Linter + - - - - -
-
- {% block content %} - {% endblock content %} -
- - - - - - - - + +
+
+ {% block content %} + {% endblock content %} +
+ + + + +
+ + \ No newline at end of file diff --git a/github_linter/web/templates/index.html b/github_linter/web/templates/index.vue similarity index 56% rename from github_linter/web/templates/index.html rename to github_linter/web/templates/index.vue index 93f0ac6a..6319b811 100644 --- a/github_linter/web/templates/index.html +++ b/github_linter/web/templates/index.vue @@ -7,16 +7,20 @@

Repositories (|totalRepos|)

-Update Repositories + + - + @@ -24,15 +28,18 @@

Repositories (|totalRepos|)

- - - + + diff --git a/justfile b/justfile new file mode 100644 index 00000000..9fa3997d --- /dev/null +++ b/justfile @@ -0,0 +1,41 @@ + + + +[private] +default: + just --list + +check: format lint jslint + just test -m 'not\ network' + +set positional-arguments + +test *args='': + uv run pytest {{ args }} + +lint: + uv run ty check + +format: + uv run ruff format tests github_linter + uv run ruff check tests github_linter + +jslint: + biome check --verbose + +docker_build: + docker build -t 'ghcr.io/yaleman/github_linter:latest' \ + . + +docker_run: + docker run --rm -it \ + -p 8000:8000 \ + --env-file .envrc -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ + 'ghcr.io/yaleman/github_linter:latest' \ + python -m github_linter.web + +workflow_stats: + docker run --rm -it \ + --env-file .envrc -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ + 'ghcr.io/yaleman/github_linter:latest' \ + python -m github_linter.workflow_stats -f yaleman/pygoodwe \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5645a0e9..cceddd06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,25 +37,16 @@ dependencies = [ Home = "https://github.com/yaleman/github_linter" [project.optional-dependencies] -dev = ["flit", "pytest", "pylint", "mypy", "types-PyYAML", "types-pytz"] +dev = ["flit", "pytest", "mypy", "types-PyYAML", "types-pytz"] [project.scripts] "github-linter" = 'github_linter.__main__:cli' "github-linter-web" = 'github_linter.web.__main__:cli' -[tool.pylint.MASTER] -max-line-length = 200 -disable = "W0511,consider-using-dict-items,duplicate-code" -# https://github.com/samuelcolvin/pydantic/issues/1961#issuecomment-759522422 -extension-pkg-whitelist = "pydantic" -load-plugins = "pylint_pydantic" [tool.ruff] line-length = 200 exclude = ["W0511", "consider-using-dict-items", "duplicate-code"] -# https://github.com/samuelcolvin/pydantic/issues/1961#issuecomment-759522422 -# extension-pkg-whitelist="pydantic" -# load-plugins="pylint_pydantic" [tool.pytest.ini_options] markers = ["network: things which need authentication, '-m \"not network\"'"] diff --git a/run_web.sh b/run_web.sh index 2cd2908d..2418c319 100755 --- a/run_web.sh +++ b/run_web.sh @@ -1,3 +1,6 @@ #!/bin/bash -LOG_LEVEL=DEBUG uvicorn github_linter.web:app --reload-dir ./github_linter --debug +LOG_LEVEL=DEBUG \ + uvicorn \ + github_linter.web:app \ + --reload-dir ./github_linter diff --git a/tests/test_branch_protection.py b/tests/test_branch_protection.py index 434bf3d3..3b2da206 100644 --- a/tests/test_branch_protection.py +++ b/tests/test_branch_protection.py @@ -14,7 +14,7 @@ def create_mock_workflow_file(name: str, content: str) -> Mock: """Create a mock workflow file ContentFile object""" mock_file = Mock() mock_file.name = name - mock_file.decoded_content = content.encode('utf-8') + mock_file.decoded_content = content.encode("utf-8") return mock_file @@ -47,9 +47,7 @@ def test_get_available_checks_with_simple_workflow() -> None: mock_repo.repository = Mock() # Mock get_contents to return our test workflow - workflow_files = [ - create_mock_workflow_file("test.yml", workflow_content) - ] + workflow_files = [create_mock_workflow_file("test.yml", workflow_content)] mock_repo.repository.get_contents.return_value = workflow_files # Call the function @@ -119,11 +117,8 @@ def test_get_available_checks_with_no_workflows() -> None: # Mock get_contents to raise UnknownObjectException from github.GithubException import UnknownObjectException - mock_repo.repository.get_contents.side_effect = UnknownObjectException( - status=404, - data={"message": "Not Found"}, - headers={} - ) + + mock_repo.repository.get_contents.side_effect = UnknownObjectException(status=404, data={"message": "Not Found"}, headers={}) # Call the function available_checks = _get_available_checks_for_repo(mock_repo) diff --git a/tests/test_dependabot.py b/tests/test_dependabot.py index 8abe93e6..2fbb7c43 100644 --- a/tests/test_dependabot.py +++ b/tests/test_dependabot.py @@ -1,4 +1,4 @@ -""" testing dependabot """ +"""testing dependabot""" # from github_linter.tests.dependabot import load_file diff --git a/tests/test_generic.py b/tests/test_generic.py index 082a188b..77fef92a 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -1,4 +1,4 @@ -""" testing pyproject """ +"""testing pyproject""" from utils import generate_test_repo @@ -49,9 +49,7 @@ def test_generate_funding_file() -> None: def test_generate_funding_file_simple_with_quote() -> None: """tests generator""" - result = parse_funding_file( - "# test funding file\ngithub: yaleman" - ) # need to put the leading newline to make it not be a list... because YAML? + result = parse_funding_file("# test funding file\ngithub: yaleman") # need to put the leading newline to make it not be a list... because YAML? test_parse_funding_file() output = generate_funding_file(result) assert output == "github: yaleman\n" @@ -59,9 +57,7 @@ def test_generate_funding_file_simple_with_quote() -> None: def test_generate_funding_file_simple() -> None: """tests generator""" - result: FundingDict = parse_funding_file( - " github: yaleman" - ) # need to put the leading newline to make it not be a list... because YAML? + result: FundingDict = parse_funding_file(" github: yaleman") # need to put the leading newline to make it not be a list... because YAML? test_parse_funding_file() output = generate_funding_file(result) assert output == "github: yaleman\n" diff --git a/tests/test_github_data.py b/tests/test_github_data.py new file mode 100644 index 00000000..935deedb --- /dev/null +++ b/tests/test_github_data.py @@ -0,0 +1,24 @@ +import pytest + + +from github_linter import GithubLinter +from github_linter.web import get_all_user_repos + + +@pytest.mark.network +def test_get_all_user_repos() -> None: + """tests what we get back from it, can be slow and burn things""" + linter = GithubLinter() + linter.do_login() + config = { + "linter": { + "owner_list": [ + "TerminalOutcomes", + ] + } + } + result = get_all_user_repos(linter, config) + + for repo in result: + print(repo) + print(f"Found {len(result)} repositories") diff --git a/tests/test_hcl2.py b/tests/test_hcl2.py index d60bdd03..0df895b3 100644 --- a/tests/test_hcl2.py +++ b/tests/test_hcl2.py @@ -1,5 +1,4 @@ -""" tests things interacting with HCL-format files (terraform, hopefully)""" - +"""tests things interacting with HCL-format files (terraform, hopefully)""" import hcl2.api diff --git a/tests/test_pyproject.py b/tests/test_pyproject.py index 3698cde7..681cec22 100644 --- a/tests/test_pyproject.py +++ b/tests/test_pyproject.py @@ -1,4 +1,4 @@ -""" testing pyproject """ +"""testing pyproject""" # from io import BytesIO @@ -6,7 +6,6 @@ # from github_linter.tests.pyproject import validate_project_name, validate_readme_configured -# pylint: disable=too-few-public-methods # class TestRepoFoo: # """ just for testing """ # name = "foobar" @@ -19,7 +18,6 @@ # return readme # return BytesIO() -# pylint: disable=too-few-public-methods # class TestGithub: # """ test instance """ # config = { diff --git a/tests/test_repo_search.py b/tests/test_repo_search.py index b4c49748..25b97347 100644 --- a/tests/test_repo_search.py +++ b/tests/test_repo_search.py @@ -1,4 +1,4 @@ -""" testing the search filter generator """ +"""testing the search filter generator""" from utils import generate_test_repo from github_linter import filter_by_repo, generate_repo_search_string @@ -44,10 +44,5 @@ def test_generate_repo_search_string() -> None: owner_filter = ["yaleman", "terminaloutcomes "] repo_filter = ["github_linter", "cheese"] - result = generate_repo_search_string( - repo_filter=repo_filter, owner_filter=owner_filter - ) - assert ( - result.search_string - == "repo:yaleman/github_linter repo:yaleman/cheese repo:terminaloutcomes/github_linter repo:terminaloutcomes/cheese" - ) + result = generate_repo_search_string(repo_filter=repo_filter, owner_filter=owner_filter) + assert result.search_string == "repo:yaleman/github_linter repo:yaleman/cheese repo:terminaloutcomes/github_linter repo:terminaloutcomes/cheese" diff --git a/tests/test_web.py b/tests/test_web.py index 1071afc9..c7be2fb4 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,10 +1,8 @@ """test the web interface a bit""" -import pytest from fastapi.testclient import TestClient -from github_linter import GithubLinter -from github_linter.web import app, get_all_user_repos +from github_linter.web import app client = TestClient(app) @@ -15,22 +13,3 @@ def test_read_main() -> None: response = client.get("/") assert response.status_code == 200 assert b"Github Linter" in response.content - - -@pytest.mark.network -def test_get_all_user_repos() -> None: - """tests what we get back from it""" - linter = GithubLinter() - linter.do_login() - config = { - "linter": { - "owner_list": [ - "yaleman", - ] - } - } - result = get_all_user_repos(linter, config) - - for repo in result: - print(repo) - print(f"Found {len(result)} repositories") diff --git a/uv.lock b/uv.lock index 85ed5863..7b6d9c6f 100644 --- a/uv.lock +++ b/uv.lock @@ -42,15 +42,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[package]] -name = "astroid" -version = "4.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/c17d0f83016532a1ad87d1de96837164c99d47a3b6bbba28bd597c25b37a/astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3", size = 406224, upload-time = "2026-01-03T22:14:26.096Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/66/686ac4fc6ef48f5bacde625adac698f41d5316a9753c2b20bb0931c9d4e2/astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14", size = 276443, upload-time = "2026-01-03T22:14:24.412Z" }, -] - [[package]] name = "bandit" version = "1.9.2" @@ -337,15 +328,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, ] -[[package]] -name = "dill" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, -] - [[package]] name = "distro" version = "1.9.0" @@ -436,7 +418,6 @@ dependencies = [ dev = [ { name = "flit" }, { name = "mypy" }, - { name = "pylint" }, { name = "pytest" }, { name = "types-pytz" }, { name = "types-pyyaml" }, @@ -470,7 +451,6 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'" }, { name = "pydantic", specifier = ">=2.10.5" }, { name = "pygithub", specifier = ">=2.5.0" }, - { name = "pylint", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "python-hcl2", specifier = ">=5.1.1" }, { name = "pytz", specifier = ">=2024.2" }, @@ -611,15 +591,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "isort" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -790,15 +761,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -877,15 +839,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/00/5ac7aa77688ec4d34148b423d34dc0c9bc4febe0d872a9a1ad9860b2f6f1/pip-26.0-py3-none-any.whl", hash = "sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754", size = 1787564, upload-time = "2026-01-31T01:40:52.252Z" }, ] -[[package]] -name = "platformdirs" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -1029,24 +982,6 @@ crypto = [ { name = "cryptography" }, ] -[[package]] -name = "pylint" -version = "4.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "astroid" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "dill" }, - { name = "isort" }, - { name = "mccabe" }, - { name = "platformdirs" }, - { name = "tomlkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425, upload-time = "2025-11-30T13:29:02.53Z" }, -] - [[package]] name = "pynacl" version = "1.6.2" @@ -1471,15 +1406,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] -[[package]] -name = "tomlkit" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, -] - [[package]] name = "ty" version = "0.0.12"
Name  Archived Open Issues (| totalFilteredOpenIssues |) PRs (| totalFilteredPRs |)
| repo.full_name | (P)Open in github🪦 + + + + 🪦 - |repo.open_issues| + |repo.open_issues| - |repo.open_prs| + |repo.open_prs|