From 5177390efa0c24bf7512ac4e64100f452639760b Mon Sep 17 00:00:00 2001 From: Patrick Kern Date: Wed, 17 Aug 2022 10:23:26 +0200 Subject: [PATCH] 5.1.0: Migrate to poetry, python 3.10 support (#50) * migrated to poetry, black formatted * fix: incorrect workflow; flake8 unused import * fix: flake8 import error; make install; workflows * fix: cli installation * add support for python 3.7 * fix: client closing after request * upgrade dependencies * use ubuntu latest for ci * fix version number * fix: version number * cleanup typing * fix: parameter names * fix: pydantic default value definition * fix: import * add: python-multipart * add removed comments back in Co-authored-by: patrick --- .github/workflows/build.yml | 25 +- .github/workflows/pytest.yml | 20 +- Makefile | 11 +- poetry.lock | 546 ++++++++++++++++++ pyproject.toml | 47 +- redact/__init__.py | 2 +- redact/{__main__.py => main.py} | 0 redact/redact_requests.py | 20 +- redact/settings.py | 6 +- redact/tools/redact_folder.py | 24 +- tests/__init__.py | 0 tests/conftest.py | 8 +- tests/functional/test_redact_folder.py | 13 +- tests/functional/test_subscription_key.py | 8 +- tests/functional/test_warnings.py | 4 +- tests/integration/mock_server.py | 2 +- tests/integration/test_requests.py | 2 +- tests/requirements.txt | 11 - .../resources/licence_plate_custom_stamp.png | Bin 8858 -> 9859 bytes 19 files changed, 646 insertions(+), 103 deletions(-) create mode 100644 poetry.lock rename redact/{__main__.py => main.py} (100%) create mode 100644 tests/__init__.py delete mode 100644 tests/requirements.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cdb96e5..e0fdc1c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,28 +4,25 @@ on: [push] jobs: build: - - runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [ 3.7, 3.8, 3.9 ] + python-version: ["3.7", "3.8", "3.9", "3.10"] + poetry-version: ["1.0", "1.1.11"] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install pip-autoremove - pip install flit + - name: Run image + uses: abatilo/actions-poetry@v2.0.0 + with: + poetry-version: ${{ matrix.poetry-version }} - name: Build & Install Package run: | make install - - name: Remove Aux. Dependencies - run: | - pip-autoremove flit -y - name: Test CLI Endpoints run: | - make test-cmd-install + make test-cmd-install \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 8e53f21..4333bca 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -4,24 +4,28 @@ on: [push] jobs: build: - - runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [ 3.7, 3.8, 3.9 ] + python-version: ["3.7", "3.8", "3.9", "3.10"] + poetry-version: ["1.0", "1.1.11"] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Run image + uses: abatilo/actions-poetry@v2.0.0 + with: + poetry-version: ${{ matrix.poetry-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -r tests/requirements.txt + poetry install - name: Unit tests run: | make test-unit - name: Integration tests run: | - make test-integration + make test-integration \ No newline at end of file diff --git a/Makefile b/Makefile index 7570637..1cb2503 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,24 @@ -VERSION=5.0.1 +VERSION=5.1.0 SHELL := /bin/bash .PHONY: build install test-functional test-unit test-integration test-cmd-install build: - cd redact - flit build + poetry build install: make build pip install . --upgrade test-functional: - python3 -m pytest tests/functional/ --api_key $(api_key) --redact_url $(redact_url) + poetry run pytest tests/functional/ --api_key $(api_key) --redact_url $(redact_url) test-unit: - python3 -m pytest tests/unit/ + poetry run pytest tests/unit/ test-integration: - python3 -m pytest tests/integration/ + poetry run pytest tests/integration/ test-cmd-install: redact_file --help && redact_folder --help && echo "OK: Command-line endpoints installed" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..7e842ea --- /dev/null +++ b/poetry.lock @@ -0,0 +1,546 @@ +[[package]] +name = "anyio" +version = "3.6.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "certifi" +version = "2022.6.15" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "charset-normalizer" +version = "2.1.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"] + +[[package]] +name = "fastapi" +version = "0.79.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = "0.19.1" + +[package.extras] +all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] +dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<7.0.0)"] +test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==22.3.0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==4.2.1)", "types-orjson (==3.6.2)", "types-dataclasses (==0.6.5)"] + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "httpcore" +version = "0.14.7" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4.0.0" +certifi = "*" +h11 = ">=0.11,<0.13" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.22.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +certifi = "*" +charset-normalizer = "*" +httpcore = ">=0.14.5,<0.15.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.2.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pillow" +version = "9.2.0" +description = "Python Imaging Library (Fork)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pydantic" +version = "1.9.2" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +email = ["email-validator (>=1.0.3)"] +dotenv = ["python-dotenv (>=0.10.4)"] + +[[package]] +name = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pygments" +version = "2.12.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-timeout" +version = "2.1.0" +description = "pytest plugin to abort hanging tests" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=5.0.0" + +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.4.0" + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "12.5.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.3,<4.0.0" + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "shellingham" +version = "1.5.0" +description = "Tool to Detect Surrounding Shell" +category = "main" +optional = false +python-versions = ">=3.4" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "starlette" +version = "0.19.1" +description = "The little ASGI library that shines." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tqdm" +version = "4.64.0" +description = "Fast, Extensible Progress Meter" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "typer" +version = "0.6.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +click = ">=7.1.1,<9.0.0" +colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} +rich = {version = ">=10.11.0,<13.0.0", optional = true, markers = "extra == \"all\""} +shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} + +[package.extras] +test = ["rich (>=10.11.0,<13.0.0)", "isort (>=5.0.6,<6.0.0)", "black (>=22.3.0,<23.0.0)", "mypy (==0.910)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "coverage (>=5.2,<6.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest (>=4.4.0,<5.4.0)", "shellingham (>=1.3.0,<2.0.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.3.1,<2.0.0)"] +all = ["rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)", "colorama (>=0.4.3,<0.5.0)"] + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "uvicorn" +version = "0.18.2" +description = "The lightning-fast ASGI server." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchfiles (>=0.13)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] + +[[package]] +name = "zipp" +version = "3.8.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "400527a4c9a75f8532fa42a8360925100e9759566f0b890c2284e8f2d29690f4" + +[metadata.files] +anyio = [] +atomicwrites = [] +attrs = [] +certifi = [] +charset-normalizer = [] +click = [] +colorama = [] +commonmark = [] +fastapi = [] +flake8 = [] +h11 = [] +httpcore = [] +httpx = [] +idna = [] +importlib-metadata = [] +iniconfig = [] +mccabe = [] +packaging = [] +pillow = [] +pluggy = [] +py = [] +pycodestyle = [] +pydantic = [] +pyflakes = [] +pygments = [] +pyparsing = [] +pytest = [] +pytest-timeout = [] +python-multipart = [] +rfc3986 = [] +rich = [] +shellingham = [] +six = [] +sniffio = [] +starlette = [] +tomli = [] +tqdm = [] +typer = [] +typing-extensions = [] +uvicorn = [] +zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 8bb47f8..53b6e2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,31 @@ -[build-system] -requires = ["flit_core >=2,<4"] -build-backend = "flit_core.buildapi" +[tool.poetry] +name = "redact" +version = "5.1.0" +description = "Command-line and Python client for Brighter AI's Redact" +authors = ["brighter AI "] +readme = "README.md" +license = "MIT License" + +[tool.poetry.scripts] +redact_file = "redact.main:redact_file_entry_point" +redact_folder = "redact.main:redact_folder_entry_point" -[tool.flit.scripts] -redact_file = "redact.__main__:redact_file_entry_point" -redact_folder = "redact.__main__:redact_folder_entry_point" +[tool.poetry.dependencies] +python = "^3.7" +httpx = "^0.22.0" +pydantic = "^1.9.2" +tqdm = "^4.60.0" +typer = {extras = ["all"], version = "^0.6.1"} +python-multipart = "^0.0.5" -[tool.flit.metadata] -module = "redact" -author = "brighter AI" -author-email = "dev@brighter.ai" -home-page = "https://github.com/brighter-ai" -requires-python = ">=3.7" -requires = [ - "httpx~=0.23.0", - "pydantic~=1.9.1", - "tqdm~=4.64.0", - "typer~=0.6.1", -] +[tool.poetry.dev-dependencies] +pytest = "^7.1.2" +Pillow = "^9.2.0" +uvicorn = "^0.18.2" +fastapi = "^0.79.0" +pytest-timeout = "^2.1.0" +flake8 = "^5.0.3" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/redact/__init__.py b/redact/__init__.py index 2e36227..46403f2 100644 --- a/redact/__init__.py +++ b/redact/__init__.py @@ -2,7 +2,7 @@ Python client for "brighter Redact" """ -__version__ = "5.0.1" +__version__ = "5.1.0" from .redact_instance import RedactInstance # noqa from .redact_job import RedactJob # noqa diff --git a/redact/__main__.py b/redact/main.py similarity index 100% rename from redact/__main__.py rename to redact/main.py diff --git a/redact/redact_requests.py b/redact/redact_requests.py index 94acf8b..7bf776e 100644 --- a/redact/redact_requests.py +++ b/redact/redact_requests.py @@ -8,7 +8,7 @@ import os from io import FileIO from pathlib import Path -from typing import Dict, Optional, IO, Union +from typing import Any, Dict, Optional, IO, Union from uuid import UUID import uuid @@ -58,23 +58,19 @@ def __init__( api_key: Optional[str] = None, httpx_client: Optional[httpx.Client] = None, ): - self.redact_url = normalize_url(redact_url) self.api_key = api_key self.subscription_id = subscription_id self._headers = {"Accept": "*/*"} self.retry_total_time_limit: float = 600 # 10 minutes in seconds - if api_key: + if self.api_key: self._headers["api-key"] = self.api_key - if subscription_id: + if self.subscription_id: self._headers["Subscription-Id"] = self.subscription_id # httpx.Client client is thread safe, see https://github.com/encode/httpx/discussions/1633 - if httpx_client: - self._client = httpx_client - else: - self._client = get_singleton_client() + self._client = httpx_client or get_singleton_client() def post_job( self, @@ -91,10 +87,10 @@ def post_job( try: _ = file.name - except AttributeError: + except AttributeError as e: raise ValueError( "Expecting 'file' argument to have a 'name' attribute, i.e., FileIO." - ) + ) from e url = urllib.parse.urljoin( self.redact_url, f"{service}/{self.API_VERSION}/{out_type}" @@ -209,7 +205,7 @@ def write_output_to_file( output_id: UUID, file: Path, ignore_warnings: bool = False, - ) -> JobResult: + ) -> None: """ Retrieves job result and streams it to file, greatly reducing memory load and resolving memory fragmentation problems. @@ -347,7 +343,7 @@ def _calculate_retry_backoff( def _retry_on_network_problem_with_backoff( self, func, debug_uuid: uuid.UUID, *positional_arguments, **keyword_arguments - ): + ) -> Any: retry_start = -1 retry_delay = -1 diff --git a/redact/settings.py b/redact/settings.py index d78fde5..f94c127 100644 --- a/redact/settings.py +++ b/redact/settings.py @@ -1,7 +1,7 @@ -from pydantic import BaseSettings, AnyUrl +from pydantic import BaseSettings, AnyUrl, Field class Settings(BaseSettings): log_level: str = "INFO" - redact_online_url: AnyUrl = "https://api.brighter.ai/" - redact_url_default: AnyUrl = "http://127.0.0.1:8787/" + redact_online_url: AnyUrl = Field("https://api.brighter.ai/") + redact_url_default: AnyUrl = Field("http://127.0.0.1:8787/") diff --git a/redact/tools/redact_folder.py b/redact/tools/redact_folder.py index 45d322e..5dfc920 100644 --- a/redact/tools/redact_folder.py +++ b/redact/tools/redact_folder.py @@ -6,7 +6,7 @@ from enum import Enum from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Union from redact.data_models import RedactResponseError, JobArguments, RedactConnectError from redact.redact_job import ServiceType, OutputType @@ -34,8 +34,8 @@ class InputType(str, Enum): def redact_folder( - in_dir: str, - out_dir: str, + in_dir: Union[str, Path], + out_dir: Union[str, Path], input_type: InputType, out_type: OutputType, service: ServiceType, @@ -52,9 +52,9 @@ def redact_folder( ): # Normalize paths, e.g.: '~/..' -> '/home' - in_dir = normalize_path(in_dir) - out_dir = normalize_path(out_dir) - log.info(f"Anonymize files from {in_dir} ...") + in_dir_path = normalize_path(in_dir) + out_dir_path = normalize_path(out_dir) + log.info(f"Anonymize files from {in_dir_path} ...") if auto_delete_input_file: log.warn( @@ -62,18 +62,20 @@ def redact_folder( ) # Create out_dir if not existing - if not Path(out_dir).exists(): - os.makedirs(out_dir) + if not Path(out_dir_path).exists(): + os.makedirs(out_dir_path) # List of relative input paths (only img/vid) - relative_file_paths = _get_relative_file_paths(in_dir=in_dir, input_type=input_type) + relative_file_paths = _get_relative_file_paths( + in_dir=in_dir_path, input_type=input_type + ) log.info(f"Found {len(relative_file_paths)} {input_type.value} to process") # Fix input arguments to make method mappable worker_function = functools.partial( _try_redact_file_with_relative_path, - base_dir_in=in_dir, - base_dir_out=out_dir, + base_dir_in=in_dir_path, + base_dir_out=out_dir_path, service=service, out_type=out_type, job_args=job_args, diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index 1044d81..e724d9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import shutil from pathlib import Path -from typing import IO +from typing import IO, Optional from redact.settings import Settings @@ -35,11 +35,9 @@ def redact_url(request) -> str: @pytest.fixture(scope="session") -def optional_api_key(request) -> str: +def optional_api_key(request) -> Optional[str]: api_key = request.config.getoption("--api_key") - if not api_key: - return None - return api_key + return api_key or None @pytest.fixture diff --git a/tests/functional/test_redact_folder.py b/tests/functional/test_redact_folder.py index cbd3981..d94a13d 100644 --- a/tests/functional/test_redact_folder.py +++ b/tests/functional/test_redact_folder.py @@ -1,9 +1,10 @@ -import pytest - from pathlib import Path +from typing import Union + +import pytest from redact.data_models import OutputType, ServiceType -from redact.tools.redact_folder import redact_folder, InputType +from redact.tools.redact_folder import InputType, redact_folder class TestRedactFolder: @@ -24,8 +25,8 @@ def test_all_images_in_folder_are_anonymized( # WHEN the whole folder is anonymized redact_folder( - in_dir=str(images_path), - out_dir=str(output_path), + in_dir=images_path, + out_dir=output_path, input_type=InputType.images, out_type=OutputType.images, service=ServiceType.blur, @@ -54,7 +55,7 @@ def test_all_images_in_folder_are_anonymized( assert len(files_in_out_dir) == 2 * len(files_in_in_dir) @staticmethod - def _replace_file_ext(file_path: str, new_ext: str = ".json") -> str: + def _replace_file_ext(file_path: Union[str, Path], new_ext: str = ".json") -> str: """/some/file.abc -> /some/file.xyz""" file_path = Path(file_path) return str(file_path.parent.joinpath(f"{file_path.stem}{new_ext}")) diff --git a/tests/functional/test_subscription_key.py b/tests/functional/test_subscription_key.py index 3313fb1..da8b9f4 100644 --- a/tests/functional/test_subscription_key.py +++ b/tests/functional/test_subscription_key.py @@ -128,8 +128,8 @@ def test_redact_folder_with_invalid_api_key_fails( # WHEN the folder is anonymized through Redact Online with invalid api key redact_folder( - in_dir=str(images_path), - out_dir=str(output_path), + in_dir=images_path, + out_dir=output_path, redact_url=REDACT_ONLINE_URL, input_type=InputType.images, out_type=OutputType.images, @@ -151,8 +151,8 @@ def test_redact_folder_with_valid_api_key( # WHEN the folder is anonymized through Redact Online with valid api key redact_folder( - in_dir=str(images_path), - out_dir=str(output_path), + in_dir=images_path, + out_dir=output_path, redact_url=REDACT_ONLINE_URL, input_type=InputType.images, out_type=OutputType.images, diff --git a/tests/functional/test_warnings.py b/tests/functional/test_warnings.py index e60e2a6..1210a43 100644 --- a/tests/functional/test_warnings.py +++ b/tests/functional/test_warnings.py @@ -85,8 +85,8 @@ def test_redact_folder_with_ignore_warnings( # WHEN all videos in the folder are redacted tmp_out_path = tmp_path_factory.mktemp("tmp_out") redact_folder( - in_dir=str(tmp_in_path), - out_dir=str(tmp_out_path), + in_dir=tmp_in_path, + out_dir=tmp_out_path, input_type=InputType.videos, out_type=OutputType.videos, service=ServiceType.blur, diff --git a/tests/integration/mock_server.py b/tests/integration/mock_server.py index 9943182..604eb94 100644 --- a/tests/integration/mock_server.py +++ b/tests/integration/mock_server.py @@ -93,7 +93,7 @@ async def _mock_redact_request_handler( file = form[key] val = await file.read() if repr(val) != repr(expected_form_content[key]): - raise Exception( + raise ValueError( f"Failed comparison: Expected: {expected_form_content[key]} != {val}" ) diff --git a/tests/integration/test_requests.py b/tests/integration/test_requests.py index 0916b2b..ff3235f 100644 --- a/tests/integration/test_requests.py +++ b/tests/integration/test_requests.py @@ -9,7 +9,7 @@ RedactResponseError, ) -from integration.mock_server import mock_redact_server +from tests.integration.mock_server import mock_redact_server from redact.data_models import FrameLabels, JobLabels, Label diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index fe57392..0000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -fastapi~=0.68.1 -flit~=3.2.0 -httpx~=0.23.0 -pydantic~=1.9.1 -pytest~=6.2.3 -pytest-timeout~=1.4.2 -tqdm~=4.64.0 -typer~=0.6.1 -uvicorn~=0.18.2 -Pillow~=9.0.1 -python-multipart~=0.0.5 diff --git a/tests/resources/licence_plate_custom_stamp.png b/tests/resources/licence_plate_custom_stamp.png index c48caf7b1ff965b7d1f61e4f7f4a407c542e30ad..4cad94d5e6112dee0b91483ebd7f76e20b0ee2f4 100644 GIT binary patch literal 9859 zcmc(F(^n;q_jXM+*`8dJZQGn|+c=%-#538NY}>Xy*-bhblUU|$NhJqr8l9v+K1Z7_s!Gkol z)&pjHIFk}&Y$Z+#@Wm61VWmot&T6CJZe-^?z^m_Wn0nIEjz9COJ8M}w8StXGDuU<` zf-o|1P7x3gnK6E0sF!Z~ejFzYy3=OJTmK>)*m}L48k(}a>^aW$o$>0u4S-N2t0N)f zG3iEN;Z#{L>(*l^>q?9U!6B=KQd=_p2etlxLuPf_zJ?Lqazv@$%t3<^ykRI8A@mxT zZMmk&Y-W3gO#02ZZZ|0ha6lX!JoCM#A-1>e+)uYBhuyZw2*iy2)8kz=O*+6GjKWXh zR~6&M9Kg;sy`@p-ls=;gtdi1g8~G@B6n_t_@LZ>EuOq%6=22^Y7PucCF!0BOqks|Z zk17c{X|{47P;0}gz_Iv32q8@a=7z|mv<=pFzF5S5sob!3n+YJ$;It}E*GZCW+ne+S zfbz;B7x~!`0v1>6^5o{S*X!nH z!goBcSZh9HTj1nStGo0td#Y4$jJR1V_=~g|k@PKza#s17?+Bvyck$p)amp8!GSA_$ zQRxFanVB2KDRtc)eX{E)Wf*E_Rhnt>d~DoIi5)elHi-gyQq>VSq}U%OG(QcaU`VCI zD-GYf77=$@Td^Wnm+i*V8^{WN)E&?7AK6?ViRu{U$h?KsgXXn=+Tm-4q6w1eI5y;~ z9`Y?gFHb;bbNU(#c}H>ZmE8tw8r{tAaGUH07wUgXBs{Pz2CKcObQ@!d&a~iaPzzey^kg zSuElji9RIXXL-14lYW&)x5jB>mjGVCxBdyW>B?3FTB`z3##%wj0 z$`&lyRDW4z9c!tS17Izm$&E&hl6ZOLEvu703N>70rGFCL=C3B{e6&R=o1E5RDq9R@ zAu*6CW~HLB>|gd8!KvtY44q2&FnT^~Lt?S0Ell8dXCYEd`#{jD4S4f5nf_|D-~P)a zM61e6oMK7|fSe_x zyo=!%Wd5+cG=~c%2%$(ft#en2q^1DdARa37LlI$S>Hb2AP_}~TnBb?VKLngMIb<>q zwb-rs_X-{P>&g{A>k;`rkOm1U8`plinNU$2$U~@k%B7i*P5cs3SgUbxj87rFFT`Rz z6}nLq>bletj$73_km1s7B;be3Yj323%ey&;vE@^$;J?nS>`sHQsoM>b9X<+Q+~@E& z$H(Z+bU$`4Trt61vTRqBueCOxIvtT(tGGwjZ3Q6nIP%48iI!4b0YtK;rRC(RLxzi` zfG4_SJ-%&&a|1?hTOI{BFleMgp)s6CXjqJPT}0msWjw&k?^zyQ4fIaF|;5C9_#m1;x|oQM2T;{WAJm-xZ|_>8ymx3~oT} z?}BYW0Dptwa882Z!*hEPSKSjX8*kj zRvY+Yh+YWCP4auYb_vNTD2OTK{5@HyVYwt&!1niPLp`9(x@ zysZ5`PP3B%_WP^HX#d7~=#sNs;KR_-5FMbil-=Cyvfq24%`t%hXaWAqiWz?&^+csW3&`Xi|da! zP8lcbb;HJ0>f>Q&EnzmN#(b6U#-)nTG`2h3-Wn`jp zU03H*;3mfrJS87HOf(XKu^hYPO4W*}VS@0y1hcBws{ql^J5zE*rR24phQs~0$oK@q zj&m)ak-s(R0{(EG&VpzuHMdeo#&nx~*&Y-`WAUXH)0uf}R{t0_0S@ln59>p=>+L*8 zQyU>V#(MqW$wI>@71Kpr6oLh`yJHc|^2H9}=}RMCet&LA_SmmOI}qTFmM=|hIMQd7 z9mmsfia|R%`^NK6~LRE9Q7#8{;4 zG9OW4GHFuIMkO4L!O3ljFU;jYrN$ydQrg;s4!0XW z(pD#txD3w4xo$1xnD;(t(Dlc*l?YiW94g`MH^WSLh5|#FrSz*j^ppT*xXTm8*$ zh2Hjaa9xclw%taE%ZtL%8PXEWzO8vV#Q!8ueS$L~0DiH*Gkx~odT~*=*BVjo`DVJ< zX#(pND#}Q775>M)RZ40+J+dr}N9!M!mYbBy_;oAeuXY*lyR)Ur!B61q^{H+zKoBPf zksw}2IvD6i7{bBcfQZD3-Tk&;So~g(Zo-nS`8Qvwk{CV(YZtvR1pQms!FTZ%u+uF* z=pv5Q}YJbaSqtt5sr|RkGZ{$7z>*jqp)*a6!G@@P=ecQqVQ-_lhWGsJ)gH1kJ4W z=qJc0(vP?-xH;W{;nFab-J2TITcSt(R!7E1QsIz`GZR_E5`rtMTjVG<)9b3Wj`wzi zRt{N8txyax_QlYDuZ6M41HE;|%pFS~zLzCGZjNG>-@F;5LI8iH4g!!73TXamCPsIE z6*T1YA%_9`0P%YDx?uABpDim^O(iR29$>SD(5@Z`hD1yZ}0iS37_92{+sg7*=&}x zU}%}o$W=W(VPUV~Z0Vo_-MG%5biHJVJ5tM#OVp55*`4-txKnJJWZOR+=@e#{mKsjanB_}FPCU6Y| zZ!(qZXOFj1^M`h<$`22-o?7JHk*c4|vUMCQ%}-w-4Ep^xE#1kWyn}`-J$*brZ^;FiRBvbJbqp%bxKAt$GIPp^s8EM?rNEQQH2&ymnPT0Clzj&3QidEkwB9 zYzt}IOJP9~s88r;f#GC-!)kL3Uifie{K*u@&b{r@IsEC*GbvBEsq4s^QH#?nC%>|5 zD&=+}{Lu@|mR>Kn@(|*;#_$0;#V!47d7f3V${t#UoDVc%rw!(qjKgzq@)8!rRon;^ zq&Jyj=aPZZKmC^vr2P1gU0m@vN;$<#_ymupKA@xVFPP?w17S8S?oWE@m;f*Sb!V_C zSjkut_iK?&pqoQ!R}3PXZ1oJr(hBKx_6y^;mw9n$7ytX<;Dpu(y36ikRKwpazg~Ex zyW6(?ydXM^;K8YaZGvHBl3|J2<;u3YnqRZR=TsS^uA5JA0oR zf<8~qNDwM7x^r+S@)*2uvt3dr$YTNF{?uJK49z=DRMTt$gLFotg|GXrZW;6t`Wj@o zaiM%|E-|53yt`AtIkyX?mE)JcDRSa-f3^Cp;^B;vo`N*9wn*H*1>>^nUKA|yDQGz3 z=6(^WzPn`IIA4Ub`*2!xohJJgzVB-Ef$j8<&&fs`To{T!u70BA2X*tIIwvJ4Yy*A} zwmbRoVDRW@L%qRo0Gaw^H&)|puR(N0`pLOw{`JU4JV%Bp+VvtL$Ndb?t)W9 zxp(@0Y$`5&guB0gIMZ2SX(>|@v+ma5A#UwT1f*aV{5g1;dt{JOUDi5zg*8?qt09(%(qOX@FSs{v z_7(bvF#Gpvp7esdR~|b%r9C{6*^SLxm9 zgZ3IAp}@EmW+KxAnD^p8GfzZvG+K1OtiWm=sEo09$_$MXQI`MdS+b~$oG?h<2Hf!ASXYfgoKX0@P z4t8FNe%^S=$+f;tbz+Fc0O4JToP+=c^a8mr2DYnY9PLG}sTzAvZ;egJuGFHSfK-Up zWaMF$<*;-aAeMZSU*s0t8RjwgdUa~f5jb2pWQ*Xi;)D*6GA*NR((QsQ5ZsBpBbOb@ ze!0V|fiXj-juC^|RXK#9It34hD2Ugw0YK=gqy+4Eq?m-l?Vns|B>YB~sV~2U(#Ldx z{#^^tfxBuc`}^i|9q#TFcah^&I|cfCqqV}}Fr*erJSMRyZkuKCq*4IwHaSQu$r4mQYkw!~c0+m~{6`*aR z-F7YoyHebm#fp~xl2@`l0Bc2~j=f6yCyEpXFd~N&r|o3LGCpI~^|U27Ct;qp@m9Qj z9vp)S42Zq#za?Vhu`6!0vhG|}sC5k00h3~Fw!mC>iAbA?qEXo_Oor{zvVHB|G}HP= zzCJs>A|<50l3AKuE^0a6XeBzqFWK3ChF}gGe>i=s7-scz|9gy{6D(@m?PeN#P2|2t zok?x(db^~(SOS7FVYfZQ@r#H^t?TRZDDOQpr9$%AovA-eS1#RQFj}g8;#Q>82>7u? zt1u&GqS(d%<-CDHN1Ygc}A)$a+zHzPf{1*aUo}V zwdX{--Bjgg4)kf+v**Uy^l|?)-06GyH;z6gyCTb*S>UMAbf9?m&=HruqC+a`C_DW-k<33)40JcNdn5Ij?%uhLj)w{@@}Hh{INd2RwyW*ds=l+6KB z=(njwu9~&)s_ilsZQ;55mSJ1gGZ}>`>IY%Grbzqu%?Q?#UoInQr~W1dfy{Sb}8nK~XeWP>zeC zEGta|0;+Q@4eeNTd1@L&ydwi7P~@j|H)CU*9501k@jcD9Wf?ACDm!xb1Kf>$+z5!#1bGjCQ@QH;>fV&#O*_CkcS%Gg zei_-i&78#?JZI$zOpq87K3!fRAUgSdNLedPAH#}$^3k5KTF^lc9I^YhUzmsBRzPkt z)|g^P@EC4v#=Rz5(@y}IS5Az{+)r!3jmxDoDZe;4G(%PqkR!^IqgGD`j< zU#m%Tc-?&0&F6cGiClS1Of+A>9@Dpy(h!dfr;Ydw7Tw1sZrlvsKqQ5LcD5W^9AmtK z6677#1a(frF5N=%hnIRoXI|2G4bXgvocJPTQzjW70ILfu1OpE{P|pXfDEVq7jhT3` z>+w4UG#VnyC7NYKN>P-o+mQ^YPahJ8m0TM^q!F0On;09QIyK9)8=Fa@F<3r+vfZw> zRK{gBp^{iPCXdo))2vF3INMnMN=+(G%7w)x&h8}+nsDrTBpxex*Bkw}=k;zX*?<1^ zfj=;%{h+ey|qr&o}PlQy>Sl}SPW6AfVX2v*9)nv-OIbG84v`Mk=*J9 ztb@PEqUDCF{nKUR=T`j|T}xsB*KMykrbPG-cKFnOPD~SXoxDXHM87_Y@gQ)6D3_bC zHY03MfxUGST5V5MEb>a?6R`M$bEH{YOT!k*5+i)Jmzv;=^IAv!;Qel=Od6qK;DJnc zAWrM<1}v5~nxrms>5+Gd%Z%o3&m)ESL(tF zC5%`Q0H07pc{V&^dKt*Z)0)Q}ppDk|n888H8^IB(kJTLA`$HL_f3Bye_|~c}`hl2i zYCR>Kg`o(tX>Ywd+!`V;uG}Ad|9F~vdIRLj2n7RL*PbGf@w*I9iBFblR1&uClTYFZ zk^?SfNzcRS>#8vs@#`=pKw<8?xV%1F9MEL`*L?pT(Q+r<&xh@V1dce*H+<1}Tx5W@ zY&6k*F?6$2tlmm`#B;wVZaCVe$k@%E2mjsM$A?(L-vV+;oB7%{8#>8pr*PaO55M%IR;(YxI*th6D41# z5~a!Ta~Rsj8E-b=^~T9ckw_=dIch14Tdhb2D+=)ktXz-N7(q+aLuS|4jT1~z=i|~G!y%eDLq);#X923}?6xodVQi^l zawL@#z$;l;(eALOS#2Unh$ed~d0+NkcD?F$C;frTXiuG_sRuYu7lo^>CQwEnE^F#L zHW-T9bNu3n16irl=zv2R8pnLUnb|M^-LSjFYry$FX7|rq5dQZUEM_dIAD_sBV{9J< ztnn@CmAX8W6N{v=zKZfb=05nxQ2D9bxk<5h4kHt(<<4g&T@23^Xt19=UjBD@AUZp) zBDgg)%mHI%^H*H>f;#TN{S%A$1%Qi@yjS%y5!WlDEf{01$i}~l64!c4nQX*oy@C-r z28-V846SbX_)yuZ#}eyD*lXQ~>5%KAJuw?Av8roWRgxRqm1>bVQehIGX)MYp{*-bR z9q|MVrBdgdECfnoMhO~4gP*HHFQk*&PRG$<;=((}i}c&RXP77?=PqA`IWN(=1>JFx zSnX&;L&AD|Ip$i9H9#L7ug~lw6AslMmq8=dm(zx}Eb(o9xO(0h|79Sb%s=s;kfvnm@K9#={?UV5ctI9b-w(0D-(9wwEJ-NH|29yb=znv2HzeU= zi6PZuyBvE#w0mrE?D_%_(IM>9Z$)NIyEx1l9`u#E45AB(L?WWqwgdR0j_--%PBF*)L~0D)zl_f*jCa60gFN+~g2_lEWE$pJ1d?GQ+j8%i< z>0iSXzawd*FzUF-Zd2pG#mGq{CKH5Hz%cYa%+z=JnC)G{brXlT!P0Y0y_g#zQtSge z-EL5_yx2D%#4Ob~o|hBZ^QlR}~j ze$c^Es8k$cT#3vS3S^gmIjV7-JlCx>|91~R*W+L}B_mu(Nuzm8GYP%^ymj~7*Wh2U zu}S$SVf3~!Z;6q?Z$>$|UDhHH-}ptC6zm5DaKE#nG_uW%Ok35UNRBZH%`|&|G(Z=( z(aYRyrUqyMhh+4Gtzy%3wLC~5hC~4NRU37J4M;u;q*FtvRs9`?R_N<(lkOL#S`C+ zbQ}%jSv=6NMAiFZ;D>2FP^t_on{`ICTZQ|h zz$_Z}WXfr}P1bGP#tcuxoP?P!6!Be@%E#>VciB97%U8{<8R-~n95h@ ziD0voH#=h1t>2Hy&fbz9j=H>4D9DPjE!c<4n000B zI7NiuBqBC?z{tyMY;oUN);_q=T|SeiEu9m9fJo3m-Fja@`^v~&}m z$BtWt>Up7-3a@HWY2Q#IQ!$U+K!}^yjgE?nh)T|sL?V;y#LQy8nTKYL{x74-HJqe7 z?Yl?CdaZi9==E~<9TWyFj@Aq{)#NLgi}iwICw80j2PY!Rwx9jh*gBTJb0gGP!=tGr z^nVwtxILXR0^t-}P2ZnhuP0eM_`o7bf&^L-a3Q#7o<3Yl3W(YwTKQgeo@*UF+}pmN z4l|tVJx{Qjl@U2#ip`{^2BS|@FOUR0?1Nq}Porc>8M21Qvh#Dgc*ZIg(+Y-%ujMP* zlMGfd$xM2OwZlldRPtq}E6dgY3_P`4KTYR)M_Vt?S6J9S#7GPzS$y5A;Jm|hSd|(P zT45!PI#*14F;;Kjc+F!k%WA2HY10U_zq<%^O5o*Ak|gKne=Xx=L9!u%{>D=odgNqZDBG4=?qLM-mUSFraffR+ zU5)#?@WW={aLII=Jrwt+vudCI8pG|uYVYuY4jz$B;^)To+J2k)dNTYMQjIs`=%*1E zLP0OGkq>_xw}UyoY{9H)8>q!bEG!FEMry+8OfA{txM|fsvQZHfCBKiU4RdAegGGAy$lR~*&iEX_ zm~gv4bL_q)i_vc(R;@seI>PG$1C}+$ewB!Z`7c7Mg%7My+^^pgxSQ=sHY4o@23yb+ z$2Bp$w&zo1b=EG^3(w03BTdPsHbbanC2J1gmB79fkkT}1Jn6@$(j$p@kmfB5?o(unjCFnDAF6r}D= zO+Sg$TI^qy$_{Yay$Y@SjmNv35w?lHqjfgsi((b;|Ic5*buwRf-U5}T~B|(^U+wsgY z6fp-47D>m}8esV7V{Qc~HLYPZx@EwD4(W3G5IFkUuf& zxiS#;U#ET9J=T$`O`bT*fUgXR8H)1ieT_+)SXU9|Lvw-xJEu1=bEO^${@KjB&#?no z9BPBx{N$w$MHWZ76TK7#XwMK`5@N+GdzLr|AbM^u_z)CYy|#CE`ySnR*|Ze HFb(=Yr8YW9 literal 8858 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 MN-0TJi5ms~9}d`_wg3PC