diff --git a/.cit-quality/README.md b/.cit-quality/README.md new file mode 100644 index 00000000..5f3b6cdb --- /dev/null +++ b/.cit-quality/README.md @@ -0,0 +1,59 @@ +# CIT quality tooling + +This directory contains vendored contributor tooling, separate from project +source. It is maintained and tested in `cloud-innovation-team/cit-codex-skills`. +Executing the local copy requires no GitHub authentication or source-repository +access. + +## Provenance + +- Installed file: `.cit-quality/vendor/check_nfc_filenames.py`. +- Upstream file: + `skills/cit-repository-quality-scaffolding/scripts/check_nfc_filenames.py`. +- Source revision: `2dbf02d13b73799d2fcf1d3c7bab0a8371fd82ee`. +- Source SHA-256: + `83d3bb79ebf3a5d6cb4f213f3af4421bd258693057514637ce87f3fc00a79056`. +- Upstream suite: + `skills/cit-repository-quality-scaffolding/scripts/test_scaffolding.py`. + +The upstream behavioral suite tests rejection of non-NFC tracked filenames, +acceptance after a byte-preserving rename, and normalization collisions. It also +exercises the installed copy through the generated pre-commit hook. This +provenance identifies the source; rendering alone does not establish a passing +test run. Record the source revision and actual upstream validation in the PR. + +## Upstream validation + +On 2026-10-07, three focused upstream tests passed: + +- `test_vendored_checker_is_separate_from_project_source` (includes the + installed hook). +- `test_normalization_collision_is_reported_without_renaming`. +- `test_non_nfc_tracked_name_fails_hook_until_byte_preserving_rename`. + +Command: +`uv run --script /scripts/test_scaffolding.py -v -n 2 -k "vendored_checker_is_separate or normalization_collision_is_reported or non_nfc_tracked_name_fails_hook_until_byte_preserving_rename" --durations=5`. + +Result: `3 passed` on Python 3.13.11; rerun during this refresh. This is focused +validation, not a claim that the entire upstream suite or all checker failure +paths were exercised. + +## Review and updates + +Review source provenance, updates, and the repository's hook integration. +Implementation tests are maintained upstream; duplicate project tests are not +required for an unchanged, upstream-tested copy. Local behavior changes require +normal implementation review and appropriate regression coverage. + +Update the checker and this provenance note together from the scaffolding skill. +Preserve the copied bytes; contribute fixes upstream instead of editing the +vendored copy. Keep this directory out of project source, test discovery, +coverage, and distribution inputs where those tools would otherwise traverse it. +The `linguist-vendored` attribute classifies the code; it does not disable +GitHub review requirements. + +Run the installed hook from the repository root: + +```shell +uvx --from pre-commit==4.6.2 pre-commit run check-nfc-filenames --all-files +``` diff --git a/.cit-quality/vendor/check_nfc_filenames.py b/.cit-quality/vendor/check_nfc_filenames.py new file mode 100644 index 00000000..1a17d33d --- /dev/null +++ b/.cit-quality/vendor/check_nfc_filenames.py @@ -0,0 +1,87 @@ +# /// script +# requires-python = ">=3.11" +# /// +"""Reject tracked Git paths that are not UTF-8 NFC, including collisions.""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +import unicodedata +from pathlib import Path +from typing import Any + + +def tracked_filename_report(root: Path) -> dict[str, Any]: + """Inspect raw index paths, not filesystem names or quoted Git status text.""" + process = subprocess.run( + ["git", "ls-files", "--cached", "-z"], + cwd=root, + check=False, + capture_output=True, + ) + if process.returncode != 0: + return { + "checked": False, + "error": process.stderr.decode("utf-8", errors="replace").strip(), + "issues": [], + "collisions": [], + } + + paths: list[str] = [] + invalid_utf8: list[str] = [] + for raw_path in process.stdout.split(b"\0"): + if not raw_path: + continue + try: + paths.append(raw_path.decode("utf-8")) + except UnicodeDecodeError: + invalid_utf8.append(raw_path.decode("utf-8", errors="backslashreplace")) + + by_nfc: dict[str, list[str]] = {} + for path in set(paths): + by_nfc.setdefault(unicodedata.normalize("NFC", path), []).append(path) + collisions = [ + {"nfc_path": normalized, "paths": sorted(originals)} + for normalized, originals in sorted(by_nfc.items()) + if len(originals) > 1 + ] + issues = [ + { + "path": path, + "nfc_path": normalized, + "collides_with": sorted(other for other in originals if other != path), + } + for normalized, originals in sorted(by_nfc.items()) + for path in sorted(originals) + if path != normalized + ] + return { + "checked": True, + "issues": issues, + "collisions": collisions, + "invalid_utf8": sorted(invalid_utf8), + } + + +def main() -> int: + """Print actionable failures for a local pre-commit hook.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", type=Path, default=Path.cwd()) + args = parser.parse_args() + report = tracked_filename_report(args.repo) + if not report["checked"]: + print(f"Cannot inspect tracked filenames: {report['error']}", file=sys.stderr) + return 2 + for issue in report["issues"]: + print(f"Non-NFC tracked path: {issue['path']!r} -> {issue['nfc_path']!r}") + if issue["collides_with"]: + print(f" Normalization collision with: {issue['collides_with']!r}") + for path in report["invalid_utf8"]: + print(f"Tracked path is not UTF-8: {path!r}") + return 1 if report["issues"] or report["invalid_utf8"] else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.editorconfig b/.editorconfig index b780dfd7..8fb2983d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,11 +3,14 @@ root = true [*] charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true insert_final_newline = true indent_style = space indent_size = 2 [*.md] +trim_trailing_whitespace = false indent_size = 2 max_line_length = 80 diff --git a/.editorconfig-checker.json b/.editorconfig-checker.json index 684cbc35..baefa2ef 100644 --- a/.editorconfig-checker.json +++ b/.editorconfig-checker.json @@ -1,5 +1,5 @@ { - "Version": "v3.4.0", + "Version": "v3.11.1", "Verbose": false, "Debug": false, "IgnoreDefaults": false, diff --git a/.gitattributes b/.gitattributes index adc0809a..6c7ffa50 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,7 @@ +# BEGIN source: https://github.com/gitattributes/gitattributes/blob/2c20a14833a5ab196c7fc3effcc72ef4e895f4d4/Common.gitattributes # Common settings that generally should always be used with your language specific settings # Auto detect text files and perform LF normalization -# https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ * text=auto # @@ -10,26 +10,28 @@ # Documents *.bibtex text diff=bibtex -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain -*.md text +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain +*.md text diff=markdown +*.mdx text diff=markdown *.tex text diff=tex *.adoc text *.textile text *.mustache text -*.csv text +*.csv text eol=crlf *.tab text *.tsv text *.txt text *.sql text +*.epub diff=astextplain # Graphics *.png binary @@ -39,7 +41,7 @@ *.tif binary *.tiff binary *.ico binary -# SVG treated as an asset (binary) by default. +# SVG treated as text by default. *.svg text # If you want to treat it as binary, # use the following line instead. @@ -49,7 +51,9 @@ # Scripts *.bash text eol=lf *.fish text eol=lf +*.ksh text eol=lf *.sh text eol=lf +*.zsh text eol=lf # These are explicitly windows files and should use crlf *.bat text eol=crlf *.cmd text eol=crlf @@ -64,10 +68,24 @@ # Archives *.7z binary +*.bz binary +*.bz2 binary +*.bzip2 binary *.gz binary +*.lz binary +*.lzma binary +*.rar binary *.tar binary +*.taz binary +*.tbz binary +*.tbz2 binary *.tgz binary +*.tlz binary +*.txz binary +*.xz binary +*.Z binary *.zip binary +*.zst binary # Text files where line endings should be preserved *.patch -text @@ -78,6 +96,9 @@ .gitattributes export-ignore .gitignore export-ignore +.gitkeep export-ignore +# END source: https://github.com/gitattributes/gitattributes/blob/2c20a14833a5ab196c7fc3effcc72ef4e895f4d4/Common.gitattributes + # Basic .gitattributes for a python repo. # Source files @@ -106,3 +127,13 @@ # with the python modules ``pickle``, ``dbm.*``, # ``shelve``, ``marshal``, ``anydbm``, & ``bsddb`` # (among others). + +# Service inputs must survive checkout byte-for-byte, including LF-only CSVs. +/tests/resources/** -text !eol +/examples/resources/** -text !eol +# Resource helpers are maintained Python, not immutable test data. +/tests/resources/**/*.py text eol=lf +/tests/resources/**/*.pyi text eol=lf + +# CIT quality tooling is vendored, not project source. +/.cit-quality/vendor/** linguist-vendored diff --git a/.github/workflows/basedpyright.yml b/.github/workflows/basedpyright.yml deleted file mode 100644 index 6fbd60b2..00000000 --- a/.github/workflows/basedpyright.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: basedpyright - -on: - pull_request: - push: - branches: - - main - - develop - - feature-* - -jobs: - basedpyright: - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - packages: write - pull-requests: write - steps: - - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: 0.8.22 - python-version: 3.11 - enable-cache: true - cache-suffix: pre-commit - cache-dependency-glob: uv.lock - - name: Restore venv cache - uses: actions/cache@v4 - with: - path: | - .venv - key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }} - - name: Synchronize project dependencies - run: uv sync --group dev - - name: Run basedpyright - run: uv run basedpyright diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 50c88c8a..00000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Docs - -on: - push: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: pages - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v5 - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: 0.9.18 - python-version: "3.11" - enable-cache: true - cache-suffix: docs - cache-dependency-glob: uv.lock - - name: Synchronize project dependencies - run: uv sync --group dev - - name: Build documentation - run: uv run mkdocs build --strict - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v3 - with: - path: site - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - needs: build - runs-on: ubuntu-latest - steps: - - id: deployment - name: Deploy to GitHub Pages - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index d73dcfd3..0546f8a6 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -8,38 +8,33 @@ on: - develop - feature-* +permissions: + contents: read + jobs: pre-commit: runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - packages: write - pull-requests: write + timeout-minutes: 20 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: - version: 0.8.22 + version: 0.12.18 python-version: 3.11 enable-cache: true cache-suffix: pre-commit cache-dependency-glob: uv.lock - - name: Restore venv cache - uses: actions/cache@v4 - with: - path: | - .venv - key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }} - name: Restore pre-commit cache - uses: actions/cache@v4 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - path: ~/.cache/pre-commit - key: pre-commit-4|${{ hashFiles('.pre-commit-config.yaml') }} + path: ${{ runner.temp }}/pre-commit-cache + key: pre-commit-${{ runner.os }}-${{ runner.arch }}-py3.11-uv0.12.18-pre-commit4.6.2-${{ hashFiles('uv.lock', '.pre-commit-config.yaml') }} + restore-keys: | + pre-commit-${{ runner.os }}-${{ runner.arch }}-py3.11-uv0.12.18-pre-commit4.6.2- - name: Synchronize project dependencies - run: uv sync --group dev + run: uv sync --frozen --group dev - name: Run pre-commit checks - run: uv run pre-commit run --show-diff-on-failure --all-files + run: uv run --frozen pre-commit run --show-diff-on-failure --all-files env: - SKIP: basedpyright + PRE_COMMIT_HOME: ${{ runner.temp }}/pre-commit-cache diff --git a/.gitignore b/.gitignore index f84925da..5cb242be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# Retained Toptal categories: Emacs, Git, Linux. # Created by https://www.toptal.com/developers/gitignore/api/vim,emacs,linux,macos,python,pycharm,windows,git # Edit at https://www.toptal.com/developers/gitignore?templates=vim,emacs,linux,macos,python,pycharm,windows,git @@ -81,20 +82,19 @@ flycheck_*.el # .nfs files are created when an open file is removed but is still being accessed .nfs* -### macOS ### +# BEGIN source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/macOS.gitignore # General .DS_Store +.localized +__MACOSX/ .AppleDouble .LSOverride +Icon[ ] -# Icon must end with two \r -Icon - - -# Thumbnails +# Resource forks ._* -# Files that might appear in the root of a volume +# Files and directories that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 @@ -102,6 +102,15 @@ Icon .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent +.com.apple.timemachine.supported +.PKInstallSandboxManager +.PKInstallSandboxManager-SystemSoftware +.hotfiles.btree +.vol +.file +.disk_label* +lost+found +.HFS+ Private Directory Data[ ] # Directories potentially created on remote AFP share .AppleDB @@ -110,12 +119,81 @@ Network Trash Folder Temporary Items .apdisk -### macOS Patch ### -# iCloud generated files -*.icloud +# Mac OS 6 to 9 +Desktop DB +Desktop DF +TheFindByContentFolder +TheVolumeSettingsFolder +.FBCIndex +.FBCSemaphoreFile +.FBCLockFolder + +# Quota system +.quota.group +.quota.user +.quota.ops.group +.quota.ops.user + +# TimeMachine +Backups.backupdb +.MobileBackups +.MobileBackups.trash +MobileBackups.trash +tmbootpicker.efi +# END source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/macOS.gitignore + +# BEGIN source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk +# END source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/Windows.gitignore + +# BEGIN source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/Vim.gitignore +# Swap +[._]*.s[a-v][a-z] +# comment out the next line if you don't need vector files +!*.svg +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ +# END source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/Vim.gitignore -### PyCharm ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# BEGIN source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/JetBrains.gitignore +# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff @@ -180,6 +258,8 @@ atlassian-ide-plugin.xml # SonarLint plugin .idea/sonarlint/ +# see https://community.sonarsource.com/t/is-the-file-idea-idea-idea-sonarlint-xml-intended-to-be-under-source-control/121119 +.idea/sonarlint.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml @@ -187,50 +267,39 @@ crashlytics.properties crashlytics-build.properties fabric.properties -# Editor-based Rest Client +# Editor-based HTTP Client .idea/httpRequests +http-client.private.env.json # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser -### PyCharm Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml - -# Azure Toolkit for IntelliJ plugin -# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij -.idea/**/azureSettings.xml - -### Python ### +# Apifox Helper cache +.idea/.cache/.Apifox_Helper +.idea/ApifoxUploaderProjectSetting.xml + +# Github Copilot persisted session migrations, see: https://github.com/microsoft/copilot-intellij-feedback/issues/712#issuecomment-3322062215 +.idea/**/copilot.data.migration.*.xml +# END source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/JetBrains.gitignore + +# BEGIN source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/VisualStudioCode.gitignore +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets +!*.code-workspace + +# Built Visual Studio Code Extensions +*.vsix +# END source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Global/VisualStudioCode.gitignore + +# BEGIN source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Python.gitignore # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] +*.py[codz] *$py.class # C extensions @@ -240,6 +309,7 @@ __pycache__/ .Python build/ develop-eggs/ +dist/ downloads/ eggs/ .eggs/ @@ -256,8 +326,8 @@ share/python-wheels/ MANIFEST # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -275,7 +345,8 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py,cover +*.py.cover +*.lcov .hypothesis/ .pytest_cache/ cover/ @@ -321,28 +392,65 @@ ipython_config.py # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +# Pipfile.lock + +# uv +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# in version control. - +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi/* +!.pixi/config.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff -celerybeat-schedule +celerybeat-schedule* celerybeat.pid +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + # SageMath parsed files *.sage.py # Environments .env +.envrc .venv env/ venv/ @@ -357,7 +465,7 @@ venv.bak/ # Rope project settings .ropeproject -# mkdocs documentation +# mkdocs/Zensical documentation /site # mypy @@ -375,70 +483,43 @@ dmypy.json cython_debug/ # PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that +# can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, you +# could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: .ruff_cache/ -# LSP config files -pyrightconfig.json - -### Vim ### -# Swap -[._]*.s[a-v][a-z] -!*.svg # comment out if you don't need vector files -[._]*.sw[a-p] -[._]s[a-rt-v][a-z] -[._]ss[a-gi-z] -[._]sw[a-p] +# PyPI configuration file +.pypirc -# Session -Session.vim -Sessionx.vim +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ -# Temporary -.netrwhist -# Auto-generated tag files -tags -# Persistent undo -[._]*.un~ - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# End of https://www.toptal.com/developers/gitignore/api/vim,emacs,linux,macos,python,pycharm,windows,git +# Streamlit +.streamlit/secrets.toml +# END source: https://github.com/github/gitignore/blob/0e5d690153ca3da8a4a1aef2d053406f408f531c/Python.gitignore +# Repository-specific amendments # Files for building Docker containers that don't have to get checked in /installers/ @@ -455,3 +536,6 @@ $RECYCLE.BIN/ !pyrightconfig.json /coverage/ /coverage.json + +# Existing coverage.py annotate outputs +*.py,cover diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d69eb22..45eca81a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,30 +13,45 @@ repos: hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] + # Preserve service inputs, vendored bytes, and immutable ignore modules. + exclude: (^\.cit-quality/vendor/|^\.gitignore$|^(tests/resources/|examples/resources/)(?!.*\.pyi?$)) - id: end-of-file-fixer + exclude: (^\.cit-quality/vendor/|^\.gitignore$|^(tests/resources/|examples/resources/)(?!.*\.pyi?$)) + - id: check-json - id: check-toml - id: check-yaml + - id: check-case-conflict + - id: check-merge-conflict + - id: detect-private-key + # Public test fixture for the self-signed pdfrest-test certificate. + exclude: ^tests/resources/signing_key\.pem$ - id: check-added-large-files args: [--maxkb=4000] - repo: https://github.com/JoC0de/pre-commit-prettier rev: v3.6.2 hooks: - id: prettier - exclude: .md$ + exclude: (\.md$|^(tests/resources/|examples/resources/)(?!.*\.pyi?$)) - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.34.0 + rev: 0.38.1 hooks: - id: check-github-workflows + - repo: https://github.com/rhysd/actionlint + rev: v1.7.12 + hooks: + - id: actionlint - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: 3.4.0 + rev: 3.11.1 hooks: - id: editorconfig-checker + # The upstream macOS ignore module contains literal carriage returns. + exclude: (^\.cit-quality/vendor/|^\.gitignore$|^(tests/resources/|examples/resources/)(?!.*\.pyi?$)) - repo: https://github.com/executablebooks/mdformat rev: 0.7.22 hooks: - id: mdformat name: mdformat on non-.github files - exclude: ^(.github/|docs/) + exclude: ^(\.github/|docs/|tests/resources/|examples/resources/) args: ["--wrap", "80", "--number"] additional_dependencies: - mdformat-gfm @@ -57,28 +72,42 @@ repos: hooks: - id: shellcheck args: [-x] - - repo: https://github.com/maxwinterstein/shfmt-py - rev: v3.12.0.1 + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.14.1-1 hooks: - id: shfmt + args: [--write] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.16.8 hooks: - id: ruff-check + exclude: ^\.cit-quality/vendor/ types_or: [python, pyi, jupyter] - id: ruff-format - types_or: [python, pyi, jupyter] + # Format Python examples in Markdown while preserving service inputs. + exclude: (^\.cit-quality/vendor/|^(tests/resources/|examples/resources/)(?!.*\.pyi?$)) + types_or: [python, pyi, jupyter, markdown] - repo: https://github.com/pappasam/toml-sort - rev: v0.24.3 + rev: v0.25.0 hooks: - id: toml-sort-fix exclude: ^uv\.lock$ - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.8.24 + rev: 0.12.18 hooks: - id: uv-lock - - repo: https://github.com/DetachHead/basedpyright-pre-commit-mirror - rev: 1.34.0 # or whatever the latest version is at the time + - repo: local hooks: + - id: check-nfc-filenames + name: Check tracked filename NFC normalization + entry: uv run --script .cit-quality/vendor/check_nfc_filenames.py + language: system + pass_filenames: false + always_run: true - id: basedpyright + name: basedpyright + entry: uv run --frozen basedpyright + language: system + pass_filenames: false + always_run: true diff --git a/AGENTS.md b/AGENTS.md index 06a36200..b029b08f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -449,9 +449,10 @@ ## CI & Publishing Notes -- GitHub Actions run three workflows: `pre-commit` (no AWS credentials), - `Test and Publish` (Python 3.10–3.14 matrix), and `Docs` (GitHub Pages build - and deploy on `main` push/manual dispatch). +- GitHub Actions run `pre-commit`, `basedpyright`, and `Test and Publish` + (Python 3.10–3.14 test and example matrices). `Test and Publish` also runs + `Docs Check` on Python 3.11 with `uv run mkdocs build --strict`; PRs upload a + docs preview artifact. Netlify hosts the documentation site. - Only the release job assumes the AWS OIDC role to `uv build` and publish with `uv publish`. - Keep CodeArtifact credentials out of source control; day-to-day development diff --git a/README.md b/README.md index bbf6b0a9..1a09cf0a 100644 --- a/README.md +++ b/README.md @@ -140,3 +140,13 @@ asyncio.run(main()) ## For contributors Contributor workflows live in `CONTRIBUTING.md`. + +Check tracked filenames for UTF-8 NFC normalization and collisions: + +```bash +uv run --script .cit-quality/vendor/check_nfc_filenames.py +``` + +This read-only check also runs through pre-commit. It requires Python 3.11 or +newer and reports offending filenames without renaming them. Source provenance +and upstream validation are recorded in `.cit-quality/README.md`. diff --git a/docs/using-files.md b/docs/using-files.md index 13afbde9..15b1044b 100644 --- a/docs/using-files.md +++ b/docs/using-files.md @@ -36,6 +36,7 @@ is documented on ```python from pdfrest import AsyncPdfRestClient + async def process_pdf() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./input.pdf"]) @@ -64,6 +65,7 @@ multipart tuples. from pathlib import Path from pdfrest import AsyncPdfRestClient + async def upload() -> None: async with AsyncPdfRestClient() as client, Path("input.pdf").open("rb") as fh: uploaded = await client.files.create([("input.pdf", fh, "application/pdf")]) @@ -99,6 +101,7 @@ Best default for local files. Each entry can be: ```python from pdfrest import AsyncPdfRestClient + async def upload_from_paths() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths( @@ -118,15 +121,14 @@ Use this when pdfRest should fetch files from `http`/`https` URLs. from pdfrest import PdfRestClient with PdfRestClient() as client: - uploaded = client.files.create_from_urls( - ["https://example.com/document.pdf"] - ) + uploaded = client.files.create_from_urls(["https://example.com/document.pdf"]) ``` === "Async" ```python from pdfrest import AsyncPdfRestClient + async def upload_from_urls() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_urls( @@ -172,6 +174,7 @@ not need metadata first. existing_id = "1de305d2-b6a0-4b5d-9a55-4e4e6d8c2d39" + async def work_with_existing_id() -> None: async with AsyncPdfRestClient() as client: # Hydrate metadata into PdfRestFile @@ -195,6 +198,7 @@ not need metadata first. ```python from pdfrest import AsyncPdfRestClient + async def fetch_metadata() -> None: async with AsyncPdfRestClient() as client: file_meta = await client.files.get("1de305d2-b6a0-4b5d-9a55-4e4e6d8c2d39") @@ -233,6 +237,7 @@ or a file ID string for all methods below. ```python from pdfrest import AsyncPdfRestClient + async def read_as_bytes() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./input.pdf"]) @@ -254,6 +259,7 @@ or a file ID string for all methods below. ```python from pdfrest import AsyncPdfRestClient + async def read_as_text() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./notes.txt"]) @@ -275,6 +281,7 @@ or a file ID string for all methods below. ```python from pdfrest import AsyncPdfRestClient + async def read_as_json() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./metadata.json"]) @@ -296,6 +303,7 @@ or a file ID string for all methods below. ```python from pdfrest import AsyncPdfRestClient + async def write_to_disk() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./input.pdf"]) @@ -322,6 +330,7 @@ Use this when files are large or you need chunk/line-level processing. ```python from pdfrest import AsyncPdfRestClient + async def stream_download() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./input.pdf"]) @@ -365,6 +374,7 @@ Both stream wrappers also expose text/line/raw iterators: ```python from pdfrest import AsyncPdfRestClient + async def stream_bytes() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./input.pdf"]) @@ -392,6 +402,7 @@ Both stream wrappers also expose text/line/raw iterators: ```python from pdfrest import AsyncPdfRestClient + async def stream_text() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./notes.txt"]) @@ -419,6 +430,7 @@ Both stream wrappers also expose text/line/raw iterators: ```python from pdfrest import AsyncPdfRestClient + async def stream_lines() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./events.ndjson"]) @@ -446,6 +458,7 @@ Both stream wrappers also expose text/line/raw iterators: ```python from pdfrest import AsyncPdfRestClient + async def stream_raw() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./input.pdf"]) @@ -482,6 +495,7 @@ upload calls can be passed directly into many endpoint helpers. ```python from pdfrest import AsyncPdfRestClient + async def chain_calls() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./input.pdf"]) @@ -510,6 +524,7 @@ Delete uploaded files when you no longer need them: ```python from pdfrest import AsyncPdfRestClient + async def delete_files() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./input.pdf"]) @@ -547,6 +562,7 @@ that failed, so you can report each file-level failure precisely. ```python from pdfrest import AsyncPdfRestClient, PdfRestDeleteError, PdfRestErrorGroup + async def delete_with_error_handling() -> None: async with AsyncPdfRestClient() as client: uploaded = await client.files.create_from_paths(["./input.pdf", "./second.pdf"]) diff --git a/pyproject.toml b/pyproject.toml index 2bdc5ded..a0dea141 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,12 +2,12 @@ name = "pdfrest" version = "1.1.0" description = "Python client library for interacting with the pdfRest API" -readme = {file = "README.md", content-type = "text/markdown"} +readme = { file = "README.md", content-type = "text/markdown" } authors = [ - {name = "Datalogics"}, + { name = "Datalogics" }, ] maintainers = [ - {name = "Datalogics"}, + { name = "Datalogics" }, ] keywords = [ "api", @@ -49,8 +49,8 @@ build-backend = "uv_build" [dependency-groups] dev = [ - "pre-commit>=3.7.0", - "ruff>=0.6.9", + "pre-commit==4.6.2", + "ruff==0.16.8", "pytest>=8.3.3", "pytest-cov>=5.0.0", "pip-audit>=2.7.3", @@ -79,10 +79,13 @@ markers = [ ] [tool.ruff] +extend-exclude = [".cit-quality/vendor"] extend-include = ["*.ipynb"] target-version = "py310" [tool.ruff.lint] +# Keep the established Ruff defaults explicit across tool upgrades. +select = ["E4", "E7", "E9", "F"] # Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default. # Rules cribbed from the PDM sources themselves. extend-select = [ diff --git a/uv.lock b/uv.lock index a48fd046..e315631b 100644 --- a/uv.lock +++ b/uv.lock @@ -1013,7 +1013,7 @@ dev = [ { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.3" }, { name = "nox", specifier = ">=2025.5.1" }, { name = "pip-audit", specifier = ">=2.7.3" }, - { name = "pre-commit", specifier = ">=3.7.0" }, + { name = "pre-commit", specifier = "==4.6.2" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "pytest-cov", specifier = ">=5.0.0" }, @@ -1024,7 +1024,7 @@ dev = [ { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "rich", specifier = ">=14.1.0" }, - { name = "ruff", specifier = ">=0.6.9" }, + { name = "ruff", specifier = "==0.16.8" }, ] [[package]] @@ -1101,7 +1101,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.3.0" +version = "4.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -1110,9 +1110,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, ] [[package]] @@ -1519,28 +1519,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/df/8d7d8c515d33adfc540e2edf6c6021ea1c5a58a678d8cfce9fae59aabcab/ruff-0.13.2.tar.gz", hash = "sha256:cb12fffd32fb16d32cef4ed16d8c7cdc27ed7c944eaa98d99d01ab7ab0b710ff", size = 5416417, upload-time = "2025-09-25T14:54:09.936Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/84/5716a7fa4758e41bf70e603e13637c42cfb9dbf7ceb07180211b9bbf75ef/ruff-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:3796345842b55f033a78285e4f1641078f902020d8450cade03aad01bffd81c3", size = 12343254, upload-time = "2025-09-25T14:53:27.784Z" }, - { url = "https://files.pythonhosted.org/packages/9b/77/c7042582401bb9ac8eff25360e9335e901d7a1c0749a2b28ba4ecb239991/ruff-0.13.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ff7e4dda12e683e9709ac89e2dd436abf31a4d8a8fc3d89656231ed808e231d2", size = 13040891, upload-time = "2025-09-25T14:53:31.38Z" }, - { url = "https://files.pythonhosted.org/packages/c6/15/125a7f76eb295cb34d19c6778e3a82ace33730ad4e6f28d3427e134a02e0/ruff-0.13.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75e9d2a2fafd1fdd895d0e7e24b44355984affdde1c412a6f6d3f6e16b22d46", size = 12243588, upload-time = "2025-09-25T14:53:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/9e/eb/0093ae04a70f81f8be7fd7ed6456e926b65d238fc122311293d033fdf91e/ruff-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cceac74e7bbc53ed7d15d1042ffe7b6577bf294611ad90393bf9b2a0f0ec7cb6", size = 12491359, upload-time = "2025-09-25T14:53:35.892Z" }, - { url = "https://files.pythonhosted.org/packages/43/fe/72b525948a6956f07dad4a6f122336b6a05f2e3fd27471cea612349fedb9/ruff-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae3f469b5465ba6d9721383ae9d49310c19b452a161b57507764d7ef15f4b07", size = 12162486, upload-time = "2025-09-25T14:53:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e3/0fac422bbbfb2ea838023e0d9fcf1f30183d83ab2482800e2cb892d02dfe/ruff-0.13.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f8f9e3cd6714358238cd6626b9d43026ed19c0c018376ac1ef3c3a04ffb42d8", size = 13871203, upload-time = "2025-09-25T14:53:41.943Z" }, - { url = "https://files.pythonhosted.org/packages/6b/82/b721c8e3ec5df6d83ba0e45dcf00892c4f98b325256c42c38ef136496cbf/ruff-0.13.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed79584a8f6cbe2e5d7dbacf7cc1ee29cbdb5df1172e77fbdadc8bb85a1f89", size = 14929635, upload-time = "2025-09-25T14:53:43.953Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a0/ad56faf6daa507b83079a1ad7a11694b87d61e6bf01c66bd82b466f21821/ruff-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aed130b2fde049cea2019f55deb939103123cdd191105f97a0599a3e753d61b0", size = 14338783, upload-time = "2025-09-25T14:53:46.205Z" }, - { url = "https://files.pythonhosted.org/packages/47/77/ad1d9156db8f99cd01ee7e29d74b34050e8075a8438e589121fcd25c4b08/ruff-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887c230c2c9d65ed1b4e4cfe4d255577ea28b718ae226c348ae68df958191aa", size = 13355322, upload-time = "2025-09-25T14:53:48.164Z" }, - { url = "https://files.pythonhosted.org/packages/64/8b/e87cfca2be6f8b9f41f0bb12dc48c6455e2d66df46fe61bb441a226f1089/ruff-0.13.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bcb10276b69b3cfea3a102ca119ffe5c6ba3901e20e60cf9efb53fa417633c3", size = 13354427, upload-time = "2025-09-25T14:53:50.486Z" }, - { url = "https://files.pythonhosted.org/packages/7f/df/bf382f3fbead082a575edb860897287f42b1b3c694bafa16bc9904c11ed3/ruff-0.13.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:afa721017aa55a555b2ff7944816587f1cb813c2c0a882d158f59b832da1660d", size = 13537637, upload-time = "2025-09-25T14:53:52.887Z" }, - { url = "https://files.pythonhosted.org/packages/51/70/1fb7a7c8a6fc8bd15636288a46e209e81913b87988f26e1913d0851e54f4/ruff-0.13.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dbc875cf3720c64b3990fef8939334e74cb0ca65b8dbc61d1f439201a38101b", size = 12340025, upload-time = "2025-09-25T14:53:54.88Z" }, - { url = "https://files.pythonhosted.org/packages/4c/27/1e5b3f1c23ca5dd4106d9d580e5c13d9acb70288bff614b3d7b638378cc9/ruff-0.13.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939a1b2a960e9742e9a347e5bbc9b3c3d2c716f86c6ae273d9cbd64f193f22", size = 12133449, upload-time = "2025-09-25T14:53:57.089Z" }, - { url = "https://files.pythonhosted.org/packages/2d/09/b92a5ccee289f11ab128df57d5911224197d8d55ef3bd2043534ff72ca54/ruff-0.13.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:50e2d52acb8de3804fc5f6e2fa3ae9bdc6812410a9e46837e673ad1f90a18736", size = 13051369, upload-time = "2025-09-25T14:53:59.124Z" }, - { url = "https://files.pythonhosted.org/packages/89/99/26c9d1c7d8150f45e346dc045cc49f23e961efceb4a70c47dea0960dea9a/ruff-0.13.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3196bc13ab2110c176b9a4ae5ff7ab676faaa1964b330a1383ba20e1e19645f2", size = 13523644, upload-time = "2025-09-25T14:54:01.622Z" }, - { url = "https://files.pythonhosted.org/packages/f7/00/e7f1501e81e8ec290e79527827af1d88f541d8d26151751b46108978dade/ruff-0.13.2-py3-none-win32.whl", hash = "sha256:7c2a0b7c1e87795fec3404a485096bcd790216c7c146a922d121d8b9c8f1aaac", size = 12245990, upload-time = "2025-09-25T14:54:03.647Z" }, - { url = "https://files.pythonhosted.org/packages/ee/bd/d9f33a73de84fafd0146c6fba4f497c4565fe8fa8b46874b8e438869abc2/ruff-0.13.2-py3-none-win_amd64.whl", hash = "sha256:17d95fb32218357c89355f6f6f9a804133e404fc1f65694372e02a557edf8585", size = 13324004, upload-time = "2025-09-25T14:54:06.05Z" }, - { url = "https://files.pythonhosted.org/packages/c3/12/28fa2f597a605884deb0f65c1b1ae05111051b2a7030f5d8a4ff7f4599ba/ruff-0.13.2-py3-none-win_arm64.whl", hash = "sha256:da711b14c530412c827219312b7d7fbb4877fb31150083add7e8c5336549cea7", size = 12484437, upload-time = "2025-09-25T14:54:08.022Z" }, +version = "0.16.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, ] [[package]]