From 4312b5e31cc3cf1e3f4ba8472a1df6e08368dd79 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 7 Dec 2023 19:16:35 +0100 Subject: [PATCH 01/19] #277 drop Python 3.6 --- Dockerfile_py36_test | 17 ----------------- docs/index.rst | 2 +- pyproject.toml | 30 +++++++----------------------- pyuploadcare/__init__.py | 2 +- 4 files changed, 9 insertions(+), 42 deletions(-) delete mode 100644 Dockerfile_py36_test diff --git a/Dockerfile_py36_test b/Dockerfile_py36_test deleted file mode 100644 index a3e7d35b..00000000 --- a/Dockerfile_py36_test +++ /dev/null @@ -1,17 +0,0 @@ -# Dockerfile to test old-style client usage based on httpx for old Python 3.6 - -FROM python:3.6-slim-buster - -RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* - -RUN pip install --no-cache-dir --upgrade pip setuptools poetry - -WORKDIR /app - -COPY pyproject.toml poetry.lock /app/ -RUN poetry install - -COPY tests /app/tests -COPY pyuploadcare /app/pyuploadcare - -CMD ["poetry", "run", "pytest", "-v", "tests/"] diff --git a/docs/index.rst b/docs/index.rst index d8825a15..892b03e4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,7 +53,7 @@ Features Requirements ============ -``pyuploadcare`` requires Python 3.6, 3.7, 3.8, 3.9, 3.10, 3.11 +``pyuploadcare`` requires Python 3.7, 3.8, 3.9, 3.10, 3.11 To use ``pyuploadcare`` with Python 2.7 please install ``pyuploadcare < 3.0``. diff --git a/pyproject.toml b/pyproject.toml index 061605e6..8080609e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyuploadcare" -version = "4.2.2" +version = "5.0.0" description = "Python library for Uploadcare.com" authors = ["Uploadcare Inc "] readme = "README.md" @@ -10,7 +10,6 @@ classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', @@ -26,28 +25,19 @@ classifiers = [ ucare = 'pyuploadcare.ucare_cli.main:main' [tool.poetry.dependencies] -python = "^3.6.2" -httpx = [ - {version = "^0.18.2", python = ">=3.6,<3.7"}, - {version = "^0.24.1", python = "^3.7"} -] +python = "^3.7.0" +httpx = "^0.24.1" pydantic = {extras = ["email"], version = "^1.8.2"} python-dateutil = "^2.8.2" pytz = "^2022.4" -typing-extensions = [ - {version = "^3.10.0", python = ">=3.6,<3.7"}, - {version = "^4.3.0", python = "^3.7"} -] +typing-extensions = "^4.3.0" Django = {version = ">=1.11", optional = true} [tool.poetry.extras] django = ["Django"] [tool.poetry.dev-dependencies] -pytest = [ - {version = "^5.2", python = ">=3.6,<3.7"}, - {version = "^7.1", python = "^3.7"} -] +pytest = "^7.1" tox = "^3.24.1" black = "^22.3.0" isort = "^5.9.3" @@ -55,15 +45,9 @@ flake8 = "^3.9.2" mypy = "^0.910" flake8-print = "^4.0.0" pytest-vcr = "^1.0.2" -yarl = [ - {version = "^1.7.2", python = ">=3.6,<3.7"}, - {version = "^1.9.2", python = "^3.7"} -] +yarl = "^1.9.2" Django = "^3.2.7" -coverage = [ - {version = "^6.2", python = ">=3.6,<3.7"}, - {version = "^7.2.5", python = "^3.7"} -] +coverage = "^7.2.5" pytest-cov = "^2.12.1" python-coveralls = "^2.9.3" tox-pyenv = "^1.1.0" diff --git a/pyuploadcare/__init__.py b/pyuploadcare/__init__.py index d9a43179..b260e986 100644 --- a/pyuploadcare/__init__.py +++ b/pyuploadcare/__init__.py @@ -1,5 +1,5 @@ # isort: skip_file -__version__ = "4.2.2" +__version__ = "5.0.0" from pyuploadcare.resources.file import File # noqa: F401 from pyuploadcare.resources.file_group import FileGroup # noqa: F401 From 5a2ee98357ea6a97e6c850f05a2e1e4ca41ac16a Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 7 Dec 2023 19:57:36 +0100 Subject: [PATCH 02/19] #277 revised test matrix --- .github/workflows/test.yml | 257 ++++++++++++++++--------------------- 1 file changed, 109 insertions(+), 148 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 25796ef3..88b6c056 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,12 +5,12 @@ name: Tests on: push: branches: - - main + - main pull_request: branches: - - 'main' - - 'feature/**' - - 'version-3.x/**' + - "main" + - "feature/**" + - "version-3.x/**" jobs: lint: @@ -20,21 +20,21 @@ jobs: python-version: [3.9] steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: false - - name: Install dependencies - run: | - poetry install - - name: Run linters - run: make lint + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: false + - name: Install dependencies + run: | + poetry install + - name: Run linters + run: make lint functional: needs: lint @@ -43,138 +43,99 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 1.4.2 - virtualenvs-create: true - virtualenvs-in-project: false - - name: Install dependencies - run: | - poetry install - - name: Test with pytest - run: | - make test-functional - - functional-legacy-py: - needs: lint - - runs-on: ${{ matrix.os }} + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + virtualenvs-create: true + virtualenvs-in-project: false + - name: Install dependencies + run: | + poetry install + - name: Test with pytest + run: | + make test-functional + + django-1-2-3: + needs: functional + runs-on: ubuntu-latest strategy: matrix: - os: [ubuntu-20.04] - python-version: ['3.6'] + # https://docs.djangoproject.com/en/2.2/faq/install/#what-python-version-can-i-use-with-django + # https://docs.djangoproject.com/en/3.2/faq/install/#what-python-version-can-i-use-with-django + python-version: ["3.7", "3.8", "3.9"] + django-version: ["2.2", "3.0", "3.1", "3.2"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 1.1.15 - virtualenvs-create: true - virtualenvs-in-project: false - - name: Install dependencies - run: | - poetry install - - name: Test with pytest - run: | - make test-functional - - django: + include: + - python-version: "3.7" + django-version: "1.11" + + - python-version: "3.7" + django-version: "2.0" + + - python-version: "3.7" + django-version: "2.1" + + - python-version: "3.10" + django-version: "3.2" + + django-4: needs: functional runs-on: ubuntu-latest strategy: matrix: # https://docs.djangoproject.com/en/4.2/faq/install/#what-python-version-can-i-use-with-django - # this is a bit excessive, probably should run only before - # releasing new version - python-version: ['3.7', '3.8', '3.9'] - django-version: ['2.2', '3.0', '3.1', '3.2'] + python-version: ["3.8", "3.9", "3.10"] + django-version: ["4.0", "4.1", "4.2"] include: - - python-version: '3.7' - django-version: 1.11 - - # django versions that support py3.10 - - python-version: '3.10' - django-version: 3.2 - - python-version: '3.10' - django-version: 4.0 + - python-version: "3.11" + django-version: "4.1" - # django versions that support py3.11 - - python-version: '3.11' - django-version: 4.1 - - python-version: '3.11' - django-version: 4.2 + - python-version: "3.11" + django-version: "4.2" - # no django versions that support py3.12 - as of 2023-10-04 + - python-version: "3.12" + django-version: "4.2" - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 1.4.2 - virtualenvs-create: true - virtualenvs-in-project: false - - name: Install dependencies - run: | - poetry install - - name: Install specific Django version - run: | - poetry run pip install django~=${{ matrix.django-version }}.0 - - name: Test with pytest - run: | - make test-django - - django-legacy-py: - needs: functional-legacy-py - runs-on: ubuntu-20.04 + django-5: + needs: functional + runs-on: ubuntu-latest strategy: matrix: - # https://docs.djangoproject.com/en/3.2/faq/install/#what-python-version-can-i-use-with-django - # this is a bit excessive, probably should run only before - # releasing new version - python-version: ['3.6'] - django-version: ['1.11', '2.2', '3.0', '3.1', '3.2'] + # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django + python-version: ["3.10", "3.11", "3.12"] + django-version: ["5.0"] steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 1.1.15 # needed for py3.6 - virtualenvs-create: true - virtualenvs-in-project: false - - name: Install dependencies - run: | - poetry install - - name: Install specific Django version - run: | - poetry run pip install django~=${{ matrix.django-version }}.0 - - name: Test with pytest - run: | - make test-django + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + virtualenvs-create: true + virtualenvs-in-project: false + - name: Install dependencies + run: | + poetry install + - name: Install specific Django version + run: | + poetry run pip install django~=${{ matrix.django-version }} + - name: Test with pytest + run: | + make test-django integration: needs: functional @@ -184,19 +145,19 @@ jobs: python-version: [3.9] steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: false - - name: Install dependencies - run: | - poetry install - - name: Test with pytest - run: | - make test-integration + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: false + - name: Install dependencies + run: | + poetry install + - name: Test with pytest + run: | + make test-integration From 2d5033b6f568220185304632a3fb820ca98c45e5 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 7 Dec 2023 20:19:58 +0100 Subject: [PATCH 03/19] #277 update vcrpy for py38+, add changelog entry --- HISTORY.md | 10 ++++++++++ pyproject.toml | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 05541c6b..68cb603d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.0.0](https://github.com/uploadcare/pyuploadcare/compare/v4.2.3...v5.0.0) - unreleased + +### Breaking changes + +- Python 3.6 is no longer supported. + +### Added + +### Changed + ## [4.2.3](https://github.com/uploadcare/pyuploadcare/compare/v4.2.2...v4.2.3) - unreleased ### Deprecated diff --git a/pyproject.toml b/pyproject.toml index 8080609e..53e0527c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ classifiers = [ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'License :: OSI Approved :: MIT License', ] @@ -45,6 +46,10 @@ flake8 = "^3.9.2" mypy = "^0.910" flake8-print = "^4.0.0" pytest-vcr = "^1.0.2" +vcrpy = [ + {version = "^4.4.0", python = "<3.8"}, + {version = "^5.1.0", python = ">=3.8"} +] yarl = "^1.9.2" Django = "^3.2.7" coverage = "^7.2.5" From 44a56d08c6878d9f02ede6028904215085a1864d Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 7 Dec 2023 21:55:38 +0100 Subject: [PATCH 04/19] #277 ditched tox in favor of github actions --- HISTORY.md | 5 +++++ Makefile | 3 +++ docs/index.rst | 18 ++++++++++++++--- docs/install.rst | 5 +++++ docs/testing.rst | 8 +++++--- pyproject.toml | 2 -- tox.ini | 51 ------------------------------------------------ 7 files changed, 33 insertions(+), 59 deletions(-) delete mode 100644 tox.ini diff --git a/HISTORY.md b/HISTORY.md index 68cb603d..0aeab1f1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,11 +11,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Breaking changes - Python 3.6 is no longer supported. +- Removed `tox.ini`. The recommended way to run tests locally is by using [act](https://github.com/nektos/act) with Docker. ### Added +... + ### Changed +- Python 3.12 and Django 5.0 were added to the test matrix. + ## [4.2.3](https://github.com/uploadcare/pyuploadcare/compare/v4.2.2...v4.2.3) - unreleased ### Deprecated diff --git a/Makefile b/Makefile index 54bce36a..f57e9bb5 100644 --- a/Makefile +++ b/Makefile @@ -27,5 +27,8 @@ test-django: test-integration: poetry run pytest tests/integration --cov=pyuploadcare +test-with-github-actions: + act -W .github/workflows/test.yml --container-architecture linux/amd64 + docs_html: poetry run sh -c "cd docs && make html" diff --git a/docs/index.rst b/docs/index.rst index 892b03e4..9199f7ab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,13 +53,25 @@ Features Requirements ============ -``pyuploadcare`` requires Python 3.7, 3.8, 3.9, 3.10, 3.11 +``pyuploadcare`` requires Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 + +To use ``pyuploadcare`` with Python 3.6 please install ``pyuploadcare < 5.0``. To use ``pyuploadcare`` with Python 2.7 please install ``pyuploadcare < 3.0``. -If you're using ``pyuploadcare`` with Django, check ``tox.ini`` or -``.github/workflows`` for supported Python-Django combinations. +If you're using ``pyuploadcare`` with Django, refer the following compatibility table: + +.. csv-table:: Django compatibility + :header-rows: 1 + :align: center + Py/Dj,1.11,2.0,2.1,2.2,3.0,3.1,3.2,4.0,4.1,4.2,5.0 + 3.7,X,X,X,X,X,X,X,,,, + 3.8,,,,X,X,X,X,X,X,X, + 3.9,,,,X,X,X,X,X,X,X, + 3.10,,,,,,,X,X,X,X,X + 3.11,,,,,,,,,X,X,X + 3.12,,,,,,,, ,,X,X Contents ======== diff --git a/docs/install.rst b/docs/install.rst index 46057098..0780b76e 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -115,3 +115,8 @@ A version 4.0 uses REST API 0.7 and contains the next breaking changes: * For ``File``: * Removed method ``copy`` in favor of ``local_copy`` and ``remote_copy`` methods. * Files to upload must be opened in a binary mode. + +Update to version 5.0 +--------------------- + +... diff --git a/docs/testing.rst b/docs/testing.rst index c2fd3a26..42388fbe 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -4,11 +4,13 @@ Testing ======= -Besides the `Github Actions`_ we use tox. In order to run tests just: +To run tests using `Github Actions`_ workflows, but locally, install the `act`_ utility, and then run it: .. code-block:: console - $ pip install tox - $ tox + make test-with-github-actions + +This runs the full suite of tests across Python and Django versions. .. _Github Actions: https://github.com/uploadcare/pyuploadcare/actions +.. _act: https://github.com/nektos/act diff --git a/pyproject.toml b/pyproject.toml index 53e0527c..b20cb128 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ django = ["Django"] [tool.poetry.dev-dependencies] pytest = "^7.1" -tox = "^3.24.1" black = "^22.3.0" isort = "^5.9.3" flake8 = "^3.9.2" @@ -55,7 +54,6 @@ Django = "^3.2.7" coverage = "^7.2.5" pytest-cov = "^2.12.1" python-coveralls = "^2.9.3" -tox-pyenv = "^1.1.0" types-python-dateutil = "^2.8.0" types-pytz = "^2021.1.2" Sphinx = "^4.2.0" diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 9d1a9659..00000000 --- a/tox.ini +++ /dev/null @@ -1,51 +0,0 @@ -# https://devguide.python.org/versions/ -# https://www.djangoproject.com/download/#supported-versions -# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django - -[tox] -isolated_build = True -envlist= - # core lib tests - py{36,37,38,39,310,311}-api, - - # old django versions - py{36,37}-dj1.11-django - py{36,37}-dj2.0-django - py{36,37}-dj2.1-django - - # current django versions - py{36,37,38,39}-dj2.2-django - py{36,37,38,39}-dj3.0-django - py{36,37,38,39}-dj3.1-django - py{36,37,38,39}-dj3.2-django - - # integration tests - py39-integration - -[testenv] -passenv = SIGNED_UPLOADS* -basepython = - py36: python3.6 - py37: python3.7 - py38: python3.8 - py39: python3.9 - py310: python3.10 - py311: python3.11 -deps = - pytest - mock - - # current - dj2.2: Django~=2.2.0 - dj3.0: Django~=3.0.0 - dj3.1: Django~=3.1.0 - dj3.2: Django~=3.2.0 - - # eol - dj1.11: Django~=1.11.0 - dj2.0: Django~=2.0.0 - dj2.1: Django~=2.1.0 -commands = - api: pytest tests/functional - django: pytest tests/dj - integration: pytest tests/integration From f89c08fafa1d7660c1e53cddc95c49a59bbc1b7b Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 7 Dec 2023 22:43:15 +0100 Subject: [PATCH 05/19] #277 fix test.yml format --- .github/workflows/test.yml | 54 ++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 88b6c056..558beafc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,6 +87,28 @@ jobs: - python-version: "3.10" django-version: "3.2" + steps: + - uses: actions/checkout@v3 + - name: set up python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: install poetry + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + virtualenvs-create: true + virtualenvs-in-project: false + - name: install dependencies + run: | + poetry install + - name: install specific django version + run: | + poetry run pip install django~=${{ matrix.django-version }} + - name: test with pytest + run: | + make test-django + django-4: needs: functional runs-on: ubuntu-latest @@ -106,6 +128,28 @@ jobs: - python-version: "3.12" django-version: "4.2" + steps: + - uses: actions/checkout@v3 + - name: set up python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: install poetry + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + virtualenvs-create: true + virtualenvs-in-project: false + - name: install dependencies + run: | + poetry install + - name: install specific django version + run: | + poetry run pip install django~=${{ matrix.django-version }} + - name: test with pytest + run: | + make test-django + django-5: needs: functional runs-on: ubuntu-latest @@ -117,23 +161,23 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} + - name: set up python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install Poetry + - name: install poetry uses: snok/install-poetry@v1 with: version: 1.4.2 virtualenvs-create: true virtualenvs-in-project: false - - name: Install dependencies + - name: install dependencies run: | poetry install - - name: Install specific Django version + - name: install specific django version run: | poetry run pip install django~=${{ matrix.django-version }} - - name: Test with pytest + - name: test with pytest run: | make test-django From 78cbf1e017601002ffe81320a16a561bab27d7fb Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 8 Dec 2023 01:04:04 +0100 Subject: [PATCH 06/19] #277 pydantic 2.5.2 (#274) --- pyproject.toml | 2 +- pyuploadcare/api/addon_entities.py | 7 +- pyuploadcare/api/api.py | 6 +- pyuploadcare/api/entities.py | 137 ++++++++++++++--------------- pyuploadcare/api/responses.py | 28 +++--- 5 files changed, 90 insertions(+), 90 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b20cb128..a9907dd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ ucare = 'pyuploadcare.ucare_cli.main:main' [tool.poetry.dependencies] python = "^3.7.0" httpx = "^0.24.1" -pydantic = {extras = ["email"], version = "^1.8.2"} +pydantic = {extras = ["email"], version = "^2.5.2"} python-dateutil = "^2.8.2" pytz = "^2022.4" typing-extensions = "^4.3.0" diff --git a/pyuploadcare/api/addon_entities.py b/pyuploadcare/api/addon_entities.py index da735180..176589c3 100644 --- a/pyuploadcare/api/addon_entities.py +++ b/pyuploadcare/api/addon_entities.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Optional, TypeVar, Union from uuid import UUID -from pydantic import Field, validator +from pydantic import Field, field_validator from pyuploadcare.api.entities import Entity @@ -23,9 +23,10 @@ class AddonExecutionParams(Entity): class AddonExecutionGeneralRequestData(Entity): target: UUID - params: Optional[Union[Dict[str, Any], AddonExecutionParams]] + params: Optional[Union[Dict[str, Any], AddonExecutionParams]] = None - @validator("target") + @field_validator("target") + @classmethod def coerce_target_to_str(cls, v): return str(v) diff --git a/pyuploadcare/api/api.py b/pyuploadcare/api/api.py index efcc4952..589d8c62 100644 --- a/pyuploadcare/api/api.py +++ b/pyuploadcare/api/api.py @@ -480,7 +480,7 @@ def update_or_create_key( url = self._build_url(file_uuid, suffix=suffix) response_class = self._get_response_class("update") json_response = self._client.put(url, json=mvalue).json() - response = self._parse_response(json_response, response_class).__root__ # type: ignore + response = self._parse_response(json_response, response_class).root # type: ignore return cast(str, response) def get_all_metadata(self, file_uuid: Union[UUID, str]) -> dict: @@ -496,7 +496,7 @@ def get_all_metadata(self, file_uuid: Union[UUID, str]) -> dict: ) json_response = {} - response = self._parse_response(json_response, response_class).__root__ # type: ignore + response = self._parse_response(json_response, response_class).root # type: ignore return cast(dict, response) def delete_key(self, file_uuid: Union[UUID, str], mkey: str) -> None: @@ -511,7 +511,7 @@ def get_key(self, file_uuid: Union[UUID, str], mkey: str) -> str: url = self._build_url(file_uuid, suffix=suffix) response_class = self._get_response_class("get_key") json_response = self._client.get(url).json() - response = self._parse_response(json_response, response_class).__root__ # type: ignore + response = self._parse_response(json_response, response_class).root # type: ignore return cast(str, response) diff --git a/pyuploadcare/api/entities.py b/pyuploadcare/api/entities.py index f5c599cb..d56d750b 100644 --- a/pyuploadcare/api/entities.py +++ b/pyuploadcare/api/entities.py @@ -5,8 +5,8 @@ from typing import Any, Dict, List, Optional, Tuple, TypeVar, Union from uuid import UUID -from pydantic import BaseModel, ConstrainedStr, EmailStr, Field, PrivateAttr -from typing_extensions import Literal +from pydantic import BaseModel, EmailStr, Field, PrivateAttr +from typing_extensions import Annotated, Literal from .metadata import META_KEY_MAX_LEN, META_KEY_PATTERN, META_VALUE_MAX_LEN @@ -64,36 +64,36 @@ class GEOPoint(Entity): class ImageInfo(Entity): color_mode: ColorMode - orientation: Optional[int] + orientation: Optional[int] = None format: str sequence: bool height: int width: int - geo_location: Optional[GEOPoint] - datetime_original: Optional[datetime] - dpi: Optional[Tuple[int, int]] + geo_location: Optional[GEOPoint] = None + datetime_original: Optional[datetime] = None + dpi: Optional[Tuple[int, int]] = None class AudioStreamInfo(Entity): - bitrate: Optional[Decimal] - codec: Optional[str] - sample_rate: Optional[Decimal] - profile: Optional[str] - channels: Optional[str] + bitrate: Optional[Decimal] = None + codec: Optional[str] = None + sample_rate: Optional[Decimal] = None + profile: Optional[str] = None + channels: Optional[int] = None class VideoStreamInfo(Entity): height: Decimal width: Decimal frame_rate: float - bitrate: Optional[Decimal] - codec: Optional[str] + bitrate: Optional[Decimal] = None + codec: Optional[str] = None class VideoInfo(Entity): - duration: Optional[Decimal] + duration: Optional[Decimal] = None format: str - bitrate: Optional[Decimal] + bitrate: Optional[Decimal] = None audio: List[AudioStreamInfo] video: List[VideoStreamInfo] @@ -105,9 +105,9 @@ class MIMEInfo(Entity): class ContentInfo(Entity): - mime: Optional[MIMEInfo] - image: Optional[ImageInfo] - video: Optional[VideoInfo] + mime: Optional[MIMEInfo] = None + image: Optional[ImageInfo] = None + video: Optional[VideoInfo] = None class ApllicationDataDetails(Entity): @@ -136,7 +136,7 @@ class AWSRecognitionLabel(Entity): class AWSRecognitionDetectLabelsDetails(ApllicationDataDetails): - label_model_version: Optional[str] = Field(alias="LabelModelVersion") + label_model_version: Optional[str] = Field(None, alias="LabelModelVersion") labels: List[AWSRecognitionLabel] = Field( alias="Labels", default_factory=list ) @@ -153,7 +153,9 @@ class AWSRecognitionModerationLabel(Entity): class AWSRecognitionDetectModerationLabelsDetails(ApllicationDataDetails): - label_model_version: Optional[str] = Field(alias="ModerationModelVersion") + label_model_version: Optional[str] = Field( + None, alias="ModerationModelVersion" + ) labels: List[AWSRecognitionModerationLabel] = Field( alias="ModerationLabels", default_factory=list ) @@ -164,7 +166,7 @@ class AWSRecognitionDetectModerationLabelsApplicationData(ApplicationDataBase): class RemoveBackgroundDetails(ApllicationDataDetails): - foreground_type: Optional[str] + foreground_type: Optional[str] = None class RemoveBackgroundApplicationData(ApplicationDataBase): @@ -172,8 +174,8 @@ class RemoveBackgroundApplicationData(ApplicationDataBase): class UCClamAVDetails(ApllicationDataDetails): - infected: Optional[bool] - infected_with: Optional[str] + infected: Optional[bool] = None + infected_with: Optional[str] = None class UCClamAVApplicationData(ApplicationDataBase): @@ -183,22 +185,19 @@ class UCClamAVApplicationData(ApplicationDataBase): class ApplicationDataSet(Entity): aws_rekognition_detect_labels: Optional[ AWSRecognitionDetectLabelsApplicationData - ] + ] = None aws_rekognition_detect_moderation_labels: Optional[ AWSRecognitionDetectModerationLabelsApplicationData - ] - remove_bg: Optional[RemoveBackgroundApplicationData] - uc_clamav_virus_scan: Optional[UCClamAVApplicationData] - + ] = None + remove_bg: Optional[RemoveBackgroundApplicationData] = None + uc_clamav_virus_scan: Optional[UCClamAVApplicationData] = None -class MetadataKeyConStrType(ConstrainedStr): - regex = re.compile(META_KEY_PATTERN) - max_length = META_KEY_MAX_LEN +MetadataKeyConStrType = Annotated[ + str, Field(pattern=META_KEY_PATTERN, max_length=META_KEY_MAX_LEN) +] -class MetadataValueConStrType(ConstrainedStr): - max_length = META_VALUE_MAX_LEN - +MetadataValueConStrType = Annotated[str, Field(max_length=META_VALUE_MAX_LEN)] MetadataDict = Dict[MetadataKeyConStrType, MetadataValueConStrType] @@ -212,55 +211,55 @@ class FileInfo(UUIDEntity): """ - datetime_removed: Optional[datetime] - datetime_stored: Optional[datetime] - datetime_uploaded: Optional[datetime] - metadata: Optional[MetadataDict] - is_image: Optional[bool] - is_ready: Optional[bool] - mime_type: Optional[str] - original_file_url: Optional[str] - original_filename: Optional[str] - size: Optional[int] - url: Optional[str] - variations: Optional[Dict[str, UUID]] - source: Optional[str] - content_info: Optional[ContentInfo] - appdata: Optional[ApplicationDataSet] + datetime_removed: Optional[datetime] = None + datetime_stored: Optional[datetime] = None + datetime_uploaded: Optional[datetime] = None + metadata: Optional[MetadataDict] = None + is_image: Optional[bool] = None + is_ready: Optional[bool] = None + mime_type: Optional[str] = None + original_file_url: Optional[str] = None + original_filename: Optional[str] = None + size: Optional[int] = None + url: Optional[str] = None + variations: Optional[Dict[str, UUID]] = None + source: Optional[str] = None + content_info: Optional[ContentInfo] = None + appdata: Optional[ApplicationDataSet] = None class GroupInfo(Entity): id: str _fetched: Optional[bool] = PrivateAttr(default=False) - datetime_created: Optional[datetime] - datetime_stored: Optional[datetime] - files_count: Optional[int] - cdn_url: Optional[str] - url: Optional[str] - files: Optional[List[Optional[FileInfo]]] + datetime_created: Optional[datetime] = None + datetime_stored: Optional[datetime] = None + files_count: Optional[int] = None + cdn_url: Optional[str] = None + url: Optional[str] = None + files: Optional[List[Optional[FileInfo]]] = None class ColaboratorInfo(Entity): - email: Optional[EmailStr] - name: Optional[str] + email: Optional[EmailStr] = None + name: Optional[str] = None class ProjectInfo(Entity): - collaborators: Optional[List[ColaboratorInfo]] + collaborators: Optional[List[ColaboratorInfo]] = None name: str pub_key: str - autostore_enabled: Optional[bool] + autostore_enabled: Optional[bool] = None class Webhook(Entity): id: int - created: Optional[datetime] - updated: Optional[datetime] - event: Optional[str] - target_url: Optional[str] - project: Optional[str] - is_active: Optional[bool] - signing_secret: Optional[str] + created: Optional[datetime] = None + updated: Optional[datetime] = None + event: Optional[str] = None + target_url: Optional[str] = None + project: Optional[int] = None + is_active: Optional[bool] = None + signing_secret: Optional[str] = None class DocumentConvertShortInfo(Entity): @@ -274,7 +273,7 @@ class DocumentConvertInfo(DocumentConvertShortInfo): class DocumentConvertStatus(Entity): status: str - error: Optional[str] + error: Optional[str] = None result: DocumentConvertShortInfo @@ -289,7 +288,7 @@ class DocumentConvertFormat(Entity): class DocumentConvertFormatInfo(Entity): - error: Optional[str] + error: Optional[str] = None format: DocumentConvertFormat @@ -305,5 +304,5 @@ class VideoConvertInfo(VideoConvertShortInfo): class VideoConvertStatus(Entity): status: str - error: Optional[str] + error: Optional[str] = None result: VideoConvertShortInfo diff --git a/pyuploadcare/api/responses.py b/pyuploadcare/api/responses.py index 53091080..a307cd6a 100644 --- a/pyuploadcare/api/responses.py +++ b/pyuploadcare/api/responses.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List, Optional, TypeVar from uuid import UUID -from pydantic import BaseModel +from pydantic import BaseModel, RootModel from pyuploadcare.api.entities import ( DocumentConvertInfo, @@ -23,8 +23,8 @@ class EntityListResponse(Response): class PaginatedResponse(EntityListResponse): - next: Optional[str] - previous: Optional[str] + next: Optional[str] = None + previous: Optional[str] = None total: int per_page: int @@ -42,8 +42,8 @@ class GroupListResponse(PaginatedResponse): class BatchFileOperationResponse(Response): # https://uploadcare.com/api-refs/rest-api/v0.6.0/#operation/filesStoring status: str - problems: Optional[Dict[str, Any]] - result: Optional[List[FileInfo]] + problems: Optional[Dict[str, Any]] = None + result: Optional[List[FileInfo]] = None class CreateLocalCopyResponse(Response): @@ -59,25 +59,25 @@ class CreateRemoteCopyResponse(Response): class DocumentConvertResponse(Entity): - problems: Optional[Dict[str, Any]] - result: Optional[List[DocumentConvertInfo]] + problems: Optional[Dict[str, Any]] = None + result: Optional[List[DocumentConvertInfo]] = None class VideoConvertResponse(Entity): - problems: Optional[Dict[str, Any]] - result: Optional[List[VideoConvertInfo]] + problems: Optional[Dict[str, Any]] = None + result: Optional[List[VideoConvertInfo]] = None -class UpdateMetadataKeyResponse(Entity): - __root__: str +class UpdateMetadataKeyResponse(RootModel, Entity): + root: str class DeleteMetadataKeyResponse(Entity): pass -class GetAllMetadataResponse(Entity): - __root__: MetadataDict +class GetAllMetadataResponse(RootModel, Entity): + root: MetadataDict class AddonResponseResult(Entity): @@ -100,4 +100,4 @@ class AddonExecuteResponse(Response): class AddonResponse(Response): status: AddonStatus - result: Optional[Dict[str, Any]] + result: Optional[Dict[str, Any]] = None From 0fd1c0879916a3a22fb4bc393ebec05dd19f100f Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 8 Dec 2023 01:06:31 +0100 Subject: [PATCH 07/19] #277 #274 fix lint --- pyuploadcare/api/entities.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyuploadcare/api/entities.py b/pyuploadcare/api/entities.py index d56d750b..2511e297 100644 --- a/pyuploadcare/api/entities.py +++ b/pyuploadcare/api/entities.py @@ -1,4 +1,3 @@ -import re from datetime import datetime from decimal import Decimal from enum import Enum From 7fbddb2d43f84645cfc761d99390b33bee39d4e5 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 8 Dec 2023 20:26:32 +0100 Subject: [PATCH 08/19] #277 update mypy --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a9907dd8..0afc681d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,10 @@ pytest = "^7.1" black = "^22.3.0" isort = "^5.9.3" flake8 = "^3.9.2" -mypy = "^0.910" +mypy = [ + {version = "^1.4.1", python = "<3.8"}, + {version = "^1.7.1", python = ">=3.8"} +] flake8-print = "^4.0.0" pytest-vcr = "^1.0.2" vcrpy = [ From 3e5c769bc73dd44517f62b1df1df5f4e2b511596 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 8 Dec 2023 21:36:25 +0100 Subject: [PATCH 09/19] #277 #274 fix warnings --- pyproject.toml | 11 +++++++---- pyuploadcare/api/addon_entities.py | 2 +- pyuploadcare/api/api.py | 6 +++--- pyuploadcare/api/base.py | 4 ++-- pyuploadcare/resources/base.py | 2 +- pyuploadcare/resources/file.py | 8 +++++--- pyuploadcare/resources/file_group.py | 4 +++- pyuploadcare/ucare_cli/commands/create_webhook.py | 2 +- pyuploadcare/ucare_cli/commands/get_project.py | 2 +- pyuploadcare/ucare_cli/commands/list_webhooks.py | 2 +- pyuploadcare/ucare_cli/commands/update_webhook.py | 2 +- tests/test_project/settings.py | 2 ++ 12 files changed, 28 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0afc681d..0aa256e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,16 +53,19 @@ vcrpy = [ {version = "^5.1.0", python = ">=3.8"} ] yarl = "^1.9.2" -Django = "^3.2.7" -coverage = "^7.2.5" -pytest-cov = "^2.12.1" +Django = [ + {version = "^3.2.23", python = "<3.8"}, + {version = "^4.2.8", python = ">=3.8"} +] +coverage = "^7.2.7" +pytest-cov = "^4.1.0" python-coveralls = "^2.9.3" types-python-dateutil = "^2.8.0" types-pytz = "^2021.1.2" Sphinx = "^4.2.0" sphinx-argparse = "^0.3.1" types-dataclasses = "^0.1.7" -pytest-freezegun = "^0.4.2" +pytest-freezer = "^0.4.8" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/pyuploadcare/api/addon_entities.py b/pyuploadcare/api/addon_entities.py index 176589c3..e62e7269 100644 --- a/pyuploadcare/api/addon_entities.py +++ b/pyuploadcare/api/addon_entities.py @@ -22,7 +22,7 @@ class AddonExecutionParams(Entity): class AddonExecutionGeneralRequestData(Entity): - target: UUID + target: Union[UUID, str] params: Optional[Union[Dict[str, Any], AddonExecutionParams]] = None @field_validator("target") diff --git a/pyuploadcare/api/api.py b/pyuploadcare/api/api.py index 589d8c62..93e196b4 100644 --- a/pyuploadcare/api/api.py +++ b/pyuploadcare/api/api.py @@ -533,15 +533,15 @@ def _get_request_data( cleaned_params = {} if params: if isinstance(params, AddonExecutionParams): - cleaned_params = params.dict( + cleaned_params = params.model_dump( exclude_unset=True, exclude_none=True ) else: cleaned_params = params - execution_request_data = self.request_type.parse_obj( + execution_request_data = self.request_type.model_validate( dict(target=str(file_uuid), params=cleaned_params) ) - return execution_request_data.dict( + return execution_request_data.model_dump( exclude_unset=True, exclude_none=True ) diff --git a/pyuploadcare/api/base.py b/pyuploadcare/api/base.py index f7f1e4e3..42550ba9 100644 --- a/pyuploadcare/api/base.py +++ b/pyuploadcare/api/base.py @@ -3,7 +3,7 @@ from uuid import UUID from httpx._types import RequestFiles -from pydantic import parse_obj_as +from pydantic import TypeAdapter from typing_extensions import Protocol from pyuploadcare.api.client import Client @@ -34,7 +34,7 @@ def _parse_response( raw_resource: Dict[str, Any], response_class: Union[Type[Response], Type[Entity]], ) -> Union[Response, Entity]: - return parse_obj_as(response_class, raw_resource) # type: ignore + return TypeAdapter(response_class).validate_python(raw_resource) def _build_url( # noqa: C901 self, diff --git a/pyuploadcare/resources/base.py b/pyuploadcare/resources/base.py index 6f11b5a2..c48f2827 100644 --- a/pyuploadcare/resources/base.py +++ b/pyuploadcare/resources/base.py @@ -66,7 +66,7 @@ def query_parameters(self, **parameters): def __iter__(self): qs = self.query_parameters() for entity in self.resource_api.list(**qs): - resource_info = entity.dict() + resource_info = entity.model_dump() resource_id = resource_info.get(self.resource_id_field) constructor = getattr(self._client, self.constructor_name) yield constructor(resource_id, resource_info) diff --git a/pyuploadcare/resources/file.py b/pyuploadcare/resources/file.py index de3fe26a..70fd23e6 100644 --- a/pyuploadcare/resources/file.py +++ b/pyuploadcare/resources/file.py @@ -191,7 +191,7 @@ def update_info(self, include_appdata=False): """Updates and returns file information by requesting Uploadcare API.""" self._info_cache = self._client.files_api.retrieve( self.uuid, include_appdata=include_appdata - ).dict() + ).model_dump() return self._info_cache @property @@ -297,7 +297,7 @@ def store(self): on S3. """ - self._info_cache = self._client.files_api.store(self.uuid).dict() + self._info_cache = self._client.files_api.store(self.uuid).model_dump() def create_local_copy( self, @@ -373,7 +373,9 @@ def create_remote_copy( def delete(self) -> "None": """Deletes file by requesting Uploadcare API.""" - self._info_cache = self._client.files_api.delete(self.uuid).dict() + self._info_cache = self._client.files_api.delete( + self.uuid + ).model_dump() def convert( self, diff --git a/pyuploadcare/resources/file_group.py b/pyuploadcare/resources/file_group.py index bb14619b..d3f6b9ff 100644 --- a/pyuploadcare/resources/file_group.py +++ b/pyuploadcare/resources/file_group.py @@ -144,7 +144,9 @@ def info(self): def update_info(self): """Updates and returns group information by requesting Uploadcare API.""" - self._info_cache = self._client.groups_api.retrieve(self.id).dict() + self._info_cache = self._client.groups_api.retrieve( + self.id + ).model_dump() if self.is_stored and self._stored_at: self._info_cache["datetime_stored"] = self._stored_at.isoformat() diff --git a/pyuploadcare/ucare_cli/commands/create_webhook.py b/pyuploadcare/ucare_cli/commands/create_webhook.py index 0af55e4e..88b6f1ec 100644 --- a/pyuploadcare/ucare_cli/commands/create_webhook.py +++ b/pyuploadcare/ucare_cli/commands/create_webhook.py @@ -27,4 +27,4 @@ def create_webhook(arg_namespace, client: Uploadcare): event=arg_namespace.event, signing_secret=arg_namespace.signing_secret, ) - pprint(webhook.dict()) + pprint(webhook.model_dump()) diff --git a/pyuploadcare/ucare_cli/commands/get_project.py b/pyuploadcare/ucare_cli/commands/get_project.py index 5b8a5fe0..fef54b89 100644 --- a/pyuploadcare/ucare_cli/commands/get_project.py +++ b/pyuploadcare/ucare_cli/commands/get_project.py @@ -11,4 +11,4 @@ def register_arguments(subparsers): def get_project(arg_namespace, client: Uploadcare): project: ProjectInfo = client.get_project_info() - pprint(project.dict()) + pprint(project.model_dump()) diff --git a/pyuploadcare/ucare_cli/commands/list_webhooks.py b/pyuploadcare/ucare_cli/commands/list_webhooks.py index ff6219ea..81beec08 100644 --- a/pyuploadcare/ucare_cli/commands/list_webhooks.py +++ b/pyuploadcare/ucare_cli/commands/list_webhooks.py @@ -11,5 +11,5 @@ def register_arguments(subparsers): def list_webhooks(arg_namespace, client: Uploadcare): - webhooks = [webhook.dict() for webhook in client.list_webhooks()] + webhooks = [webhook.model_dump() for webhook in client.list_webhooks()] pprint(webhooks) diff --git a/pyuploadcare/ucare_cli/commands/update_webhook.py b/pyuploadcare/ucare_cli/commands/update_webhook.py index a2f00048..d7b3617c 100644 --- a/pyuploadcare/ucare_cli/commands/update_webhook.py +++ b/pyuploadcare/ucare_cli/commands/update_webhook.py @@ -42,4 +42,4 @@ def update_webhook(arg_namespace, client: Uploadcare): # noqa: C901 webhook = client.update_webhook( webhook_id=arg_namespace.webhook_id, **kwargs ) - pprint(webhook.dict()) + pprint(webhook.model_dump()) diff --git a/tests/test_project/settings.py b/tests/test_project/settings.py index 00cd525f..64887110 100644 --- a/tests/test_project/settings.py +++ b/tests/test_project/settings.py @@ -30,6 +30,8 @@ ROOT_URLCONF = "test_project.urls" +USE_TZ = False + INSTALLED_APPS = ( "django.contrib.auth", "django.contrib.contenttypes", From 9d789c0c72b03874cbeda5fa2e8802ae718bb5bf Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 8 Dec 2023 22:13:12 +0100 Subject: [PATCH 10/19] #277 fix mypy errors --- pyuploadcare/api/base.py | 30 ++++++++++++++++++------------ pyuploadcare/api/client.py | 36 ++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/pyuploadcare/api/base.py b/pyuploadcare/api/base.py index 42550ba9..549898b6 100644 --- a/pyuploadcare/api/base.py +++ b/pyuploadcare/api/base.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Type, Union, cast +from typing import Any, Dict, Optional, Type, TypeVar, Union, cast from urllib.parse import urlencode, urljoin from uuid import UUID @@ -12,6 +12,9 @@ from pyuploadcare.exceptions import DefaultResponseClassNotDefined +EntityOrResponse = TypeVar("EntityOrResponse", Entity, Response) + + class API: resource_type: str response_classes: Dict[str, Union[Type[Response], Type[Entity]]] @@ -32,9 +35,12 @@ def __init__( def _parse_response( self, raw_resource: Dict[str, Any], - response_class: Union[Type[Response], Type[Entity]], - ) -> Union[Response, Entity]: - return TypeAdapter(response_class).validate_python(raw_resource) + response_class: Type[EntityOrResponse], + ) -> EntityOrResponse: + return cast( + response_class, + TypeAdapter(response_class).validate_python(raw_resource), + ) def _build_url( # noqa: C901 self, @@ -85,7 +91,7 @@ def _get( def _put( self, - resource_uuid: Union[UUID, str, UUIDEntity] = None, + resource_uuid: Optional[Union[UUID, str, UUIDEntity]] = None, data: Optional[Dict] = None, ) -> Dict[str, Any]: url = self._build_url(resource_uuid) @@ -93,13 +99,13 @@ def _put( return document.json() def _delete( - self, resource_uuid: Union[UUID, str, UUIDEntity] = None + self, resource_uuid: Optional[Union[UUID, str, UUIDEntity]] = None ) -> None: url = self._build_url(resource_uuid) self._client.delete(url) def _delete_with_response( - self, resource_uuid: Union[UUID, str, UUIDEntity] = None + self, resource_uuid: Optional[Union[UUID, str, UUIDEntity]] = None ) -> Dict[str, Any]: url = self._build_url(resource_uuid, suffix="storage") document = self._client.delete(url) @@ -114,8 +120,8 @@ class APIProtocol(Protocol): def _parse_response( self, raw_resource: Dict[str, Any], - response_class: Union[Type[Response], Type[Entity]], - ) -> Union[Response, Entity]: + response_class: Type[EntityOrResponse], + ) -> EntityOrResponse: ... def _build_url( @@ -144,18 +150,18 @@ def _get( def _put( self, - resource_uuid: Union[UUID, str, UUIDEntity] = None, + resource_uuid: Optional[Union[UUID, str, UUIDEntity]] = None, data: Optional[Dict] = None, ) -> Dict[str, Any]: ... def _delete( - self, resource_uuid: Union[UUID, str, UUIDEntity] = None + self, resource_uuid: Optional[Union[UUID, str, UUIDEntity]] = None ) -> None: ... def _delete_with_response( - self, resource_uuid: Union[UUID, str, UUIDEntity] = None + self, resource_uuid: Optional[Union[UUID, str, UUIDEntity]] = None ) -> Dict[str, Any]: ... diff --git a/pyuploadcare/api/client.py b/pyuploadcare/api/client.py index 994a4fde..f568a87b 100644 --- a/pyuploadcare/api/client.py +++ b/pyuploadcare/api/client.py @@ -49,12 +49,12 @@ def delete_with_payload( self, url: URLTypes, *, - content: RequestContent = None, - data: RequestData = None, - json: typing.Any = None, - params: QueryParamTypes = None, - headers: HeaderTypes = None, - cookies: CookieTypes = None, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, follow_redirects: typing.Optional[bool] = None, allow_redirects: typing.Optional[bool] = None, @@ -87,13 +87,13 @@ def request( # type: ignore # noqa: C901 method: str, url: URLTypes, *, - content: RequestContent = None, - data: RequestData = None, - files: RequestFiles = None, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, json: typing.Any = None, - params: QueryParamTypes = None, - headers: HeaderTypes = None, - cookies: CookieTypes = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, follow_redirects: typing.Optional[bool] = None, allow_redirects: typing.Optional[bool] = None, @@ -208,13 +208,13 @@ def _perform_request( # noqa: C901 self, method: str, url: URLTypes, - content: RequestContent = None, - data: RequestData = None, - files: RequestFiles = None, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, json: typing.Any = None, - params: QueryParamTypes = None, - headers: HeaderTypes = None, - cookies: CookieTypes = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, follow_redirects: typing.Optional[bool] = None, allow_redirects: typing.Optional[bool] = None, From 95b39d55bb442b8d9e7326e0d01542358788d43e Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 14 Dec 2023 20:08:05 +0100 Subject: [PATCH 11/19] #277 revert type changes a bit --- pyuploadcare/api/base.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pyuploadcare/api/base.py b/pyuploadcare/api/base.py index 549898b6..72b12b90 100644 --- a/pyuploadcare/api/base.py +++ b/pyuploadcare/api/base.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Type, TypeVar, Union, cast +from typing import Any, Dict, Optional, Type, Union, cast from urllib.parse import urlencode, urljoin from uuid import UUID @@ -12,9 +12,6 @@ from pyuploadcare.exceptions import DefaultResponseClassNotDefined -EntityOrResponse = TypeVar("EntityOrResponse", Entity, Response) - - class API: resource_type: str response_classes: Dict[str, Union[Type[Response], Type[Entity]]] @@ -35,12 +32,9 @@ def __init__( def _parse_response( self, raw_resource: Dict[str, Any], - response_class: Type[EntityOrResponse], - ) -> EntityOrResponse: - return cast( - response_class, - TypeAdapter(response_class).validate_python(raw_resource), - ) + response_class: Union[Type[Response], Type[Entity]], + ) -> Union[Response, Entity]: + return TypeAdapter(response_class).validate_python(raw_resource) def _build_url( # noqa: C901 self, @@ -120,8 +114,8 @@ class APIProtocol(Protocol): def _parse_response( self, raw_resource: Dict[str, Any], - response_class: Type[EntityOrResponse], - ) -> EntityOrResponse: + response_class: Union[Type[Response], Type[Entity]], + ) -> Union[Response, Entity]: ... def _build_url( From 1f59a6f2af9ce9ab4bdeaa394957cddb7c3bc2db Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 14 Dec 2023 20:52:56 +0100 Subject: [PATCH 12/19] #277 fix types for _parse_response --- pyuploadcare/api/base.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyuploadcare/api/base.py b/pyuploadcare/api/base.py index 72b12b90..84b905ab 100644 --- a/pyuploadcare/api/base.py +++ b/pyuploadcare/api/base.py @@ -4,7 +4,7 @@ from httpx._types import RequestFiles from pydantic import TypeAdapter -from typing_extensions import Protocol +from typing_extensions import Protocol, TypeVar from pyuploadcare.api.client import Client from pyuploadcare.api.entities import Entity, UUIDEntity @@ -12,6 +12,9 @@ from pyuploadcare.exceptions import DefaultResponseClassNotDefined +ResponseOrEntity = TypeVar("ResponseOrEntity", bound=Union[Response, Entity]) + + class API: resource_type: str response_classes: Dict[str, Union[Type[Response], Type[Entity]]] @@ -32,8 +35,8 @@ def __init__( def _parse_response( self, raw_resource: Dict[str, Any], - response_class: Union[Type[Response], Type[Entity]], - ) -> Union[Response, Entity]: + response_class: Type[ResponseOrEntity], + ) -> ResponseOrEntity: return TypeAdapter(response_class).validate_python(raw_resource) def _build_url( # noqa: C901 @@ -114,8 +117,8 @@ class APIProtocol(Protocol): def _parse_response( self, raw_resource: Dict[str, Any], - response_class: Union[Type[Response], Type[Entity]], - ) -> Union[Response, Entity]: + response_class: Type[ResponseOrEntity], + ) -> ResponseOrEntity: ... def _build_url( From 91f0091cd333a9c4f22d61c9ca0e801e37537a07 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 14 Dec 2023 20:53:05 +0100 Subject: [PATCH 13/19] #277 changelog... --- HISTORY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 0aeab1f1..695cd722 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,6 +11,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Breaking changes - Python 3.6 is no longer supported. +- [Pydantic](https://docs.pydantic.dev) has been updated to V2. If your project relies on Pydantic V1, you may encounter errors, as versions 1 and 2 of this library are not fully compatible with each other. - Removed `tox.ini`. The recommended way to run tests locally is by using [act](https://github.com/nektos/act) with Docker. ### Added @@ -19,6 +20,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed +- Bumped dependencies: ... - Python 3.12 and Django 5.0 were added to the test matrix. ## [4.2.3](https://github.com/uploadcare/pyuploadcare/compare/v4.2.2...v4.2.3) - unreleased From 987fc3be37a74bd02a36b770814145fffb0e6878 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 14 Dec 2023 21:51:41 +0100 Subject: [PATCH 14/19] #277 update dependencies --- HISTORY.md | 6 +++--- pyproject.toml | 42 +++++++++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 695cd722..33cfafb1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -16,12 +16,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added -... +- Python 3.12 and Django 5.0 were added to the test matrix. ### Changed -- Bumped dependencies: ... -- Python 3.12 and Django 5.0 were added to the test matrix. +- Updated dependencies: `httpx`, `pydantic`, `pytz`, `typing-extensions`. +- Updated dev-dependencies: `mypy`, `pytest`, `black`, `isort`, `flake8`, `vcrpy`, `yarl`, `coverage`, `sphinx`, `sphinx-argparse`, `types-*`; `pytest-freezegun` has been replaced with `pytest-freezer`. ## [4.2.3](https://github.com/uploadcare/pyuploadcare/compare/v4.2.2...v4.2.3) - unreleased diff --git a/pyproject.toml b/pyproject.toml index 0aa256e1..c50c5dde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,21 +27,34 @@ ucare = 'pyuploadcare.ucare_cli.main:main' [tool.poetry.dependencies] python = "^3.7.0" -httpx = "^0.24.1" +httpx = [ + {version = "^0.24.1", python = "<3.8"}, + {version = "^0.25.1", python = ">=3.8"} +] pydantic = {extras = ["email"], version = "^2.5.2"} python-dateutil = "^2.8.2" -pytz = "^2022.4" -typing-extensions = "^4.3.0" +pytz = "^2023.3.post1" +typing-extensions = [ + {version = "^4.7.1", python = "<3.8"}, + {version = "^4.9.0", python = ">=3.8"} +] Django = {version = ">=1.11", optional = true} [tool.poetry.extras] django = ["Django"] [tool.poetry.dev-dependencies] -pytest = "^7.1" -black = "^22.3.0" -isort = "^5.9.3" +pytest = "^7.4" +black = [ + {version = "^23.3.0", python = "<3.8"}, + {version = "^23.12.0", python = ">=3.8"} +] +isort = [ + {version = "^5.11.5", python = "<3.8"}, + {version = "^5.13.2", python = ">=3.8"} +] flake8 = "^3.9.2" +# flake8 = "^6.1.0" mypy = [ {version = "^1.4.1", python = "<3.8"}, {version = "^1.7.1", python = ">=3.8"} @@ -52,19 +65,22 @@ vcrpy = [ {version = "^4.4.0", python = "<3.8"}, {version = "^5.1.0", python = ">=3.8"} ] -yarl = "^1.9.2" +yarl = "^1.9.4" Django = [ {version = "^3.2.23", python = "<3.8"}, {version = "^4.2.8", python = ">=3.8"} ] -coverage = "^7.2.7" +coverage = [ + {version = "^5.5", python = "<3.8"}, + {version = "^7.3.3", python = ">=3.8"} +] pytest-cov = "^4.1.0" python-coveralls = "^2.9.3" -types-python-dateutil = "^2.8.0" -types-pytz = "^2021.1.2" -Sphinx = "^4.2.0" -sphinx-argparse = "^0.3.1" -types-dataclasses = "^0.1.7" +types-python-dateutil = "^2.8.19.14" +types-pytz = "^2023.3.1.1" +Sphinx = "^5.3.0" +sphinx-argparse = "^0.4.0" +types-dataclasses = "^0.6.6" pytest-freezer = "^0.4.8" [build-system] From c0fa036cb722bec2d9134744a6cd45b6ae4b392d Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 14 Dec 2023 22:05:43 +0100 Subject: [PATCH 15/19] #277 drop Python 3.7 --- .github/workflows/test.yml | 13 ++-------- HISTORY.md | 6 ++++- docs/index.rst | 17 ++++++------- pyproject.toml | 52 ++++++++++---------------------------- 4 files changed, 28 insertions(+), 60 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 558beafc..1eccd0a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 @@ -71,19 +71,10 @@ jobs: matrix: # https://docs.djangoproject.com/en/2.2/faq/install/#what-python-version-can-i-use-with-django # https://docs.djangoproject.com/en/3.2/faq/install/#what-python-version-can-i-use-with-django - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.8", "3.9"] django-version: ["2.2", "3.0", "3.1", "3.2"] include: - - python-version: "3.7" - django-version: "1.11" - - - python-version: "3.7" - django-version: "2.0" - - - python-version: "3.7" - django-version: "2.1" - - python-version: "3.10" django-version: "3.2" diff --git a/HISTORY.md b/HISTORY.md index 33cfafb1..a289a371 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,6 +11,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Breaking changes - Python 3.6 is no longer supported. +- Python 3.7 is no longer supported. +- Django 1.11 is no longer supported. +- Django 2.0 is no longer supported. +- Django 2.1 is no longer supported. - [Pydantic](https://docs.pydantic.dev) has been updated to V2. If your project relies on Pydantic V1, you may encounter errors, as versions 1 and 2 of this library are not fully compatible with each other. - Removed `tox.ini`. The recommended way to run tests locally is by using [act](https://github.com/nektos/act) with Docker. @@ -21,7 +25,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed - Updated dependencies: `httpx`, `pydantic`, `pytz`, `typing-extensions`. -- Updated dev-dependencies: `mypy`, `pytest`, `black`, `isort`, `flake8`, `vcrpy`, `yarl`, `coverage`, `sphinx`, `sphinx-argparse`, `types-*`; `pytest-freezegun` has been replaced with `pytest-freezer`. +- Updated dev-dependencies: `mypy`, `pytest`, `black`, `isort`, `flake8`, `flake8-print`, `vcrpy`, `yarl`, `coverage`, `sphinx`, `sphinx-argparse`, `types-*`; `pytest-freezegun` has been replaced with `pytest-freezer`. ## [4.2.3](https://github.com/uploadcare/pyuploadcare/compare/v4.2.2...v4.2.3) - unreleased diff --git a/docs/index.rst b/docs/index.rst index 9199f7ab..4239bb6b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,9 +53,9 @@ Features Requirements ============ -``pyuploadcare`` requires Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 +``pyuploadcare`` requires Python 3.8, 3.9, 3.10, 3.11, 3.12 -To use ``pyuploadcare`` with Python 3.6 please install ``pyuploadcare < 5.0``. +To use ``pyuploadcare`` with Python 3.6 or 3.7 please install ``pyuploadcare < 5.0``. To use ``pyuploadcare`` with Python 2.7 please install ``pyuploadcare < 3.0``. @@ -65,13 +65,12 @@ If you're using ``pyuploadcare`` with Django, refer the following compatibility :header-rows: 1 :align: center - Py/Dj,1.11,2.0,2.1,2.2,3.0,3.1,3.2,4.0,4.1,4.2,5.0 - 3.7,X,X,X,X,X,X,X,,,, - 3.8,,,,X,X,X,X,X,X,X, - 3.9,,,,X,X,X,X,X,X,X, - 3.10,,,,,,,X,X,X,X,X - 3.11,,,,,,,,,X,X,X - 3.12,,,,,,,, ,,X,X + Py/Dj,2.2,3.0,3.1,3.2,4.0,4.1,4.2,5.0 + 3.8,X,X,X,X,X,X,X, + 3.9,X,X,X,X,X,X,X, + 3.10,,,,X,X,X,X,X + 3.11,,,,,,X,X,X + 3.12,,,,,,,X,X Contents ======== diff --git a/pyproject.toml b/pyproject.toml index c50c5dde..e9d93a32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', @@ -26,59 +25,34 @@ classifiers = [ ucare = 'pyuploadcare.ucare_cli.main:main' [tool.poetry.dependencies] -python = "^3.7.0" -httpx = [ - {version = "^0.24.1", python = "<3.8"}, - {version = "^0.25.1", python = ">=3.8"} -] +python = "^3.8.1" +httpx = "^0.25.1" pydantic = {extras = ["email"], version = "^2.5.2"} python-dateutil = "^2.8.2" pytz = "^2023.3.post1" -typing-extensions = [ - {version = "^4.7.1", python = "<3.8"}, - {version = "^4.9.0", python = ">=3.8"} -] -Django = {version = ">=1.11", optional = true} +typing-extensions = "^4.9.0" +Django = {version = ">=2.2", optional = true} [tool.poetry.extras] django = ["Django"] [tool.poetry.dev-dependencies] pytest = "^7.4" -black = [ - {version = "^23.3.0", python = "<3.8"}, - {version = "^23.12.0", python = ">=3.8"} -] -isort = [ - {version = "^5.11.5", python = "<3.8"}, - {version = "^5.13.2", python = ">=3.8"} -] -flake8 = "^3.9.2" -# flake8 = "^6.1.0" -mypy = [ - {version = "^1.4.1", python = "<3.8"}, - {version = "^1.7.1", python = ">=3.8"} -] -flake8-print = "^4.0.0" +black = "^23.12.0" +isort = "^5.13.2" +flake8 = "^6.1.0" +mypy = "^1.7.1" +flake8-print = "^5.0.0" pytest-vcr = "^1.0.2" -vcrpy = [ - {version = "^4.4.0", python = "<3.8"}, - {version = "^5.1.0", python = ">=3.8"} -] +vcrpy = "^5.1.0" yarl = "^1.9.4" -Django = [ - {version = "^3.2.23", python = "<3.8"}, - {version = "^4.2.8", python = ">=3.8"} -] -coverage = [ - {version = "^5.5", python = "<3.8"}, - {version = "^7.3.3", python = ">=3.8"} -] +Django = "^4.2.8" +coverage = "^7.3.3" pytest-cov = "^4.1.0" python-coveralls = "^2.9.3" types-python-dateutil = "^2.8.19.14" types-pytz = "^2023.3.1.1" -Sphinx = "^5.3.0" +Sphinx = "^7.1.2" sphinx-argparse = "^0.4.0" types-dataclasses = "^0.6.6" pytest-freezer = "^0.4.8" From cc913ac42f12f4e8908b56521a33822b729cc5f4 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 14 Dec 2023 22:06:12 +0100 Subject: [PATCH 16/19] #277 format --- .github/workflows/test.yml | 2 +- pyuploadcare/secure_url.py | 2 -- tests/functional/test_client_arguments.py | 2 -- tests/functional/ucare_cli/test_config_file.py | 1 - tests/test_project/gallery/admin.py | 2 -- tests/test_project/gallery/models.py | 3 --- 6 files changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1eccd0a1..8c5bef0c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: run: | make test-functional - django-1-2-3: + django-2-and-3: needs: functional runs-on: ubuntu-latest strategy: diff --git a/pyuploadcare/secure_url.py b/pyuploadcare/secure_url.py index 270f21af..0ecf2be2 100644 --- a/pyuploadcare/secure_url.py +++ b/pyuploadcare/secure_url.py @@ -60,7 +60,6 @@ def _build_expire_time(self) -> int: def _build_signature( self, uuid_or_url: str, expire: int, acl: Optional[str] ) -> str: - hash_source = [ f"exp={expire}", f"acl={acl}" if acl else f"url={uuid_or_url}", @@ -75,7 +74,6 @@ def _build_signature( return signature def _build_token(self, expire: int, acl: Optional[str], signature: str): - token_parts = [ f"exp={expire}", f"acl={acl}" if acl else None, diff --git a/tests/functional/test_client_arguments.py b/tests/functional/test_client_arguments.py index fef8a0f4..01b9ad91 100644 --- a/tests/functional/test_client_arguments.py +++ b/tests/functional/test_client_arguments.py @@ -21,7 +21,6 @@ def test_deprecated_argument_for_PY37_if_allow_is_set(test_client, caplog): real_PY36 = pyuploadcare.api.client.PY36 with mock.patch("pyuploadcare.api.client.PY36", False): - if real_PY36: # allow_redirects converted into follow_redirects # but there is an old version of httpx on real PY36 environment @@ -64,7 +63,6 @@ def test_deprecated_argument_is_OK_for_PY36_if_allow_is_set(test_client): real_PY36 = pyuploadcare.api.client.PY36 with mock.patch("pyuploadcare.api.client.PY36", True): - if real_PY36: """`allow_redirects` is valid argument""" with pytest.raises(httpx.UnsupportedProtocol): diff --git a/tests/functional/ucare_cli/test_config_file.py b/tests/functional/ucare_cli/test_config_file.py index 6a9a8bd7..324b0166 100644 --- a/tests/functional/ucare_cli/test_config_file.py +++ b/tests/functional/ucare_cli/test_config_file.py @@ -22,7 +22,6 @@ def test_use_pub_key_from_config_file(config_file): def test_redefine_pub_key_by_second_config_file(config_file): - config_file.write("[ucare]\n" "pub_key = demopublickey") config_file.close() diff --git a/tests/test_project/gallery/admin.py b/tests/test_project/gallery/admin.py index 4241ef90..28813404 100644 --- a/tests/test_project/gallery/admin.py +++ b/tests/test_project/gallery/admin.py @@ -4,12 +4,10 @@ class PhotoInline(admin.StackedInline): - model = Photo class GalleryAdmin(admin.ModelAdmin): - inlines = [ PhotoInline, ] diff --git a/tests/test_project/gallery/models.py b/tests/test_project/gallery/models.py index 295cecbd..af583ff9 100644 --- a/tests/test_project/gallery/models.py +++ b/tests/test_project/gallery/models.py @@ -4,7 +4,6 @@ class Gallery(models.Model): - title = models.CharField(max_length=255) def __unicode__(self): @@ -12,7 +11,6 @@ def __unicode__(self): class Photo(models.Model): - gallery = models.ForeignKey(Gallery, on_delete=models.CASCADE) title = models.CharField(max_length=255) arbitrary_file = FileField(blank=True, null=True) @@ -23,7 +21,6 @@ def __unicode__(self): class GalleryMultiupload(models.Model): - title = models.CharField(max_length=255) photos = ImageGroupField() From d1f39d5a1ce18f5989f6f0bb72aa61290745400f Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 14 Dec 2023 23:15:20 +0100 Subject: [PATCH 17/19] #277 fix mypy errors --- .flake8 | 2 +- Makefile | 2 +- mypy.ini | 1 + pyuploadcare/api/base.py | 4 +++- pyuploadcare/dj/forms.py | 2 +- pyuploadcare/dj/subclassing.py | 2 +- pyuploadcare/resources/base.py | 6 +++--- pyuploadcare/resources/file.py | 2 +- pyuploadcare/resources/file_group.py | 1 + pyuploadcare/transformations/base.py | 6 +++--- pyuploadcare/transformations/video.py | 14 +++++++------- 11 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.flake8 b/.flake8 index 08e69508..048d04fe 100644 --- a/.flake8 +++ b/.flake8 @@ -5,4 +5,4 @@ exclude = .venv,venv,**/migrations/*,snapshots per-file-ignores = tests/**: S101 **/tests/**: S101 - pyuploadcare/ucare_cli/**: T003,T001,T004 + pyuploadcare/ucare_cli/**: T003,T001,T004,T201,T203,T204 diff --git a/Makefile b/Makefile index f57e9bb5..8c2cccb0 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ lint: poetry run black --check . poetry run isort --check . poetry run flake8 . - poetry run mypy --namespace-packages --show-error-codes . + poetry run mypy --namespace-packages --show-error-codes --check-untyped-defs . test: poetry run pytest -v tests/ --cov=pyuploadcare diff --git a/mypy.ini b/mypy.ini index 91590b19..bebcf1b3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,7 @@ [mypy] ignore_missing_imports = True allow_untyped_globals = True +plugins = pydantic.mypy [mypy-*.migrations.*] ignore_errors = True diff --git a/pyuploadcare/api/base.py b/pyuploadcare/api/base.py index 84b905ab..086c87e9 100644 --- a/pyuploadcare/api/base.py +++ b/pyuploadcare/api/base.py @@ -194,7 +194,9 @@ def list( # noqa: C901 if request_limit is not None: query_parameters["limit"] = request_limit - next_ = self._build_url(query_parameters=query_parameters) + next_: Optional[str] = self._build_url( + query_parameters=query_parameters + ) while next_: document = self._client.get(next_) diff --git a/pyuploadcare/dj/forms.py b/pyuploadcare/dj/forms.py index 503ada38..2121d112 100644 --- a/pyuploadcare/dj/forms.py +++ b/pyuploadcare/dj/forms.py @@ -75,7 +75,7 @@ def widget_attrs(self, widget): attrs["data-clearable"] = "" if dj_conf.signed_uploads: expire, signature = self._client.generate_upload_signature() - attrs["data-secure-expire"] = expire + attrs["data-secure-expire"] = str(expire) attrs["data-secure-signature"] = signature return attrs diff --git a/pyuploadcare/dj/subclassing.py b/pyuploadcare/dj/subclassing.py index ff03d5e7..70e047a4 100644 --- a/pyuploadcare/dj/subclassing.py +++ b/pyuploadcare/dj/subclassing.py @@ -23,7 +23,7 @@ class SubfieldBase(type): def __new__(cls, name, bases, attrs): new_class = super(SubfieldBase, cls).__new__(cls, name, bases, attrs) - new_class.contribute_to_class = make_contrib( + new_class.contribute_to_class = make_contrib( # type: ignore[attr-defined] new_class, attrs.get("contribute_to_class") ) return new_class diff --git a/pyuploadcare/resources/base.py b/pyuploadcare/resources/base.py index c48f2827..58f09145 100644 --- a/pyuploadcare/resources/base.py +++ b/pyuploadcare/resources/base.py @@ -1,6 +1,6 @@ import itertools from datetime import date, datetime -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from pyuploadcare.api.base import ListCountMixin @@ -29,8 +29,8 @@ def __init__( self.starting_point = starting_point self.limit = limit self.request_limit = request_limit - self._count = None - self._client = client + self._count: Optional[int] = None + self._client: "Uploadcare" = client @property def starting_point(self): diff --git a/pyuploadcare/resources/file.py b/pyuploadcare/resources/file.py index 70fd23e6..544c1c10 100644 --- a/pyuploadcare/resources/file.py +++ b/pyuploadcare/resources/file.py @@ -541,7 +541,7 @@ def __init__(self, token, client: "Uploadcare"): self.token = token self._client = client - self._info_cache = None + self._info_cache: Optional[Dict[str, Any]] = None def __repr__(self): return f"" diff --git a/pyuploadcare/resources/file_group.py b/pyuploadcare/resources/file_group.py index d3f6b9ff..ba757dc7 100644 --- a/pyuploadcare/resources/file_group.py +++ b/pyuploadcare/resources/file_group.py @@ -148,6 +148,7 @@ def update_info(self): self.id ).model_dump() if self.is_stored and self._stored_at: + assert self._info_cache self._info_cache["datetime_stored"] = self._stored_at.isoformat() return self._info_cache diff --git a/pyuploadcare/transformations/base.py b/pyuploadcare/transformations/base.py index a9c740b0..c8306720 100644 --- a/pyuploadcare/transformations/base.py +++ b/pyuploadcare/transformations/base.py @@ -1,6 +1,8 @@ from enum import Enum from typing import List, Optional, Union +from typing_extensions import Self + class StrEnum(str, Enum): def __str__(self): @@ -19,9 +21,7 @@ def __init__( transformation = transformation.rstrip("/") # type: ignore self._effects.append(transformation) - def set( - self, transformation_name: str, parameters: List[str] - ) -> "BaseTransformation": + def set(self, transformation_name: str, parameters: List[str]) -> Self: effect = transformation_name if parameters: effect += "/" + "/".join(parameters) diff --git a/pyuploadcare/transformations/video.py b/pyuploadcare/transformations/video.py index 098a327a..0aedd845 100644 --- a/pyuploadcare/transformations/video.py +++ b/pyuploadcare/transformations/video.py @@ -1,5 +1,7 @@ from typing import Optional, Union +from typing_extensions import Self + from pyuploadcare.transformations.base import BaseTransformation, StrEnum @@ -25,9 +27,7 @@ class Quality(StrEnum): class VideoTransformation(BaseTransformation): - def format( - self, file_format: Union[VideoFormat, str] - ) -> "VideoTransformation": + def format(self, file_format: Union[VideoFormat, str]) -> Self: self.set("format", [file_format]) return self @@ -36,22 +36,22 @@ def size( width: Optional[int] = None, height: Optional[int] = None, resize_mode: Optional[Union[str, ResizeMode]] = None, - ) -> "VideoTransformation": + ) -> Self: parameters = [f'{width or ""}x{height or ""}'] if resize_mode: parameters.append(resize_mode) self.set("size", parameters) return self - def quality(self, file_quality: Quality) -> "VideoTransformation": + def quality(self, file_quality: Quality) -> Self: self.set("quality", [file_quality]) return self - def cut(self, start_time: str, length: str) -> "VideoTransformation": + def cut(self, start_time: str, length: str) -> Self: self.set("cut", [start_time, length]) return self - def thumbs(self, amount: int) -> "VideoTransformation": + def thumbs(self, amount: int) -> Self: self.set("thumbs", [str(amount)]) return self From 658c26e7cefe97475eaca6dfdbc0644a875d5712 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 14 Dec 2023 23:24:36 +0100 Subject: [PATCH 18/19] #277 updated changelog for 5.0.0 --- HISTORY.md | 19 ++++++++----------- docs/install.rst | 5 ----- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index a289a371..75007977 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,26 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [5.0.0](https://github.com/uploadcare/pyuploadcare/compare/v4.2.3...v5.0.0) - unreleased +## [5.0.0](https://github.com/uploadcare/pyuploadcare/compare/v4.2.3...v5.0.0) - Unreleased -### Breaking changes +### Breaking Changes -- Python 3.6 is no longer supported. -- Python 3.7 is no longer supported. -- Django 1.11 is no longer supported. -- Django 2.0 is no longer supported. -- Django 2.1 is no longer supported. -- [Pydantic](https://docs.pydantic.dev) has been updated to V2. If your project relies on Pydantic V1, you may encounter errors, as versions 1 and 2 of this library are not fully compatible with each other. -- Removed `tox.ini`. The recommended way to run tests locally is by using [act](https://github.com/nektos/act) with Docker. +- Python 3.6 and 3.7 are no longer supported. +- Django 1.11, 2.0, and 2.1 are no longer supported. +- [Pydantic](https://docs.pydantic.dev) has been updated to Version 2. Projects dependent on Pydantic Version 1 may encounter errors due to incompatibility between Versions 1 and 2. +- Removed `tox.ini`. The recommended method for running tests locally is now through [act](https://github.com/nektos/act) with Docker. ### Added -- Python 3.12 and Django 5.0 were added to the test matrix. +- Added Python 3.12 and Django 5.0 to the test matrix. ### Changed - Updated dependencies: `httpx`, `pydantic`, `pytz`, `typing-extensions`. -- Updated dev-dependencies: `mypy`, `pytest`, `black`, `isort`, `flake8`, `flake8-print`, `vcrpy`, `yarl`, `coverage`, `sphinx`, `sphinx-argparse`, `types-*`; `pytest-freezegun` has been replaced with `pytest-freezer`. +- Updated development dependencies: `mypy`, `pytest`, `black`, `isort`, `flake8`, `flake8-print`, `vcrpy`, `yarl`, `coverage`, `pytest-cov`, `sphinx`, `sphinx-argparse`, `types-*`. Replaced `pytest-freezegun` with `pytest-freezer`. ## [4.2.3](https://github.com/uploadcare/pyuploadcare/compare/v4.2.2...v4.2.3) - unreleased diff --git a/docs/install.rst b/docs/install.rst index 0780b76e..46057098 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -115,8 +115,3 @@ A version 4.0 uses REST API 0.7 and contains the next breaking changes: * For ``File``: * Removed method ``copy`` in favor of ``local_copy`` and ``remote_copy`` methods. * Files to upload must be opened in a binary mode. - -Update to version 5.0 ---------------------- - -... From 7a142e970d345b27de3c7991ff30b58e0a2e1b1e Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 28 Dec 2023 13:28:19 +0100 Subject: [PATCH 19/19] #277 bump to 5.0.0 --- HISTORY.md | 2 +- pyproject.toml | 2 +- pyuploadcare/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 6d6d77d3..c540c659 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [4.4.0](https://github.com/uploadcare/pyuploadcare/compare/v4.3.0...v4.4.0) - Unreleased +## [5.0.0](https://github.com/uploadcare/pyuploadcare/compare/v4.3.0...v5.0.0) - 2023-12-28 ### Breaking Changes diff --git a/pyproject.toml b/pyproject.toml index bb92bfae..e9d93a32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyuploadcare" -version = "4.4.0" +version = "5.0.0" description = "Python library for Uploadcare.com" authors = ["Uploadcare Inc "] readme = "README.md" diff --git a/pyuploadcare/__init__.py b/pyuploadcare/__init__.py index edbf97ad..b260e986 100644 --- a/pyuploadcare/__init__.py +++ b/pyuploadcare/__init__.py @@ -1,5 +1,5 @@ # isort: skip_file -__version__ = "4.4.0" +__version__ = "5.0.0" from pyuploadcare.resources.file import File # noqa: F401 from pyuploadcare.resources.file_group import FileGroup # noqa: F401