[
  {
    "path": ".coveragerc",
    "content": "[run]\nbranch = True\nsource = aiomcache, tests\nomit = site-packages\n\n[html]\ndirectory = htmlcov\n"
  },
  {
    "path": ".flake8",
    "content": "[flake8]\nenable-extensions = G\nexclude = build/ venv/\nmax-doc-length = 90\nmax-line-length = 90\nselect = A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,B901,B902,B903,B950\n# E226: Missing whitespace around arithmetic operators can help group things together.\n# E501: Superseeded by B950 (from Bugbear)\n# E722: Superseeded by B001 (from Bugbear)\n# W503: Mutually exclusive with W504.\nignore = E226,E501,E722,W503\nper-file-ignores =\n    # S101: Pytest uses assert\n    tests/*:S101\n\n# flake8-import-order\napplication-import-names = aiomcache\nimport-order-style = pycharm\n\n# flake8-quotes\ninline-quotes = \"\n\n# flake8-requirements\nknown-modules = docker-py:[docker],python-memcached:[memcache]\nrequirements-file = requirements.txt\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: pip\n    directory: \"/\"\n    schedule:\n      interval: daily\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n"
  },
  {
    "path": ".github/workflows/auto-merge.yml",
    "content": "name: Dependabot auto-merge\non: pull_request_target\n\npermissions:\n  pull-requests: write\n  contents: write\n\njobs:\n  dependabot:\n    runs-on: ubuntu-latest\n    if: ${{ github.actor == 'dependabot[bot]' }}\n    steps:\n      - name: Dependabot metadata\n        id: metadata\n        uses: dependabot/fetch-metadata@v3.1.0\n        with:\n          github-token: \"${{ secrets.GITHUB_TOKEN }}\"\n      - name: Enable auto-merge for Dependabot PRs\n        run: gh pr merge --auto --squash \"$PR_URL\"\n        env:\n          PR_URL: ${{github.event.pull_request.html_url}}\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - master\n      - '[0-9].[0-9]+'  # matches to backport branches, e.g. 3.6\n    tags: [ 'v*' ]\n  pull_request:\n    branches:\n      - master\n      - '[0-9].[0-9]+'\n\njobs:\n  lint:\n    name: Linter\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v6\n    - name: Setup Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: 3.9\n        cache: 'pip'\n        cache-dependency-path: '**/requirements*.txt'\n    - name: Install system dependencies\n      run: sudo apt-get install -y libmemcached-dev\n    - name: Install python dependencies\n      uses: py-actions/py-dependency-install@v4\n      with:\n        path: requirements-dev.txt\n    - name: Mypy\n      run: |\n        mypy\n    - name: Flake8\n      run: |\n        flake8\n    - name: Prepare twine checker\n      run: |\n        pip install -U twine wheel\n        python setup.py sdist bdist_wheel\n    - name: Run twine checker\n      run: |\n        twine check dist/*\n\n  test:\n    name: Tests\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']\n\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v6\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v6\n      with:\n        allow-prereleases: true\n        python-version: ${{ matrix.python-version }}\n    - name: Install system dependencies\n      run: sudo apt-get install -y libmemcached-dev\n    - name: Install python dependencies\n      run: |\n        pip install --upgrade pip\n        pip install -U setuptools\n        pip install -r requirements.txt\n        pip install codecov\n    - name: Run memcached service\n      uses: jkeys089/actions-memcached@master\n    - name: Run tests\n      run: pytest\n    - run: python -m coverage xml\n    - name: Upload coverage\n      uses: codecov/codecov-action@v6\n      with:\n        fail_ci_if_error: true\n        token: ${{ secrets.CODECOV_TOKEN }}\n\n  check:  # This job does nothing and is only used for the branch protection\n    if: always()\n    needs: [lint, test]\n    runs-on: ubuntu-latest\n    steps:\n    - name: Decide whether the needed jobs succeeded or failed\n      uses: re-actors/alls-green@release/v1\n      with:\n        jobs: ${{ toJSON(needs) }}\n\n  deploy:\n    name: Deploy\n    environment: release\n    if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')\n    needs: [check]\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v6\n    - name: Update pip, wheel, setuptools, build, twine\n      run: |\n        python -m pip install -U pip wheel setuptools build twine\n    - name: Build dists\n      run: |\n        python -m build\n    - name: Make Release\n      uses: aio-libs/create-release@v1.6.6\n      with:\n        changes_file: CHANGES.rst\n        name: aiomcache\n        version_file: aiomcache/__init__.py\n        github_token: ${{ secrets.GITHUB_TOKEN }}\n        pypi_token: ${{ secrets.PYPI_API_TOKEN }}\n        dist_dir: dist\n        fix_issue_regex: \"`#(\\\\d+) <https://github.com/aio-libs/aiomcache/issues/\\\\1>`\"\n        fix_issue_repl: \"(#\\\\1)\"\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [ 'master' ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ 'master' ]\n  schedule:\n    - cron: '11 11 * * 5'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'python' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v6\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        \n        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs\n        # queries: security-extended,security-and-quality\n\n        \n    # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v4\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n\n    #   If the Autobuild fails above, remove it and uncomment the following three lines. \n    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.\n\n    # - run: |\n    #   echo \"Run, Build Application using script\"\n    #   ./location_of_script_within_repo/buildscript.sh\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".gitignore",
    "content": "*~\nbin\nparts\ncoverage\neggs\nsources\ndist\nvenv\nhtmlcov\n.installed.cfg\ndevelop-eggs\nvar/*\n*.egg-info\n*.pyc\n*.pyo\n*.bak\n*.egg\n*.tar.gz\n*.so\n.tox\n.DS_Store\n.coverage\n.idea\ndocs/_build/\nbuild\n.cache\n.python-version\n.pytest_cache/\n"
  },
  {
    "path": ".mypy.ini",
    "content": "[mypy]\nfiles = aiomcache, examples, tests\ncheck_untyped_defs = True\nfollow_imports_for_stubs = True\ndisallow_any_decorated = True\ndisallow_any_generics = True\ndisallow_any_unimported = True\ndisallow_incomplete_defs = True\ndisallow_subclassing_any = True\ndisallow_untyped_calls = True\ndisallow_untyped_decorators = True\ndisallow_untyped_defs = True\nenable_error_code = redundant-expr, truthy-bool, ignore-without-code, unused-awaitable\nimplicit_reexport = False\nno_implicit_optional = True\npretty = True\nshow_column_numbers = True\nshow_error_codes = True\nstrict_equality = True\nwarn_incomplete_stub = True\nwarn_redundant_casts = True\nwarn_return_any = True\nwarn_unreachable = True\nwarn_unused_ignores = True\n\n[mypy-tests.*]\ndisallow_any_decorated = False\n\n\n[mypy-docker.*]\nignore_missing_imports = True\n\n[mypy-memcache.*]\nignore_missing_imports = True\n"
  },
  {
    "path": "CHANGES.rst",
    "content": "=======\nCHANGES\n=======\n\n.. towncrier release notes start\n\n0.8.2 (2024-05-07)\n==================\n- Fix a static typing error with ``Client.get()``.\n\n0.8.1 (2023-02-10)\n==================\n- Add ``conn_args`` to ``Client`` to allow TLS and other options when connecting to memcache.\n\n0.8.0 (2022-12-11)\n==================\n- Add ``FlagClient`` to support memcached flags.\n- Fix type annotations for ``@acquire``.\n- Fix rare exception caused by memcached server dying in middle of operation.\n- Fix get method to not use CAS.\n\n0.7.0 (2022-01-20)\n=====================\n\n- Added support for Python 3.10\n- Added support for non-ascii keys\n- Added type annotations\n\n0.6.0 (2017-12-03)\n==================\n\n- Drop python 3.3 support\n\n0.5.2 (2017-05-27)\n==================\n\n- Fix issue with pool concurrency and task cancellation\n\n0.5.1 (2017-03-08)\n==================\n\n- Added MANIFEST.in\n\n0.5.0 (2017-02-08)\n==================\n\n- Added gets and cas commands\n\n0.4.0 (2016-09-26)\n==================\n\n- Make max_size strict #14\n\n0.3.0 (2016-03-11)\n==================\n\n- Dockerize tests\n\n- Reuse memcached connections in Client Pool #4\n\n- Fix stats parse to compatible more mc class software #5\n\n0.2 (2015-12-15)\n================\n\n- Make the library Python 3.5 compatible\n\n0.1 (2014-06-18)\n================\n\n- Initial release\n"
  },
  {
    "path": "CONTRIBUTORS.txt",
    "content": "Contributors\n------------\n\nMaarten Draijer\nManuel Miranda\nNikolay Novik\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2013-2016, Nikolay Kim, KeepSafe\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n1. Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE\ninclude CHANGES.rst\ninclude README.rst\ngraft aiomcache\nglobal-exclude *.pyc *.swp\n"
  },
  {
    "path": "Makefile",
    "content": "# Some simple testing tasks (sorry, UNIX only).\n\ndoc:\n\tcd docs && make html\n\techo \"open file://`pwd`/docs/_build/html/index.html\"\n\n\ncov cover coverage:\n\tpy.test --cov=aiomcache --cov-report=html --cov-report=xml --cov-report=term-missing tests\n\t@echo \"open file://`pwd`/htmlcov/index.html\"\n\n\nclean:\n\tfind . -name __pycache__ |xargs rm -rf\n\tfind . -type f -name '*.py[co]' -delete\n\tfind . -type f -name '*~' -delete\n\tfind . -type f -name '.*~' -delete\n\tfind . -type f -name '@*' -delete\n\tfind . -type f -name '#*#' -delete\n\tfind . -type f -name '*.orig' -delete\n\tfind . -type f -name '*.rej' -delete\n\trm -f .coverage\n\trm -rf coverage\n\trm -rf docs/_build\n\n.PHONY: all cov clean\n"
  },
  {
    "path": "README.rst",
    "content": "memcached client for asyncio\n============================\n\nasyncio (PEP 3156) library to work with memcached.\n\n\nGetting started\n---------------\n\nThe API looks very similar to the other memcache clients:\n\n.. code:: python\n\n    import asyncio\n    import aiomcache\n\n    async def hello_aiomcache():\n        mc = aiomcache.Client(\"127.0.0.1\", 11211)\n        await mc.set(b\"some_key\", b\"Some value\")\n        value = await mc.get(b\"some_key\")\n        print(value)\n        values = await mc.multi_get(b\"some_key\", b\"other_key\")\n        print(values)\n        await mc.delete(b\"another_key\")\n\n    asyncio.run(hello_aiomcache())\n\n\nVersion 0.8 introduces `FlagClient` which allows registering callbacks to\nset or process flags.  See `examples/simple_with_flag_handler.py`\n"
  },
  {
    "path": "aiomcache/__init__.py",
    "content": "\"\"\"memcached client, based on mixpanel's memcache_client library\n\nUsage example::\n\n    import aiomcache\n    mc = aiomcache.Client(\"127.0.0.1\", 11211)\n    await mc.set(\"some_key\", \"Some value\")\n    value = await mc.get(\"some_key\")\n    await mc.delete(\"another_key\")\n\"\"\"\n\nfrom .client import Client, FlagClient\nfrom .exceptions import ClientException, ValidationException\n\n__all__ = (\"Client\", \"ClientException\", \"FlagClient\", \"ValidationException\")\n\n__version__ = \"0.8.2\"\n"
  },
  {
    "path": "aiomcache/client.py",
    "content": "import functools\nimport re\nimport sys\nfrom typing import (Any, Awaitable, Callable, Dict, Generic, Literal, Mapping, Optional,\n                    Tuple, TypeVar, Union, overload)\n\nfrom . import constants as const\nfrom .exceptions import ClientException, ValidationException\nfrom .pool import Connection, MemcachePool\n\nif sys.version_info >= (3, 10):\n    from typing import Concatenate, ParamSpec\nelse:\n    from typing_extensions import Concatenate, ParamSpec\n\n__all__ = ['Client']\n\n_P = ParamSpec(\"_P\")\n_T = TypeVar(\"_T\")\n_U = TypeVar(\"_U\")\n_Client = TypeVar(\"_Client\", bound=\"FlagClient[Any]\")\n_Result = Tuple[Dict[bytes, Union[bytes, _T]], Dict[bytes, _U]]\n\n_GetFlagHandler = Callable[[bytes, int], Awaitable[_T]]\n_SetFlagHandler = Callable[[_T], Awaitable[Tuple[bytes, int]]]\n\n\ndef acquire(\n    func: Callable[Concatenate[_Client, Connection, _P], Awaitable[_T]]\n) -> Callable[Concatenate[_Client, _P], Awaitable[_T]]:\n\n    @functools.wraps(func)\n    async def wrapper(self: _Client, *args: _P.args,  # type: ignore[misc]\n                      **kwargs: _P.kwargs) -> _T:\n        conn = await self._pool.acquire()\n        try:\n            return await func(self, conn, *args, **kwargs)\n        except Exception as exc:\n            conn[0].set_exception(exc)\n            raise\n        finally:\n            self._pool.release(conn)\n\n    return wrapper\n\n\nclass FlagClient(Generic[_T]):\n    def __init__(self, host: str, port: int = 11211, *,\n                 pool_size: int = 2, pool_minsize: Optional[int] = None,\n                 conn_args: Optional[Mapping[str, Any]] = None,\n                 get_flag_handler: Optional[_GetFlagHandler[_T]] = None,\n                 set_flag_handler: Optional[_SetFlagHandler[_T]] = None):\n        \"\"\"\n        Creates new Client instance.\n\n        :param host: memcached host\n        :param port: memcached port\n        :param pool_size: max connection pool size\n        :param pool_minsize: min connection pool size\n        :param conn_args: extra arguments passed to\n            asyncio.open_connection(). For details, see:\n            https://docs.python.org/3/library/asyncio-stream.html#asyncio.open_connection.\n        :param get_flag_handler: async method to call to convert flagged\n            values. Method takes tuple: (value, flags) and should return\n            processed value or raise ClientException if not supported.\n        :param set_flag_handler: async method to call to convert non bytes\n            value to flagged value. Method takes value and must return tuple:\n            (value, flags).\n        \"\"\"\n        if not pool_minsize:\n            pool_minsize = pool_size\n\n        self._pool = MemcachePool(\n            host, port, minsize=pool_minsize, maxsize=pool_size,\n            conn_args=conn_args)\n\n        self._get_flag_handler = get_flag_handler\n        self._set_flag_handler = set_flag_handler\n\n    # key may be anything except whitespace and control chars, upto 250 characters.\n    # Must be str for unicode-aware regex.\n    _valid_key_re = re.compile(\"^[^\\\\s\\x00-\\x1F\\x7F-\\x9F]{1,250}$\")\n\n    def _validate_key(self, key: bytes) -> bytes:\n        if not isinstance(key, bytes):  # avoid bugs subtle and otherwise\n            raise ValidationException('key must be bytes', key)\n\n        # Must decode to str for unicode-aware comparison.\n        key_str = key.decode()\n        m = self._valid_key_re.match(key_str)\n        if m:\n            # in python re, $ matches either end of line or right before\n            # \\n at end of line. We can't allow latter case, so\n            # making sure length matches is simplest way to detect\n            if len(m.group(0)) != len(key_str):\n                raise ValidationException('trailing newline', key)\n        else:\n            raise ValidationException('invalid key', key)\n\n        return key\n\n    async def _execute_simple_command(self, conn: Connection, raw_command: bytes) -> bytes:\n        response, line = bytearray(), b''\n\n        conn.writer.write(raw_command)\n        await conn.writer.drain()\n\n        while not line.endswith(b'\\r\\n'):\n            line = await conn.reader.readline()\n            response.extend(line)\n        return response[:-2]\n\n    async def close(self) -> None:\n        \"\"\"Closes the sockets if its open.\"\"\"\n        await self._pool.clear()\n\n    @overload\n    async def _multi_get(self, conn: Connection, *keys: bytes,\n                         with_cas: Literal[True] = ...) -> _Result[_T, int]:\n        ...\n\n    @overload\n    async def _multi_get(self, conn: Connection, *keys: bytes,\n                         with_cas: Literal[False]) -> _Result[_T, None]:\n        ...\n\n    async def _multi_get(  # type: ignore[misc]\n        self, conn: Connection, *keys: bytes,\n            with_cas: bool = True) -> _Result[_T, Optional[int]]:\n        # req  - get <key> [<key> ...]\\r\\n\n        # resp - VALUE <key> <flags> <bytes> [<cas unique>]\\r\\n\n        #        <data block>\\r\\n (if exists)\n        #        [...]\n        #        END\\r\\n\n        if not keys:\n            return {}, {}\n\n        for key in keys:\n            self._validate_key(key)\n\n        if len(set(keys)) != len(keys):\n            raise ClientException('duplicate keys passed to multi_get')\n\n        cmd = b'gets ' if with_cas else b'get '\n        conn.writer.write(cmd + b' '.join(keys) + b'\\r\\n')\n\n        received = {}\n        cas_tokens = {}\n        line = await conn.reader.readline()\n\n        while line != b'END\\r\\n':\n            terms = line.split()\n\n            if terms and terms[0] == b\"VALUE\":  # exists\n                key = terms[1]\n                flags = int(terms[2])\n                length = int(terms[3])\n\n                val_bytes = (await conn.reader.readexactly(length+2))[:-2]\n                if key in received:\n                    raise ClientException('duplicate results from server')\n\n                if flags:\n                    if not self._get_flag_handler:\n                        raise ClientException(\"received flags without handler\")\n\n                    val: Union[bytes, _T] = await self._get_flag_handler(val_bytes, flags)\n                else:\n                    val = val_bytes\n\n                received[key] = val\n                cas_tokens[key] = int(terms[4]) if with_cas else None\n            else:\n                raise ClientException('get failed', line)\n\n            line = await conn.reader.readline()\n\n        if len(received) > len(keys):\n            raise ClientException('received too many responses')\n\n        return received, cas_tokens\n\n    @acquire\n    async def delete(self, conn: Connection, key: bytes) -> bool:\n        \"\"\"Deletes a key/value pair from the server.\n\n        :param key: is the key to delete.\n        :return: True if case values was deleted or False to indicate\n        that the item with this key was not found.\n        \"\"\"\n        self._validate_key(key)\n\n        command = b'delete ' + key + b'\\r\\n'\n        response = await self._execute_simple_command(conn, command)\n\n        if response not in (const.DELETED, const.NOT_FOUND):\n            raise ClientException('Memcached delete failed', response)\n\n        return response == const.DELETED\n\n    @acquire\n    @overload\n    async def get(self, conn: Connection, /, key: bytes,\n                  default: None = ...) -> Union[bytes, _T, None]:\n        ...\n\n    @acquire\n    @overload\n    async def get(self, conn: Connection, /, key: bytes, default: _U) -> Union[bytes, _T, _U]:\n        ...\n\n    @acquire\n    async def get(\n        self, conn: Connection, /, key: bytes, default: Optional[_U] = None\n    ) -> Union[bytes, _T, _U, None]:\n        \"\"\"Gets a single value from the server.\n\n        :param key: ``bytes``, is the key for the item being fetched\n        :param default: default value if there is no value.\n        :return: ``bytes``, is the data for this specified key.\n        \"\"\"\n        values, _ = await self._multi_get(conn, key, with_cas=False)\n        return values.get(key, default)\n\n    @acquire\n    async def gets(\n        self, conn: Connection, key: bytes, default: Optional[bytes] = None\n    ) -> Tuple[Union[bytes, _T, None], Optional[int]]:\n        \"\"\"Gets a single value from the server together with the cas token.\n\n        :param key: ``bytes``, is the key for the item being fetched\n        :param default: default value if there is no value.\n        :return: ``bytes``, ``bytes tuple with the value and the cas\n        \"\"\"\n        values, cas_tokens = await self._multi_get(conn, key, with_cas=True)\n        return values.get(key, default), cas_tokens.get(key)\n\n    @acquire\n    async def multi_get(\n        self, conn: Connection, *keys: bytes\n    ) -> Tuple[Union[bytes, _T, None], ...]:\n        \"\"\"Takes a list of keys and returns a list of values.\n\n        :param keys: ``list`` keys for the item being fetched.\n        :return: ``list`` of values for the specified keys.\n        :raises:``ValidationException``, ``ClientException``,\n        and socket errors\n        \"\"\"\n        values, _ = await self._multi_get(conn, *keys)\n        return tuple(values.get(key) for key in keys)\n\n    @acquire\n    async def stats(\n        self, conn: Connection, args: Optional[bytes] = None\n    ) -> Dict[bytes, Optional[bytes]]:\n        \"\"\"Runs a stats command on the server.\"\"\"\n        # req  - stats [additional args]\\r\\n\n        # resp - STAT <name> <value>\\r\\n (one per result)\n        #        END\\r\\n\n        if args is None:\n            args = b''\n\n        conn.writer.write(b''.join((b'stats ', args, b'\\r\\n')))\n\n        result: Dict[bytes, Optional[bytes]] = {}\n\n        resp = await conn.reader.readline()\n        while resp != b'END\\r\\n':\n            terms = resp.split()\n\n            if len(terms) == 2 and terms[0] == b'STAT':\n                result[terms[1]] = None\n            elif len(terms) == 3 and terms[0] == b'STAT':\n                result[terms[1]] = terms[2]\n            elif len(terms) >= 3 and terms[0] == b'STAT':\n                result[terms[1]] = b' '.join(terms[2:])\n            else:\n                raise ClientException('stats failed', resp)\n\n            resp = await conn.reader.readline()\n\n        return result\n\n    async def _storage_command(self, conn: Connection, command: bytes, key: bytes,\n                               value: Union[bytes, _T], exptime: int = 0,\n                               cas: Optional[int] = None) -> bool:\n        # req  - set <key> <flags> <exptime> <bytes> [noreply]\\r\\n\n        #        <data block>\\r\\n\n        # resp - STORED\\r\\n (or others)\n        # req  - set <key> <flags> <exptime> <bytes> <cas> [noreply]\\r\\n\n        #        <data block>\\r\\n\n        # resp - STORED\\r\\n (or others)\n\n        # typically, if val is > 1024**2 bytes server returns:\n        #   SERVER_ERROR object too large for cache\\r\\n\n        # however custom-compiled memcached can have different limit\n        # so, we'll let the server decide what's too much\n        self._validate_key(key)\n\n        if not isinstance(exptime, int):\n            raise ValidationException('exptime not int', exptime)\n        elif exptime < 0:\n            raise ValidationException('exptime negative', exptime)\n\n        flags = 0\n        if not isinstance(value, bytes):\n            # flag handler only invoked on non-byte values,\n            # consistent with only being invoked on non-zero flags on retrieval\n            if self._set_flag_handler is None:\n                raise ValidationException(\"flag handler must be set for non-byte values\")\n            value, flags = await self._set_flag_handler(value)\n        cas_value = b\" %a\" % cas if cas else b\"\"\n        cmd = b\"%b %b %a %a %a%b\\r\\n%b\\r\\n\" % (\n            command, key, flags, exptime, len(value), cas_value, value\n        )\n        resp = await self._execute_simple_command(conn, cmd)\n\n        if resp not in (\n                const.STORED, const.NOT_STORED, const.EXISTS, const.NOT_FOUND):\n            raise ClientException('stats {} failed'.format(command.decode()), resp)\n        return resp == const.STORED\n\n    @acquire\n    async def set(self, conn: Connection, key: bytes, value: Union[bytes, _T],\n                  exptime: int = 0) -> bool:\n        \"\"\"Sets a key to a value on the server\n        with an optional exptime (0 means don't auto-expire)\n\n        :param key: ``bytes``, is the key of the item.\n        :param value: ``bytes``, data to store.\n        :param exptime: ``int``, is expiration time. If it's 0, the\n        item never expires.\n        :return: ``bool``, True in case of success.\n        \"\"\"\n        return await self._storage_command(conn, b\"set\", key, value, exptime)\n\n    @acquire\n    async def cas(self, conn: Connection, key: bytes, value: Union[bytes, _T], cas_token: int,\n                  exptime: int = 0) -> bool:\n        \"\"\"Sets a key to a value on the server\n        with an optional exptime (0 means don't auto-expire)\n        only if value hasn't changed from first retrieval\n\n        :param key: ``bytes``, is the key of the item.\n        :param value: ``bytes``, data to store.\n        :param exptime: ``int``, is expiration time. If it's 0, the\n        item never expires.\n        :param cas_token: ``int``, unique cas token retrieve from previous\n            ``gets``\n        :return: ``bool``, True in case of success.\n        \"\"\"\n        return await self._storage_command(conn, b\"cas\", key, value, exptime,\n                                           cas=cas_token)\n\n    @acquire\n    async def add(self, conn: Connection, key: bytes, value: Union[bytes, _T],\n                  exptime: int = 0) -> bool:\n        \"\"\"Store this data, but only if the server *doesn't* already\n        hold data for this key.\n\n        :param key: ``bytes``, is the key of the item.\n        :param value: ``bytes``,  data to store.\n        :param exptime: ``int`` is expiration time. If it's 0, the\n        item never expires.\n        :return: ``bool``, True in case of success.\n        \"\"\"\n        return await self._storage_command(conn, b\"add\", key, value, exptime)\n\n    @acquire\n    async def replace(self, conn: Connection, key: bytes, value: Union[bytes, _T],\n                      exptime: int = 0) -> bool:\n        \"\"\"Store this data, but only if the server *does*\n        already hold data for this key.\n\n        :param key: ``bytes``, is the key of the item.\n        :param value: ``bytes``,  data to store.\n        :param exptime: ``int`` is expiration time. If it's 0, the\n        item never expires.\n        :return: ``bool``, True in case of success.\n        \"\"\"\n        return await self._storage_command(conn, b\"replace\", key, value, exptime)\n\n    @acquire\n    async def append(self, conn: Connection, key: bytes, value: Union[bytes, _T],\n                     exptime: int = 0) -> bool:\n        \"\"\"Add data to an existing key after existing data\n\n        :param key: ``bytes``, is the key of the item.\n        :param value: ``bytes``,  data to store.\n        :param exptime: ``int`` is expiration time. If it's 0, the\n        item never expires.\n        :return: ``bool``, True in case of success.\n        \"\"\"\n        return await self._storage_command(conn, b\"append\", key, value, exptime)\n\n    @acquire\n    async def prepend(self, conn: Connection, key: bytes, value: bytes, exptime: int = 0) -> bool:\n        \"\"\"Add data to an existing key before existing data\n\n        :param key: ``bytes``, is the key of the item.\n        :param value: ``bytes``, data to store.\n        :param exptime: ``int`` is expiration time. If it's 0, the\n        item never expires.\n        :return: ``bool``, True in case of success.\n        \"\"\"\n        return await self._storage_command(conn, b\"prepend\", key, value, exptime)\n\n    async def _incr_decr(\n        self, conn: Connection, command: bytes, key: bytes, delta: int\n    ) -> Optional[int]:\n        cmd = b\"%b %b %a\\r\\n\" % (command, key, delta)\n        resp = await self._execute_simple_command(conn, cmd)\n        if not resp.isdigit() or resp == const.NOT_FOUND:\n            raise ClientException(\n                'Memcached {} command failed'.format(str(command)), resp)\n        return int(resp) if resp.isdigit() else None\n\n    @acquire\n    async def incr(self, conn: Connection, key: bytes, increment: int = 1) -> Optional[int]:\n        \"\"\"Command is used to change data for some item in-place,\n        incrementing it. The data for the item is treated as decimal\n        representation of a 64-bit unsigned integer.\n\n        :param key: ``bytes``, is the key of the item the client wishes\n        to change\n        :param increment: ``int``, is the amount by which the client\n        wants to increase the item.\n        :return: ``int``, new value of the item's data,\n        after the increment or ``None`` to indicate the item with\n        this value was not found\n        \"\"\"\n        self._validate_key(key)\n        return await self._incr_decr(conn, b\"incr\", key, increment)\n\n    @acquire\n    async def decr(self, conn: Connection, key: bytes, decrement: int = 1) -> Optional[int]:\n        \"\"\"Command is used to change data for some item in-place,\n        decrementing it. The data for the item is treated as decimal\n        representation of a 64-bit unsigned integer.\n\n        :param key: ``bytes``, is the key of the item the client wishes\n        to change\n        :param decrement: ``int``, is the amount by which the client\n        wants to decrease the item.\n        :return: ``int`` new value of the item's data,\n        after the increment or ``None`` to indicate the item with\n        this value was not found\n        \"\"\"\n        self._validate_key(key)\n        return await self._incr_decr(conn, b\"decr\", key, decrement)\n\n    @acquire\n    async def touch(self, conn: Connection, key: bytes, exptime: int) -> bool:\n        \"\"\"The command is used to update the expiration time of\n        an existing item without fetching it.\n\n        :param key: ``bytes``, is the key to update expiration time\n        :param exptime: ``int``, is expiration time. This replaces the existing\n        expiration time.\n        :return: ``bool``, True in case of success.\n        \"\"\"\n        self._validate_key(key)\n\n        cmd = b\"touch %b %a\\r\\n\" % (key, exptime)\n        resp = await self._execute_simple_command(conn, cmd)\n        if resp not in (const.TOUCHED, const.NOT_FOUND):\n            raise ClientException('Memcached touch failed', resp)\n        return resp == const.TOUCHED\n\n    @acquire\n    async def version(self, conn: Connection) -> bytes:\n        \"\"\"Current version of the server.\n\n        :return: ``bytes``, memcached version for current the server.\n        \"\"\"\n\n        command = b'version\\r\\n'\n        response = await self._execute_simple_command(conn, command)\n        if not response.startswith(const.VERSION):\n            raise ClientException('Memcached version failed', response)\n        version, number = response.rstrip(b\"\\r\\n\").split(maxsplit=1)\n        return number\n\n    @acquire\n    async def flush_all(self, conn: Connection) -> None:\n        \"\"\"Its effect is to invalidate all existing items immediately\"\"\"\n        command = b'flush_all\\r\\n'\n        response = await self._execute_simple_command(conn, command)\n\n        if const.OK != response:\n            raise ClientException('Memcached flush_all failed', response)\n\n\nclass Client(FlagClient[bytes]):\n    def __init__(self, host: str, port: int = 11211, *,\n                 pool_size: int = 2, pool_minsize: Optional[int] = None,\n                 conn_args: Optional[Mapping[str, Any]] = None):\n        super().__init__(host, port, pool_size=pool_size, pool_minsize=pool_minsize,\n                         conn_args=conn_args,\n                         get_flag_handler=None, set_flag_handler=None)\n"
  },
  {
    "path": "aiomcache/constants.py",
    "content": "STORED = b'STORED'\nNOT_STORED = b'NOT_STORED'\nTOUCHED = b'TOUCHED'\nNOT_FOUND = b'NOT_FOUND'\nDELETED = b'DELETED'\nVERSION = b'VERSION'\nEXISTS = b'EXISTS'\nOK = b'OK'\n"
  },
  {
    "path": "aiomcache/exceptions.py",
    "content": "from typing import Optional\n\n__all__ = ['ClientException', 'ValidationException']\n\n\nclass ClientException(Exception):\n    \"\"\"Raised when the server does something we don't expect.\"\"\"\n\n    def __init__(self, msg: str, item: Optional[object] = None):\n        if item is not None:\n            msg = '%s: %r' % (msg, item)\n        super().__init__(msg)\n\n\nclass ValidationException(ClientException):\n    \"\"\"Raised when an invalid parameter is passed to a ``Client`` function.\"\"\"\n"
  },
  {
    "path": "aiomcache/pool.py",
    "content": "import asyncio\nfrom typing import Any, Mapping, NamedTuple, Optional, Set\n\n__all__ = ['MemcachePool']\n\n\nclass Connection(NamedTuple):\n    reader: asyncio.StreamReader\n    writer: asyncio.StreamWriter\n\n\nclass MemcachePool:\n    def __init__(self, host: str, port: int, *, minsize: int, maxsize: int,\n                 conn_args: Optional[Mapping[str, Any]] = None):\n        self._host = host\n        self._port = port\n        self._minsize = minsize\n        self._maxsize = maxsize\n        self.conn_args = conn_args or {}\n        self._pool: asyncio.Queue[Connection] = asyncio.Queue()\n        self._in_use: Set[Connection] = set()\n\n    async def clear(self) -> None:\n        \"\"\"Clear pool connections.\"\"\"\n        while not self._pool.empty():\n            conn = await self._pool.get()\n            self._do_close(conn)\n\n    def _do_close(self, conn: Connection) -> None:\n        conn.reader.feed_eof()\n        conn.writer.close()\n\n    async def acquire(self) -> Connection:\n        \"\"\"Acquire connection from the pool, or spawn new one\n        if pool maxsize permits.\n\n        :return: ``tuple`` (reader, writer)\n        \"\"\"\n        while self.size() == 0 or self.size() < self._minsize:\n            _conn = await self._create_new_conn()\n            if _conn is None:\n                break\n            self._pool.put_nowait(_conn)\n\n        conn: Optional[Connection] = None\n        while not conn:\n            _conn = await self._pool.get()\n            if _conn.reader.at_eof() or _conn.reader.exception() is not None:\n                self._do_close(_conn)\n                conn = await self._create_new_conn()\n            else:\n                conn = _conn\n\n        self._in_use.add(conn)\n        return conn\n\n    def release(self, conn: Connection) -> None:\n        \"\"\"Releases connection back to the pool.\n\n        :param conn: ``namedtuple`` (reader, writer)\n        \"\"\"\n        self._in_use.remove(conn)\n        if conn.reader.at_eof() or conn.reader.exception() is not None:\n            self._do_close(conn)\n        else:\n            self._pool.put_nowait(conn)\n\n    async def _create_new_conn(self) -> Optional[Connection]:\n        if self.size() < self._maxsize:\n            reader, writer = await asyncio.open_connection(\n                self._host, self._port, **self.conn_args)\n            if self.size() < self._maxsize:\n                return Connection(reader, writer)\n            else:\n                reader.feed_eof()\n                writer.close()\n                return None\n        else:\n            return None\n\n    def size(self) -> int:\n        return self._pool.qsize() + len(self._in_use)\n"
  },
  {
    "path": "aiomcache/py.typed",
    "content": ""
  },
  {
    "path": "examples/simple.py",
    "content": "import asyncio\n\nimport aiomcache\n\n\nasync def hello_aiomcache() -> None:\n    mc = aiomcache.Client(\"127.0.0.1\", 11211)\n    await mc.set(b\"some_key\", b\"Some value\")\n    value = await mc.get(b\"some_key\")\n    print(value)\n    values = await mc.multi_get(b\"some_key\", b\"other_key\")\n    print(values)\n    await mc.delete(b\"another_key\")\n\n\nasyncio.run(hello_aiomcache())\n"
  },
  {
    "path": "examples/simple_with_flag_handler.py",
    "content": "import asyncio\nimport datetime\nimport pickle  # noqa: S403\nfrom enum import IntEnum\nfrom typing import Any, Tuple\n\nimport aiomcache\n\n\nclass SimpleFlags(IntEnum):\n    DEMO_FLAG_PICKLE = 1\n\n\nasync def simple_get_flag_handler(value: bytes, flags: int) -> Any:\n    print(\"get flag handler invoked\")\n\n    if flags == SimpleFlags.DEMO_FLAG_PICKLE:\n        return pickle.loads(value)  # noqa: S301\n\n    raise ValueError(f\"unrecognized flag: {flags}\")\n\n\nasync def simple_set_flag_handler(value: Any) -> Tuple[bytes, int]:\n    print(\"set flag handler invoked\")\n\n    return pickle.dumps(value), SimpleFlags.DEMO_FLAG_PICKLE.value\n\n\nasync def hello_aiomcache_with_flag_handlers() -> None:\n    mc = aiomcache.FlagClient(\"127.0.0.1\", 11211,\n                              get_flag_handler=simple_get_flag_handler,\n                              set_flag_handler=simple_set_flag_handler)\n\n    await mc.set(b\"some_first_key\", b\"Some value\")\n    value = await mc.get(b\"some_first_key\")\n\n    print(f\"retrieved value {repr(value)} without flag handler\")\n\n    date_value = datetime.date(2015, 12, 28)\n\n    # flag handlers only triggered for non-byte values\n    await mc.set(b\"some_key_with_flag_handlers\", date_value)\n    value = await mc.get(b\"some_key_with_flag_handlers\")\n\n    print(f'retrieved value with flag handler: {repr(value)}')\n\n\nasyncio.run(hello_aiomcache_with_flag_handlers())\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\naddopts =\n    # show 10 slowest invocations:\n    --durations=10\n\n    # a bit of verbosity doesn't hurt:\n    -v\n\n    # report all the things == -rxXs:\n    -ra\n\n    # show values of the local vars in errors:\n    --showlocals\n    # coverage reports\n    --cov=aiomcache/ --cov=tests/ --cov-report term\nasyncio_mode = auto\nfilterwarnings =\n    error\ntestpaths = tests/\nxfail_strict = true\n"
  },
  {
    "path": "requirements-dev.txt",
    "content": "-r requirements.txt\n\nmypy==1.19.1\nflake8==7.3.0\nflake8-bandit==4.1.1\nflake8-bugbear==24.12.12\nflake8-import-order==0.19.2\nflake8-requirements==2.3.0\n"
  },
  {
    "path": "requirements.txt",
    "content": "docker-py==1.10.6\npytest==8.4.2\npytest-asyncio==1.2.0\npytest-cov==7.1.0\npython-memcached==1.62\ntyping_extensions==4.15.0; python_version<\"3.11\"\n"
  },
  {
    "path": "setup.cfg",
    "content": "[easy_install]\nzip_ok = false\n\n[nosetests]\nnocapture = 1\ncover-package = aiomcache\ncover-erase = 1\n"
  },
  {
    "path": "setup.py",
    "content": "import codecs\nimport os\nimport re\n\nfrom setuptools import setup\n\n\nwith codecs.open(os.path.join(os.path.abspath(os.path.dirname(\n        __file__)), 'aiomcache', '__init__.py'), 'r', 'latin1') as fp:\n    try:\n        version = re.findall(r'^__version__ = \"([^\"]+)\"\\r?$', fp.read(), re.M)[0]\n    except IndexError:\n        raise RuntimeError('Unable to determine version.')\n\n\ndef read(f):\n    return open(os.path.join(os.path.dirname(__file__), f)).read().strip()\n\n\nsetup(name='aiomcache',\n      version=version,\n      description=('Minimal pure python memcached client'),\n      long_description='\\n\\n'.join((read('README.rst'), read('CHANGES.rst'))),\n      long_description_content_type='text/x-rst',\n      classifiers=[\n          'License :: OSI Approved :: BSD License',\n          'Intended Audience :: Developers',\n          'Programming Language :: Python',\n          'Programming Language :: Python :: 3.9',\n          'Programming Language :: Python :: 3.10',\n          'Programming Language :: Python :: 3.11',\n          'Programming Language :: Python :: 3.12',\n          \"Programming Language :: Python :: 3.13\",\n          'Operating System :: POSIX',\n          'Operating System :: MacOS :: MacOS X',\n          'Operating System :: Microsoft :: Windows',\n          'Environment :: Web Environment',\n          'Framework :: AsyncIO',\n      ],\n      author='Nikolay Kim',\n      author_email='fafhrd91@gmail.com',\n      maintainer=', '.join(('Nikolay Kim <fafhrd91@gmail.com>',\n                            'Andrew Svetlov <andrew.svetlov@gmail.com>')),\n      maintainer_email='aio-libs@googlegroups.com',\n      url='https://github.com/aio-libs/aiomcache/',\n      license='BSD',\n      packages=(\"aiomcache\",),\n      python_requires=\">=3.9\",\n      install_requires=('typing_extensions>=4; python_version<\"3.11\"',),\n      tests_require=(\"nose\",),\n      test_suite='nose.collector',\n      include_package_data=True)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/commands_test.py",
    "content": "import asyncio\nimport datetime\nfrom typing import Any\nfrom unittest import mock\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom aiomcache import Client, FlagClient\nfrom aiomcache.exceptions import ClientException, ValidationException\nfrom .flag_helper import FlagHelperDemo\n\n\n@pytest.mark.parametrize(\"key\", (\n    b\"key\",\n    b\"123\",\n    bytes(\"!@#\", \"utf-8\"),\n    bytes(\"中文\", \"utf-8\"),\n    bytes(\"こんにちは\", \"utf-8\"),\n    bytes(\"안녕하세요\", \"utf-8\"),\n))\nasync def test_valid_key(mcache: Client, key: bytes) -> None:\n    assert mcache._validate_key(key) == key\n\n\n@pytest.mark.parametrize(\"key\", (\n    # Whitespace\n    b\"foo bar\",\n    b\"foo\\t\",\n    b\"\\nbar\",\n    b\"foo\\x20\\x0Dbar\",\n    b\"\\x18\\x0E\",\n    b\"\\x20\\x60\",\n    b\"\\x30\\x00\",\n    b\"\\x20\\x01\",\n    # Control characters\n    b\"foo\\x00bar\",\n    b\"\\x1F\",\n    b\"\\x7F\",\n    \"\\u0080\".encode(),\n    \"\\u009F\".encode(),\n))\nasync def test_invalid_key(mcache: Client, key: bytes) -> None:\n    with pytest.raises(ValidationException, match=\"invalid key\"):\n        mcache._validate_key(key)\n\n\nasync def test_version(mcache: Client) -> None:\n    version = await mcache.version()\n    stats = await mcache.stats()\n    assert version == stats[b'version']\n\n    with mock.patch.object(\n            mcache,\n            \"_execute_simple_command\",\n            new_callable=MagicMock) as patched:\n        fut: asyncio.Future[bytes] = asyncio.Future()\n        fut.set_result(b'SERVER_ERROR error\\r\\n')\n        patched.return_value = fut\n        with pytest.raises(ClientException):\n            await mcache.version()\n\n\nasync def test_flush_all(mcache: Client) -> None:\n    key, value = b'key:flush_all', b'flush_all_value'\n    await mcache.set(key, value)\n    # make sure value exists\n    test_value = await mcache.get(key)\n    assert test_value == value\n    # flush data\n    await mcache.flush_all()\n    # make sure value does not exists\n    test_value = await mcache.get(key)\n    assert test_value is None\n\n    with mock.patch.object(mcache, '_execute_simple_command') as patched:\n        fut: asyncio.Future[bytes] = asyncio.Future()\n        fut.set_result(b'SERVER_ERROR error\\r\\n')\n        patched.return_value = fut\n        with pytest.raises(ClientException):\n            await mcache.flush_all()\n\n\nasync def test_set_get(mcache: Client) -> None:\n    key, value = b'key:set', b'1'\n    await mcache.set(key, value)\n    test_value = await mcache.get(key)\n    assert test_value == value\n    test_value = await mcache.get(b\"not:\" + key)\n    assert test_value is None\n    test_value = await mcache.get(b\"not:\" + key, default=value)\n    assert test_value == value\n\n    with mock.patch.object(mcache, '_execute_simple_command') as patched:\n        fut: asyncio.Future[bytes] = asyncio.Future()\n        fut.set_result(b'SERVER_ERROR error\\r\\n')\n        patched.return_value = fut\n        with pytest.raises(ClientException):\n            await mcache.set(key, value)\n\n\nasync def test_gets(mcache: Client) -> None:\n    key, value = b'key:set', b'1'\n    await mcache.set(key, value)\n\n    test_value, cas = await mcache.gets(key)\n    assert test_value == value\n    assert isinstance(cas, int)\n\n    test_value, cas = await mcache.gets(b\"not:\" + key)\n    assert test_value is None\n    assert cas is None\n\n    test_value, cas = await mcache.gets(b\"not:\" + key, default=value)\n    assert test_value == value\n    assert cas is None\n\n\nasync def test_multi_get(mcache: Client) -> None:\n    key1, value1 = b'key:multi_get:1', b'1'\n    key2, value2 = b'key:multi_get:2', b'2'\n    await mcache.set(key1, value1)\n    await mcache.set(key2, value2)\n    test_value = await mcache.multi_get(key1, key2)\n    assert test_value == (value1, value2)\n\n    test_value = await mcache.multi_get(b'not' + key1, key2)\n    assert test_value == (None, value2)\n    test_value = await mcache.multi_get()\n    assert test_value == ()\n\n\nasync def test_multi_get_doubling_keys(mcache: Client) -> None:\n    key, value = b'key:multi_get:3', b'1'\n    await mcache.set(key, value)\n\n    with pytest.raises(ClientException):\n        await mcache.multi_get(key, key)\n\n\nasync def test_set_expire(mcache: Client) -> None:\n    key, value = b'key:set', b'1'\n    await mcache.set(key, value, exptime=1)\n    test_value = await mcache.get(key)\n    assert test_value == value\n\n    await asyncio.sleep(1)\n\n    test_value = await mcache.get(key)\n    assert test_value is None\n\n\nasync def test_set_errors(mcache: Client) -> None:\n    key, value = b'key:set', b'1'\n    await mcache.set(key, value, exptime=1)\n\n    with pytest.raises(ValidationException):\n        await mcache.set(key, value, exptime=-1)\n\n    with pytest.raises(ValidationException):\n        await mcache.set(key, value, exptime=3.14)  # type: ignore[arg-type]\n\n\nasync def test_gets_cas(mcache: Client) -> None:\n    key, value = b'key:set', b'1'\n    await mcache.set(key, value)\n\n    test_value, cas = await mcache.gets(key)\n\n    assert cas is not None\n\n    stored = await mcache.cas(key, value, cas)\n    assert stored is True\n\n    stored = await mcache.cas(key, value, cas)\n    assert stored is False\n\n\nasync def test_cas_missing(mcache: Client) -> None:\n    key, value = b'key:set', b'1'\n    stored = await mcache.cas(key, value, 123)\n    assert stored is False\n\n\nasync def test_add(mcache: Client) -> None:\n    key, value = b'key:add', b'1'\n    await mcache.set(key, value)\n\n    test_value1 = await mcache.add(key, b\"2\")\n    assert not test_value1\n\n    test_value2 = await mcache.add(b\"not:\" + key, b\"2\")\n    assert test_value2\n\n    test_value3 = await mcache.get(b\"not:\" + key)\n    assert test_value3 == b\"2\"\n\n\nasync def test_replace(mcache: Client) -> None:\n    key, value = b'key:replace', b'1'\n    await mcache.set(key, value)\n\n    test_value1 = await mcache.replace(key, b\"2\")\n    assert test_value1\n    # make sure value exists\n    test_value2 = await mcache.get(key)\n    assert test_value2 == b\"2\"\n\n    test_value3 = await mcache.replace(b\"not:\" + key, b\"3\")\n    assert not test_value3\n    # make sure value exists\n    test_value4 = await mcache.get(b\"not:\" + key)\n    assert test_value4 is None\n\n\nasync def test_append(mcache: Client) -> None:\n    key, value = b'key:append', b'1'\n    await mcache.set(key, value)\n\n    test_value1 = await mcache.append(key, b\"2\")\n    assert test_value1\n\n    # make sure value exists\n    test_value2 = await mcache.get(key)\n    assert test_value2 == b\"12\"\n\n    test_value3 = await mcache.append(b\"not:\" + key, b\"3\")\n    assert not test_value3\n    # make sure value exists\n    test_value4 = await mcache.get(b\"not:\" + key)\n    assert test_value4 is None\n\n\nasync def test_prepend(mcache: Client) -> None:\n    key, value = b'key:prepend', b'1'\n    await mcache.set(key, value)\n\n    test_value1 = await mcache.prepend(key, b\"2\")\n    assert test_value1\n\n    # make sure value exists\n    test_value2 = await mcache.get(key)\n    assert test_value2 == b\"21\"\n\n    test_value3 = await mcache.prepend(b\"not:\" + key, b\"3\")\n    assert not test_value3\n    # make sure value exists\n    test_value4 = await mcache.get(b\"not:\" + key)\n    assert test_value4 is None\n\n\nasync def test_delete(mcache: Client) -> None:\n    key, value = b'key:delete', b'value'\n    await mcache.set(key, value)\n\n    # make sure value exists\n    test_value = await mcache.get(key)\n    assert test_value == value\n\n    is_deleted = await mcache.delete(key)\n    assert is_deleted\n    # make sure value does not exists\n    test_value = await mcache.get(key)\n    assert test_value is None\n\n    with mock.patch.object(mcache, '_execute_simple_command') as patched:\n        fut: asyncio.Future[bytes] = asyncio.Future()\n        fut.set_result(b'SERVER_ERROR error\\r\\n')\n        patched.return_value = fut\n\n        with pytest.raises(ClientException):\n            await mcache.delete(key)\n\n\nasync def test_delete_key_not_exists(mcache: Client) -> None:\n    is_deleted = await mcache.delete(b\"not:key\")\n    assert not is_deleted\n\n\nasync def test_incr(mcache: Client) -> None:\n    key, value = b'key:incr:1', b'1'\n    await mcache.set(key, value)\n\n    test_value1 = await mcache.incr(key, 2)\n    assert test_value1 == 3\n\n    # make sure value exists\n    test_value2 = await mcache.get(key)\n    assert test_value2 == b\"3\"\n\n\nasync def test_incr_errors(mcache: Client) -> None:\n    key, value = b'key:incr:2', b'string'\n    await mcache.set(key, value)\n\n    with pytest.raises(ClientException):\n        await mcache.incr(key, 2)\n\n    with pytest.raises(ClientException):\n        await mcache.incr(key, 3.14)  # type: ignore[arg-type]\n\n\nasync def test_decr(mcache: Client) -> None:\n    key, value = b'key:decr:1', b'17'\n    await mcache.set(key, value)\n\n    test_value1 = await mcache.decr(key, 2)\n    assert test_value1 == 15\n\n    test_value2 = await mcache.get(key)\n    assert test_value2 == b\"15\"\n\n    test_value3 = await mcache.decr(key, 1000)\n    assert test_value3 == 0\n\n\nasync def test_decr_errors(mcache: Client) -> None:\n    key, value = b'key:decr:2', b'string'\n    await mcache.set(key, value)\n\n    with pytest.raises(ClientException):\n        await mcache.decr(key, 2)\n\n    with pytest.raises(ClientException):\n        await mcache.decr(key, 3.14)  # type: ignore[arg-type]\n\n\nasync def test_stats(mcache: Client) -> None:\n    stats = await mcache.stats()\n    assert b'pid' in stats\n\n\nasync def test_touch(mcache: Client) -> None:\n    key, value = b'key:touch:1', b'17'\n    await mcache.set(key, value)\n\n    test_value1 = await mcache.touch(key, 1)\n    assert test_value1\n\n    test_value2 = await mcache.get(key)\n    assert test_value2 == value\n\n    await asyncio.sleep(1)\n\n    test_value3 = await mcache.get(key)\n    assert test_value3 is None\n\n    test_value4 = await mcache.touch(b\"not:\" + key, 1)\n    assert not test_value4\n\n    with mock.patch.object(mcache, '_execute_simple_command') as patched:\n        fut: asyncio.Future[bytes] = asyncio.Future()\n        fut.set_result(b'SERVER_ERROR error\\r\\n')\n        patched.return_value = fut\n\n        with pytest.raises(ClientException):\n            await mcache.touch(b\"not:\" + key, 1)\n\n\nasync def test_close(mcache: Client) -> None:\n    await mcache.close()\n    assert mcache._pool.size() == 0\n\n\n@pytest.mark.parametrize(\n    \"value\",\n    [\n        \"key\",\n        b\"bkey\",\n        False,\n        1,\n        None,\n        0.5,\n        [1, 2, 3],\n        tuple([1, 2, 3]),\n        [datetime.date(2015, 12, 28)],\n        bytes(\"!@#\", \"utf-8\"),\n        bytes(\"안녕하세요\", \"utf-8\"),\n    ]\n)\nasync def test_flag_helper(\n        mcache_flag_client: FlagClient[Any], value: object) -> None:\n    key = b\"key:test_flag_helper\"\n\n    await mcache_flag_client.set(key, value)\n    v2 = await mcache_flag_client.get(key)\n    assert v2 == value\n\n\nasync def test_objects_not_supported_without_flag_handler(mcache: Client) -> None:\n    key = b\"key:test_objects_not_supported_without_flag_handler\"\n\n    date_value = datetime.date(2015, 12, 28)\n\n    with pytest.raises(ValidationException):\n        await mcache.set(key, date_value)  # type: ignore[arg-type]\n\n    result = await mcache.get(key)\n    assert result is None\n\n\nasync def test_flag_handler_invoked_only_when_expected(\n        mcache_flag_client: FlagClient[Any], demo_flag_helper: FlagHelperDemo) -> None:\n    key = b\"key:test_flag_handler_invoked_only_when_expected\"\n\n    orig_get_count = demo_flag_helper.get_invocation_count\n    orig_set_count = demo_flag_helper.set_invocation_count\n\n    # should be invoked on non-byte values\n\n    date_value = datetime.date(2015, 12, 28)\n\n    await mcache_flag_client.set(key, date_value)\n    v2 = await mcache_flag_client.get(key)\n    assert v2 == date_value\n\n    assert orig_get_count + 1 == demo_flag_helper.get_invocation_count\n    assert orig_set_count + 1 == demo_flag_helper.set_invocation_count\n\n    # should not be invoked on byte values\n\n    byte_value = bytes(\"안녕하세요\", \"utf-8\")\n\n    await mcache_flag_client.set(key, byte_value)\n    v3 = await mcache_flag_client.get(key)\n    assert v3 == byte_value\n\n    assert orig_get_count + 1 == demo_flag_helper.get_invocation_count\n    assert orig_set_count + 1 == demo_flag_helper.set_invocation_count\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "import contextlib\nimport socket\nimport sys\nimport time\nimport uuid\nfrom typing import Any, AsyncIterator, Callable, Iterator, TypedDict\n\nimport docker as docker_mod\nimport memcache\nimport pytest\n\nimport aiomcache\nfrom .flag_helper import FlagHelperDemo\n\nif sys.version_info < (3, 11):\n    from typing_extensions import NotRequired\nelse:\n    from typing import NotRequired\n\n\nclass McacheParams(TypedDict):\n    host: str\n    port: int\n\n\nclass ServerParams(TypedDict):\n    Id: NotRequired[str]\n    host: str\n    port: int\n    mcache_params: McacheParams\n\n\nmcache_server_option = \"localhost\"\n\n\ndef pytest_addoption(parser: pytest.Parser) -> None:\n    parser.addoption(\n        '--memcached', help='Memcached server')\n\n\n@pytest.fixture(scope='session')\ndef unused_port() -> Callable[[], int]:\n    def f() -> int:\n        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n            s.bind(('127.0.0.1', 0))\n            return s.getsockname()[1]  # type: ignore[no-any-return]\n    return f\n\n\ndef pytest_runtest_setup(item: pytest.Item) -> None:\n    global mcache_server_option\n    mcache_server_option = item.config.getoption(\"--memcached\", \"localhost\")\n\n\n@pytest.fixture(scope='session')\ndef session_id() -> str:\n    '''Unique session identifier, random string.'''\n    return str(uuid.uuid4())\n\n\n@pytest.fixture(scope='session')\ndef docker() -> docker_mod.Client:  # type: ignore[no-any-unimported]\n    return docker_mod.from_env()\n\n\ndef mcache_server_actual(host: str, port: int = 11211) -> ServerParams:\n    port = int(port)\n    return {\n        \"host\": host,\n        \"port\": port,\n        \"mcache_params\": {\"host\": host, \"port\": port}\n    }\n\n\n@contextlib.contextmanager\ndef mcache_server_docker(  # type: ignore[no-any-unimported]\n        unused_port: Callable[[], int], docker: docker_mod.Client, session_id: str\n) -> Iterator[ServerParams]:\n    docker.images.pull(\"memcached:alpine\")\n    container = docker.containers.run(\n        image='memcached:alpine',\n        name='memcached-test-server-{}'.format(session_id),\n        ports={\"11211/tcp\": None},\n        detach=True,\n    )\n    try:\n        container.start()\n        container.reload()\n        net_settings = container.attrs[\"NetworkSettings\"]\n        host = net_settings[\"IPAddress\"]\n        port = int(net_settings[\"Ports\"][\"11211/tcp\"][0][\"HostPort\"])\n        mcache_params: McacheParams = {\"host\": host, \"port\": port}\n        delay = 0.001\n        for _i in range(10):\n            try:\n                conn = memcache.Client([\"{host}:{port}\".format_map(mcache_params)])\n                conn.get_stats()\n                break\n            except Exception:\n                time.sleep(delay)\n                delay *= 2\n        else:\n            pytest.fail(\"Cannot start memcached\")\n        ret: ServerParams = {\n            \"Id\": container.id,\n            \"host\": host,\n            \"port\": port,\n            \"mcache_params\": mcache_params\n        }\n        time.sleep(0.1)\n        yield ret\n    finally:\n        container.kill()\n        container.remove()\n\n\n@pytest.fixture(scope='session')\ndef mcache_server() -> ServerParams:\n    return mcache_server_actual(\"localhost\")\n\n\n@pytest.fixture\ndef mcache_params(mcache_server: ServerParams) -> McacheParams:\n    return mcache_server[\"mcache_params\"]\n\n\n@pytest.fixture\nasync def mcache(mcache_params: McacheParams) -> AsyncIterator[aiomcache.Client]:\n    client = aiomcache.Client(**mcache_params)\n    yield client\n    await client.close()\n\n\ntest_only_demo_flag_helper = FlagHelperDemo()\n\n\n@pytest.fixture\nasync def demo_flag_helper() -> FlagHelperDemo:\n    return test_only_demo_flag_helper\n\n\n@pytest.fixture\nasync def mcache_flag_client(\n    mcache_params: McacheParams, demo_flag_helper: FlagHelperDemo\n) -> AsyncIterator[aiomcache.FlagClient[Any]]:\n\n    client = aiomcache.FlagClient(\n        get_flag_handler=demo_flag_helper.demo_get_flag_handler,\n        set_flag_handler=demo_flag_helper.demo_set_flag_handler,\n        **mcache_params)\n    try:\n        yield client\n    finally:\n        await client.close()\n"
  },
  {
    "path": "tests/conn_args_test.py",
    "content": "import ssl\nfrom asyncio import StreamReader, StreamWriter\nfrom unittest import mock\n\nimport pytest\n\nfrom aiomcache import Client\nfrom .conftest import McacheParams\n\n\nasync def test_params_forwarded_from_client() -> None:\n    client = Client(\"host\", port=11211, conn_args={\n        \"ssl\": True, \"ssl_handshake_timeout\": 20\n    })\n\n    with mock.patch(\n        \"asyncio.open_connection\",\n        return_value=(\n            mock.create_autospec(StreamReader),\n            mock.create_autospec(StreamWriter),\n        ),\n        autospec=True,\n    ) as oc:\n        await client._pool.acquire()\n\n    oc.assert_called_with(\"host\", 11211, ssl=True, ssl_handshake_timeout=20)\n\n\nasync def test_ssl_client_fails_against_plaintext_server(\n    mcache_params: McacheParams,\n) -> None:\n    client = Client(**mcache_params, conn_args={\"ssl\": True})\n    # If SSL was correctly enabled, this should\n    # fail, since SSL isn't enabled on the memcache\n    # server.\n    with pytest.raises(ssl.SSLError):\n        await client.get(b\"key\")\n"
  },
  {
    "path": "tests/flag_helper.py",
    "content": "import pickle  # noqa: S403\nfrom enum import IntEnum\nfrom typing import Any, Tuple\n\n\n# See also:\n# https://github.com/lericson/pylibmc/blob/master/src/_pylibmcmodule.h#L63\nclass DemoFlags(IntEnum):\n    DEMO_FLAG_PICKLE = 1\n\n\n# demo/ref flag handler, for more elaborate potential handlers, see:\n# https://github.com/lericson/pylibmc/blob/master/src/_pylibmcmodule.c#L640\nclass FlagHelperDemo:\n\n    get_invocation_count = 0\n    set_invocation_count = 0\n\n    async def demo_get_flag_handler(self, value: bytes, flags: int) -> Any:\n        self.get_invocation_count += 1\n\n        if flags == DemoFlags.DEMO_FLAG_PICKLE:\n            return pickle.loads(value)  # noqa: S301\n\n        raise ValueError(f\"unrecognized flag: {flags}\")\n\n    # demo/ref flag handler, for more elaborate potential handlers, see:\n    # https://github.com/lericson/pylibmc/blob/master/src/_pylibmcmodule.c#L1241\n    async def demo_set_flag_handler(self, value: Any) -> Tuple[bytes, int]:\n        self.set_invocation_count += 1\n\n        # in this example exclusively use Pickle, more elaborate handler\n        # could use additional/alternate flags\n        return pickle.dumps(value), DemoFlags.DEMO_FLAG_PICKLE.value\n"
  },
  {
    "path": "tests/pool_test.py",
    "content": "import asyncio\nimport random\nimport socket\n\nimport pytest\n\nfrom aiomcache.client import Client, acquire\nfrom aiomcache.pool import Connection, MemcachePool\nfrom .conftest import McacheParams\n\n\nasync def test_pool_creation(mcache_params: McacheParams) -> None:\n    pool = MemcachePool(minsize=1, maxsize=5, **mcache_params)\n    assert pool.size() == 0\n    assert pool._minsize == 1\n\n\nasync def test_pool_acquire_release(mcache_params: McacheParams) -> None:\n    pool = MemcachePool(minsize=1, maxsize=5, **mcache_params)\n    conn = await pool.acquire()\n    assert isinstance(conn.reader, asyncio.StreamReader)\n    assert isinstance(conn.writer, asyncio.StreamWriter)\n    pool.release(conn)\n    await pool.clear()\n\n\nasync def test_pool_acquire_release2(mcache_params: McacheParams) -> None:\n    pool = MemcachePool(minsize=1, maxsize=5, **mcache_params)\n    reader, writer = await asyncio.open_connection(\n        mcache_params[\"host\"], mcache_params[\"port\"])\n    # put dead connection to the pool\n    writer.close()\n    reader.feed_eof()\n    conn = Connection(reader, writer)\n    await pool._pool.put(conn)\n    conn = await pool.acquire()\n    assert isinstance(conn.reader, asyncio.StreamReader)\n    assert isinstance(conn.writer, asyncio.StreamWriter)\n    pool.release(conn)\n    await pool.clear()\n\n\nasync def test_pool_clear(mcache_params: McacheParams) -> None:\n    pool = MemcachePool(minsize=1, maxsize=5, **mcache_params)\n    conn = await pool.acquire()\n    pool.release(conn)\n    assert pool.size() == 1\n    await pool.clear()\n    assert pool._pool.qsize() == 0\n\n\nasync def test_acquire_dont_create_new_connection_if_have_conn_in_pool(\n    mcache_params: McacheParams,\n) -> None:\n    pool = MemcachePool(minsize=1, maxsize=5, **mcache_params)\n    assert pool.size() == 0\n\n    # Add a valid connection\n    _conn = await pool._create_new_conn()\n    assert _conn is not None\n    await pool._pool.put(_conn)\n    assert pool.size() == 1\n\n    conn = await pool.acquire()\n    assert conn is _conn\n    assert pool.size() == 1\n    pool.release(conn)\n    await pool.clear()\n\n\nasync def test_acquire_limit_maxsize(mcache_params: McacheParams) -> None:\n    pool = MemcachePool(minsize=1, maxsize=1, **mcache_params)\n    assert pool.size() == 0\n\n    # Create up to max connections\n    _conn = await pool.acquire()\n    assert pool.size() == 1\n    pool.release(_conn)\n\n    async def acquire_wait_release() -> None:\n        conn = await pool.acquire()\n        assert conn is _conn\n        await asyncio.sleep(0.01)\n        assert len(pool._in_use) == 1\n        assert pool.size() == 1\n        assert pool._pool.qsize() == 0\n        pool.release(conn)\n\n    await asyncio.gather(*([acquire_wait_release()] * 50))\n    assert pool.size() == 1\n    assert len(pool._in_use) == 0\n    assert pool._pool.qsize() == 1\n    await pool.clear()\n\n\nasync def test_acquire_task_cancellation(mcache_params: McacheParams) -> None:\n\n    class TestClient(Client):\n        def __init__(self, pool_size: int = 4):\n            self._pool = MemcachePool(\n                minsize=pool_size, maxsize=pool_size,\n                **mcache_params)\n\n        @acquire\n        async def acquire_wait_release(self, conn: Connection) -> str:\n            assert self._pool.size() <= pool_size\n            await asyncio.sleep(random.uniform(0.01, 0.02))  # noqa: S311\n            return \"foo\"\n\n    pool_size = 4\n    client = TestClient(pool_size=pool_size)\n    tasks = [\n        asyncio.wait_for(\n            client.acquire_wait_release(),\n            random.uniform(1, 2)) for x in range(1000)  # noqa: S311\n    ]\n    results = await asyncio.gather(*tasks, return_exceptions=True)\n    assert client._pool.size() <= pool_size\n    assert len(client._pool._in_use) == 0\n    assert \"foo\" in results\n    await client._pool.clear()\n\n\nasync def test_maxsize_greater_than_minsize(mcache_params: McacheParams) -> None:\n    pool = MemcachePool(minsize=5, maxsize=1, **mcache_params)\n    conn = await pool.acquire()\n    assert isinstance(conn.reader, asyncio.StreamReader)\n    assert isinstance(conn.writer, asyncio.StreamWriter)\n    pool.release(conn)\n    await pool.clear()\n\n\nasync def test_0_minsize(mcache_params: McacheParams) -> None:\n    pool = MemcachePool(minsize=0, maxsize=5, **mcache_params)\n    conn = await pool.acquire()\n    assert isinstance(conn.reader, asyncio.StreamReader)\n    assert isinstance(conn.writer, asyncio.StreamWriter)\n    pool.release(conn)\n    await pool.clear()\n\n\nasync def test_bad_connection(mcache_params: McacheParams) -> None:\n    pool = MemcachePool(minsize=5, maxsize=1, **mcache_params)\n    pool._host = \"INVALID_HOST\"\n    assert pool.size() == 0\n    with pytest.raises(socket.gaierror):\n        conn = await pool.acquire()\n        assert isinstance(conn.reader, asyncio.StreamReader)\n        assert isinstance(conn.writer, asyncio.StreamWriter)\n        pool.release(conn)\n    assert pool.size() == 0\n"
  }
]