"""
Unit tests for hummingbot.core.utils.ethereum
"""

import asyncio
from hummingbot.core.utils.ethereum import check_web3, check_transaction_exceptions, fetch_trading_pairs
import unittest.mock

class EthereumTest(unittest.TestCase):
    @unittest.mock.patch('hummingbot.core.utils.ethereum.is_connected_to_web3')
    def test_check_web3(self, is_connected_to_web3_mock):
        """
        Unit tests for hummingbot.core.utils.ethereum.check_web3
        """

        # unable to connect to web3
        is_connected_to_web3_mock.return_value = False
        self.assertEqual(check_web3('doesnt-exist'), False)

        # connect to web3
        is_connected_to_web3_mock.return_value = True
        self.assertEqual(check_web3('ethereum.node'), True)

    def test_check_transaction_exceptions(self):
        """
        Unit tests for hummingbot.core.utils.ethereum.transaction_exceptions
        """

        # create transactions data that should result in no warnings
        transaction = {
            "allowances": {"WBTC": 1000},
            "balances": {"ETH": 1000},
            "base": "ETH",
            "quote": "WBTC",
            "amount": 1000,
            "side": 100,
            "gas_limit": 22000,
            "gas_price": 90,
            "gas_cost": 90,
            "price": 100
        }
        self.assertEqual(check_transaction_exceptions(transaction), [])

        # ETH balance less than gas_cost
        invalid_transaction_1 = transaction.copy()
        invalid_transaction_1["balances"] = {"ETH": 10}
        self.assertRegexpMatches(check_transaction_exceptions(invalid_transaction_1)[0], r"^Insufficient ETH balance to cover gas")

        # Gas limit set too low, gas_limit is less than 21000
        invalid_transaction_2 = transaction.copy()
        invalid_transaction_2["gas_limit"] = 10000
        self.assertRegexpMatches(check_transaction_exceptions(invalid_transaction_2)[0], r"^Gas limit")

        # Insufficient token allowance, allowance of quote less than amount
        invalid_transaction_3 = transaction.copy()
        invalid_transaction_3["allowances"] = {"WBTC": 500}
        self.assertRegexpMatches(check_transaction_exceptions(invalid_transaction_3)[0], r"^Insufficient")


import Queue
import threading
import getopt
import sys
import urllib2
import hashlib
import socket
import time
import os
import re
import netaddr
import subprocess
from C_display import *

#Module

IP = ip_address.strip()##
	try:
		socket.inet_aton(IP)
		showDisplay(displayMode, " - [LOG] IP Check Ok.")
	except:
		showDisplay(displayMode, "%s,invalid IP" % IP)
		return "0x02"
	try:
		showDisplay(displayMode, " - [LOG] Start SSL Connection / Gathering Information")
		result = subprocess.Popen(['timeout','4','openssl','s_client','-connect',ip_address+":"+str(iPort),"-cipher","EXPORT"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
		showDisplay(displayMode, " - [LOG] Ending Get Information")
		#showDisplay(displayMode, result
		if "Cipher is EXP" in result:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' in Response")
			return "0x01"
		else:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' not in Response")
			return "0x00"
	except:
		showDisplay(displayMode, "[INF] Error FREAK Module")
		return "0x02"


import Queue
import threading
import getopt
import sys
import urllib2
import hashlib
import socket
import time
import os
import re
import netaddr
import subprocess
from C_display import *

#Module
def m_freak_run(ip_address,iPort,displayMode):

	try:
		socket.inet_aton(IP)
		showDisplay(displayMode, " - [LOG] IP Check Ok.")
	except:
		showDisplay(displayMode, "%s,invalid IP" % IP)
		return "0x02"
	try:
		showDisplay(displayMode, " - [LOG] Start SSL Connection / Gathering Information")
		result = subprocess.Popen(['timeout','4','openssl','s_client','-connect',ip_address+":"+str(iPort),"-cipher","EXPORT"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
		showDisplay(displayMode, " - [LOG] Ending Get Information")
		#showDisplay(displayMode, result
		if "Cipher is EXP" in result:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' in Response")
			return "0x01"
		else:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' not in Response")
			return "0x00"
	except:
		showDisplay(displayMode, "[INF] Error FREAK Module")
		return "0x02"


try:
		socket.inet_aton(IP)
		showDisplay(displayMode, " - [LOG] IP Check Ok.")
	except:
		showDisplay(displayMode, "%s,invalid IP" % IP)
		return "0x02"

import Queue
import threading
import getopt
import sys
import urllib2
import hashlib
import socket
import time
import os
import re
import netaddr
import subprocess
from C_display import *

#Module
def m_freak_run(ip_address,iPort,displayMode):
	IP = ip_address.strip()##

	try:
		showDisplay(displayMode, " - [LOG] Start SSL Connection / Gathering Information")
		result = subprocess.Popen(['timeout','4','openssl','s_client','-connect',ip_address+":"+str(iPort),"-cipher","EXPORT"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
		showDisplay(displayMode, " - [LOG] Ending Get Information")
		#showDisplay(displayMode, result
		if "Cipher is EXP" in result:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' in Response")
			return "0x01"
		else:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' not in Response")
			return "0x00"
	except:
		showDisplay(displayMode, "[INF] Error FREAK Module")
		return "0x02"


try:
		showDisplay(displayMode, " - [LOG] Start SSL Connection / Gathering Information")
		result = subprocess.Popen(['timeout','4','openssl','s_client','-connect',ip_address+":"+str(iPort),"-cipher","EXPORT"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
		showDisplay(displayMode, " - [LOG] Ending Get Information")
		#showDisplay(displayMode, result
		if "Cipher is EXP" in result:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' in Response")
			return "0x01"
		else:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' not in Response")
			return "0x00"
	except:
		showDisplay(displayMode, "[INF] Error FREAK Module")
		return "0x02"

import Queue
import threading
import getopt
import sys
import urllib2
import hashlib
import socket
import time
import os
import re
import netaddr
import subprocess
from C_display import *

#Module
def m_freak_run(ip_address,iPort,displayMode):
	IP = ip_address.strip()##
	try:
		socket.inet_aton(IP)
		showDisplay(displayMode, " - [LOG] IP Check Ok.")
	except:
		showDisplay(displayMode, "%s,invalid IP" % IP)
		return "0x02"


import Queue
import threading
import getopt
import sys
import urllib2
import hashlib
import socket
import time
import os
import re
import netaddr
import subprocess
from C_display import *

#Module
def m_freak_run(ip_address,iPort,displayMode):
	IP

##
	try:
		socket.inet_aton(IP)
		showDisplay(displayMode, " - [LOG] IP Check Ok.")
	except:
		showDisplay(displayMode, "%s,invalid IP" % IP)
		return "0x02"
	try:
		showDisplay(displayMode, " - [LOG] Start SSL Connection / Gathering Information")
		result = subprocess.Popen(['timeout','4','openssl','s_client','-connect',ip_address+":"+str(iPort),"-cipher","EXPORT"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
		showDisplay(displayMode, " - [LOG] Ending Get Information")
		#showDisplay(displayMode, result
		if "Cipher is EXP" in result:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' in Response")
			return "0x01"
		else:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' not in Response")
			return "0x00"
	except:
		showDisplay(displayMode, "[INF] Error FREAK Module")
		return "0x02"


if "Cipher is EXP" in result:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' in Response")
			return "0x01"
		else:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' not in Response")
			return "0x00"

import Queue
import threading
import getopt
import sys
import urllib2
import hashlib
import socket
import time
import os
import re
import netaddr
import subprocess
from C_display import *

#Module
def m_freak_run(ip_address,iPort,displayMode):
	IP = ip_address.strip()##
	try:
		socket.inet_aton(IP)
		showDisplay(displayMode, " - [LOG] IP Check Ok.")
	except:
		showDisplay(displayMode, "%s,invalid IP" % IP)
		return "0x02"
	try:
		showDisplay(displayMode, " - [LOG] Start SSL Connection / Gathering Information")
		result = subprocess.Popen(['timeout','4','openssl','s_client','-connect',ip_address+":"+str(iPort),"-cipher","EXPORT"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
		showDisplay(displayMode, " - [LOG] Ending Get Information")
		#showDisplay(displayMode, result

	except:
		showDisplay(displayMode, "[INF] Error FREAK Module")
		return "0x02"


import Queue
import threading
import getopt
import sys
import urllib2
import hashlib
import socket
import time
import os
import re
import netaddr
import subprocess
from C_display import *

#Module
def m_freak_run(ip_address,iPort,displayMode):
	IP = ip_address.strip()##
	try:
		socket.inet_aton(IP)
		showDisplay(displayMode, " - [LOG] IP Check Ok.")
	except:
		showDisplay(displayMode, "%s,invalid IP" % IP)
		return "0x02"
	try:
		showDisplay(displayMode, " - [LOG] Start SSL Connection / Gathering Information")
		result = subprocess.Popen(['timeout','4','openssl','s_client','-connect',ip_address+":"+str(iPort

),"-cipher","EXPORT"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
		showDisplay(displayMode, " - [LOG] Ending Get Information")
		#showDisplay(displayMode, result
		if "Cipher is EXP" in result:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' in Response")
			return "0x01"
		else:
			showDisplay(displayMode, " - [LOG] 'Cipher is EXP' not in Response")
			return "0x00"
	except:
		showDisplay(displayMode, "[INF] Error FREAK Module")
		return "0x02"


if __name__ == "__main__":
    m = MainOptionalOperator(imgPath)

    m._help()

    m.setCropAttributes(rect_or_poly='rect',noise=True,number=2)
    m.setDisortAttributes()
    m.setInpaintAttributes(rect_or_poly='poly')
    m.setPerspectiveAttributes(height=0.8,width=0.5,factor=0.8)
    m.setResizeAttributes(height=0.8,width=0.9)

    img = m.do()

    io.imsave(BASE_DIR + os.sep + '1110.jpg',img)

'''
lanhuage: python
Descripttion:
version: beta
Author: xiaoshuyui
Date: 2020-11-10 10:51:46
LastEditors: xiaoshuyui
LastEditTime: 2020-11-11 08:44:23
'''
import sys

sys.path.append("..")

import os
from convertmask.utils.auglib.optionsOperatorWithoutLabel import MainOptionalOperator
from skimage import io

BASE_DIR = os.path.abspath(os.path.dirname(os.getcwd())) + os.sep + 'static'
imgPath = BASE_DIR + os.sep + 'multi_objs_test.jpg'


cripttion:
version: beta
Author: xiaoshuyui
Date: 2020-11-10 10:51:46
LastEditors: xiaoshuyui
LastEditTime: 2020-11-11 08:44:23
'''
import sys

sys.path.append("..")

import os
from convertmask.utils.auglib.optionsOperatorWithoutLabel import MainOptionalOperator
from skimage import io

BASE_DIR = os.path.abspath(os.path.dirname(os.getcwd())) + os.sep + 'static'
imgPath = BASE_DIR + os.sep + 'multi_objs_test.jpg'

if __name__ == "__main__":
    m = MainOptionalOperator(imgPath)

    m._help()

    m.setCropAttributes(rect_or_poly='rect',noise=True,number=2)
    m.setDisortAttributes()
    m.setInpaintAttributes(rect_or_poly='poly')
    m.setPerspectiveAttributes(height=0.8,width=0.5,factor=0.8)
    m.setResizeAttributes(height=0.8,width=0.9)

    img = m.do()

    io.imsave(BASE_DIR + os.sep + '1110.jpg',img)


def post(self, request: WSGIRequest) -> HttpResponse:

from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.views import View
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin

from .forms import PostForm

class AddPost(LoginRequiredMixin, View):
    template_name = "posts/add-post.html"

    def get(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm()
        context = {"post_form": post_form}
        return render(request, self.template_name, context)

post_form = PostForm(request.POST, request.FILES)
        if post_form.is_valid():
            obj = post_form.save(commit=False)
            obj.user = request.user
            obj.save()
            return redirect("posts:list-posts")
        context = {"post_form": PostForm()}
        return render(request, self.template_name, context)

class ListPosts(LoginRequiredMixin, TemplateView):
    template_name = "posts/list-posts.html"

class DetailPost(LoginRequiredMixin, TemplateView):
    template_name = "posts/detail-post.html"


post_form = PostForm()
        context = {"post_form": post_form}
        return render(request, self.template_name, context)

from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.views import View
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin

from .forms import PostForm

class AddPost(LoginRequiredMixin, View):
    template_name = "posts/add-post.html"

    def get(self, request: WSGIRequest) -> HttpResponse:

    def post(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm(request.POST, request.FILES)
        if post_form.is_valid():
            obj = post_form.save(commit=False)
            obj.user = request.user
            obj.save()
            return redirect("posts:list-posts")
        context = {"post_form": PostForm()}
        return render(request, self.template_name, context)

class ListPosts(LoginRequiredMixin, TemplateView):
    template_name = "posts/list-posts.html"

class DetailPost(LoginRequiredMixin, TemplateView):
    template_name = "posts/detail-post.html"


post_form = PostForm(request.POST, request.FILES)
        if post_form.is_valid():
            obj = post_form.save(commit=False)
            obj.user = request.user
            obj.save()
            return redirect("posts:list-posts")

from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.views import View
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin

from .forms import PostForm

class AddPost(LoginRequiredMixin, View):
    template_name = "posts/add-post.html"

    def get(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm()
        context = {"post_form": post_form}
        return render(request, self.template_name, context)

    def post(self, request: WSGIRequest) -> HttpResponse:

        context = {"post_form": PostForm()}
        return render(request, self.template_name, context)

class ListPosts(LoginRequiredMixin, TemplateView):
    template_name = "posts/list-posts.html"

class DetailPost(LoginRequiredMixin, TemplateView):
    template_name = "posts/detail-post.html"


from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.views import View
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin

from .forms import PostForm

class AddPost(LoginRequiredMixin, View):
    template_name = "posts/add-post.html"

    def get(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm()
        context = {"post_form": post_form}
        return render(request, self.template_name, context)

    def post(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm(request.POST, request.FILES)
        if post_form.is_valid():
            obj = post_form.save(commit=False)
            obj.user = request.user
            obj.save()
            return redirect("posts:list-posts")

        return render(request, self.template_name, context)

class ListPosts(LoginRequiredMixin, TemplateView):
    template_name = "posts/list-posts.html"

class DetailPost(LoginRequiredMixin, TemplateView):
    template_name = "posts/detail-post.html"


from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.views import View
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin

from .forms import PostForm

class AddPost(LoginRequiredMixin, View):
    template_name = "posts/add-post.html"

    def get(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm()
        context = {"post_form": post_form}
        return render(request, self.template_name, context)

    def post(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm(request.POST, request.FILES)
        if post_form.is_valid():
            obj = post_form.save(commit=False)
            obj.user = request.user
            obj.save()
            return redirect("posts:list-posts")
        context = {"post_form": PostForm()}

class ListPosts(LoginRequiredMixin, TemplateView):
    template_name = "posts/list-posts.html"

class DetailPost(LoginRequiredMixin, TemplateView):
    template_name = "posts/detail-post.html"


from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.views import View
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin

from .forms import PostForm

class AddPost(LoginRequiredMixin, View):
    template_name = "posts/add-post.html"

    def get(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm()
        context = {"post_form": post_form}
        return render(request, self.template_name, context)

    def post(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm(request.POST, request.FILES)
        if post_form.is_valid():
            obj = post_form.save(commit=False)
            obj.user = request.user
            obj.save()
            return redirect("posts:list-posts")
        context = {"post_form": PostForm()}
        return render(request, self.tem


if post_form.is_valid():
            obj = post_form.save(commit=False)
            obj.user = request.user
            obj.save()
            return redirect("posts:list-posts")

from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.views import View
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin

from .forms import PostForm

class AddPost(LoginRequiredMixin, View):
    template_name = "posts/add-post.html"

    def get(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm()
        context = {"post_form": post_form}
        return render(request, self.template_name, context)

    def post(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm(request.POST, request.FILES)


from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.views import View
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequi

redMixin

from .forms import PostForm

class AddPost(LoginRequiredMixin, View):
    template_name = "posts/add-post.html"

    def get(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm()
        context = {"post_form": post_form}
        return render(request, self.template_name, context)

    def post(self, request: WSGIRequest) -> HttpResponse:
        post_form = PostForm(request.POST, request.FILES)
        if post_form.is_valid():
            obj = post_form.save(commit=False)
            obj.user = request.user
            obj.save()
            return redirect("posts:list-posts")
        context = {"post_form": PostForm()}
        return render(request, self.template_name, context)

class ListPosts(LoginRequiredMixin, TemplateView):
    template_name = "posts/list-posts.html"

class DetailPost(LoginRequiredMixin, TemplateView):
    template_name = "posts/detail-post.html"


if number in phonebook:
        print("{}={}".format(number, phonebook[number]))
    else:
        print("Not found")

n = int(input().strip())
phonebook = {}
for i in range(n):
    line = input()
    k, v = line.split()
    phonebook[k] = v

while True:
    try:
        number = input()
    except EOFError:
        break


for i in range(n):
    line = input()
    k, v = line.split()
    phonebook[k] = v

while True:
    try:
        number = input()
    except EOFError:
        break
    if number in phonebook:
        print("{}={}".format(number, phonebook[number]))
    else:
        print("Not found")


while True:
    try:
        number = input()
    except EOFError:
        break
    if number in phonebook:
        print("{}={}".format(number, phonebook[number]))
    else:
        print("Not found")

n = int(input().strip())
phonebook = {}
for i in range(n):
    line = input()
    k, v = line.split()
    phonebook[k] = v


try:
        number = input()
    except EOFError:
        break

n = int(input().strip())
phonebook = {}
for i in range(n):
    line = input()
    k, v = line.split()
    phonebook[k] = v

while True:

    if number in phonebook:
        print("{}={}".format(number, phonebook[number]))
    else:
        print("Not found")


n = int(input().strip())
phonebook = {}
for i in range(n):
    line =

 input()
    k, v = line.split()
    phonebook[k] = v

while True:
    try:
        number = input()
    except EOFError:
        break
    if number in phonebook:
        print("{}={}".format(number, phonebook[number]))
    else:
        print("Not found")


import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."
        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []
        for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.
            if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):
        return self._transactions[-1]

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):
        current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]
        memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of
        with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

                try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        return lock_value


import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."
        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []
        for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.
            if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):
        current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]
        memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of
        with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

                try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        return lock_value


current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]

import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."
        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []
        for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.
            if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):
        return self._transactions[-1]

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):

        memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of
        with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

                try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        return lock_value


memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of

import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."
        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []
        for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.
            if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):
        return self._transactions[-1]

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):
        current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]

        with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

                try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        return lock_value


with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."
        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []
        for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.
            if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):
        return self._transactions[-1]

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):
        current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]
        memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

                try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        return lock_value


import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."
        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []
        for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.
            if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):
        return self._transactions[-1]

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):
        current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]
        memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of
        with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

                try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        re


        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []
        for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.
            if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):
        return self._transactions[-1]

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):
        current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]
        memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of
        with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

                try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        return lock_value


if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."
        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []
        for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):
        return self._transactions[-1]

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):
        current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]
        memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of
        with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

                try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        return lock_value


for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.
            if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."
        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):
        return self._transactions[-1]

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):
        current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]
        memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of
        with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

                try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        return lock_value


try:
                    return client.cas(key, data, cid, self._item_timeout)

                # There is a small chance that between the `gets` and
                # "now" the key will have been deleted by a concurrent
                # process, so we account for that possibility here.
                except pylibmc.NotFound:  # pragma: no cover
                    return

import pylibmc
import uuid

from contextlib import contextmanager
from hashlib import md5
from threading import local

from .. import Adapter, Transaction
from ..properties import Msgpack

class _MemcacheOuterTransaction(Transaction):
    def __init__(self, adapter, ds_transaction):
        self.adapter = adapter
        self.ds_transaction = ds_transaction

        self.batch = []
        self.begin = self.ds_transaction.begin
        self.rollback = self.ds_transaction.rollback

    def _push_keys(self, keys):
        self.batch.extend(keys)

    def commit(self):
        with self.adapter._bust(self.batch):
            self.ds_transaction.commit()

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

class _MemcacheInnerTransaction(Transaction):
    def __init__(self, parent, ds_transaction):
        self.parent = parent
        self.ds_transaction = ds_transaction

        self.begin = ds_transaction.begin
        self.commit = ds_transaction.commit
        self.rollback = ds_transaction.rollback

    def end(self):
        self.ds_transaction.end()
        self.adapter._transactions.remove(self)

    def __getattr__(self, name):
        return getattr(self.parent, name)

class MemcacheAdapter(Adapter):
    """Transparently adds memcached-based strongly-consistent caching
    on top of another adapter for delete, get and put operations.

    Parameters:
      client(pylibmc.Client): The memcached client instance to use.
        This is automatically wrapped inside a ThreadMappedPool.
      adapter(Adapter): The adapter to wrap.
      prefix(str, optional): The string keys should be prefixed
        with.  Defaults to ``anom``.
    """

    _state = local()

    _lock_prefix = b"LOCK@"
    _lock_timeout = 60  # seconds
    _item_timeout = 86400  # one day in seconds

    def __init__(self, client, adapter, *, prefix="anom"):
        self.client_pool = pylibmc.ThreadMappedPool(client)
        self.adapter = adapter
        self.prefix = prefix

        self.query = self.adapter.query

    @property
    def _transactions(self):
        "list[Transaction]: The current stack of Transactions."
        transactions = getattr(self._state, "transactions", None)
        if transactions is None:
            transactions = self._state.transactions = []
        return transactions

    def delete_multi(self, keys):
        if self.in_transaction:
            self.current_transaction._push_keys(keys)
            return self.adapter.delete_multi(keys)

        with self._bust(keys):
            return self.adapter.delete_multi(keys)

    def get_multi(self, keys):
        if self.in_transaction:
            return self.adapter.get_multi(keys)

        # Get all the cached keys.
        pairs = {self._convert_key_to_memcache(key): key for key in keys}
        with self.client_pool.reserve() as client:
            mapping = client.get_multi(pairs.keys())

        # Sort out which ones were found in Memcache and which ones we
        # need to get from Datastore.
        found, missing = [None] * len(keys), []
        for memcache_key, anom_key in pairs.items():
            data = mapping.get(memcache_key)
            # If there is no data or the data appears to be locked, we
            # have to grab the entity from datastore.
            if data is None or data.startswith(self._lock_prefix):
                missing.append(anom_key)
                continue

            index = keys.index(anom_key)
            found[index] = Msgpack._loads(data)

        # Get and cache missing keys from Datastore.
        ds_results = self.adapter.get_multi(missing)
        for anom_key, entity in zip(missing, ds_results):
            index = keys.index(anom_key)
            found[index] = entity
            if entity is None:
                continue

            key = self._convert_key_to_memcache(anom_key)
            data = Msgpack._dumps(entity)
            self._cache(key, data)

        return found

    def put_multi(self, requests):
        # Partial keys' cache doesn't need to be cleared since they
        # can't have been already set so we have to collect the full
        # keys and pass them to the transaction.
        full_keys = [request.key for request in requests if not request.key.is_partial]
        if self.in_transaction:
            self.current_transaction._push_keys(full_keys)
            return self.adapter.put_multi(requests)

        with self._bust(full_keys):
            return self.adapter.put_multi(requests)

    def transaction(self, propagation):
        ds_transaction = self.adapter.transaction(propagation)

        if propagation == Transaction.Propagation.Independent:
            transaction = _MemcacheOuterTransaction(self, ds_transaction)
            self._transactions.append(transaction)
            return transaction

        elif propagation == Transaction.Propagation.Nested:
            if self._transactions:
                transaction = _MemcacheInnerTransaction(self.current_transaction, ds_transaction)
            else:
                transaction = _MemcacheOuterTransaction(self, ds_transaction)

            self._transactions.append(transaction)
            return transaction

        else:  # pragma: no cover
            raise ValueError(f"Invalid propagation option {propagation!r}.")

    @property
    def in_transaction(self):
        return bool(self._transactions)

    @property
    def current_transaction(self):
        return self._transactions[-1]

    def _convert_key_to_memcache(self, anom_key):
        digest = md5(str(anom_key).encode("utf-8")).hexdigest()
        return f"{self.prefix}:{digest}"

    @contextmanager
    def _bust(self, keys):
        current_lock = self._lock_value()
        memcache_keys = [self._convert_key_to_memcache(key) for key in keys]
        memcache_pairs = {key: current_lock for key in memcache_keys}

        # Lock the keys so that they can't be set for the duration of
        with self.client_pool.reserve() as client:
            client.set_multi(memcache_pairs, self._lock_timeout)

        try:
            # Delete the keys from Datastore.
            yield

        finally:
            # Finally, delete the keys from Memcache.
            with self.client_pool.reserve() as client:
                client.delete_multi(memcache_keys)

    def _cache(self, key, data):
        current_lock = self._lock_value()
        with self.client_pool.reserve() as client:
            while True:
                value, cid = client.gets(key)
                # If there is a lock value in Memcache that is
                # different from our own we have to bail.
                if value and value.startswith(self._lock_prefix) and value != current_lock:
                    return

                # If there isn't a value at all, we have to add one
                if cid is None:
                    client.add(key, current_lock, self._lock_timeout)
                    continue

    def _lock_value(self):
        random_value = str(uuid.uuid4()).encode("ascii")
        lock_value = self._lock_prefix + random_value
        return lock_value