class _GidcLite(_BaseLite):
    ''' Lightweight description of a GIDC.
    '''
    __slots__ = [
        'identity'
    ]
    
    def __init__(self, ghid, identity):
        self.ghid = ghid
        self.identity = identity
        
    @classmethod
    def from_golix(cls, golix_obj):
        ''' Convert the golix object to a lightweight representation.
        '''
        identity = SecondParty.from_identity(golix_obj)
        return cls(
            ghid = golix_obj.ghid,
            identity = identity,
        ) class _GeocLite(_BaseLite):
    ''' Lightweight description of a GEOC.
    '''
    __slots__ = [
        'author',
    ]
    
    def __init__(self, ghid, author):
        self.ghid = ghid
        self.author = author
        
    def __eq__(self, other):
        try:
            return (
                super().__eq__(other) and
                self.author == other.author
            )
        # This will not catch a super() TyperError, so we want to be able to
        # compare anything with a ghid. In reality, any situation where the
        # authors don't match but the ghids do is almost certainly a bug; but,
        # compare it anyways just in case.
        except AttributeError as exc:
            return False
        
    @classmethod
    def from_golix(cls, golix_obj):
        ''' Convert the golix object to a lightweight representation.
        '''
        return cls(
            ghid = golix_obj.ghid,
            author = golix_obj.author,
        ) class _GobsLite(_BaseLite):
    ''' Lightweight description of a GOBS.
    '''
    __slots__ = [
        'author',
        'target',
    ]
    
    def __init__(self, ghid, author, target):
        self.ghid = ghid
        self.author = author
        self.target = target
        
    def __eq__(self, other):
        try:
            return (
                super().__eq__(other) and
                self.author == other.author and
                self.target == other.target
            )
            
        # This will not catch a super() TyperError, so we want to be able to
        # compare anything with a ghid. In reality, any situation where the
        # authors don't match but the ghids do is almost certainly a bug; but,
        # compare it anyways just in case.
        except AttributeError as exc:
            return False
        
    @classmethod
    def from_golix(cls, golix_obj):
        ''' Convert the golix object to a lightweight representation.
        '''
        return cls(
            ghid = golix_obj.ghid,
            author = golix_obj.binder,
            target = golix_obj.target,
        ) class _GobdLite(_BaseLite):
    ''' Lightweight description of a GOBD.
    '''
    __slots__ = [
        'author',
        'counter',
        'target_vector',
        'frame_ghid'
    ]
    
    def __init__(self, ghid, author, counter, target_vector, frame_ghid):
        self.ghid = ghid
        self.author = author
        self.counter = counter
        self.target_vector = target_vector
        self.frame_ghid = frame_ghid
        
    def __eq__(self, other):
        try:
            return (
                super().__eq__(other) and
                self.author == other.author and
                self.target == other.target and
                self.frame_ghid == other.frame_ghid
                # Skip history, because it could potentially vary
                # self.history == other.history
            )
            
        # This will not catch a super() TyperError, so we want to be able to
        # compare anything with a ghid. In reality, any situation where the
        # authors don't match but the ghids do is almost certainly a bug; but,
        # compare it anyways just in case.
        except AttributeError as exc:
            return False
        
    @classmethod
    def from_golix(cls, golix_obj):
        ''' Convert the golix object to a lightweight representation.
        '''
        return cls(
            ghid = golix_obj.ghid_dynamic,
            author = golix_obj.binder,
            counter = golix_obj.counter,
            target_vector = golix_obj.target_vector,
            frame_ghid = golix_obj.ghid
        )
        
    @property
    def target(self):
        ''' Access the current target.
        '''
        return self.target_vector[0] class _GdxxLite(_BaseLite):
    ''' Lightweight description of a GDXX.
    '''
    __slots__ = [
        'author',
        'target',
        '_debinding',
    ]
    
    def __init__(self, ghid, author, target):
        self.ghid = ghid
        self.author = author
        self.target = target
        self._debinding = True
        
    def __eq__(self, other):
        try:
            return (
                super().__eq__(other) and
                self.author == other.author and
                self._debinding == other._debinding
            )
            
        # This will not catch a super() TyperError, so we want to be able to
        # compare anything with a ghid. In reality, any situation where the
        # authors don't match but the ghids do is almost certainly a bug; but,
        # compare it anyways just in case.
        except AttributeError as exc:
            return False
        
    @classmethod
    def from_golix(cls, golix_obj):
        ''' Convert the golix object to a lightweight representation.
        '''
        return cls(
            ghid = golix_obj.ghid,
            author = golix_obj.debinder,
            target = golix_obj.target,
        ) class _GarqLite(_BaseLite):
    ''' Lightweight description of a GARQ.
    '''
    __slots__ = [
        'recipient',
    ]
    
    def __init__(self, ghid, recipient):
        self.ghid = ghid
        self.recipient = recipient
        
    def __eq__(self, other):
        try:
            return (
                super().__eq__(other) and
                self.recipient == other.recipient
            )
            
        # This will not catch a super() TyperError, so we want to be able to
        # compare anything with a ghid. In reality, any situation where the
        # authors don't match but the ghids do is almost certainly a bug; but,
        # compare it anyways just in case.
        except AttributeError as exc:
            return False
        
    @classmethod
    def from_golix(cls, golix_obj):
        ''' Convert the golix object to a lightweight representation.
        '''
        return cls(
            ghid = golix_obj.ghid,
            recipient = golix_obj.recipient,
        ) class Enforcer(metaclass=API):
    ''' Enforces valid target selections.
    '''
    _librarian = weak_property('__librarian')
    
    @fixture_api
    def __init__(self, librarian, *args, **kwargs):
        super(Enforcer.__fixture__, self).__init__(*args, **kwargs)
        self._librarian = librarian
        
    def assemble(self, librarian):
        # Call before using.
        self._librarian = librarian
        
    async def validate_gidc(self, obj):
        ''' GIDC need no target verification.
        '''
        return True
        
    async def validate_geoc(self, obj):
        ''' GEOC need no target validation.
        '''
        return True
        
    async def validate_gobs(self, obj):
        ''' Check if target is known, and if it is, validate it.
        '''
        try:
            target = await self._librarian.summarize(obj.target)
        # TODO: think more about this, and whether everything has been updated
        # appropriately to raise a DoesNotExist instead of a KeyError.
        # This could be more specific and say DoesNotExist
        except KeyError:
            logger.debug(str(obj) + ' target missing from librarian: ' +
                         str(obj.target))
        else:
            for forbidden in (_GidcLite, _GobsLite, _GdxxLite, _GarqLite):
                if isinstance(target, forbidden):
                    raise InvalidTarget(str(obj) + ' target invalid: ' +
                                        str(target))
        return True
        
    async def validate_gobd(self, obj):
        ''' Check if target is known, and if it is, validate it.
        
        Also do a state check on the dynamic binding.
        '''
        try:
            target = await self._librarian.summarize(obj.target)
        except KeyError:
            logger.debug(str(obj) + ' target missing from librarian: ' +
                         str(obj.target))
        else:
            for forbidden in (_GidcLite, _GobsLite, _GdxxLite, _GarqLite):
                if isinstance(target, forbidden):
                    raise InvalidTarget(str(obj) + ' target invalid: ' +
                                        str(target))
                    
        await self._validate_dynamic_history(obj)
                    
        return True
        
    async def validate_gdxx(self, obj, target_obj=None):
        ''' Check if target is known, and if it is, validate it.
        '''
        try:
            if target_obj is None:
                target = await self._librarian.summarize(obj.target)
            else:
                target = target_obj
        except KeyError:
            logger.warning(str(obj) + ' validated by Enforcer, but its ' +
                           'target was unknown: ' + str(obj.target))
            logger.debug(str(obj) + ' missing target traceback:\n' +
                         ''.join(traceback.format_exc()))
            
        else:
            # NOTE: if this changes, will need to modify place_gdxx in _Bookie
            for forbidden in (_GidcLite, _GeocLite):
                if isinstance(target, forbidden):
                    raise InvalidTarget(str(obj) + ' target invalid: ' +
                                        str(target))
        return True
        
    async def validate_garq(self, obj):
        ''' No additional validation needed.
        '''
        return True
        
    async def _validate_dynamic_history(self, obj):
        ''' Enforces state flow / progression for dynamic objects. In
        other words, ensures monotonic counter.
        '''
        # Try getting an existing binding.
        try:
            existing = await self._librarian.summarize(obj.ghid)
        
        # TOFU (trust on first upload lulz)
        except KeyError:
            pass
                
        else:
            if existing.counter >= obj.counter:
                logger.debug('New obj frame:     ' +
                             str(obj.frame_ghid))
                logger.debug('New obj tarvec:    ' +
                             str(obj.target_vector))
                logger.debug('Existing frame:    ' +
                             str(existing.frame_ghid))
                logger.debug('Existing tarvec:   ' +
                             str(existing.target_vector))
                raise IllegalDynamicFrame(str(obj.ghid) + ' counter ' +
                                          'decreased from ' +
                                          str(existing.counter) + ' to ' +
                                          str(obj.counter)) class LawyerCore(metaclass=API):
    ''' Enforces authorship requirements, including both having a known
    entity as author/recipient and consistency for eg. bindings and
    debindings.
    
    Threadsafe.
    '''
    _librarian = weak_property('__librarian')
    
    @fixture_api
    def __init__(self, librarian, *args, **kwargs):
        super(LawyerCore.__fixture__, self).__init__(*args, **kwargs)
        self._librarian = librarian
        
    def assemble(self, librarian):
        # Call before using.
        self._librarian = librarian
    
    @fixture_return(True)
    @public_api
    async def _validate_author(self, obj):
        try:
            author = await self._librarian.summarize(obj.author)
        
        except KeyError as exc:
            raise InvalidIdentity('Unknown author: ' +
                                  str(obj.author)) from exc
        
        else:
            if not isinstance(author, _GidcLite):
                raise InvalidIdentity('Invalid author: ' + str(author))
                
        return True
        
    async def validate_gidc(self, obj):
        ''' GIDC need no validation.
        '''
        return True
        
    async def validate_geoc(self, obj):
        ''' Ensure author is known and valid.
        '''
        return (await self._validate_author(obj))
        
    async def validate_gobs(self, obj):
        ''' Ensure author is known and valid.
        '''
        return (await self._validate_author(obj))
        
    async def validate_gobd(self, obj):
        ''' Ensure author is known and valid, and consistent with the
        previous author for the binding (if it already exists).
        '''
        await self._validate_author(obj)
        
        if obj.counter > 0:
            try:
                existing = await self._librarian.summarize(obj.ghid)
            
            except KeyError:
                logger.warning(str(obj) + ' previous binding missing; ' +
                               'cannot validate author self-consistency.')
            
            else:
                if existing.author != obj.author:
                    raise InconsistentAuthor('Existing author for ' +
                                             str(obj) + ': ' +
                                             str(existing.author) +
                                             ' (existing) vs ' +
                                             str(obj.author) + ' (attempted)')
        
        return True
        
    async def validate_gdxx(self, obj, target_obj=None):
        ''' Ensure author is known and valid, and consistent with the
        previous author for the binding.
        
        If other is not None, specifically checks it against that object
        instead of obtaining it from librarian.
        '''
        await self._validate_author(obj)
        
        try:
            if target_obj is None:
                existing = await self._librarian.summarize(obj.target)
            else:
                existing = target_obj
                
        except KeyError:
            logger.debug(str(obj) + ' target missing from librarian; ' +
                         'cannot validate author self-consistency: ' +
                         str(obj.target))
            
        else:
            if isinstance(existing, _GarqLite):
                if existing.recipient != obj.author:
                    raise InconsistentAuthor('Existing author for ' +
                                             str(obj) + ': ' +
                                             str(existing.recipient) +
                                             ' (existing) vs ' +
                                             str(obj.author) + ' (attempted)')
                
            else:
                if existing.author != obj.author:
                    raise InconsistentAuthor('Existing author for ' +
                                             str(obj) + ': ' +
                                             str(existing.author) +
                                             ' (existing) vs ' +
                                             str(obj.author) + ' (attempted)')
        return True
        
    async def validate_garq(self, obj):
        ''' Validate recipient.
        '''
        try:
            recipient = await self._librarian.summarize(obj.recipient)
        
        except KeyError as exc:
            raise InvalidIdentity('Unknown recipient: ' +
                                  str(obj.recipient)) from exc
        
        else:
            if not isinstance(recipient, _GidcLite):
                raise InvalidIdentity('Invalid recipient: ' + str(recipient))
                
        return True class GAO(GAOCore):
    ''' A bare-bones GAO that performs trivial serialization (the
    serialization of the state is the state itself).
    '''
    
    def __init__(self, *args, state, **kwargs):
        super().__init__(*args, **kwargs)
        self.state = state
        
    async def pack_gao(self):
        ''' Packs self into a bytes object. May be overwritten in subs
        to pack more complex objects. Should always be a staticmethod or
        classmethod.
        
        May be used to implement, for example, packing self into a
        DispatchableState, etc etc.
        '''
        return self.state
        
    async def unpack_gao(self, packed):
        ''' Unpacks state from a bytes object and applies state to self.
        May be overwritten in subs to unpack more complex objects.
        
        May be used to implement, for example, dicts performing a
        clear() operation before an update() instead of just reassigning
        the object.
        '''
        self.state = packed
        
    def __eq__(self, other):
        ''' Ensure the other object is the same gao with the same state.
        '''
        equal = True
        
        try:
            equal &= (self.dynamic == other.dynamic)
            equal &= (self.ghid == other.ghid)
            equal &= (self.author == other.author)
            equal &= (self.state == other.state)
            
        except AttributeError:
            equal = False
        
        return equal class API(type):
    ''' Metaclass for defining an interfaceable API.
    '''
    
    def __new__(mcls, clsname, bases, namespace, *args, _short_circuit=False,
                **kwargs):
        ''' Modify the existing namespace:
        1.  remove any @fixture_api methods.
        2.  extract any @public_api.fixture methods
        3.  extract any @public_api.interface methods
        4.  create cls.__fixture__ class object
        5.  create cls.__interface__ class object
        '''
        # Dead-end for fixtures and interfaces
        if _short_circuit:
            cls = super().__new__(mcls, clsname, bases, namespace, *args,
                                  **kwargs)
        
        # For the parent/public class...
        else:
            public_name = clsname
            fixture_name = clsname + 'Fixture'
            interface_name = clsname + 'Interface'
                
            public_namespace = {}
            fixture_namespace = {}
            interface_namespace = {}
        
            # No need to modify bases, either for the actual type or the
            # fixture/interface
            
            # Iterate over the entire defined namespace.
            for name, obj in namespace.items():
                is_fixture = getattr(obj, '__is_fixture__', False)
                is_interface = getattr(obj, '__is_interface__', False)
                
                # If the .fixture magic attr was defined, use it; else, just
                # fall back to inheritance
                fixture = getattr(obj, '__fixture__', None)
                interface = getattr(obj, '__interface__', None)
                
                # __is_fixture__ get sent only to the fixture.
                if is_fixture:
                    fixture_namespace[name] = obj
                    
                elif is_interface:
                    interface_namespace[name] = obj
                
                # All other objects pass via inheritance.
                else:
                    public_namespace[name] = obj
                
                # If we have an actual fixture defined, then it supercedes
                # public_namespace
                if fixture is not None:
                    fixture_namespace[name] = fixture
                    
                # Same goes with the interface.
                if interface is not None:
                    interface_namespace[name] = interface
            
            # Create the class
            cls = super().__new__(
                mcls,
                public_name,
                bases,
                public_namespace,
                *args,
                **kwargs
            )
            
            # Figure out the fixture inheritance tree. Fixtures always take
            # precedent over parents, but also always inherit them.
            fixture_bases = (cls, *(
                base.__fixture__ if hasattr(base, '__fixture__') else base
                for base in bases
            ))
            interface_bases = (cls, *(
                base.__interface__ if hasattr(base, '__interface__') else base
                for base in bases
            ))
            
            # NOTE:
            # See Python docs on super() with zero args: "the compiler fills in
            # the necessary details to correctly retrieve the class being
            # defined". Therefore, this is necessary when using fixture_bases:
            # super(Class, self).__init__(*args, **kwargs)
            
            # Now add in the types for both the fixture and the interface.
            # Reuse same bases for both, but use the REAL metaclass for it.
            # TODO: make the fixture inherit from the cls above, so that it can
            # have access to the original methods using super()
            cls.__fixture__ = mcls(
                fixture_name,
                fixture_bases,
                fixture_namespace,
                *args,
                _short_circuit = True,
                **kwargs
            )
            cls.__interface__ = mcls(
                interface_name,
                interface_bases,
                interface_namespace,
                *args,
                _short_circuit = True,
                **kwargs
            )
        
        # And don't forget to return the final cls object.
        return cls
        
    def __init__(cls, *args, _short_circuit=False, **kwargs):
        # Just strip out _short_circuit
        super().__init__(*args, **kwargs) def public_api(func):
    ''' Decorator to automatically mark the object as the normal thing.
    '''
    
    def fixture_closure(fixture_func, public_func=func):
        ''' Defines the decorator for @method.fixture.
        '''
        # This is the actual __fixture__ method, to be defined via decorator
        public_func.__fixture__ = fixture_func
        return public_func
    
    def interface_closure(interface_func, public_func=func):
        ''' Defines the decorator for @method.fixture.
        '''
        # This is the actual __interface method, to be defined via decorator
        public_func.__interface__ = interface_func
        return public_func
        
    func.fixture = fixture_closure
    func.interface = interface_closure
    
    # This denotes that it is an API
    func.__is_api__ = True
    
    return func def fixture_api(func):
    ''' Decorator to mark the method as a fixture-only object.
    '''
    ''' Decorator to automatically mark the object as the normal thing.
    '''
    
    # Huh, well, this is easy.
    func.__is_fixture__ = True
    return func def fixture_noop(func):
    ''' Create a no-op version of the function to use as its fixture.
    Should be used as a decorator above @public_api.
    '''
    signature = inspect.Signature.from_callable(func)
    
    if inspect.iscoroutinefunction(func):
        @func.fixture
        @functools.wraps(func)
        async def noop_fixture(*args, __signature=signature, **kwargs):
            # Make sure the passed args, kwargs are correct, but otherwise,
            # do nothing.
            __signature.bind(*args, **kwargs)
    
    else:
        @func.fixture
        @functools.wraps(func)
        def noop_fixture(*args, __signature=signature, **kwargs):
            # Make sure the passed args, kwargs are correct, but otherwise,
            # do nothing.
            __signature.bind(*args, **kwargs)
        
    return noop_fixture class HypergolixException(Exception):
    ''' This is suclassed for all exceptions and warnings, so that code
    using hypergolix as an import can successfully catch all hypergolix
    exceptions with a single except.
    '''
    pass class RemoteNak(PersistenceError):
    ''' This exception (or a subclass thereof) is raised for all failed
    operations with remote persister servers.
    '''
    pass class MalformedGolixPrimitive(RemoteNak):
    ''' This RemoteNak is raised when a packed Golix primitive appears to
    be malformed.
    '''
    pass class VerificationFailure(RemoteNak):
    ''' This RemoteNak is raised when signature verification fails on a
    Golix object.
    '''
    pass class UnboundContainer(RemoteNak):
    ''' This RemoteNak is raised when a persistence provider has no
    binding for the attempted container, and it was therefore passed
    immediately to garbage collection.
    '''
    pass class InvalidIdentity(RemoteNak):
    ''' This RemoteNak is raised when a persistence provider has received
    a primitive with an unknown or invalid author or recipient.
    '''
    pass class DoesNotExist(RemoteNak, KeyError):
    ''' This RemoteNak is raised when a persistence provider has received
    a request for a ghid that does not exist in its object store.
    '''
    pass class AlreadyDebound(RemoteNak):
    ''' This RemoteNak is raised when a persistence provider has already
    received a debinding for the primitive being published.
    '''
    
    def __init__(self, *args, ghid=None, **kwargs):
        ''' If passed, set the ghid that was rejected.
        '''
        super().__init__(*args, **kwargs)
        self.ghid = ghid class InvalidTarget(RemoteNak):
    ''' This RemoteNak is raised when a persistence provider has received
    a primitive targeting an inappropriate object (for example, trying
    to debind a GEOC directly).
    '''
    pass class StillBoundWarning(HypergolixException, RuntimeWarning):
    ''' Raised when a debinding did not result in the removal of its
    target -- for example, if another binding remains on the target
    object.
    '''
    pass class RequestError(CommsError):
    ''' Raised when something goes wrong with IPC (bad commands, etc).
    '''
    pass class InconsistentAuthor(RemoteNak):
    ''' This RemoteNak is raised when a persistence provider has received
    a primitive targeting an inappropriate object (for example, trying
    to debind a GEOC directly).
    '''
    pass class IllegalDynamicFrame(RemoteNak):
    ''' This RemoteNak is raised when a persistence provider has received
    an illegal dynamic frame, ie. if the existing frame is not contained
    within the new frame's history, or if the zeroth frame contains
    history.
    '''
    pass class IntegrityError(PersistenceError):
    ''' This PersistenceError is raised when a packed Golix primitive
    appears to be corrupted in local persistence.
    '''
    pass class UnavailableUpstream(PersistenceError):
    ''' This PersistenceError is raised when an object is unavailable or
    unacceptable from (an) upstream remote(s).
    '''
    pass def weak_property(name):
    ''' Make a weakly-referenced property using name.
    '''
    @_WeakProperty
    def prop(self, name=name):
        return getattr(self, name)
        
    @prop.setter
    def prop(self, value, name=name):
        return setattr(self, name, value)
        
    return prop def readonly_property(name):
    ''' Make a read-only property for the name.
    '''
    @property
    def prop(self, name=name):
        return getattr(self, name)
    return prop class TruthyLock:
    ''' Glues together a semaphore and an event, such that they can be
    used as a threadsafe blocking conditional.
    '''
    def __init__(self):
        self._opslock = threading.RLock()
        self._mutexlock = threading.RLock()
        self._cond = False
        
    def __bool__(self):
        with self._opslock:
            return self._cond
        
    def set(self):
        ''' Sets the internal flag to True. Indempotent.
        '''
        with self._opslock:
            self._cond = True
        
    def clear(self):
        ''' Sets the internal flag to False. Indempotent.
        '''
        with self._opslock:
            self._cond = False
            
    @property
    def mutex(self):
        ''' Returns the internal lock.
        Indended to be used with the context manager to provide mutually
        exclusive execution WITHOUT setting the condition.
        '''
        return self._mutexlock
            
    def __enter__(self):
        with self._opslock:
            self._mutexlock.acquire()
            self._cond = True
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        with self._opslock:
            self._cond = False
            self._mutexlock.release() class SetMap:
    ''' Combines a mapping with a set. Threadsafe.
    '''
    
    def __init__(self):
        ''' Create a lookup!
        
        Currently does not support pre-population during __init__().
        '''
        self._mapping = {}
        self._lock = threading.RLock()
    
    def __getitem__(self, key):
        ''' Pass-through to the core lookup. Will return a frozenset.
        Raises keyerror if missing.
        '''
        with self._lock:
            return frozenset(self._mapping[key])
        
    def get_any(self, key):
        ''' Pass-through to the core lookup. Will return a frozenset.
        Will never raise a keyerror; if key not in self, returns empty
        frozenset.
        '''
        with self._lock:
            try:
                return frozenset(self._mapping[key])
            except KeyError:
                return frozenset()
                
    def pop_any(self, key):
        ''' Unlike other methods, pop_any returns the actual set,
        instead of a frozenset copy.
        '''
        with self._lock:
            try:
                return self._mapping.pop(key)
            except KeyError:
                return set()
        
    def __contains__(self, key):
        ''' Check to see if the key exists.
        '''
        with self._lock:
            return key in self._mapping
        
    def contains_within(self, key, value):
        ''' Check to see if the key exists, AND the value exists at key.
        '''
        with self._lock:
            try:
                return value in self._mapping[key]
            except KeyError:
                return False
        
    def add(self, key, value):
        ''' Adds the value to the set at key. Creates a new set there if
        none already exists.
        '''
        with self._lock:
            try:
                self._mapping[key].add(value)
            except KeyError:
                self._mapping[key] = {value}
                
    def update(self, key, value):
        ''' Updates the key with the value. Value must support being
        passed to set.update(), and the set constructor.
        '''
        with self._lock:
            try:
                self._mapping[key].update(value)
            except KeyError:
                self._mapping[key] = set(value)
                
    def update_all(self, other):
        ''' Updates all keys in other to self.
        '''
        with self._lock:
            for key in other:
                try:
                    self._mapping[key].update(other[key])
                except KeyError:
                    self._mapping[key] = set(other[key])
            
    def _remove_if_empty(self, key):
        ''' Removes a key entirely if it no longer has any values. Will
        suppress KeyError if the key is not found.
        '''
        try:
            if len(self._mapping[key]) == 0:
                del self._mapping[key]
        except KeyError:
            pass
        
    def remove(self, key, value):
        ''' Removes the value from the set at key. Will raise KeyError
        if either the key is missing, or the value is not contained at
        the key.
        '''
        with self._lock:
            try:
                self._mapping[key].remove(value)
            finally:
                self._remove_if_empty(key)
        
    def discard(self, key, value):
        ''' Same as remove, but will never raise KeyError.
        '''
        with self._lock:
            try:
                self._mapping[key].discard(value)
            except KeyError:
                pass
            finally:
                self._remove_if_empty(key)
        
    def clear(self, key):
        ''' Clears the specified key. Raises KeyError if key is not
        found.
        '''
        with self._lock:
            del self._mapping[key]
            
    def clear_any(self, key):
        ''' Clears the specified key, if it exists. If not, suppresses
        KeyError.
        '''
        with self._lock:
            try:
                del self._mapping[key]
            except KeyError:
                pass
        
    def clear_all(self):
        ''' Clears the entire mapping.
        '''
        with self._lock:
            self._mapping.clear()
            
    def __len__(self):
        ''' Returns the length of the mapping only.
        '''
        return len(self._mapping)
            
    def __iter__(self):
        ''' Send this through to the dict, bypassing the usual route,
        because otherwise we'll have a deadlock.
        '''
        with self._lock:
            for key in self._mapping:
                yield key
            
    def __eq__(self, other):
        ''' Expand comparison to search insides.
        '''
        with self._lock, other._lock:
            # First make sure both have same mapping keys.
            if set(self._mapping) != set(other._mapping):
                return False
            
            # Okay, now check each key has identical sets
            else:
                for this_key, this_set in self._mapping.items():
                    if other._mapping[this_key] != this_set:
                        return False
                        
        # If we successfully got through all of that, they are identical.
        return True
        
    def combine(self, other):
        ''' Returns a new SetMap with the union of both.
        '''
        new = type(self)()
        # Note that the iterator will take the lock, so we don't need to.
        for key in self:
            new.update(key, self._mapping[key])
        for key in other:
            new.update(key, other._mapping[key])
        return new

    def __getstate__(self):
        ''' Ignore self._lock to support pickling. Basically boilerplate
        copy from the pickle reference docs.
        '''
        # Copy the object's state from self.__dict__ which contains
        # all our instance attributes. Always use the dict.copy()
        # method to avoid modifying the original state.
        state = self.__dict__.copy()
        # Remove the unpicklable entries.
        del state['_lock']
        return state

    def __setstate__(self, state):
        ''' Ignore self._lock to support pickling. Basically boilerplate
        copy from the pickle reference docs.
        '''
        # Restore instance attributes (i.e., filename and lineno).
        self.__dict__.update(state)
        # Restore the lock.
        self._lock = threading.RLock()
        
    def __bool__(self):
        # Pass bool straight to mapping.
        return bool(self._mapping) class WeakSetMap(SetMap):
    ''' SetMap that uses WeakerSets internally.
    '''
    
    def add(self, key, value):
        ''' Adds the value to the set at key. Creates a new set there if
        none already exists.
        '''
        with self._lock:
            try:
                self._mapping[key].add(value)
            except KeyError:
                self._mapping[key] = _KeyedWeakSet(
                    data = (value,),
                    parent = self,
                    key = key,
                )
                
    def update(self, key, value):
        ''' Updates the key with the value. Value must support being
        passed to set.update(), and the set constructor.
        '''
        with self._lock:
            try:
                self._mapping[key].update(value)
            except KeyError:
                self._mapping[key] = _KeyedWeakSet(
                    data = (item for item in value),
                    parent = self,
                    key = key,
                )
                
    def update_all(self, other):
        ''' Updates all keys in other to self.
        '''
        with self._lock:
            for key in other:
                try:
                    self._mapping[key].update(other[key])
                except KeyError:
                    self._mapping[key] = _KeyedWeakSet(
                        data = other[key],
                        parent = self,
                        key = key
                    )
                
    def __getitem__(self, key):
        ''' Resolves all of our references.
        '''
        result = super().__getitem__(key)
        return frozenset(ref() for ref in result)
        
    def get_any(self, key):
        ''' Resolve references.
        '''
        result = super().get_any(key)
        return frozenset(ref() for ref in result) def _generate_threadnames(*prefixes):
    ''' Generates a matching set of unique threadnames, of the form
    prefix[0] + '-1', prefix[1] + '-1', etc.
    '''
    ctr = 0
    names = []
    
    # Get existing thread NAMES (not the threads themselves!)
    existing_threadnames = set()
    for t in threading.enumerate():
        existing_threadnames.add(t.name)
        
    while len(names) != len(prefixes):
        candidates = [prefix + '-' + str(ctr) for prefix in prefixes]
        # Check the intersection of candidates and existing names
        if len(existing_threadnames & set(candidates)) > 0:
            ctr += 1
        else:
            names.extend(candidates)
            
    return names class FiniteDict:
    ''' A dict-like bounded cache. Beyond max_size, object inserts will
    remove older elements. Element "freshness" is reset upon any and all
    operations specifically concerning that key:
    __getitem__
    __setitem__
    get (IFF the key existed)
    setdefault (IFF the key existed)
    '''
    
    def __init__(self, items=None, *args, maxlen, **kwargs):
        ''' Set us up with an internal data store and a max length.
        Don't directly subclass ordereddict, because there appear to be
        some inconsistencies re: documentation and implementation that
        are resulting in reentrancy-driven errors.
        '''
        super().__init__(*args, **kwargs)
        
        if items is None:
            self._data = collections.OrderedDict()
        else:
            self._data = collections.OrderedDict(items)
        
        self._maxlen = maxlen
        
    def _truncate(self):
        ''' Shrink the dict to, at most, they correct size.
        '''
        while len(self._data) > self._maxlen:
            self._data.popitem(last=True)
        
    def __getitem__(self, key):
        # Get the item, or raise KeyError if missing
        item = self._data[key]
        
        # Success! Bring the key to the front.
        self._data.move_to_end(key, last=False)
        
        # Now return the item.
        return item
        
    def __setitem__(self, key, value):
        # Set the item (or raise whatever if impossible)
        self._data[key] = value
        
        # Success! Bring the key to the front.
        self._data.move_to_end(key, last=False)
        
        # Truncate to the correct length.
        self._truncate()
        
    def __delitem__(self, key):
        del self._data[key]
        
    def get(self, key, default=None):
        # Only refresh if the key actually exists in self.
        try:
            item = self._data[key]
            
        except KeyError:
            item = default
            
        else:
            self._data.move_to_end(key, last=False)
            
        return item
        
    def setdefault(self, key, default=None):
        try:
            item = self._data[key]
            
        except KeyError:
            item = default
            self[key] = default
            # Let setting take care of truncation
            
        else:
            self._data.move_to_end(key, last=False)
            
        return item
        
    def update(self, *args, **kwargs):
        # Update with truncation.
        self._data.update(*args, **kwargs)
        self._truncate()
        
    def __eq__(self, other):
        if isinstance(other, FiniteDict):
            return self._data == other._data
        else:
            return self._data == other
            
    def __len__(self):
        return len(self._data)
        
    def __contains__(self, key):
        return key in self._data
        
    def __iter__(self):
        return iter(self._data)
        
    def __reversed__(self):
        return reversed(self._data)
        
    def clear(self):
        return self._data.clear()
        
    def items(self):
        return self._data.items()
        
    def keys(self):
        return self._data.keys()
        
    def values(self):
        return self._data.values()
        
    def pop(self, *args, **kwargs):
        return self._data.pop(*args, **kwargs)
        
    def popitem(self, *args, **kwargs):
        return self._data.popitem(*args, **kwargs) class KeyedAsyncioLock:
    
    def __init__(self, loop, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._loop = loop
        self._lookup = weakref.WeakValueDictionary()
    
    def __call__(self, key):
        try:
            return self._lookup[key]
        except KeyError:
            lock = asyncio.Lock(loop=self._loop)
            self._lookup[key] = lock
            return lock
import logging
import collections
import weakref
import queue
import threading
import traceback
import asyncio
import loopa
import pathlib
from golix import ThirdParty
from golix import SecondParty
from golix import Ghid
from golix import Secret
from golix import ParseError
from golix import SecurityError
from golix.crypto_utils import generate_ghidlist_parser
from golix._getlow import GIDC
from golix._getlow import GEOC
from golix._getlow import GOBS
from golix._getlow import GOBD
from golix._getlow import GDXX
from golix._getlow import GARQ
from .persistence import _GidcLite
from .persistence import _GeocLite
from .persistence import _GobsLite
from .persistence import _GobdLite
from .persistence import _GdxxLite
from .persistence import _GarqLite
from .persistence import Enforcer
from .lawyer import LawyerCore
from .gao import GAO
from .hypothetical import API
from .hypothetical import public_api
from .hypothetical import fixture_api
from .hypothetical import fixture_noop
from .exceptions import HypergolixException
from .exceptions import RemoteNak
from .exceptions import MalformedGolixPrimitive
from .exceptions import VerificationFailure
from .exceptions import UnboundContainer
from .exceptions import InvalidIdentity
from .exceptions import DoesNotExist
from .exceptions import AlreadyDebound
from .exceptions import InvalidTarget
from .exceptions import StillBoundWarning
from .exceptions import RequestError
from .exceptions import InconsistentAuthor
from .exceptions import IllegalDynamicFrame
from .exceptions import IntegrityError
from .exceptions import UnavailableUpstream
from .utils import weak_property
from .utils import readonly_property
from .utils import TruthyLock
from .utils import SetMap
from .utils import WeakSetMap
from .utils import _generate_threadnames
from .utils import FiniteDict
from .utils import KeyedAsyncioLock
        
        # Lookup for dynamic ghid -> frame ghid
        self._dyn_resolver = {}
        
        # Lookup <bound ghid>: set(<binding obj>)
        self._bound_by_ghid = SetMap()
        
        # Lookup <debound ghid>: set(<debinding ghid>)
        self._debound_by_ghid = SetMap()
        
        # Lookup <recipient>: set(<request ghid>)
        self._requests_for_recipient = SetMap()
        
        # Make sure we don't do concurrent write/reads into the cache, which
        # could potentially create contention issues for stuff.
        # TODO: make this keyed, so that we can do simultaneous read/write of
        # different things at the same time (just not the same thing at the
        # same time)
        self._cache_lock = KeyedAsyncioLock(loop=self._loop)
        
    def __read_from_disk(self, ghid):
        ''' Gets a file path from the disk cache, wrapping misses in
        DoesNotExist.
        '''
        fpath = self._make_path(ghid)
        
        try:
            return fpath.read_bytes()
        
        except FileNotFoundError as exc:
            # Remove the filename from the exception context so as not to
            # disclose the full GHID
            raise DoesNotExist(str(ghid)) from None
            
    def __remove_from_disk(self, ghid):
        ''' Removes a ghid from the disk cache, wrapping misses in
        DoesNotExist.
        '''
        try:
            fpath = self._make_path(ghid)
            fpath.unlink()
            
        except FileNotFoundError:
            # Suppress the full name of the file to prevent knowing its whole
            # GHID
            raise DoesNotExist(str(ghid)) from None
        
    async def get_from_cache(self, ghid):
        ''' Returns the raw data associated with the ghid.
        '''
        try:
            ghid = await self.resolve_frame(ghid)
        except KeyError:
            pass
        
        async with self._cache_lock(ghid):
            return (await self._loop.run_in_executor(self._executor,
                                                     self.__read_from_disk,
                                                     ghid))
            
    async def add_to_cache(self, obj, data):
        ''' Adds the passed raw data to the cache.
        '''
        if isinstance(obj, _GobsLite):
            reference_ghid = obj.ghid
            self._bound_by_ghid.add(obj.target, obj.ghid)
            
        elif isinstance(obj, _GobdLite):
            reference_ghid = obj.frame_ghid
            try:
                existing = await self.summarize(obj.ghid)
            except KeyError:
                pass
            else:
                self._bound_by_ghid.remove(existing.target, obj.ghid)
                
            # Now we have a clean slate and need to update things accordingly.
            self._bound_by_ghid.add(obj.target, obj.ghid)
            self._dyn_resolver[obj.ghid] = obj.frame_ghid
                
        elif isinstance(obj, _GdxxLite):
            reference_ghid = obj.ghid
            self._debound_by_ghid.add(obj.target, obj.ghid)
            
        elif isinstance(obj, _GarqLite):
            reference_ghid = obj.ghid
            # NOTE: this is only necessary for a persistence server
            self._requests_for_recipient.add(obj.recipient, obj.ghid)
            
        else:
            reference_ghid = obj.ghid
            
        if not self._restoration_flag:
            fpath = self._make_path(reference_ghid)
            
            async with self._cache_lock(reference_ghid):
                await self._loop.run_in_executor(self._executor,
                                                 fpath.write_bytes, data)
    
    async def remove_from_cache(self, ghid):
        ''' Removes the data associated with the passed ghid from the
        cache.
        '''
        obj = await self.summarize(ghid)
        
        if isinstance(obj, _GobsLite):
            reference_ghid = obj.ghid
            self._bound_by_ghid.discard(obj.target, obj.ghid)
            
        elif isinstance(obj, _GobdLite):
            reference_ghid = obj.frame_ghid
            self._bound_by_ghid.discard(obj.target, obj.ghid)
            
            if self._dyn_resolver.get(obj.ghid, None) == obj.frame_ghid:
                self._dyn_resolver.pop(obj.ghid, None)
            
        elif isinstance(obj, _GdxxLite):
            reference_ghid = obj.ghid
            self._debound_by_ghid.discard(obj.target, obj.ghid)
            

        elif isinstance(obj, _GarqLite):