From c8acd51226fe7da64027c08b0c46078219789ea7 Mon Sep 17 00:00:00 2001 From: tbsch Date: Fri, 8 Mar 2024 11:19:49 +0100 Subject: [PATCH] Refactor CI, deps, bots (#127) * Realign project files * Update project files * Apply new ruff rules * Apply ruff format * Apply prettier reformat * Apply yamllint * Revoke exception changes * Apply more ruff rules * Update ci * Add renovate * Improve coverage * Remove shebangs check * Update license --- .codecov.yml | 9 - .devcontainer/Dockerfile.dev | 42 ---- .devcontainer/devcontainer.json | 97 ++++++---- .editorconfig | 12 ++ .gitattributes | 2 + .github/dependabot.yml | 19 -- .github/release-drafter.yml | 4 +- .github/renovate.json | 48 +++++ .github/workflows/labels.yml | 5 +- .github/workflows/linting.yaml | 183 ++++++++++++++++++ .github/workflows/lock.yml | 1 + .github/workflows/pr-labels.yml | 12 +- .github/workflows/pre-commit-updater.yml | 35 ---- .github/workflows/release-drafter.yml | 2 +- .github/workflows/release.yml | 42 ++-- .github/workflows/stale.yml | 3 +- .github/workflows/tests.yml | 73 ++++--- .github/workflows/typing-linting.yml | 44 ----- .github/workflows/typing.yml | 37 ++++ .gitignore | 102 ++++++---- .nvmrc | 1 + .pre-commit-config.yaml | 164 +++++++++++----- .prettierignore | 1 + .vscode/launch.json | 2 +- .vscode/settings.json | 18 -- .yamllint | 66 +++++++ LICENSE.md | 2 +- MANIFEST.in | 4 - docs/usage.md | 1 - package-lock.json | 31 +++ package.json | 14 ++ poetry.lock | 149 ++++++++++++-- poetry.toml | 3 - pypaperless/helpers.py | 17 -- pyproject.toml | 110 +++++------ script/bootstrap | 11 -- script/run-in-env.sh | 22 --- script/setup | 29 --- sonar-project.properties | 17 ++ {pypaperless => src/pypaperless}/__init__.py | 0 {pypaperless => src/pypaperless}/api.py | 39 ++-- {pypaperless => src/pypaperless}/const.py | 2 +- .../pypaperless}/exceptions.py | 20 +- src/pypaperless/helpers.py | 21 ++ .../pypaperless}/models/__init__.py | 8 +- .../pypaperless}/models/base.py | 12 +- .../pypaperless}/models/classifiers.py | 10 +- .../pypaperless}/models/common.py | 18 +- .../pypaperless}/models/config.py | 2 +- .../pypaperless}/models/custom_fields.py | 2 +- .../pypaperless}/models/documents.py | 80 +++++--- .../models/generators/__init__.py | 0 .../pypaperless}/models/generators/page.py | 9 +- .../pypaperless}/models/mails.py | 6 +- .../pypaperless}/models/mixins/__init__.py | 0 .../models/mixins/helpers/__init__.py | 0 .../models/mixins/helpers/callable.py | 5 +- .../models/mixins/helpers/draftable.py | 11 +- .../models/mixins/helpers/iterable.py | 6 + .../models/mixins/helpers/securable.py | 0 .../models/mixins/models/__init__.py | 0 .../models/mixins/models/creatable.py | 10 +- .../models/mixins/models/data_fields.py | 0 .../models/mixins/models/deletable.py | 6 +- .../models/mixins/models/securable.py | 0 .../models/mixins/models/updatable.py | 4 +- .../pypaperless}/models/pages.py | 4 +- .../pypaperless}/models/permissions.py | 8 +- .../pypaperless}/models/saved_views.py | 2 +- .../pypaperless}/models/share_links.py | 4 +- .../pypaperless}/models/status.py | 6 +- .../pypaperless}/models/tasks.py | 10 +- .../pypaperless}/models/utils/__init__.py | 24 +-- .../pypaperless}/models/workflows.py | 10 +- {pypaperless => src/pypaperless}/py.typed | 0 tests/conftest.py | 41 ++-- tests/data/__init__.py | 7 +- tests/data/v0_0_0.py | 10 +- tests/ruff.toml | 13 ++ tests/test_common.py | 117 +++++++---- tests/test_models_matrix.py | 21 +- tests/test_models_specific.py | 33 ++-- 82 files changed, 1297 insertions(+), 718 deletions(-) delete mode 100644 .codecov.yml delete mode 100644 .devcontainer/Dockerfile.dev create mode 100644 .editorconfig create mode 100644 .gitattributes delete mode 100644 .github/dependabot.yml create mode 100644 .github/renovate.json create mode 100644 .github/workflows/linting.yaml delete mode 100644 .github/workflows/pre-commit-updater.yml delete mode 100644 .github/workflows/typing-linting.yml create mode 100644 .github/workflows/typing.yml create mode 100644 .nvmrc create mode 120000 .prettierignore delete mode 100644 .vscode/settings.json create mode 100644 .yamllint delete mode 100644 MANIFEST.in create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 poetry.toml delete mode 100644 pypaperless/helpers.py delete mode 100755 script/bootstrap delete mode 100755 script/run-in-env.sh delete mode 100755 script/setup create mode 100644 sonar-project.properties rename {pypaperless => src/pypaperless}/__init__.py (100%) rename {pypaperless => src/pypaperless}/api.py (91%) rename {pypaperless => src/pypaperless}/const.py (97%) rename {pypaperless => src/pypaperless}/exceptions.py (81%) create mode 100644 src/pypaperless/helpers.py rename {pypaperless => src/pypaperless}/models/__init__.py (90%) rename {pypaperless => src/pypaperless}/models/base.py (92%) rename {pypaperless => src/pypaperless}/models/classifiers.py (95%) rename {pypaperless => src/pypaperless}/models/common.py (88%) rename {pypaperless => src/pypaperless}/models/config.py (95%) rename {pypaperless => src/pypaperless}/models/custom_fields.py (95%) rename {pypaperless => src/pypaperless}/models/documents.py (90%) rename {pypaperless => src/pypaperless}/models/generators/__init__.py (100%) rename {pypaperless => src/pypaperless}/models/generators/page.py (89%) rename {pypaperless => src/pypaperless}/models/mails.py (93%) rename {pypaperless => src/pypaperless}/models/mixins/__init__.py (100%) rename {pypaperless => src/pypaperless}/models/mixins/helpers/__init__.py (100%) rename {pypaperless => src/pypaperless}/models/mixins/helpers/callable.py (90%) rename {pypaperless => src/pypaperless}/models/mixins/helpers/draftable.py (72%) rename {pypaperless => src/pypaperless}/models/mixins/helpers/iterable.py (98%) rename {pypaperless => src/pypaperless}/models/mixins/helpers/securable.py (100%) rename {pypaperless => src/pypaperless}/models/mixins/models/__init__.py (100%) rename {pypaperless => src/pypaperless}/models/mixins/models/creatable.py (89%) rename {pypaperless => src/pypaperless}/models/mixins/models/data_fields.py (100%) rename {pypaperless => src/pypaperless}/models/mixins/models/deletable.py (91%) rename {pypaperless => src/pypaperless}/models/mixins/models/securable.py (100%) rename {pypaperless => src/pypaperless}/models/mixins/models/updatable.py (96%) rename {pypaperless => src/pypaperless}/models/pages.py (99%) rename {pypaperless => src/pypaperless}/models/permissions.py (91%) rename {pypaperless => src/pypaperless}/models/saved_views.py (94%) rename {pypaperless => src/pypaperless}/models/share_links.py (96%) rename {pypaperless => src/pypaperless}/models/status.py (91%) rename {pypaperless => src/pypaperless}/models/tasks.py (92%) rename {pypaperless => src/pypaperless}/models/utils/__init__.py (93%) rename {pypaperless => src/pypaperless}/models/workflows.py (94%) rename {pypaperless => src/pypaperless}/py.typed (100%) create mode 100644 tests/ruff.toml diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index 8879097..0000000 --- a/.codecov.yml +++ /dev/null @@ -1,9 +0,0 @@ -codecov: - branch: main - -coverage: - range: "97..100" - -comment: - layout: "diff, files" - require_changes: true diff --git a/.devcontainer/Dockerfile.dev b/.devcontainer/Dockerfile.dev deleted file mode 100644 index c37a134..0000000 --- a/.devcontainer/Dockerfile.dev +++ /dev/null @@ -1,42 +0,0 @@ -FROM mcr.microsoft.com/vscode/devcontainers/python:3.12 - -ENV PYTHONUNBUFFERED 1 - -SHELL ["/bin/bash", "-o", "pipefail", "-c"] - -RUN \ - pipx uninstall black \ - && pipx uninstall pydocstyle \ - && pipx uninstall pycodestyle \ - && pipx uninstall mypy \ - && pipx uninstall pylint - -RUN \ - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - # Additional library needed by some tests and accordingly by VScode Tests Discovery - bluez \ - libudev-dev \ - libavformat-dev \ - libavcodec-dev \ - libavdevice-dev \ - libavutil-dev \ - libswscale-dev \ - libswresample-dev \ - libavfilter-dev \ - libpcap-dev \ - libturbojpeg0 \ - libyaml-dev \ - libxml2 \ - git \ - cmake \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /workspaces - -RUN pip3 install --upgrade pip - -# Set the default shell to bash instead of sh -ENV SHELL /bin/bash diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index acc43d9..ea36e56 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,43 +1,58 @@ { - "name": "PyPaperless Dev", - "context": "..", - "dockerFile": "Dockerfile.dev", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - "containerEnv": { - "DEVCONTAINER": "1" - }, - "postCreateCommand": "script/setup", - "postStartCommand": "script/bootstrap", - "runArgs": ["-e", "GIT_EDITOR=code --wait"], - "remoteUser": "vscode", - "customizations": { - "vscode": { - "extensions": [ - "ms-python.black-formatter", - "ms-python.pylint", - "ms-python.vscode-pylance", - "visualstudioexptteam.vscodeintellicode", - "esbenp.prettier-vscode", - "GitHub.vscode-pull-request-github", - "ms-python.mypy-type-checker" - ], - "settings": { - "python.pythonPath": "/usr/local/bin/python", - "python.testing.pytestArgs": [ - "--no-cov" - ], - "python.testing.pytestEnabled": false, - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true, - "terminal.integrated.profiles.linux": { - "zsh": { - "path": "/usr/bin/zsh" - } - }, - "terminal.integrated.defaultProfile.linux": "zsh" - } - } - } + "containerEnv": { + "DEVCONTAINER": "true", + "POETRY_VIRTUALENVS_IN_PROJECT": "true" + }, + "customizations": { + "codespaces": { + "openFiles": ["README.md", "src/pypaperless/api.py"] + }, + "vscode": { + "extensions": [ + "ms-python.python", + "redhat.vscode-yaml", + "esbenp.prettier-vscode", + "GitHub.vscode-pull-request-github", + "charliermarsh.ruff", + "GitHub.vscode-github-actions", + "ryanluker.vscode-coverage-gutters" + ], + "settings": { + "[python]": { + "editor.codeActionsOnSave": { + "source.fixAll": "always", + "source.organizeImports": "always" + } + }, + "coverage-gutters.customizable.context-menu": true, + "coverage-gutters.customizable.status-bar-toggler-watchCoverageAndVisibleEditors-enabled": true, + "coverage-gutters.showGutterCoverage": true, + "coverage-gutters.showLineCoverage": true, + "coverage-gutters.xmlname": "coverage.xml", + "python.analysis.extraPaths": ["${workspaceFolder}/src"], + "python.defaultInterpreterPath": ".venv/bin/python", + "python.formatting.provider": "ruff", + "python.testing.cwd": "${workspaceFolder}", + "python.testing.pytestArgs": [ + "--cov-report=xml" + ], + "python.testing.pytestEnabled": true, + "ruff.importStrategy": "fromEnvironment", + "ruff.interpreter": [".venv/bin/python"], + "terminal.integrated.defaultProfile.linux": "zsh" + } + } + }, + "features": { + "ghcr.io/devcontainers-contrib/features/poetry:2": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12", + "installTools": false + } + }, + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "name": "pypaperless Developer", + "updateContentCommand": ". ${NVM_DIR}/nvm.sh && nvm install && nvm use && npm install && poetry install && poetry run pre-commit install" } diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8b8d851 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7ba5289 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text eol=lf +*.py whitespace=error diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 34b9c38..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: weekly - labels: - - dependencies - - auto - - no-stale - - skip-changelog - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: weekly - labels: - - dependencies - - auto - - no-stale diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index bd3a766..f294201 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -2,7 +2,6 @@ name-template: "v$RESOLVED_VERSION ๐ŸŒˆ" tag-template: "v$RESOLVED_VERSION" change-template: "- $TITLE @$AUTHOR (#$NUMBER)" -change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. exclude-labels: - "skip-changelog" @@ -14,7 +13,7 @@ categories: - title: "โœจ New features" labels: - "new-feature" - - title: "๐Ÿ› Bug Fixes" + - title: "๐Ÿ› Bug fixes" labels: - "bugfix" - title: "๐Ÿš€ Enhancements" @@ -45,6 +44,7 @@ version-resolver: patch: labels: - "bugfix" + - "ci" - "dependencies" - "enhancement" - "performance" diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..9cefd40 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "schedule": ["before 2am"], + "rebaseWhen": "behind-base-branch", + "dependencyDashboard": true, + "labels": ["dependencies", "no-stale"], + "lockFileMaintenance": { + "enabled": true, + "automerge": true + }, + "commitMessagePrefix": "โฌ†๏ธ", + "packageRules": [ + { + "matchManagers": ["poetry"], + "addLabels": ["python"] + }, + { + "matchManagers": ["poetry"], + "matchDepTypes": ["dev"], + "rangeStrategy": "pin" + }, + { + "matchManagers": ["poetry"], + "matchUpdateTypes": ["minor", "patch"], + "automerge": true + }, + { + "matchManagers": ["npm", "nvm"], + "addLabels": ["javascript"], + "rangeStrategy": "pin" + }, + { + "matchManagers": ["npm", "nvm"], + "matchUpdateTypes": ["minor", "patch"], + "automerge": true + }, + { + "matchManagers": ["github-actions"], + "addLabels": ["github_actions"], + "rangeStrategy": "pin" + }, + { + "matchManagers": ["github-actions"], + "matchUpdateTypes": ["minor", "patch"], + "automerge": true + } + ] +} diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 3bdde0d..f0cbdb1 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -1,6 +1,7 @@ --- name: Sync labels +# yamllint disable-line rule:truthy on: push: branches: @@ -14,9 +15,9 @@ jobs: name: ๐Ÿท Sync labels runs-on: ubuntu-latest steps: - - name: โคต๏ธ Checkout code + - name: โคต๏ธ Check out code from GitHub uses: actions/checkout@v4.1.1 - - name: ๐Ÿš€ Run label sync + - name: ๐Ÿš€ Run Label Syncer uses: micnncim/action-label-syncer@v1.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml new file mode 100644 index 0000000..42d84ea --- /dev/null +++ b/.github/workflows/linting.yaml @@ -0,0 +1,183 @@ +--- +name: Linting + +# yamllint disable-line rule:truthy +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +env: + DEFAULT_PYTHON: "3.11" + +jobs: + codespell: + name: codespell + runs-on: ubuntu-latest + steps: + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install Python dependencies + run: poetry install --no-interaction + - name: ๐Ÿš€ Check code for common misspellings + run: poetry run pre-commit run codespell --all-files + + ruff: + name: Ruff + runs-on: ubuntu-latest + steps: + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install Python dependencies + run: poetry install --no-interaction + - name: ๐Ÿš€ Run ruff linter + run: poetry run ruff check --output-format=github . + - name: ๐Ÿš€ Run ruff formatter + run: poetry run ruff format --check . + + pre-commit-hooks: + name: pre-commit-hooks + runs-on: ubuntu-latest + steps: + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install Python dependencies + run: poetry install --no-interaction + - name: ๐Ÿš€ Check Python AST + run: poetry run pre-commit run check-ast --all-files + - name: ๐Ÿš€ Check for case conflicts + run: poetry run pre-commit run check-case-conflict --all-files + - name: ๐Ÿš€ Check docstring is first + run: poetry run pre-commit run check-docstring-first --all-files + # - name: ๐Ÿš€ Check that executables have shebangs + # run: poetry run pre-commit run check-executables-have-shebangs --all-files + - name: ๐Ÿš€ Check JSON files + run: poetry run pre-commit run check-json --all-files + - name: ๐Ÿš€ Check for merge conflicts + run: poetry run pre-commit run check-merge-conflict --all-files + - name: ๐Ÿš€ Check for broken symlinks + run: poetry run pre-commit run check-symlinks --all-files + - name: ๐Ÿš€ Check TOML files + run: poetry run pre-commit run check-toml --all-files + - name: ๐Ÿš€ Check YAML files + run: poetry run pre-commit run check-yaml --all-files + - name: ๐Ÿš€ Detect Private Keys + run: poetry run pre-commit run detect-private-key --all-files + - name: ๐Ÿš€ Check End of Files + run: poetry run pre-commit run end-of-file-fixer --all-files + - name: ๐Ÿš€ Trim Trailing Whitespace + run: poetry run pre-commit run trailing-whitespace --all-files + + pylint: + name: pylint + runs-on: ubuntu-latest + steps: + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install Python dependencies + run: poetry install --no-interaction + - name: ๐Ÿš€ Run pylint + run: poetry run pre-commit run pylint --all-files + + yamllint: + name: yamllint + runs-on: ubuntu-latest + steps: + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install Python dependencies + run: poetry install --no-interaction + - name: ๐Ÿš€ Run yamllint + run: poetry run yamllint . + + prettier: + name: Prettier + runs-on: ubuntu-latest + steps: + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install Python dependencies + run: poetry install --no-interaction + - name: ๐Ÿ— Set up Node.js + uses: actions/setup-node@v4.0.2 + with: + node-version-file: ".nvmrc" + cache: "npm" + - name: ๐Ÿ— Install NPM dependencies + run: npm install + - name: ๐Ÿš€ Run prettier + run: poetry run pre-commit run prettier --all-files diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index fba0d22..806ee5f 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -1,6 +1,7 @@ --- name: Lock +# yamllint disable-line rule:truthy on: schedule: - cron: "0 5 * * *" diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 5f54d18..d5b2f1c 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -1,6 +1,7 @@ --- name: PR Labels +# yamllint disable-line rule:truthy on: pull_request_target: types: @@ -11,14 +12,16 @@ on: workflow_call: jobs: - pr-labels: + pr_labels: name: Verify runs-on: ubuntu-latest steps: - - name: ๐Ÿท Verify valid PR label - uses: ludeeus/action-require-labels@1.1.0 + - name: ๐Ÿท Verify PR has a valid label + uses: jesusvasquez333/verify-pr-label-action@v1.4.0 with: - labels: >- + pull-request-number: "${{ github.event.pull_request.number }}" + github-token: "${{ secrets.GITHUB_TOKEN }}" + valid-labels: >- breaking-change, bugfix, ci, dependencies, documentation, @@ -27,3 +30,4 @@ jobs: new-feature, performance, refactor + disable-reviews: true diff --git a/.github/workflows/pre-commit-updater.yml b/.github/workflows/pre-commit-updater.yml deleted file mode 100644 index b9e393e..0000000 --- a/.github/workflows/pre-commit-updater.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: Pre-commit auto-update - -on: - schedule: - - cron: "0 0 * * 1" - -env: - DEFAULT_PYTHON: "3.11" - -jobs: - auto-update: - runs-on: ubuntu-latest - steps: - - name: โคต๏ธ Checkout code - uses: actions/checkout@v4.1.1 - - name: ๐Ÿ Setup Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: ๐Ÿšง Install pre-commit - run: pip install pre-commit - - name: ๐Ÿšง Run pre-commit autoupdate - run: pre-commit autoupdate - - name: ๐Ÿ…ฟ๏ธ Create Pull Request - uses: peter-evans/create-pull-request@v6.0.1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - branch: chore/pre-commit-autoupdate - title: Auto-update pre-commit hooks - commit-message: Auto-update pre-commit hooks - body: | - Update versions of tools in pre-commit - configs to latest version - labels: dependencies, auto, no-stale, skip-changelog diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 3456d98..875a0a0 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,13 +1,13 @@ --- name: Release Drafter +# yamllint disable-line rule:truthy on: push: branches: - main workflow_dispatch: - jobs: update_release_draft: name: ๐Ÿ“ Draft release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc539ea..bf5c742 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,7 @@ --- name: Release +# yamllint disable-line rule:truthy on: release: types: @@ -14,37 +15,42 @@ jobs: name: Releasing to PyPi runs-on: ubuntu-latest environment: - name: pypi + name: release url: https://pypi.org/p/pypaperless permissions: contents: write id-token: write # important for trusted publishing (pypi) - outputs: - version: ${{ steps.vars.outputs.tag }} steps: - - name: โคต๏ธ Checkout code + - name: โคต๏ธ Check out code from GitHub uses: actions/checkout@v4.1.1 - - name: ๐Ÿšง Setup Poetry - run: pip install poetry - - name: ๐Ÿ Setup Python ${{ env.DEFAULT_PYTHON }} + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python uses: actions/setup-python@v5.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - cache: poetry - - name: ๐Ÿšง Install dependencies + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install dependencies run: poetry install --no-interaction - - name: ๐Ÿท Get repository tag - id: vars - run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - - name: ๐Ÿšง Set project version from tag - run: poetry version --no-interaction "${{ steps.vars.outputs.tag }}" - - name: ๐Ÿš€ Build package + - name: ๐Ÿ— Set package version + run: | + version="${{ github.event.release.tag_name }}" + version="${version,,}" + version="${version#v}" + poetry version --no-interaction "${version}" + - name: ๐Ÿ— Build package run: poetry build --no-interaction - - name: โฌ†๏ธ Publish to PyPi - uses: pypa/gh-action-pypi-publish@v1.8.12 + - name: ๐Ÿš€ Publish to PyPi + uses: pypa/gh-action-pypi-publish@v1.8.14 with: + verbose: true print-hash: true - - name: ๐Ÿ” Sign published artifacts + - name: โœ๏ธ Sign published artifacts uses: sigstore/gh-action-sigstore-python@v2.1.1 with: inputs: ./dist/*.tar.gz ./dist/*.whl diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7894afe..35fcc26 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,6 +1,7 @@ --- name: Stale +# yamllint disable-line rule:truthy on: schedule: - cron: "0 3 * * *" @@ -15,7 +16,7 @@ jobs: uses: actions/stale@v9.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-stale: 14 + days-before-stale: 30 days-before-close: 7 remove-stale-when-updated: true stale-issue-label: "stale" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 81842f4..c268b3d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,7 @@ --- -name: Tests +name: Testing +# yamllint disable-line rule:truthy on: push: branches: @@ -13,56 +14,72 @@ env: jobs: pytest: - name: Testing on Python ${{ matrix.python-version }} + name: Python ${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11", "3.12"] + python: ["3.11", "3.12"] steps: - - name: โคต๏ธ Checkout code + - name: โคต๏ธ Check out code from GitHub uses: actions/checkout@v4.1.1 - - name: ๐Ÿšง Setup Poetry - run: pip install poetry - - name: ๐Ÿ Setup Python ${{ matrix.python-version }} + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ matrix.python }} + id: python uses: actions/setup-python@v5.0.0 with: - python-version: ${{ matrix.python-version }} - cache: poetry - - name: ๐Ÿšง Install dependencies - run: poetry install --no-root --only main,test --no-interaction - - name: ๐Ÿš€ Run Pytest + python-version: ${{ matrix.python }} + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install Python dependencies + run: poetry install --no-interaction + - name: ๐Ÿš€ Run pytest run: poetry run pytest --cov pypaperless tests - - name: โฌ†๏ธ Upload coverage + - name: โฌ†๏ธ Upload coverage artifact uses: actions/upload-artifact@v4.3.1 with: - name: coverage-${{ matrix.python-version }} + name: coverage-${{ matrix.python }} path: .coverage coverage: - name: Coverage report runs-on: ubuntu-latest needs: pytest steps: - - name: โคต๏ธ Checkout code + - name: โคต๏ธ Check out code from GitHub uses: actions/checkout@v4.1.1 - - name: โฌ‡๏ธ Download coverage + with: + fetch-depth: 0 + - name: โฌ‡๏ธ Download coverage data uses: actions/download-artifact@v4.1.4 - - name: ๐Ÿšง Setup Poetry - run: pip install poetry - - name: ๐Ÿ Setup Python ${{ env.DEFAULT_PYTHON }} + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python uses: actions/setup-python@v5.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - cache: poetry - - name: ๐Ÿšง Install dependencies - run: poetry install --no-root --only main,test --no-interaction + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install dependencies + run: poetry install --no-interaction - name: ๐Ÿš€ Process coverage results run: | poetry run coverage combine coverage*/.coverage* poetry run coverage xml -i - - name: โฌ†๏ธ Upload coverage to Codecov - uses: codecov/codecov-action@v4.1.0 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: ๐Ÿš€ Upload coverage report + uses: codecov/codecov-action@v3.1.6 with: - file: ./coverage.xml + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + - name: ๐Ÿš€ SonarCloud Scan + if: github.event.pull_request.head.repo.fork == false + uses: SonarSource/sonarcloud-github-action@v2.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/typing-linting.yml b/.github/workflows/typing-linting.yml deleted file mode 100644 index 6a873f8..0000000 --- a/.github/workflows/typing-linting.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: Typing and Linting - -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - -env: - DEFAULT_PYTHON: "3.11" - -jobs: - typing-and-linting: - name: Run pre-commit hooks - runs-on: ubuntu-latest - steps: - - name: โคต๏ธ Checkout code - uses: actions/checkout@v4.1.1 - - name: ๐Ÿšง Setup Poetry - run: pip install poetry - - name: ๐Ÿ Setup Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache: poetry - - name: ๐Ÿšง Install dependencies - run: poetry install --no-root --no-interaction - - name: ๐Ÿš€ Run pre-commit hooks - run: | - poetry run pre-commit run check-yaml --all-files - poetry run pre-commit run check-toml --all-files - poetry run pre-commit run check-ast --all-files - poetry run pre-commit run check-docstring-first --all-files - poetry run pre-commit run debug-statements --all-files - poetry run pre-commit run end-of-file-fixer --all-files - poetry run pre-commit run trailing-whitespace --all-files - poetry run pre-commit run ruff --all-files - poetry run pre-commit run ruff-format --all-files - poetry run pre-commit run black --all-files - poetry run pre-commit run codespell --all-files - poetry run pre-commit run mypy --all-files - poetry run pre-commit run pylint --all-files diff --git a/.github/workflows/typing.yml b/.github/workflows/typing.yml new file mode 100644 index 0000000..1fe7753 --- /dev/null +++ b/.github/workflows/typing.yml @@ -0,0 +1,37 @@ +--- +name: Typing + +# yamllint disable-line rule:truthy +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +env: + DEFAULT_PYTHON: "3.11" + +jobs: + mypy: + name: mypy + runs-on: ubuntu-latest + steps: + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install Python dependencies + run: poetry install --no-interaction + - name: ๐Ÿš€ Run mypy + run: poetry run mypy src tests diff --git a/.gitignore b/.gitignore index 69003bc..f6f346a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,36 @@ __pycache__/ *.py[cod] *$py.class +# OSX useful to ignore +*.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + # C extensions *.so # Distribution / packaging .Python +env/ build/ develop-eggs/ dist/ @@ -19,13 +44,9 @@ lib64/ parts/ sdist/ var/ -wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST -.venv/ -++*.py # PyInstaller # Usually these files are written by a python script from a template @@ -45,65 +66,60 @@ htmlcov/ .cache nosetests.xml coverage.xml -*.cover +*,cover .hypothesis/ .pytest_cache/ -# Ruff -.ruff_cache/ - # Translations *.mo *.pot # Django stuff: *.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy # Sphinx documentation docs/_build/ -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - # pyenv .python-version -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env +# virtualenv .venv -env/ venv/ ENV/ -env.bak/ -venv.bak/ -# Spyder project settings -.spyderproject -.spyproject +# mypy +.mypy_cache/ -# Rope project settings -.ropeproject +# ruff +.ruff_cache -# mkdocs documentation -/site +# Visual Studio Code +.vscode -# mypy -.mypy_cache/ +# IntelliJ Idea family of suites +.idea +*.iml + +## File-based project format: +*.ipr +*.iws + +## mpeltonen/sbt-idea plugin +.idea_modules/ + +# PyBuilder +target/ + +# Cookiecutter +output/ +python_boilerplate/ + +# Node +node_modules/ + +# Deepcode AI +.dccache + +# run +run/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2dbbe00 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.11.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d915c51..2610ffd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,59 +1,133 @@ --- repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + - repo: local hooks: - - id: check-yaml - name: โœ… Check yaml files - - id: check-toml - name: โœ… Check toml files + - id: ruff-check + name: ๐Ÿถ Ruff Linter + language: system + types: [python] + entry: poetry run ruff check --fix + require_serial: true + stages: [commit, push, manual] + - id: ruff-format + name: ๐Ÿถ Ruff Formatter + language: system + types: [python] + entry: poetry run ruff format + require_serial: true + stages: [commit, push, manual] - id: check-ast - name: โœ… Check files are valid python + name: ๐Ÿ Check Python AST + language: system + types: [python] + entry: poetry run check-ast + - id: check-case-conflict + name: ๐Ÿ”  Check for case conflicts + language: system + entry: poetry run check-case-conflict - id: check-docstring-first - name: โœ… Check docstrings first - - id: debug-statements - name: โœ… Check debug statements - - id: end-of-file-fixer - name: โฎ Fix end of files - - id: trailing-whitespace - name: โฎ Trailing whitespace - - id: no-commit-to-branch - name: ๐Ÿ›‘ No commit to main branch - args: - - --branch=main - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.3.0 - hooks: - - id: ruff - name: ๐Ÿถ Ruff Check - - id: ruff-format - name: ๐Ÿถ Ruff Format - - repo: https://github.com/psf/black - rev: 24.2.0 - hooks: - - id: black - name: โœ… Black Format - args: - - --safe - - --quiet - - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 - hooks: + name: โ„น๏ธ Check docstring is first + language: system + types: [python] + entry: poetry run check-docstring-first + # - id: check-executables-have-shebangs + # name: ๐Ÿง Check that executables have shebangs + # language: system + # types: [text, executable] + # entry: poetry run check-executables-have-shebangs + # stages: [commit, push, manual] + - id: check-json + name: ๏ฝ› Check JSON files + language: system + types: [json] + entry: poetry run check-json + - id: check-merge-conflict + name: ๐Ÿ’ฅ Check for merge conflicts + language: system + types: [text] + entry: poetry run check-merge-conflict + - id: check-symlinks + name: ๐Ÿ”— Check for broken symlinks + language: system + types: [symlink] + entry: poetry run check-symlinks + - id: check-toml + name: โœ… Check TOML files + language: system + types: [toml] + entry: poetry run check-toml + - id: check-xml + name: โœ… Check XML files + entry: poetry run check-xml + language: system + types: [xml] + - id: check-yaml + name: โœ… Check YAML files + language: system + types: [yaml] + entry: poetry run check-yaml - id: codespell - name: โœ… Check code for proper spelling - exclude_types: [csv, json] - additional_dependencies: - - tomli - - repo: local - hooks: + name: โœ… Check code for common misspellings + language: system + types: [text] + exclude: ^poetry\.lock$ + entry: poetry run codespell + - id: detect-private-key + name: ๐Ÿ•ต๏ธ Detect Private Keys + language: system + types: [text] + entry: poetry run detect-private-key + - id: end-of-file-fixer + name: โฎ Fix End of Files + language: system + types: [text] + entry: poetry run end-of-file-fixer + stages: [commit, push, manual] - id: mypy - name: ๐Ÿ†Ž Type checking with mypy + name: ๐Ÿ†Ž Static type checking using mypy language: system types: [python] entry: poetry run mypy require_serial: true + - id: no-commit-to-branch + name: ๐Ÿ›‘ Don't commit to main branch + language: system + entry: poetry run no-commit-to-branch + pass_filenames: false + always_run: true + args: + - --branch=main + - id: poetry + name: ๐Ÿ“œ Check pyproject with Poetry + language: system + entry: poetry check + pass_filenames: false + always_run: true + - id: prettier + name: ๐Ÿ’„ Ensuring files are prettier + language: system + types: [yaml, json, markdown] + entry: npm run prettier + pass_filenames: false - id: pylint - name: ๐ŸŒŸ Rating code with pylint + name: ๐ŸŒŸ Starring code with pylint + language: system + types: [python] + entry: poetry run pylint + - id: pytest + name: ๐Ÿงช Running tests and test coverage with pytest language: system types: [python] - entry: poetry run pylint pypaperless -j 0 + entry: poetry run pytest + pass_filenames: false + - id: trailing-whitespace + name: โœ„ Trim Trailing Whitespace + language: system + types: [text] + entry: poetry run trailing-whitespace-fixer + stages: [commit, push, manual] + - id: yamllint + name: ๐ŸŽ— Check YAML files with yamllint + language: system + types: [yaml] + entry: poetry run yamllint diff --git a/.prettierignore b/.prettierignore new file mode 120000 index 0000000..3e4e48b --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 83799fe..90d8861 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "name": "Python: Aktuelle Datei", "type": "debugpy", "request": "launch", - "program": "++debug.py", + "program": "run/debug.py", "console": "integratedTerminal", "justMyCode": true } diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6dd03d4..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "[python]": { - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "editor.defaultFormatter": "ms-python.black-formatter", - "editor.formatOnSave": true, - - }, - "python.testing.pytestArgs": [ - "tests", - "--no-cov" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "python.analysis.typeCheckingMode": "basic" -} diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..e6b1813 --- /dev/null +++ b/.yamllint @@ -0,0 +1,66 @@ +--- +ignore: + - .venv +rules: + braces: + level: error + min-spaces-inside: 0 + max-spaces-inside: 1 + min-spaces-inside-empty: -1 + max-spaces-inside-empty: -1 + brackets: + level: error + min-spaces-inside: 0 + max-spaces-inside: 0 + min-spaces-inside-empty: -1 + max-spaces-inside-empty: -1 + colons: + level: error + max-spaces-before: 0 + max-spaces-after: 1 + commas: + level: error + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + comments: + level: error + require-starting-space: true + min-spaces-from-content: 1 + comments-indentation: + level: error + document-end: + level: error + present: false + document-start: + level: error + present: true + empty-lines: + level: error + max: 1 + max-start: 0 + max-end: 1 + hyphens: + level: error + max-spaces-after: 1 + indentation: + level: error + spaces: 2 + indent-sequences: true + check-multi-line-strings: false + key-duplicates: + level: error + line-length: + level: warning + max: 120 + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: true + new-line-at-end-of-file: + level: error + new-lines: + level: error + type: unix + trailing-spaces: + level: error + truthy: + level: error diff --git a/LICENSE.md b/LICENSE.md index ca9a151..86279bb 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # MIT License -Copyright (c) 2021 hugohabicht01 +Copyright (c) 2022-2024 tb1337 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index f9203b9..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include *.md -include *.txt -include LICENSE -graft pypaperless diff --git a/docs/usage.md b/docs/usage.md index 64decbf..0f4d02c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -100,7 +100,6 @@ token = Paperless.generate_api_token( As for `Paperless` itself, you can provide a custom `aiohttp.ClientSession` object. - ```python url = "localhost:8000" my_session = aiohttp.ClientSession() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4fac7a3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "pypaperless", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pypaperless", + "version": "0.0.0", + "license": "MIT", + "devDependencies": { + "prettier": "3.2.5" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4dd34d3 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "pypaperless", + "version": "0.0.0", + "private": true, + "description": "Little api client for paperless(-ngx).", + "scripts": { + "prettier": "prettier --write **/*.{json,js,md,yml,yaml} *.{json,js,md,yml,yaml}" + }, + "author": "Tobias Schulz ", + "license": "MIT", + "devDependencies": { + "prettier": "3.2.5" + } +} diff --git a/poetry.lock b/poetry.lock index 4393239..0fb6f96 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiohttp" @@ -757,6 +757,20 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pre-commit-hooks" +version = "4.5.0" +description = "Some out-of-the-box hooks for pre-commit." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit_hooks-4.5.0-py2.py3-none-any.whl", hash = "sha256:b779d5c44ede9b1fda48e2d96b08e9aa5b1d2fdb8903ca09f0dbaca22d529edb"}, + {file = "pre_commit_hooks-4.5.0.tar.gz", hash = "sha256:ffbe2af1c85ac9a7695866955680b4dee98822638b748a6f3debefad79748c8a"}, +] + +[package.dependencies] +"ruamel.yaml" = ">=0.15" + [[package]] name = "pylint" version = "3.1.0" @@ -919,30 +933,107 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "ruamel-yaml" +version = "0.18.6" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, + {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.8" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.6" +files = [ + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, + {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, + {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, +] + [[package]] name = "ruff" -version = "0.3.0" +version = "0.3.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"}, - {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"}, - {file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"}, - {file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"}, - {file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"}, - {file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"}, + {file = "ruff-0.3.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6b82e3937d0d76554cd5796bc3342a7d40de44494d29ff490022d7a52c501744"}, + {file = "ruff-0.3.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ae7954c8f692b70e6a206087ae3988acc9295d84c550f8d90b66c62424c16771"}, + {file = "ruff-0.3.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b730f56ccf91225da0f06cfe421e83b8cc27b2a79393db9c3df02ed7e2bbc01"}, + {file = "ruff-0.3.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c78bfa85637668f47bd82aa2ae17de2b34221ac23fea30926f6409f9e37fc927"}, + {file = "ruff-0.3.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6abaad602d6e6daaec444cbf4d9364df0a783e49604c21499f75bb92237d4af"}, + {file = "ruff-0.3.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0c21b6914c3c9a25a59497cbb1e5b6c2d8d9beecc9b8e03ee986e24eee072e"}, + {file = "ruff-0.3.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:434c3fc72e6311c85cd143c4c448b0e60e025a9ac1781e63ba222579a8c29200"}, + {file = "ruff-0.3.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78a7025e6312cbba496341da5062e7cdd47d95f45c1b903e635cdeb1ba5ec2b9"}, + {file = "ruff-0.3.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52b02bb46f1a79b0c1fa93f6495bc7e77e4ef76e6c28995b4974a20ed09c0833"}, + {file = "ruff-0.3.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11b5699c42f7d0b771c633d620f2cb22e727fb226273aba775a91784a9ed856c"}, + {file = "ruff-0.3.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:54e5dca3e411772b51194b3102b5f23b36961e8ede463776b289b78180df71a0"}, + {file = "ruff-0.3.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:951efb610c5844e668bbec4f71cf704f8645cf3106e13f283413969527ebfded"}, + {file = "ruff-0.3.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:09c7333b25e983aabcf6e38445252cff0b4745420fc3bda45b8fce791cc7e9ce"}, + {file = "ruff-0.3.1-py3-none-win32.whl", hash = "sha256:d937f9b99ebf346e0606c3faf43c1e297a62ad221d87ef682b5bdebe199e01f6"}, + {file = "ruff-0.3.1-py3-none-win_amd64.whl", hash = "sha256:c0318a512edc9f4e010bbaab588b5294e78c5cdc9b02c3d8ab2d77c7ae1903e3"}, + {file = "ruff-0.3.1-py3-none-win_arm64.whl", hash = "sha256:d3b60e44240f7e903e6dbae3139a65032ea4c6f2ad99b6265534ff1b83c20afa"}, + {file = "ruff-0.3.1.tar.gz", hash = "sha256:d30db97141fc2134299e6e983a6727922c9e03c031ae4883a6d69461de722ae7"}, ] [[package]] @@ -1003,6 +1094,24 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "yamllint" +version = "1.35.1" +description = "A linter for YAML files." +optional = false +python-versions = ">=3.8" +files = [ + {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"}, + {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"}, +] + +[package.dependencies] +pathspec = ">=0.5.3" +pyyaml = "*" + +[package.extras] +dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] + [[package]] name = "yarl" version = "1.9.4" @@ -1109,4 +1218,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "b03cf32d60bad2a9dc751bb7195fd8a8abebbdaa596f869d6052ff58f2422f25" +content-hash = "a4bb8523eca6a6114d9369b39b1dd6247111a699db45674de5a35a9db62d1a0a" diff --git a/poetry.toml b/poetry.toml deleted file mode 100644 index 62e2dff..0000000 --- a/poetry.toml +++ /dev/null @@ -1,3 +0,0 @@ -[virtualenvs] -in-project = true -create = true diff --git a/pypaperless/helpers.py b/pypaperless/helpers.py deleted file mode 100644 index baeea71..0000000 --- a/pypaperless/helpers.py +++ /dev/null @@ -1,17 +0,0 @@ -"""PyPaperless helpers.""" - -# pylint: disable=unused-import - -from .models.base import HelperBase # noqa F401 -from .models.classifiers import CorrespondentHelper # noqa F401 -from .models.classifiers import DocumentTypeHelper, StoragePathHelper, TagHelper # noqa F401 -from .models.config import ConfigHelper # noqa F401 -from .models.custom_fields import CustomFieldHelper # noqa F401 -from .models.documents import DocumentHelper, DocumentMetaHelper, DocumentNoteHelper # noqa F401 -from .models.mails import MailAccountHelper, MailRuleHelper # noqa F401 -from .models.permissions import GroupHelper, UserHelper # noqa F401 -from .models.saved_views import SavedViewHelper # noqa F401 -from .models.share_links import ShareLinkHelper # noqa F401 -from .models.tasks import TaskHelper # noqa F401 -from .models.workflows import WorkflowHelper # noqa F401 -from .models.status import StatusHelper # noqa F401 diff --git a/pyproject.toml b/pyproject.toml index 2ae965f..dea7e70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ requires = ["poetry-core>=1.0.0"] [tool.poetry] name = "pypaperless" -version = "0.0.0" # will be set by ci on release -description = "Little api client for the paperless(-ngx) dms." +version = "0.0.0" +description = "Little api client for paperless(-ngx)." authors = ["Tobias Schulz "] maintainers = ["Tobias Schulz "] license = "MIT" @@ -25,7 +25,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules" ] packages = [ - { include = "pypaperless" }, + { include = "pypaperless", from = "src" }, ] [tool.poetry.dependencies] @@ -33,28 +33,22 @@ python = "^3.11" aiohttp = "^3.9.3" yarl = "^1.9.4" -[tool.poetry.group.dev.dependencies] +[tool.poetry.dev-dependencies] +aioresponses = "^0.7.6" black = "^24.1.1" codespell = "^2.2.6" covdefaults = "^2.3.0" coverage = {version = "^7.4.1", extras = ["toml"]} mypy = "^1.8.0" pre-commit = "^3.6.1" +pre-commit-hooks = "^4.5.0" pylint = "^3.0.3" pytest = "^8.0.0" pytest-aiohttp = "^1.0.5" pytest-asyncio = "^0.23.5" pytest-cov = "^4.1.0" -ruff = ">=0.2.1,<0.4.0" - -[tool.poetry.group.test.dependencies] -aioresponses = "^0.7.6" -covdefaults = "^2.3.0" -coverage = {version = "^7.4.1", extras = ["toml"]} -pytest = "^8.0.0" -pytest-aiohttp = "^1.0.5" -pytest-asyncio = "^0.23.5" -pytest-cov = "^4.1.0" +ruff = "^0.3.0" +yamllint = "^1.35.1" [tool.poetry.urls] "Homepage" = "https://github.com/tb1337/paperless-api" @@ -62,34 +56,20 @@ pytest-cov = "^4.1.0" "GitHub: Issues" = "https://github.com/tb1337/paperless-api/issues" "Coverage: codecov" = "https://codecov.io/gh/tb1337/paperless-api" -[tool.petry.report] -fail_under = 50 -show_missing = true - -[tool.black] -target-version = ['py312'] -line-length = 100 - [tool.coverage.run] plugins = ["covdefaults"] source = ["pypaperless"] [tool.coverage.report] -exclude_also = [ - "if TYPE_CHECKING:", -] -fail_under = 97 - -[tool.codespell] -skip = "poetry.lock" -ignore-words-list = "dependees," +fail_under = 95 +show_missing = true [tool.mypy] platform = "linux" python_version = "3.12" -exclude = ["tests/"] follow_imports = "normal" +ignore_missing_imports = true check_untyped_defs = true disallow_any_generics = false @@ -113,6 +93,21 @@ ignore = [ "tests/", ] +[tool.pylint.BASIC] +good-names = [ + "_", + "ex", + "fp", + "i", + "id", + "j", + "k", + "on", + "Run", + "T", + "wv", +] + [tool.pylint."MESSAGES CONTROL"] disable = [ "duplicate-code", @@ -123,40 +118,45 @@ disable = [ "too-many-public-methods", ] +[tool.pylint.SIMILARITIES] +ignore-imports = true + +[tool.pylint.FORMAT] +max-line-length = 100 + +[tool.pylint.DESIGN] +max-attributes = 20 + [tool.pytest.ini_options] addopts = "--cov" asyncio_mode = "auto" [tool.ruff] -fix = true -show-fixes = true line-length = 100 target-version = "py312" [tool.ruff.lint] -ignore = ["PLR2004", "N818"] -select = ["E", "F", "W", "I", "N", "D", "UP", "PL", "Q", "SIM", "TID", "ARG"] -unfixable = ["F841"] - -[tool.ruff.lint.flake8-annotations] -allow-star-arg-any = true -suppress-dummy-args = true +ignore = [ + "ANN101", # Self... explanatory + "ANN401", # Opinioated warning on disallowing dynamically typed expressions + "D203", # Conflicts with other rules + "D213", # Conflicts with other rules + "D417", # False positives in some occasions + "PLR2004", # Just annoying, not really useful + "RUF012", # Just annoying + + # Conflicts with the Ruff formatter + "COM812", + "ISC001", +] +select = ["ALL"] -[tool.ruff.lint.flake8-builtins] -builtins-ignorelist = ["id"] +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false [tool.ruff.lint.isort] known-first-party = ["pypaperless"] -force-sort-within-sections = true -split-on-trailing-comma = false -combine-as-imports = true - -[tool.ruff.lint.pydocstyle] -# Use Google-style docstrings. -convention = "pep257" - -[tool.ruff.lint.pylint] -max-branches=25 -max-returns=15 -max-args=10 -max-statements=50 + +[tool.ruff.lint.mccabe] +max-complexity = 25 diff --git a/script/bootstrap b/script/bootstrap deleted file mode 100755 index bfe5c2b..0000000 --- a/script/bootstrap +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# Resolve all dependencies that the application requires to run. - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -echo "Installing development dependencies..." - -poetry install --no-root --only main,dev diff --git a/script/run-in-env.sh b/script/run-in-env.sh deleted file mode 100755 index 271e7a4..0000000 --- a/script/run-in-env.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env sh -set -eu - -# Activate pyenv and virtualenv if present, then run the specified command - -# pyenv, pyenv-virtualenv -if [ -s .python-version ]; then - PYENV_VERSION=$(head -n 1 .python-version) - export PYENV_VERSION -fi - -# other common virtualenvs -my_path=$(git rev-parse --show-toplevel) - -for venv in venv .venv .; do - if [ -f "${my_path}/${venv}/bin/activate" ]; then - . "${my_path}/${venv}/bin/activate" - break - fi -done - -exec "$@" diff --git a/script/setup b/script/setup deleted file mode 100755 index 3e85ebe..0000000 --- a/script/setup +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# Setups the repository - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -# Add default vscode settings if not existing -SETTINGS_FILE=./.vscode/settings.json -SETTINGS_TEMPLATE_FILE=./.vscode/settings.default.json -if [ ! -f "$SETTINGS_FILE" ]; then - echo "Copy $SETTINGS_TEMPLATE_FILE to $SETTINGS_FILE." - cp "$SETTINGS_TEMPLATE_FILE" "$SETTINGS_FILE" -fi - -if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ];then - python3 -m venv .venv - source .venv/bin/activate -fi - -# Install package manager -pip install poetry - -# Bootstrap -script/bootstrap - -# Install pre-commit hooks -poetry run pre-commit install diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..2ee55c0 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,17 @@ +sonar.organization=tbsch +sonar.projectKey=tbsch_paperless-api +sonar.projectName=Little api client for paperless(-ngx). +sonar.projectVersion=1.0 + +sonar.links.homepage=https://github.com/tb1337/paperless-api +sonar.links.ci=https://github.com/tb1337/paperless-api/actions +sonar.links.issue=https://github.com/tb1337/paperless-api/issues +sonar.links.scm=https://github.com/tb1337/paperless-api/tree/main + +sonar.language=py +sonar.sourceEncoding=UTF-8 +sonar.sources=src +sonar.tests=tests + +sonar.python.version=3.11, 3.12 +sonar.python.coverage.reportPaths=coverage.xml diff --git a/pypaperless/__init__.py b/src/pypaperless/__init__.py similarity index 100% rename from pypaperless/__init__.py rename to src/pypaperless/__init__.py diff --git a/pypaperless/api.py b/src/pypaperless/api.py similarity index 91% rename from pypaperless/api.py rename to src/pypaperless/api.py index 04a2883..df0711e 100644 --- a/pypaperless/api.py +++ b/src/pypaperless/api.py @@ -1,8 +1,9 @@ """PyPaperless.""" +import logging from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -import logging +from json.decoder import JSONDecodeError from typing import Any import aiohttp @@ -10,15 +11,14 @@ from . import helpers from .const import API_PATH, PaperlessResource -from .exceptions import BadJsonResponse, JsonResponseWithError, PaperlessException, RequestException - -# from .sessions import PaperlessSession +from .exceptions import BadJsonResponseError, JsonResponseWithError, PaperlessError, RequestError +from .models.base import HelperBase class Paperless: """Retrieves and manipulates data from and to Paperless via REST.""" - _class_map: set[tuple[str, type]] = { + _helpers_map: set[tuple[str, type[HelperBase]]] = { (PaperlessResource.CONFIG, helpers.ConfigHelper), (PaperlessResource.CORRESPONDENTS, helpers.CorrespondentHelper), (PaperlessResource.CUSTOM_FIELDS, helpers.CustomFieldHelper), @@ -172,12 +172,14 @@ async def generate_api_token( Warning: error handling is low for this method, as it is just a helper. Example: + ------- ```python token = Paperless.generate_api_token("example.com:8000", "api_user", "secret_password") paperless = Paperless("example.com:8000", token) # do something ``` + """ external_session = session is not None session = session or aiohttp.ClientSession() @@ -191,12 +193,13 @@ async def generate_api_token( data = await res.json() res.raise_for_status() return str(data["token"]) - except KeyError as exc: - raise BadJsonResponse("Token is missing in response.") from exc + except (JSONDecodeError, KeyError) as exc: + message = "Token is missing in response." + raise BadJsonResponseError(message) from exc except aiohttp.ClientResponseError as exc: raise JsonResponseWithError(payload={"error": data}) from exc except Exception as exc: - raise exc + raise exc # noqa: TRY201 finally: if not external_session: await session.close() @@ -213,8 +216,8 @@ async def initialize(self) -> None: self._version = res.headers.get("x-version", None) self._remote_resources = set(map(PaperlessResource, await res.json())) - for endpoint, cls in self._class_map: - setattr(self, f"{endpoint}", cls(self)) + for attribute, helper in self._helpers_map: + setattr(self, f"{attribute}", helper(self)) unused = self._remote_resources.difference(self._local_resources) missing = self._local_resources.difference(self._remote_resources) @@ -232,7 +235,7 @@ async def initialize(self) -> None: self._initialized = True @asynccontextmanager - async def request( + async def request( # noqa: PLR0913 self, method: str, path: str, @@ -287,10 +290,10 @@ async def request( ) self.logger.debug("%s (%d): %s", method.upper(), res.status, res.url) yield res - except PaperlessException: + except PaperlessError: raise - except Exception as exc: - raise RequestException(exc, (method, url, params), kwargs) from None + except Exception as exc: # noqa: BLE001 + raise RequestError(exc, (method, url, params), kwargs) from None async def request_json( self, @@ -301,16 +304,16 @@ async def request_json( """Make a request to the api and parse response json to dict.""" async with self.request(method, endpoint, **kwargs) as res: try: - assert res.content_type == "application/json" + assert res.content_type == "application/json" # noqa: S101 payload = await res.json() if res.status == 400: - raise JsonResponseWithError(payload) + raise JsonResponseWithError(payload) # noqa: TRY301 res.raise_for_status() except (AssertionError, ValueError) as exc: - raise BadJsonResponse(res) from exc + raise BadJsonResponseError(res) from exc except Exception as exc: - raise exc + raise exc # noqa: TRY201 return payload diff --git a/pypaperless/const.py b/src/pypaperless/const.py similarity index 97% rename from pypaperless/const.py rename to src/pypaperless/const.py index 35927a3..21c87c8 100644 --- a/pypaperless/const.py +++ b/src/pypaperless/const.py @@ -101,6 +101,6 @@ class PaperlessResource(StrEnum): UNKNOWN = UNKNOWN @classmethod - def _missing_(cls: type[PaperlessResource], value: object) -> PaperlessResource: # noqa ARG003 + def _missing_(cls: type[PaperlessResource], *_: object) -> PaperlessResource: """Set default member on unknown value.""" return cls.UNKNOWN diff --git a/pypaperless/exceptions.py b/src/pypaperless/exceptions.py similarity index 81% rename from pypaperless/exceptions.py rename to src/pypaperless/exceptions.py index aae543a..3ca53dc 100644 --- a/pypaperless/exceptions.py +++ b/src/pypaperless/exceptions.py @@ -3,18 +3,18 @@ from typing import Any -class PaperlessException(Exception): +class PaperlessError(Exception): """Base exception for PyPaperless.""" # Sessions and requests -class AuthentificationRequired(PaperlessException): +class AuthentificationRequiredError(PaperlessError): """Raise when initializing a `Paperless` instance without url/token or session.""" -class RequestException(PaperlessException): +class RequestError(PaperlessError): """Raise when issuing a request fails.""" def __init__( @@ -33,11 +33,11 @@ def __init__( super().__init__(message) -class BadJsonResponse(PaperlessException): +class BadJsonResponseError(PaperlessError): """Raise when response is no valid json.""" -class JsonResponseWithError(PaperlessException): +class JsonResponseWithError(PaperlessError): """Raise when Paperless accepted the request, but responded with an error payload.""" def __init__(self, payload: Any) -> None: @@ -57,26 +57,26 @@ def __init__(self, payload: Any) -> None: # Models -class AsnRequestError(PaperlessException): +class AsnRequestError(PaperlessError): """Raise when getting an error during requesting the next asn.""" -class DraftFieldRequired(PaperlessException): +class DraftFieldRequiredError(PaperlessError): """Raise when trying to save models with missing required fields.""" -class DraftNotSupported(PaperlessException): +class DraftNotSupportedError(PaperlessError): """Raise when trying to draft unsupported models.""" -class PrimaryKeyRequired(PaperlessException): +class PrimaryKeyRequiredError(PaperlessError): """Raise when trying to access model data without supplying a pk.""" # Tasks -class TaskNotFound(PaperlessException): +class TaskNotFoundError(PaperlessError): """Raise when trying to access a task by non-existing uuid.""" def __init__(self, task_id: str) -> None: diff --git a/src/pypaperless/helpers.py b/src/pypaperless/helpers.py new file mode 100644 index 0000000..5a4e67a --- /dev/null +++ b/src/pypaperless/helpers.py @@ -0,0 +1,21 @@ +"""PyPaperless helpers.""" + +# pylint: disable=unused-import + +from .models.base import HelperBase # noqa: F401 +from .models.classifiers import ( # noqa: F401 + CorrespondentHelper, + DocumentTypeHelper, + StoragePathHelper, + TagHelper, +) +from .models.config import ConfigHelper # noqa: F401 +from .models.custom_fields import CustomFieldHelper # noqa: F401 +from .models.documents import DocumentHelper, DocumentMetaHelper, DocumentNoteHelper # noqa: F401 +from .models.mails import MailAccountHelper, MailRuleHelper # noqa: F401 +from .models.permissions import GroupHelper, UserHelper # noqa: F401 +from .models.saved_views import SavedViewHelper # noqa: F401 +from .models.share_links import ShareLinkHelper # noqa: F401 +from .models.status import StatusHelper # noqa: F401 +from .models.tasks import TaskHelper # noqa: F401 +from .models.workflows import WorkflowHelper # noqa: F401 diff --git a/pypaperless/models/__init__.py b/src/pypaperless/models/__init__.py similarity index 90% rename from pypaperless/models/__init__.py rename to src/pypaperless/models/__init__.py index 63b7dd5..2c1bb4e 100644 --- a/pypaperless/models/__init__.py +++ b/src/pypaperless/models/__init__.py @@ -12,7 +12,13 @@ ) from .config import Config from .custom_fields import CustomField, CustomFieldDraft -from .documents import Document, DocumentDraft, DocumentMeta, DocumentNote, DocumentNoteDraft +from .documents import ( + Document, + DocumentDraft, + DocumentMeta, + DocumentNote, + DocumentNoteDraft, +) from .mails import MailAccount, MailRule from .pages import Page from .permissions import Group, User diff --git a/pypaperless/models/base.py b/src/pypaperless/models/base.py similarity index 92% rename from pypaperless/models/base.py rename to src/pypaperless/models/base.py index a83bb52..e398145 100644 --- a/pypaperless/models/base.py +++ b/src/pypaperless/models/base.py @@ -18,7 +18,7 @@ class PaperlessBase: _api_path = API_PATH["index"] - def __init__(self, api: "Paperless"): + def __init__(self, api: "Paperless") -> None: """Initialize a `PaperlessBase` instance.""" self._api = api @@ -37,7 +37,7 @@ class HelperBase(PaperlessBase, Generic[ResourceT]): _resource: PaperlessResource - def __init__(self, api: "Paperless"): + def __init__(self, api: "Paperless") -> None: """Initialize a `HelperBase` instance.""" super().__init__(api) @@ -69,7 +69,7 @@ def _set_dataclass_fields(self) -> None: ... class PaperlessModel(PaperlessBase): """Base class for all models in PyPaperless.""" - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `PaperlessModel` instance.""" super().__init__(api) self._data = {} @@ -83,6 +83,7 @@ def create_with_data( cls: type[ResourceT], api: "Paperless", data: dict[str, Any], + *, fetched: bool = False, ) -> ResourceT: """Return a new instance of `cls` from `data`. @@ -92,9 +93,10 @@ def create_with_data( Example: `document = Document.create_with_data(...)` """ item = cls(api, data=data) - item._fetched = fetched + + item._fetched = fetched # noqa: SLF001 if fetched: - item._set_dataclass_fields() + item._set_dataclass_fields() # noqa: SLF001 return item @final diff --git a/pypaperless/models/classifiers.py b/src/pypaperless/models/classifiers.py similarity index 95% rename from pypaperless/models/classifiers.py rename to src/pypaperless/models/classifiers.py index 1ddff6e..6e6dfa1 100644 --- a/pypaperless/models/classifiers.py +++ b/src/pypaperless/models/classifiers.py @@ -1,7 +1,7 @@ """Provide `Correspondent`, `DocumentType`, `StoragePath` and `Tag` related models and helpers.""" -from dataclasses import dataclass import datetime +from dataclasses import dataclass from typing import TYPE_CHECKING, Any from pypaperless.const import API_PATH, PaperlessResource @@ -31,7 +31,7 @@ class Correspondent( document_count: int | None = None last_correspondence: datetime.datetime | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Correspondent` instance.""" super().__init__(api, data) @@ -76,7 +76,7 @@ class DocumentType( name: str | None = None document_count: int | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `DocumentType` instance.""" super().__init__(api, data) @@ -123,7 +123,7 @@ class StoragePath( path: str | None = None document_count: int | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `StoragePath` instance.""" super().__init__(api, data) @@ -174,7 +174,7 @@ class Tag( is_inbox_tag: bool | None = None document_count: int | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Tag` instance.""" super().__init__(api, data) diff --git a/pypaperless/models/common.py b/src/pypaperless/models/common.py similarity index 88% rename from pypaperless/models/common.py rename to src/pypaperless/models/common.py index 8e764a0..3ba3d58 100644 --- a/pypaperless/models/common.py +++ b/src/pypaperless/models/common.py @@ -1,7 +1,7 @@ """PyPaperless common types.""" -from dataclasses import dataclass, field import datetime +from dataclasses import dataclass, field from enum import Enum, StrEnum from typing import Any @@ -21,7 +21,7 @@ class CustomFieldType(Enum): UNKNOWN = "unknown" @classmethod - def _missing_(cls: type, value: object) -> "CustomFieldType": # noqa ARG003 + def _missing_(cls: type, *_: object) -> "CustomFieldType": """Set default member on unknown value.""" return CustomFieldType.UNKNOWN @@ -71,7 +71,7 @@ class MatchingAlgorithmType(Enum): UNKNOWN = -1 @classmethod - def _missing_(cls: type, value: object) -> "MatchingAlgorithmType": # noqa ARG003 + def _missing_(cls: type, *_: object) -> "MatchingAlgorithmType": """Set default member on unknown value.""" return MatchingAlgorithmType.UNKNOWN @@ -121,7 +121,7 @@ class ShareLinkFileVersionType(Enum): UNKNOWN = "unknown" @classmethod - def _missing_(cls: type, value: object) -> "ShareLinkFileVersionType": # noqa ARG003 + def _missing_(cls: type, *_: object) -> "ShareLinkFileVersionType": """Set default member on unknown value.""" return ShareLinkFileVersionType.UNKNOWN @@ -135,7 +135,7 @@ class StatusType(Enum): UNKNOWN = "UNKNOWN" @classmethod - def _missing_(cls: type, value: object) -> "StatusType": # noqa ARG003 + def _missing_(cls: type, *_: object) -> "StatusType": """Set default member on unknown value.""" return StatusType.UNKNOWN @@ -197,7 +197,7 @@ class TaskStatusType(Enum): UNKNOWN = "UNKNOWN" @classmethod - def _missing_(cls: type, value: object) -> "TaskStatusType": # noqa ARG003 + def _missing_(cls: type, *_: object) -> "TaskStatusType": """Set default member on unknown value.""" return TaskStatusType.UNKNOWN @@ -210,7 +210,7 @@ class WorkflowActionType(Enum): UNKNOWN = -1 @classmethod - def _missing_(cls: type, value: object) -> "WorkflowActionType": # noqa ARG003 + def _missing_(cls: type, *_: object) -> "WorkflowActionType": """Set default member on unknown value.""" return WorkflowActionType.UNKNOWN @@ -225,7 +225,7 @@ class WorkflowTriggerType(Enum): UNKNOWN = -1 @classmethod - def _missing_(cls: type, value: object) -> "WorkflowTriggerType": # noqa ARG003 + def _missing_(cls: type, *_: object) -> "WorkflowTriggerType": """Set default member on unknown value.""" return WorkflowTriggerType.UNKNOWN @@ -240,6 +240,6 @@ class WorkflowTriggerSourceType(Enum): UNKNOWN = -1 @classmethod - def _missing_(cls: type, value: object) -> "WorkflowTriggerSourceType": # noqa ARG003 + def _missing_(cls: type, *_: object) -> "WorkflowTriggerSourceType": """Set default member on unknown value.""" return WorkflowTriggerSourceType.UNKNOWN diff --git a/pypaperless/models/config.py b/src/pypaperless/models/config.py similarity index 95% rename from pypaperless/models/config.py rename to src/pypaperless/models/config.py index 99d0416..4db27b1 100644 --- a/pypaperless/models/config.py +++ b/src/pypaperless/models/config.py @@ -37,7 +37,7 @@ class Config( app_title: str | None = None app_logo: str | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Config` instance.""" super().__init__(api, data) diff --git a/pypaperless/models/custom_fields.py b/src/pypaperless/models/custom_fields.py similarity index 95% rename from pypaperless/models/custom_fields.py rename to src/pypaperless/models/custom_fields.py index 459da70..4f2c5d7 100644 --- a/pypaperless/models/custom_fields.py +++ b/src/pypaperless/models/custom_fields.py @@ -27,7 +27,7 @@ class CustomField( name: str | None = None data_type: CustomFieldType | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Document` instance.""" super().__init__(api, data) diff --git a/pypaperless/models/documents.py b/src/pypaperless/models/documents.py similarity index 90% rename from pypaperless/models/documents.py rename to src/pypaperless/models/documents.py index 70a68a3..c9a866d 100644 --- a/pypaperless/models/documents.py +++ b/src/pypaperless/models/documents.py @@ -1,12 +1,12 @@ """Provide `Document` related models and helpers.""" +import datetime from collections.abc import AsyncGenerator from dataclasses import dataclass -import datetime from typing import TYPE_CHECKING, Any, cast from pypaperless.const import API_PATH, PaperlessResource -from pypaperless.exceptions import AsnRequestError, PrimaryKeyRequired +from pypaperless.exceptions import AsnRequestError, PrimaryKeyRequiredError from pypaperless.models.utils import object_to_dict_value from .base import HelperBase, PaperlessModel @@ -51,7 +51,7 @@ class Document( custom_fields: list[CustomFieldValueType] | None = None __search_hit__: DocumentSearchHitType | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Document` instance.""" super().__init__(api, data) @@ -68,30 +68,25 @@ def search_hit(self) -> DocumentSearchHitType | None: """Return the document search hit.""" return self.__search_hit__ - async def get_download(self, original: bool = False) -> "DownloadedDocument": + async def get_download(self, *, original: bool = False) -> "DownloadedDocument": """Request and return the `DownloadedDocument` class.""" - item = await self._api.documents.download(cast(int, self.id), original) - return item + return await self._api.documents.download(cast(int, self.id), original=original) async def get_metadata(self) -> "DocumentMeta": """Request and return the documents `DocumentMeta` class.""" - item = await self._api.documents.metadata(cast(int, self.id)) - return item + return await self._api.documents.metadata(cast(int, self.id)) - async def get_preview(self, original: bool = False) -> "DownloadedDocument": + async def get_preview(self, *, original: bool = False) -> "DownloadedDocument": """Request and return the `DownloadedDocument` class.""" - item = await self._api.documents.preview(cast(int, self.id), original) - return item + return await self._api.documents.preview(cast(int, self.id), original=original) async def get_suggestions(self) -> "DocumentSuggestions": """Request and return the `DocumentSuggestions` class.""" - item = await self._api.documents.suggestions(cast(int, self.id)) - return item + return await self._api.documents.suggestions(cast(int, self.id)) - async def get_thumbnail(self, original: bool = False) -> "DownloadedDocument": + async def get_thumbnail(self, *, original: bool = False) -> "DownloadedDocument": """Request and return the `DownloadedDocument` class.""" - item = await self._api.documents.thumbnail(cast(int, self.id), original) - return item + return await self._api.documents.thumbnail(cast(int, self.id), original=original) @dataclass(init=False) @@ -146,7 +141,7 @@ class DocumentNote(PaperlessModel): document: int | None = None user: int | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `DocumentNote` instance.""" super().__init__(api, data) @@ -158,6 +153,7 @@ async def delete(self) -> bool: Return `True` when deletion was successful, `False` otherwise. Example: + ------- ```python # request document notes notes = await paperless.documents.notes(42) @@ -166,14 +162,13 @@ async def delete(self) -> bool: if await note.delete(): print("Successfully deleted the note!") ``` + """ params = { "id": self.id, } async with self._api.request("delete", self._api_path, params=params) as res: - success = res.status == 204 - - return success + return res.status == 204 @dataclass(kw_only=True) @@ -190,7 +185,7 @@ class DocumentNoteDraft( note: str | None = None document: int | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `DocumentNote` instance.""" super().__init__(api, data) @@ -217,7 +212,7 @@ class DocumentMeta(PaperlessModel): archive_size: int | None = None archive_metadata: list[DocumentMetadataType] | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `DocumentMeta` instance.""" super().__init__(api, data) @@ -279,7 +274,7 @@ class DocumentSuggestions(PaperlessModel): storage_paths: list[int] | None = None dates: list[datetime.date] | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `DocumentSuggestions` instance.""" super().__init__(api, data) @@ -318,9 +313,10 @@ class DocumentSubHelperBase( async def __call__( self, pk: int, - original: bool, mode: RetrieveFileMode, api_path: str, + *, + original: bool, ) -> DownloadedDocument: """Request exactly one resource item.""" data = { @@ -329,7 +325,7 @@ async def __call__( "original": original, } item = self._resource_cls.create_with_data(self._api, data) - item._api_path = api_path + item._api_path = api_path # noqa: SLF001 await item.load() return item @@ -343,10 +339,13 @@ class DocumentFileDownloadHelper(DocumentSubHelperBase): async def __call__( # type: ignore[override] self, pk: int, + *, original: bool = False, ) -> DownloadedDocument: """Request exactly one resource item.""" - return await super().__call__(pk, original, RetrieveFileMode.DOWNLOAD, self._api_path) + return await super().__call__( + pk, RetrieveFileMode.DOWNLOAD, self._api_path, original=original + ) class DocumentFilePreviewHelper(DocumentSubHelperBase): @@ -357,10 +356,13 @@ class DocumentFilePreviewHelper(DocumentSubHelperBase): async def __call__( # type: ignore[override] self, pk: int, + *, original: bool = False, ) -> DownloadedDocument: """Request exactly one resource item.""" - return await super().__call__(pk, original, RetrieveFileMode.PREVIEW, self._api_path) + return await super().__call__( + pk, RetrieveFileMode.PREVIEW, self._api_path, original=original + ) class DocumentFileThumbnailHelper(DocumentSubHelperBase): @@ -371,10 +373,13 @@ class DocumentFileThumbnailHelper(DocumentSubHelperBase): async def __call__( # type: ignore[override] self, pk: int, + *, original: bool = False, ) -> DownloadedDocument: """Request exactly one resource item.""" - return await super().__call__(pk, original, RetrieveFileMode.THUMBNAIL, self._api_path) + return await super().__call__( + pk, RetrieveFileMode.THUMBNAIL, self._api_path, original=original + ) class DocumentMetaHelper( @@ -435,7 +440,8 @@ async def __call__( def _get_document_pk(self, pk: int | None = None) -> int: """Return the attached document pk, or the parameter.""" if not any((self._attached_to, pk)): - raise PrimaryKeyRequired(f"Accessing {type(self).__name__} data without a primary key.") + message = f"Accessing {type(self).__name__} data without a primary key." + raise PrimaryKeyRequiredError(message) return cast(int, self._attached_to or pk) def _get_api_path(self, pk: int) -> str: @@ -446,10 +452,12 @@ def draft(self, pk: int | None = None, **kwargs: Any) -> DocumentNoteDraft: """Return a fresh and empty `DocumentNoteDraft` instance. Example: + ------- ```python draft = paperless.documents.notes.draft(...) # do something ``` + """ kwargs.update({"document": self._get_document_pk(pk)}) return DocumentNoteDraft.create_with_data( @@ -490,6 +498,7 @@ def download(self) -> DocumentFileDownloadHelper: """Download the contents of an archived file. Example: + ------- ```python # request document contents directly... download = await paperless.documents.download(42) @@ -499,6 +508,7 @@ def download(self) -> DocumentFileDownloadHelper: download = await doc.get_download() ``` + """ return self._download @@ -507,6 +517,7 @@ def metadata(self) -> DocumentMetaHelper: """Return the attached `DocumentMetaHelper` instance. Example: + ------- ```python # request metadata of a document directly... metadata = await paperless.documents.metadata(42) @@ -515,6 +526,7 @@ def metadata(self) -> DocumentMetaHelper: doc = await paperless.documents(42) metadata = await doc.get_metadata() ``` + """ return self._meta @@ -523,6 +535,7 @@ def notes(self) -> DocumentNoteHelper: """Return the attached `DocumentNoteHelper` instance. Example: + ------- ```python # request document notes directly... notes = await paperless.documents.notes(42) @@ -531,6 +544,7 @@ def notes(self) -> DocumentNoteHelper: doc = await paperless.documents(42) notes = await doc.notes() ``` + """ return self._notes @@ -539,6 +553,7 @@ def preview(self) -> DocumentFilePreviewHelper: """Preview the contents of an archived file. Example: + ------- ```python # request document contents directly... download = await paperless.documents.preview(42) @@ -548,6 +563,7 @@ def preview(self) -> DocumentFilePreviewHelper: download = await doc.get_preview() ``` + """ return self._preview @@ -556,6 +572,7 @@ def suggestions(self) -> DocumentSuggestionsHelper: """Return the attached `DocumentSuggestionsHelper` instance. Example: + ------- ```python # request document suggestions directly... suggestions = await paperless.documents.suggestions(42) @@ -565,6 +582,7 @@ def suggestions(self) -> DocumentSuggestionsHelper: suggestions = await doc.get_suggestions() ``` + """ return self._suggestions @@ -573,6 +591,7 @@ def thumbnail(self) -> DocumentFileThumbnailHelper: """Download the contents of a thumbnail file. Example: + ------- ```python # request document contents directly... download = await paperless.documents.thumbnail(42) @@ -582,6 +601,7 @@ def thumbnail(self) -> DocumentFileThumbnailHelper: download = await doc.get_thumbnail() ``` + """ return self._thumbnail @@ -591,7 +611,7 @@ async def get_next_asn(self) -> int: try: res.raise_for_status() return int(await res.text()) - except Exception as exc: + except Exception as exc: # noqa: BLE001 raise AsnRequestError from exc async def more_like(self, pk: int) -> AsyncGenerator[Document, None]: diff --git a/pypaperless/models/generators/__init__.py b/src/pypaperless/models/generators/__init__.py similarity index 100% rename from pypaperless/models/generators/__init__.py rename to src/pypaperless/models/generators/__init__.py diff --git a/pypaperless/models/generators/page.py b/src/pypaperless/models/generators/page.py similarity index 89% rename from pypaperless/models/generators/page.py rename to src/pypaperless/models/generators/page.py index 2f9b7ea..c852c21 100644 --- a/pypaperless/models/generators/page.py +++ b/src/pypaperless/models/generators/page.py @@ -2,7 +2,7 @@ from collections.abc import AsyncIterator from copy import deepcopy -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self from pypaperless.models.base import PaperlessBase from pypaperless.models.pages import Page @@ -22,7 +22,7 @@ class PageGenerator(PaperlessBase, AsyncIterator): _page: Page | None - def __aiter__(self) -> AsyncIterator: + def __aiter__(self) -> Self: """Return self as iterator.""" return self @@ -39,7 +39,8 @@ async def __anext__(self) -> Page: "page_size": self.params["page_size"], } self._page = Page.create_with_data(self._api, data, fetched=True) - self._page._resource_cls = self._resource_cls # attach the resource to the data class + # dirty attach the resource to the data class + self._page._resource_cls = self._resource_cls # noqa: SLF001 # rise page by one to request next page on next iteration self.params["page"] += 1 @@ -53,7 +54,7 @@ def __init__( url: str, resource_cls: type, params: dict[str, Any] | None = None, - ): + ) -> None: """Initialize `PageGenerator` class instance.""" super().__init__(api) diff --git a/pypaperless/models/mails.py b/src/pypaperless/models/mails.py similarity index 93% rename from pypaperless/models/mails.py rename to src/pypaperless/models/mails.py index 760d805..e394ee6 100644 --- a/pypaperless/models/mails.py +++ b/src/pypaperless/models/mails.py @@ -28,11 +28,11 @@ class MailAccount( imap_security: int | None = None username: str | None = None # exclude that from the dataclass - # password: str | None = None + # password: str | None = None # noqa: ERA001 character_set: str | None = None is_token: bool | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `MailAccount` instance.""" super().__init__(api, data) @@ -71,7 +71,7 @@ class MailRule( attachment_type: int | None = None consumption_scope: int | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `MailRule` instance.""" super().__init__(api, data) diff --git a/pypaperless/models/mixins/__init__.py b/src/pypaperless/models/mixins/__init__.py similarity index 100% rename from pypaperless/models/mixins/__init__.py rename to src/pypaperless/models/mixins/__init__.py diff --git a/pypaperless/models/mixins/helpers/__init__.py b/src/pypaperless/models/mixins/helpers/__init__.py similarity index 100% rename from pypaperless/models/mixins/helpers/__init__.py rename to src/pypaperless/models/mixins/helpers/__init__.py diff --git a/pypaperless/models/mixins/helpers/callable.py b/src/pypaperless/models/mixins/helpers/callable.py similarity index 90% rename from pypaperless/models/mixins/helpers/callable.py rename to src/pypaperless/models/mixins/helpers/callable.py index 62aeef2..bae9fba 100644 --- a/pypaperless/models/mixins/helpers/callable.py +++ b/src/pypaperless/models/mixins/helpers/callable.py @@ -11,11 +11,13 @@ class CallableMixin(HelperProtocol[ResourceT]): async def __call__( self, pk: int, + *, lazy: bool = False, ) -> ResourceT: """Request exactly one resource item. Example: + ------- ```python # request a document document = await paperless.documents(42) @@ -23,6 +25,7 @@ async def __call__( # initialize a model and request it later document = await paperless.documents(42, lazy=True) ``` + """ data = { "id": pk, @@ -31,7 +34,7 @@ async def __call__( # set requesting full permissions if SecurableMixin in type(self).__bases__ and getattr(self, "_request_full_perms", False): - item._params.update({"full_perms": "true"}) + item._params.update({"full_perms": "true"}) # noqa: SLF001 if not lazy: await item.load() diff --git a/pypaperless/models/mixins/helpers/draftable.py b/src/pypaperless/models/mixins/helpers/draftable.py similarity index 72% rename from pypaperless/models/mixins/helpers/draftable.py rename to src/pypaperless/models/mixins/helpers/draftable.py index 16852d8..87aa63a 100644 --- a/pypaperless/models/mixins/helpers/draftable.py +++ b/src/pypaperless/models/mixins/helpers/draftable.py @@ -2,7 +2,7 @@ from typing import Any -from pypaperless.exceptions import DraftNotSupported +from pypaperless.exceptions import DraftNotSupportedError from pypaperless.models.base import HelperProtocol, ResourceT @@ -15,15 +15,16 @@ def draft(self, **kwargs: Any) -> ResourceT: """Return a fresh and empty `PaperlessModel` instance. Example: + ------- ```python draft = paperless.documents.draft(document=bytes(...), title="New Document") # do something ``` + """ if not hasattr(self, "_draft_cls"): - raise DraftNotSupported("Helper class has no _draft_cls attribute.") + message = "Helper class has no _draft_cls attribute." + raise DraftNotSupportedError(message) kwargs.update({"id": -1}) - item = self._draft_cls.create_with_data(self._api, data=kwargs, fetched=True) - - return item + return self._draft_cls.create_with_data(self._api, data=kwargs, fetched=True) diff --git a/pypaperless/models/mixins/helpers/iterable.py b/src/pypaperless/models/mixins/helpers/iterable.py similarity index 98% rename from pypaperless/models/mixins/helpers/iterable.py rename to src/pypaperless/models/mixins/helpers/iterable.py index d1a5a0d..ce28fde 100644 --- a/pypaperless/models/mixins/helpers/iterable.py +++ b/src/pypaperless/models/mixins/helpers/iterable.py @@ -20,10 +20,12 @@ async def __aiter__(self) -> AsyncIterator[ResourceT]: """Iterate over resource items. Example: + ------- ```python async for item in paperless.documents: # do something ``` + """ async for page in self.pages(): for item in page: @@ -40,6 +42,7 @@ async def reduce( You can provide `page` and `page_size` parameters, as well. Example: + ------- ```python filters = { "page_size": 1337, @@ -55,6 +58,7 @@ async def reduce( async for page in paperless.documents.pages(): ... ``` + """ self._aiter_filters = kwargs yield self @@ -79,10 +83,12 @@ def pages( `page_size`: The page size for each requested batch. Example: + ------- ```python async for item in paperless.documents.pages(): # do something ``` + """ params = getattr(self, "_aiter_filters", None) or {} params.setdefault("page", page) diff --git a/pypaperless/models/mixins/helpers/securable.py b/src/pypaperless/models/mixins/helpers/securable.py similarity index 100% rename from pypaperless/models/mixins/helpers/securable.py rename to src/pypaperless/models/mixins/helpers/securable.py diff --git a/pypaperless/models/mixins/models/__init__.py b/src/pypaperless/models/mixins/models/__init__.py similarity index 100% rename from pypaperless/models/mixins/models/__init__.py rename to src/pypaperless/models/mixins/models/__init__.py diff --git a/pypaperless/models/mixins/models/creatable.py b/src/pypaperless/models/mixins/models/creatable.py similarity index 89% rename from pypaperless/models/mixins/models/creatable.py rename to src/pypaperless/models/mixins/models/creatable.py index 3828cb7..d8334c7 100644 --- a/pypaperless/models/mixins/models/creatable.py +++ b/src/pypaperless/models/mixins/models/creatable.py @@ -2,7 +2,7 @@ from typing import Any, cast -from pypaperless.exceptions import DraftFieldRequired +from pypaperless.exceptions import DraftFieldRequiredError from pypaperless.models.base import PaperlessModelProtocol from pypaperless.models.utils import object_to_dict_value @@ -18,6 +18,7 @@ async def save(self) -> int | str | tuple[int, int]: Return the created item `id`, or a `task_id` in case of documents. Example: + ------- ```python draft = paperless.documents.draft(document=bytes(...)) draft.title = "Add a title" @@ -25,6 +26,7 @@ async def save(self) -> int | str | tuple[int, int]: # request Paperless to store the new item draft.save() ``` + """ self.validate() kwdict = self._serialize() @@ -59,6 +61,6 @@ def validate(self) -> None: if len(missing) == 0: return - raise DraftFieldRequired( - f"Missing fields for saving a `{type(self).__name__}`: {', '.join(missing)}." - ) + + message = f"Missing fields for saving a `{type(self).__name__}`: {', '.join(missing)}." + raise DraftFieldRequiredError(message) diff --git a/pypaperless/models/mixins/models/data_fields.py b/src/pypaperless/models/mixins/models/data_fields.py similarity index 100% rename from pypaperless/models/mixins/models/data_fields.py rename to src/pypaperless/models/mixins/models/data_fields.py diff --git a/pypaperless/models/mixins/models/deletable.py b/src/pypaperless/models/mixins/models/deletable.py similarity index 91% rename from pypaperless/models/mixins/models/deletable.py rename to src/pypaperless/models/mixins/models/deletable.py index 08cf3f3..9672b02 100644 --- a/pypaperless/models/mixins/models/deletable.py +++ b/src/pypaperless/models/mixins/models/deletable.py @@ -12,6 +12,7 @@ async def delete(self) -> bool: Return `True` when deletion was successful, `False` otherwise. Example: + ------- ```python # request a document document = await paperless.documents(42) @@ -19,8 +20,7 @@ async def delete(self) -> bool: if await document.delete(): print("Successfully deleted the document!") ``` + """ async with self._api.request("delete", self._api_path) as res: - success = res.status == 204 - - return success + return res.status == 204 diff --git a/pypaperless/models/mixins/models/securable.py b/src/pypaperless/models/mixins/models/securable.py similarity index 100% rename from pypaperless/models/mixins/models/securable.py rename to src/pypaperless/models/mixins/models/securable.py diff --git a/pypaperless/models/mixins/models/updatable.py b/src/pypaperless/models/mixins/models/updatable.py similarity index 96% rename from pypaperless/models/mixins/models/updatable.py rename to src/pypaperless/models/mixins/models/updatable.py index 7c19c5c..70f340f 100644 --- a/pypaperless/models/mixins/models/updatable.py +++ b/src/pypaperless/models/mixins/models/updatable.py @@ -14,12 +14,13 @@ class UpdatableMixin(PaperlessModelProtocol): _data: dict[str, Any] - async def update(self, only_changed: bool = True) -> bool: + async def update(self, *, only_changed: bool = True) -> bool: """Send actually changed `model data` to DRF. Return `True` when any attribute was updated, `False` otherwise. Example: + ------- ```python # request a document document = await paperless.documents(42) @@ -28,6 +29,7 @@ async def update(self, only_changed: bool = True) -> bool: if await document.update(): print("Successfully updated a field!") ``` + """ updated = False diff --git a/pypaperless/models/pages.py b/src/pypaperless/models/pages.py similarity index 99% rename from pypaperless/models/pages.py rename to src/pypaperless/models/pages.py index 3bcd48f..44d57ef 100644 --- a/pypaperless/models/pages.py +++ b/src/pypaperless/models/pages.py @@ -1,8 +1,8 @@ """Provide the `Paginated` class.""" +import math from collections.abc import Iterator from dataclasses import dataclass, field -import math from typing import Any, Generic from pypaperless.const import API_PATH @@ -56,11 +56,13 @@ def items(self) -> list[ResourceT]: """Return the results list field with mapped PyPaperless `models`. Example: + ------- ```python async for page in paperless.documents.pages(): assert isinstance(page.results.pop(), Document) # fails, it is a dict assert isinstance(page.items.pop(), Document) # ok ``` + """ def mapper(data: dict[str, Any]) -> ResourceT: diff --git a/pypaperless/models/permissions.py b/src/pypaperless/models/permissions.py similarity index 91% rename from pypaperless/models/permissions.py rename to src/pypaperless/models/permissions.py index 44b93e4..6f5e3e8 100644 --- a/pypaperless/models/permissions.py +++ b/src/pypaperless/models/permissions.py @@ -1,7 +1,7 @@ """Provide `User` and 'Group' related models and helpers.""" -from dataclasses import dataclass import datetime +from dataclasses import dataclass from typing import TYPE_CHECKING, Any from pypaperless.const import API_PATH, PaperlessResource @@ -23,7 +23,7 @@ class Group(PaperlessModel): name: str | None = None permissions: list[str] | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Group` instance.""" super().__init__(api, data) @@ -39,7 +39,7 @@ class User(PaperlessModel): id: int username: str | None = None # exclude that from the dataclass - # password: str | None = None + # password: str | None = None # noqa: ERA001 email: str | None = None first_name: str | None = None last_name: str | None = None @@ -51,7 +51,7 @@ class User(PaperlessModel): user_permissions: list[str] | None = None inherited_permissions: list[str] | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `User` instance.""" super().__init__(api, data) diff --git a/pypaperless/models/saved_views.py b/src/pypaperless/models/saved_views.py similarity index 94% rename from pypaperless/models/saved_views.py rename to src/pypaperless/models/saved_views.py index 395039c..3251844 100644 --- a/pypaperless/models/saved_views.py +++ b/src/pypaperless/models/saved_views.py @@ -30,7 +30,7 @@ class SavedView( sort_reverse: bool | None = None filter_rules: list[SavedViewFilterRuleType] | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `SavedView` instance.""" super().__init__(api, data) diff --git a/pypaperless/models/share_links.py b/src/pypaperless/models/share_links.py similarity index 96% rename from pypaperless/models/share_links.py rename to src/pypaperless/models/share_links.py index 55b0a0c..15987b2 100644 --- a/pypaperless/models/share_links.py +++ b/src/pypaperless/models/share_links.py @@ -1,7 +1,7 @@ """Provide `ShareLink` related models and helpers.""" -from dataclasses import dataclass import datetime +from dataclasses import dataclass from typing import TYPE_CHECKING, Any from pypaperless.const import API_PATH, PaperlessResource @@ -31,7 +31,7 @@ class ShareLink( document: int | None = None file_version: ShareLinkFileVersionType | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `MailAccount` instance.""" super().__init__(api, data) diff --git a/pypaperless/models/status.py b/src/pypaperless/models/status.py similarity index 91% rename from pypaperless/models/status.py rename to src/pypaperless/models/status.py index ee10ff1..76d9db1 100644 --- a/pypaperless/models/status.py +++ b/src/pypaperless/models/status.py @@ -43,7 +43,7 @@ def has_errors(self) -> bool: ], ] - return any(map(lambda st: st == StatusType.ERROR, statuses)) + return any(st == StatusType.ERROR for st in statuses) class StatusHelper(HelperBase[Status]): @@ -57,6 +57,4 @@ class StatusHelper(HelperBase[Status]): async def __call__(self) -> Status: """Request the `Status` model data.""" res = await self._api.request_json("get", self._api_path) - item = self._resource_cls.create_with_data(self._api, res, fetched=True) - - return item + return self._resource_cls.create_with_data(self._api, res, fetched=True) diff --git a/pypaperless/models/tasks.py b/src/pypaperless/models/tasks.py similarity index 92% rename from pypaperless/models/tasks.py rename to src/pypaperless/models/tasks.py index 191ed12..f2fe61f 100644 --- a/pypaperless/models/tasks.py +++ b/src/pypaperless/models/tasks.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any from pypaperless.const import API_PATH, PaperlessResource -from pypaperless.exceptions import TaskNotFound +from pypaperless.exceptions import TaskNotFoundError from .base import HelperBase, PaperlessModel from .common import TaskStatusType @@ -33,7 +33,7 @@ class Task( acknowledged: bool | None = None related_document: int | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Task` instance.""" super().__init__(api, data) @@ -54,10 +54,12 @@ async def __aiter__(self) -> AsyncIterator[Task]: """Iterate over task items. Example: + ------- ```python async for task in paperless.tasks: # do something ``` + """ res = await self._api.request_json("get", self._api_path) for data in res: @@ -70,10 +72,12 @@ async def __call__(self, task_id: int | str) -> Task: If task_id is `int`: interpret it as a primary key. Example: + ------- ```python task = await paperless.tasks("uuid-string") task = await paperless.tasks(1337) ``` + """ if isinstance(task_id, str): params = { @@ -83,7 +87,7 @@ async def __call__(self, task_id: int | str) -> Task: try: item = self._resource_cls.create_with_data(self._api, res.pop(), fetched=True) except IndexError as exc: - raise TaskNotFound(task_id) from exc + raise TaskNotFoundError(task_id) from exc else: data = { "id": task_id, diff --git a/pypaperless/models/utils/__init__.py b/src/pypaperless/models/utils/__init__.py similarity index 93% rename from pypaperless/models/utils/__init__.py rename to src/pypaperless/models/utils/__init__.py index dfd240e..197b742 100644 --- a/pypaperless/models/utils/__init__.py +++ b/src/pypaperless/models/utils/__init__.py @@ -1,5 +1,4 @@ -""" -Utils for pypaperless models. +"""Utils for pypaperless models. Since there are common use-cases in transforming dicts to dataclass et vice-versa, we borrowed some snippets from aiohue instead of re-inventing the wheel. @@ -15,10 +14,10 @@ # mypy: ignore-errors # pylint: disable=all +import logging from dataclasses import MISSING, asdict, fields, is_dataclass from datetime import date, datetime from enum import Enum -import logging from types import NoneType, UnionType from typing import TYPE_CHECKING, Any, Union, get_args, get_origin, get_type_hints @@ -28,17 +27,17 @@ from pypaperless import Paperless -def _str_to_datetime(datetimestr: str): +def _str_to_datetime(datetimestr: str) -> datetime: """Parse datetime from string.""" return datetime.fromisoformat(datetimestr.replace("Z", "+00:00")) -def _str_to_date(datestr: str): +def _str_to_date(datestr: str) -> date: """Parse date from string.""" return date.fromisoformat(datestr) -def _dateobj_to_str(value: date | datetime): +def _dateobj_to_str(value: date | datetime) -> str: """Parse string from date objects.""" return value.isoformat().replace("+00:00", "Z") @@ -62,7 +61,7 @@ def _clean_value(_value_obj: Any) -> Any: def _clean_list(_list_obj: list) -> list[Any]: final = [] for list_value in _list_obj: - final.append(_clean_value(list_value)) + final.append(_clean_value(list_value)) # noqa: PERF401 return final def _clean_dict(_dict_obj: dict) -> dict[str, Any]: @@ -172,7 +171,8 @@ def dict_value_to_object( return value # raise if value is None and the value is required according to annotations if value is None and value_type is not NoneType: - raise KeyError(f"`{name}` of type `{value_type}` is required.") + message = f"`{name}` of type `{value_type}` is required." + raise KeyError(message) try: if issubclass(value_type, Enum): @@ -193,8 +193,8 @@ def dict_value_to_object( # If we reach this point, we could not match the value with the type and we raise if not isinstance(value, value_type): - raise TypeError( - f"Value {value} of type {type(value)} is invalid for {name}, " - f"expected value of type {value_type}" - ) + message = f"Value {value} of type {type(value)} is invalid for {name}, \ + expected value of type {value_type}" + raise TypeError(message) + return value diff --git a/pypaperless/models/workflows.py b/src/pypaperless/models/workflows.py similarity index 94% rename from pypaperless/models/workflows.py rename to src/pypaperless/models/workflows.py index 4216da5..cef7be2 100644 --- a/pypaperless/models/workflows.py +++ b/src/pypaperless/models/workflows.py @@ -32,7 +32,7 @@ class WorkflowAction(PaperlessModel): assign_change_groups: list[int] | None = None assign_custom_fields: list[int] | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Workflow` instance.""" super().__init__(api, data) @@ -58,7 +58,7 @@ class WorkflowTrigger( filter_has_correspondent: int | None = None filter_has_document_type: int | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Workflow` instance.""" super().__init__(api, data) @@ -78,7 +78,7 @@ class Workflow(PaperlessModel): actions: list[WorkflowAction] | None = None triggers: list[WorkflowTrigger] | None = None - def __init__(self, api: "Paperless", data: dict[str, Any]): + def __init__(self, api: "Paperless", data: dict[str, Any]) -> None: """Initialize a `Workflow` instance.""" super().__init__(api, data) @@ -135,9 +135,11 @@ def actions(self) -> WorkflowActionHelper: """Return the attached `WorkflowActionHelper` instance. Example: + ------- ```python wf_action = await paperless.workflows.actions(5) ``` + """ return self._actions @@ -146,8 +148,10 @@ def triggers(self) -> WorkflowTriggerHelper: """Return the attached `WorkflowTriggerHelper` instance. Example: + ------- ```python wf_trigger = await paperless.workflows.triggers(23) ``` + """ return self._triggers diff --git a/pypaperless/py.typed b/src/pypaperless/py.typed similarity index 100% rename from pypaperless/py.typed rename to src/pypaperless/py.typed diff --git a/tests/conftest.py b/tests/conftest.py index 217c06f..2e13e66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,8 +3,8 @@ from collections.abc import AsyncGenerator, Generator from typing import Any -from aioresponses import aioresponses import pytest +from aioresponses import aioresponses from pypaperless import Paperless from pypaperless.const import API_PATH @@ -23,24 +23,28 @@ def aioresponses_fixture() -> Generator[aioresponses, None, None]: @pytest.fixture(name="api_latest") -async def api_version_latest_fixture(api_26) -> AsyncGenerator[Paperless, Any]: +async def api_version_latest_fixture( + api_26: Paperless, +) -> AsyncGenerator[Paperless, Any]: """Return a Paperless object with latest version.""" - yield api_26 + return api_26 @pytest.fixture(name="api") def api_obj_fixture() -> Paperless: """Return Paperless.""" - paperless = Paperless( + return Paperless( PAPERLESS_TEST_URL, PAPERLESS_TEST_TOKEN, request_args=PAPERLESS_TEST_REQ_ARGS, ) - return paperless @pytest.fixture(name="api_00") -async def api_version_00_fixture(resp: aioresponses, api) -> AsyncGenerator[Paperless, Any]: +async def api_version_00_fixture( + resp: aioresponses, + api: Paperless, +) -> AsyncGenerator[Paperless, Any]: """Return a basic Paperless object.""" resp.get( f"{PAPERLESS_TEST_URL}{API_PATH['index']}", @@ -53,7 +57,10 @@ async def api_version_00_fixture(resp: aioresponses, api) -> AsyncGenerator[Pape @pytest.fixture(name="api_18") -async def api_version_18_fixture(resp: aioresponses, api) -> AsyncGenerator[Paperless, Any]: +async def api_version_18_fixture( + resp: aioresponses, + api: Paperless, +) -> AsyncGenerator[Paperless, Any]: """Return a Paperless object with given version.""" resp.get( f"{PAPERLESS_TEST_URL}{API_PATH['index']}", @@ -66,7 +73,10 @@ async def api_version_18_fixture(resp: aioresponses, api) -> AsyncGenerator[Pape @pytest.fixture(name="api_117") -async def api_version_117_fixture(resp: aioresponses, api) -> AsyncGenerator[Paperless, Any]: +async def api_version_117_fixture( + resp: aioresponses, + api: Paperless, +) -> AsyncGenerator[Paperless, Any]: """Return a Paperless object with given version.""" resp.get( f"{PAPERLESS_TEST_URL}{API_PATH['index']}", @@ -79,7 +89,10 @@ async def api_version_117_fixture(resp: aioresponses, api) -> AsyncGenerator[Pap @pytest.fixture(name="api_20") -async def api_version_20_fixture(resp: aioresponses, api) -> AsyncGenerator[Paperless, Any]: +async def api_version_20_fixture( + resp: aioresponses, + api: Paperless, +) -> AsyncGenerator[Paperless, Any]: """Return a Paperless object with given version.""" resp.get( f"{PAPERLESS_TEST_URL}{API_PATH['index']}", @@ -92,7 +105,10 @@ async def api_version_20_fixture(resp: aioresponses, api) -> AsyncGenerator[Pape @pytest.fixture(name="api_23") -async def api_version_23_fixture(resp: aioresponses, api) -> AsyncGenerator[Paperless, Any]: +async def api_version_23_fixture( + resp: aioresponses, + api: Paperless, +) -> AsyncGenerator[Paperless, Any]: """Return a Paperless object with given version.""" resp.get( f"{PAPERLESS_TEST_URL}{API_PATH['index']}", @@ -105,7 +121,10 @@ async def api_version_23_fixture(resp: aioresponses, api) -> AsyncGenerator[Pape @pytest.fixture(name="api_26") -async def api_version_26_fixture(resp: aioresponses, api) -> AsyncGenerator[Paperless, Any]: +async def api_version_26_fixture( + resp: aioresponses, + api: Paperless, +) -> AsyncGenerator[Paperless, Any]: """Return a Paperless object with given version.""" resp.get( f"{PAPERLESS_TEST_URL}{API_PATH['index']}", diff --git a/tests/data/__init__.py b/tests/data/__init__.py index a287e7f..c6f2dfb 100644 --- a/tests/data/__init__.py +++ b/tests/data/__init__.py @@ -20,7 +20,12 @@ ) from .v1_8_0 import V1_8_0_PATHS, V1_8_0_STORAGE_PATHS from .v1_17_0 import V1_17_0_DOCUMENT_NOTES -from .v2_0_0 import V2_0_0_CONFIG, V2_0_0_CUSTOM_FIELDS, V2_0_0_PATHS, V2_0_0_SHARE_LINKS +from .v2_0_0 import ( + V2_0_0_CONFIG, + V2_0_0_CUSTOM_FIELDS, + V2_0_0_PATHS, + V2_0_0_SHARE_LINKS, +) from .v2_3_0 import ( V2_3_0_PATHS, V2_3_0_WORKFLOW_ACTIONS, diff --git a/tests/data/v0_0_0.py b/tests/data/v0_0_0.py index 6fe2b7c..6689b1c 100644 --- a/tests/data/v0_0_0.py +++ b/tests/data/v0_0_0.py @@ -510,7 +510,10 @@ "show_in_sidebar": False, "sort_field": "title", "sort_reverse": False, - "filter_rules": [{"rule_type": 6, "value": "1"}, {"rule_type": 33, "value": "7"}], + "filter_rules": [ + {"rule_type": 6, "value": "1"}, + {"rule_type": 33, "value": "7"}, + ], "owner": 1, "user_can_change": True, }, @@ -521,7 +524,10 @@ "show_in_sidebar": False, "sort_field": None, "sort_reverse": False, - "filter_rules": [{"rule_type": 6, "value": "1"}, {"rule_type": 35, "value": "7"}], + "filter_rules": [ + {"rule_type": 6, "value": "1"}, + {"rule_type": 35, "value": "7"}, + ], "owner": 1, "user_can_change": True, }, diff --git a/tests/ruff.toml b/tests/ruff.toml new file mode 100644 index 0000000..b8ccacc --- /dev/null +++ b/tests/ruff.toml @@ -0,0 +1,13 @@ +# This extend our general Ruff rules specifically for tests +extend = "../pyproject.toml" + +lint.extend-select = [ + "PT", # Use @pytest.fixture without parentheses +] + +lint.extend-ignore = [ + "S101", # As these are tests, the usage of assert could be good practise, no? + "S105", # Yes, we hardcoded passwords. It will be ok this time. + "SLF001", # Tests will access private/protected members. + "TCH002", # pytest doesn't like this one. +] diff --git a/tests/test_common.py b/tests/test_common.py index 02a1af3..4a7c212 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -5,17 +5,17 @@ from enum import Enum import aiohttp +import pytest from aiohttp.http_exceptions import InvalidURLError from aioresponses import aioresponses -import pytest from pypaperless import Paperless from pypaperless.const import API_PATH, PaperlessResource from pypaperless.exceptions import ( - BadJsonResponse, - DraftNotSupported, + BadJsonResponseError, + DraftNotSupportedError, JsonResponseWithError, - RequestException, + RequestError, ) from pypaperless.models import Page from pypaperless.models.base import HelperBase, PaperlessModel @@ -67,6 +67,45 @@ async def test_context(self, resp: aioresponses, api: Paperless) -> None: async with api: assert api.is_initialized + async def test_properties(self, api: Paperless) -> None: + """Test properties.""" + # version must be None in this case, as we test against + # an uninitialized Paperless object + assert api.host_version is None + + assert isinstance(api.local_resources, set) + assert isinstance(api.remote_resources, set) + + async def test_helper_avail_00(self, api_00: Paperless) -> None: + """Test availability of helpers against specific api version.""" + assert not api_00.custom_fields.is_available + assert not api_00.workflows.is_available + + async def test_helper_avail_latest(self, api_latest: Paperless) -> None: + """Test availability of helpers against specific api version.""" + assert api_latest.custom_fields.is_available + assert api_latest.workflows.is_available + + async def test_jsonresponsewitherror(self) -> None: + """Test JsonResponseWithError.""" + try: + payload = "sample string" + raise JsonResponseWithError(payload) # noqa: TRY301 + except JsonResponseWithError as exc: + assert exc.args[0] == "Paperless: error - unknown error" # noqa: PT017 + + try: + payload = {"failure": "something failed"} + raise JsonResponseWithError(payload) # noqa: TRY301 + except JsonResponseWithError as exc: + assert exc.args[0] == "Paperless: failure - something failed" # noqa: PT017 + + try: + payload = {"error": ["that", "should", "have", "been", "never", "happened"]} + raise JsonResponseWithError(payload) # noqa: TRY301 + except JsonResponseWithError as exc: + assert exc.args[0] == "Paperless: error - happened" # noqa: PT017 + async def test_request(self, resp: aioresponses) -> None: """Test generate request.""" # we need to use an unmocked PaperlessSession.request() method @@ -107,7 +146,7 @@ async def test_request(self, resp: aioresponses) -> None: PAPERLESS_TEST_URL, exception=InvalidURLError, ) - with pytest.raises(RequestException): + with pytest.raises(RequestError): async with api.request("get", PAPERLESS_TEST_URL) as res: pass @@ -133,7 +172,7 @@ async def test_request_json(self, resp: aioresponses, api: Paperless) -> None: headers={"Content-Type": "text/plain"}, body='{"error": "sample message"}', ) - with pytest.raises(BadJsonResponse): + with pytest.raises(BadJsonResponseError): await api.request_json("get", f"{PAPERLESS_TEST_URL}/200-text-error-payload") # test 200 ok with correct content type, but no json payload @@ -143,7 +182,7 @@ async def test_request_json(self, resp: aioresponses, api: Paperless) -> None: headers={"Content-Type": "application/json"}, body="test 5 23 42 1337", ) - with pytest.raises(BadJsonResponse): + with pytest.raises(BadJsonResponseError): await api.request_json("get", f"{PAPERLESS_TEST_URL}/200-json-text-body") async def test_create_url(self) -> None: @@ -173,8 +212,6 @@ async def test_create_url(self) -> None: async def test_generate_api_token(self, resp: aioresponses, api: Paperless) -> None: """Test generate api token.""" - session = aiohttp.ClientSession() - # test successful token creation resp.post( f"{PAPERLESS_TEST_URL}{API_PATH['token']}", @@ -185,7 +222,6 @@ async def test_generate_api_token(self, resp: aioresponses, api: Paperless) -> N PAPERLESS_TEST_URL, PAPERLESS_TEST_USER, PAPERLESS_TEST_PASSWORD, - session, ) assert token == PAPERLESS_TEST_TOKEN @@ -195,12 +231,11 @@ async def test_generate_api_token(self, resp: aioresponses, api: Paperless) -> N status=200, payload={"blah": "any string"}, ) - with pytest.raises(BadJsonResponse): + with pytest.raises(BadJsonResponseError): token = await api.generate_api_token( PAPERLESS_TEST_URL, PAPERLESS_TEST_USER, PAPERLESS_TEST_PASSWORD, - session, ) # test error 400 @@ -214,27 +249,39 @@ async def test_generate_api_token(self, resp: aioresponses, api: Paperless) -> N PAPERLESS_TEST_URL, PAPERLESS_TEST_USER, PAPERLESS_TEST_PASSWORD, - session, ) # general exception resp.post( f"{PAPERLESS_TEST_URL}{API_PATH['token']}", - status=500, - body="no json", + exception=ValueError, ) - with pytest.raises(Exception): - session.params = { - "response_content": "no json", - "status": "500", - } + with pytest.raises(ValueError): # noqa: PT011 token = await api.generate_api_token( PAPERLESS_TEST_URL, PAPERLESS_TEST_USER, PAPERLESS_TEST_PASSWORD, - session, ) + async def test_generate_api_token_with_session( + self, resp: aioresponses, api: Paperless + ) -> None: + """Test generate api token with custom session.""" + session = aiohttp.ClientSession() + + resp.post( + f"{PAPERLESS_TEST_URL}{API_PATH['token']}", + status=200, + payload=PATCHWORK["token"], + ) + token = await api.generate_api_token( + PAPERLESS_TEST_URL, + PAPERLESS_TEST_USER, + PAPERLESS_TEST_PASSWORD, + session=session, + ) + assert token == PAPERLESS_TEST_TOKEN + async def test_types(self) -> None: """Test types.""" never_str = "!never_existing_type!" @@ -252,7 +299,7 @@ async def test_types(self) -> None: async def test_dataclass_conversion(self) -> None: """Test dataclass utils.""" - class _Status(Enum): + class SomeStatus(Enum): """Test enum.""" ACTIVE = 1 @@ -260,19 +307,19 @@ class _Status(Enum): UNKNOWN = -1 @classmethod - def _missing_(cls: type, *_: object): + def _missing_(cls: "SomeStatus", *_: object) -> "SomeStatus": """Set default.""" return cls.UNKNOWN @dataclass - class _Friend: + class SomeFriend: """Test class.""" name: str age: int @dataclass - class _Person: + class SomePerson: """Test class.""" name: str @@ -280,10 +327,10 @@ class _Person: height: float birth: date last_login: datetime - friends: list[_Friend] | None + friends: list[SomeFriend] | None deleted: datetime | None is_deleted: bool - status: _Status + status: SomeStatus file: bytes meta: dict[str, str] @@ -316,9 +363,9 @@ class _Person: field.type, field.default, ) - for field in fields(_Person) + for field in fields(SomePerson) } - res = _Person(**data) + res = SomePerson(**data) assert isinstance(res.name, str) assert isinstance(res.age, int) @@ -326,12 +373,12 @@ class _Person: assert isinstance(res.birth, date) assert isinstance(res.last_login, datetime) assert isinstance(res.friends, list) - assert isinstance(res.friends[0], _Friend) + assert isinstance(res.friends[0], SomeFriend) assert isinstance(res.friends[0].age, int) assert isinstance(res.friends[1].age, int) assert res.deleted is None assert res.is_deleted is False - assert isinstance(res.status, _Status) + assert isinstance(res.status, SomeStatus) assert isinstance(res.file, bytes) # back conversion @@ -406,11 +453,11 @@ class TestHelper(HelperBase, helpers.DraftableMixin): """Test Helper.""" _api_path = "any.url" - _resource = "test" # type: ignore - # draft_cls - we "forgot" to set a draft class, which will raise an exception ... + _resource = "test" + # draft_cls - we "forgot" to set a draft class, which will raises _resource_cls = TestResource helper = TestHelper(api) - with pytest.raises(DraftNotSupported): + with pytest.raises(DraftNotSupportedError): # ... there it is - helper.draft() # noqa + helper.draft() diff --git a/tests/test_models_matrix.py b/tests/test_models_matrix.py index 4510eb2..bbaf515 100644 --- a/tests/test_models_matrix.py +++ b/tests/test_models_matrix.py @@ -3,12 +3,12 @@ import re from typing import Any -from aioresponses import CallbackResult, aioresponses import pytest +from aioresponses import CallbackResult, aioresponses from pypaperless import Paperless from pypaperless.const import API_PATH -from pypaperless.exceptions import DraftFieldRequired, RequestException +from pypaperless.exceptions import DraftFieldRequiredError, RequestError from pypaperless.models import Page from pypaperless.models.common import PermissionTableType @@ -120,7 +120,7 @@ async def test_call( f"{PAPERLESS_TEST_URL}{API_PATH[mapping.resource+'_single']}".format(pk=1337), status=404, ) - with pytest.raises(RequestException): + with pytest.raises(RequestError): await getattr(api_latest, mapping.resource)(1337) @@ -213,7 +213,7 @@ async def test_call( f"{PAPERLESS_TEST_URL}{API_PATH[mapping.resource+'_single']}".format(pk=1337), status=404, ) - with pytest.raises(RequestException): + with pytest.raises(RequestError): await getattr(api_latest, mapping.resource)(1337) async def test_create( @@ -221,7 +221,7 @@ async def test_create( ) -> None: """Test create.""" draft = getattr(api_latest, mapping.resource).draft(**mapping.draft_defaults) - assert isinstance(draft, mapping.draft_cls) # type: ignore # noqa + assert isinstance(draft, mapping.draft_cls) # test empty draft fields if mapping.model_cls not in ( SHARE_LINK_MAP.model_cls, @@ -229,7 +229,7 @@ async def test_create( ): backup = draft.name draft.name = None - with pytest.raises(DraftFieldRequired): + with pytest.raises(DraftFieldRequiredError): await draft.save() draft.name = backup # actually call the create endpoint @@ -368,11 +368,12 @@ async def test_permission_change( item = await getattr(api_latest, mapping.resource)(1) item.permissions.view.users.append(23) - def _lookup_set_permissions( - url: str, # noqa + def _lookup_set_permissions( # pylint: disable=unused-argument + url: str, json: dict[str, Any], - **kwargs, # pylint: disable=unused-argument # noqa - ): + **kwargs: Any, # noqa: ARG001 + ) -> CallbackResult: + assert url assert "set_permissions" in json return CallbackResult( status=200, diff --git a/tests/test_models_specific.py b/tests/test_models_specific.py index f450cd5..d039f9a 100644 --- a/tests/test_models_specific.py +++ b/tests/test_models_specific.py @@ -3,17 +3,17 @@ import datetime import re -from aioresponses import aioresponses import pytest +from aioresponses import aioresponses from pypaperless import Paperless from pypaperless.const import API_PATH from pypaperless.exceptions import ( AsnRequestError, - DraftFieldRequired, - PrimaryKeyRequired, - RequestException, - TaskNotFound, + DraftFieldRequiredError, + PrimaryKeyRequiredError, + RequestError, + TaskNotFoundError, ) from pypaperless.models import ( Config, @@ -34,6 +34,7 @@ StatusTasksType, ) from pypaperless.models.documents import DocumentSuggestions, DownloadedDocument +from pypaperless.models.workflows import WorkflowActionHelper, WorkflowTriggerHelper from . import DOCUMENT_MAP from .const import PAPERLESS_TEST_URL @@ -61,7 +62,7 @@ async def test_call(self, resp: aioresponses, api_latest: Paperless) -> None: f"{PAPERLESS_TEST_URL}{API_PATH['config_single']}".format(pk=1337), status=404, ) - with pytest.raises(RequestException): + with pytest.raises(RequestError): await api_latest.config(1337) @@ -76,7 +77,7 @@ async def test_create(self, resp: aioresponses, api_latest: Paperless) -> None: assert isinstance(draft, DocumentDraft) backup = draft.document draft.document = None - with pytest.raises(DraftFieldRequired): + with pytest.raises(DraftFieldRequiredError): await draft.save() draft.document = backup # actually call the create endpoint @@ -280,7 +281,7 @@ async def test_note_call(self, resp: aioresponses, api_latest: Paperless) -> Non for note in results: assert isinstance(note, DocumentNote) assert isinstance(note.created, datetime.datetime) - with pytest.raises(PrimaryKeyRequired): + with pytest.raises(PrimaryKeyRequiredError): item = await api_latest.documents.notes() async def test_note_create(self, resp: aioresponses, api_latest: Paperless) -> None: @@ -295,7 +296,7 @@ async def test_note_create(self, resp: aioresponses, api_latest: Paperless) -> N assert isinstance(draft, DocumentNoteDraft) backup = draft.note draft.note = None - with pytest.raises(DraftFieldRequired): + with pytest.raises(DraftFieldRequiredError): await draft.save() draft.note = backup # actually call the create endpoint @@ -416,7 +417,7 @@ async def test_call(self, resp: aioresponses, api_latest: Paperless) -> None: f"{PAPERLESS_TEST_URL}{API_PATH['tasks_single']}".format(pk=1337), status=404, ) - with pytest.raises(RequestException): + with pytest.raises(RequestError): await api_latest.tasks(1337) # must raise as task_id doesn't exist resp.get( @@ -424,5 +425,15 @@ async def test_call(self, resp: aioresponses, api_latest: Paperless) -> None: status=200, payload=[], ) - with pytest.raises(TaskNotFound): + with pytest.raises(TaskNotFoundError): await api_latest.tasks("dummy-not-found") + + +# test models/workflows.py +class TestModelWorkflows: + """Tasks test cases.""" + + async def test_helpers(self, api_latest: Paperless) -> None: + """Test helpers.""" + assert isinstance(api_latest.workflows.actions, WorkflowActionHelper) + assert isinstance(api_latest.workflows.triggers, WorkflowTriggerHelper)