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 GAOCore(metaclass=API):
    ''' Adds functions to a GAO.
    
    Notes:
    1.  GAO are only ever created through the oracle, via either
        oracle.new_object or oracle.get_object.
    2.  Note that callbacks are only used when defined in subclasses.
        IE, Dispatchable extends pull() to notify applications of the
        update (if applicable)
    3.  Updates for internal-use GAO should defer until a flush() call
    4.  Updates for dispatchables should immediately be pushed
    5.  Salmonator pushing should probably wait to return until all
        remotes have been contacted
    6.  Internal GAOs should probably have a delta-tracking mechanism
        for conflict resolution
    7.  Dispatchables should rely upon immediate pushing, and then raise
        immediately for any conflicts
    '''
    
    # Default a few things to prevent attributeerrors
    _legroom = None
    _target_history = tuple()
    
    # Make weak properties for the various thingajobbers
    _golcore = weak_property('__golcore')
    _ghidproxy = weak_property('__ghidproxy')
    _privateer = weak_property('__privateer')
    _percore = weak_property('__percore')
    _librarian = weak_property('__librarian')
    
    # Make readonly properties for dynamic, ghid, and author. Note that the
    # author field is technically deprecated and pending removal, but is
    # currently used to infer the binder for applications.
    dynamic = readonly_property('_dynamic')
    author = readonly_property('_author')
    # This is immutable, so that it may be set just after initting.
    ghid = immutable_property('_ghid')
    _master_secret = readonly_property('__msec')
    
    # Also, an un-deletable property for history (even if unused), etc
    isalive = immortal_property('_isalive')
    # Most recent TARGET ghids
    target_history = immortal_property('_target_history')
    _counter = immortal_property('__counter')
    
    @property
    def legroom(self):
        ''' Do the thing with the thing.
        '''
        return self._legroom
        
    @legroom.setter
    def legroom(self, value):
        ''' Cast value as int and then assign it both internally and to
        the two history deques.
        '''
        legroom = int(value)
        
        if legroom != self._legroom:
            self._legroom = legroom
            # We actually have to create new deques for this, because you
            # cannot resize existing ones.
            self.target_history = collections.deque(self.target_history,
                                                    maxlen=legroom)
            
    def _inject_msec(self, msec):
        ''' Forcibly update the master secret. ONLY call this with
        extreme foresight. Intended only for use when bootstrapping the
        privateer itself.
        '''
        setattr(self, '__msec', msec)
            
    @property
    def _local_secret(self):
        ''' This determines if the secret will be committed locally only
        (for master_secreted objects), or if it will be stored
        persistently for the account.
        '''
        return bool(self._master_secret)
    
    def __init__(self, ghid, dynamic, author, legroom, *args, golcore,
                 ghidproxy, privateer, percore, librarian, master_secret=None,
                 **kwargs):
        ''' Init should be used only to create a representation of an
        EXISTING (or about to be existing) object. If any fields are
        unknown, they must be explicitly passed None.
        
        If master_secret is None, ratcheting will be iterative.
        If master_secret is a Secret, ratcheting will use the primary
            bootstrap ratcheting mechanism.
        '''
        super().__init__(*args, **kwargs)
        
        # This is an init. It will be overwritten on either a push or a pull.
        setattr(self, '__counter', None)
        
        self._golcore = golcore
        self._ghidproxy = ghidproxy
        self._privateer = privateer
        self._percore = percore
        self._librarian = librarian
        
        # Dynamic and author be explicitly None; ghid should always be defined
        self.ghid = ghid
        self._dynamic = dynamic
        self._author = author
        
        # Note that this also initializes target_history
        self.legroom = legroom
        self.isalive = True
        
        # This determines our ratcheting behavior. Set it with setattr to
        # bypass both name mangling AND the readonly property.
        setattr(self, '__msec', master_secret)
        
        # We will only ever be created from within an event loop, so this is
        # fine to do without an explicit loop.
        self._update_lock = asyncio.Lock()
        
    @fixture_noop
    @public_api
    async def apply_delete(self, debinding):
        ''' Executes an external delete, caused by the debinding at the
        passed address. By default, just makes sure the debinding target
        is valid.
        
        Debinding is a _GdxxLite.
        
        Note that this gets used by Dispatchable to dispatch deletion to
        applications.
        '''
        if self.dynamic:
            if debinding.target != self.ghid:
                raise ValueError(
                    'Debinding target does not match GAO applying delete.'
                )
        else:
            debinding_target = \
                await self._librarian.summarize(debinding.target)
            if debinding_target.target != self.ghid:
                raise ValueError(
                    'Debinding target does not match GAO applying delete.'
                )
                
        self.isalive = False
            
    @public_api
    async def freeze(self):
        ''' Creates a static binding for the most current state of a
        dynamic binding. Returns the frozen ghid.
        '''
        if not self.dynamic:
            raise TypeError('Cannot freeze a static GAO.')
            
        container_ghid = await self._ghidproxy.resolve(self.ghid)
        binding = await self._golcore.make_binding_stat(container_ghid)
        await self._percore.direct_ingest(
            obj = _GobsLite.from_golix(binding),
            packed = binding.packed,
            remotable = True
        )
        
        return container_ghid
        
    @freeze.fixture
    async def freeze(self):
        ''' Just return a pseudorandom ghid.
        '''
        return Ghid.from_bytes(
            b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
        )
        
    @fixture_noop
    @public_api
    async def hold(self):
        ''' Make a static binding for the gao.
        Note that, since third parties can bind arbitrary objects, there
        isn't an easy way of checking for an existing static binding for
        this specific ghid.
        '''
        binding = await self._golcore.make_binding_stat(self.ghid)
        await self._percore.direct_ingest(
            obj = _GobsLite.from_golix(binding),
            packed = binding.packed,
            remotable = True
        )
        
    @fixture_noop
    @public_api
    async def delete(self):
        ''' Permanently removes the object. This method is only called
        locally; upstream deletes should call self.apply_delete.
        Incidentally, apply_delete will eventually get called through
        this. Which means (for the record) that the deleter will also
        get a notification of the delete (iff it was actually successful
        in removing the object).
        '''
        if self.dynamic:
            debinding = await self._golcore.make_debinding(self.ghid)
            await self._percore.direct_ingest(
                obj = _GdxxLite.from_golix(debinding),
                packed = debinding.packed,
                remotable = True
            )
            
        else:
            # Get frozenset of binding ghids
            bindings = await self._librarian.bind_status(self.ghid)
            
            for binding in bindings:
                obj = await self._librarian.summarize(binding)
                if isinstance(obj, _GobsLite):
                    if obj.author == self._golcore.whoami:
                        debinding = await self._golcore.make_debinding(
                            obj.ghid
                        )
                        await self._percore.direct_ingest(
                            obj = _GdxxLite.from_golix(debinding),
                            packed = debinding.packed,
                            remotable = True
                        )
            
    def _get_new_secret(self):
        ''' Gets a new secret for self.
        '''
        # If we're getting a new secret and we already have a ghid, then we
        # MUST be a dynamic object.
        if self.ghid:
            current_target = self.target_history[0]
            secret = self._privateer.ratchet_chain(
                self.ghid,
                current_target,
                self._master_secret
            )
        
        # This is a new object, and we therefore don't have a secret. Note that
        # the first secret for a bootstrapped chain will always be discarded,
        # because the salt process needs to start somewhere.
        else:
            secret = self._privateer.new_secret()
            
        return secret
    
    @public_api
    async def push(self):
        ''' Pushes the state upstream. Must be called explicitly. Unless
        push() is invoked, the state will not be synchronized.
        '''
        if not self.isalive:
            raise DeadObject('Cannot push a deleted object. Create a new one.')
        
        # We're alive. Don't do object creation here; do it in oracle. This is
        # strictly updating.
        elif self.dynamic:
            # We need to make sure we're not pushing and pulling at the same
            # time.
            async with self._update_lock:
                try:
                    await self._push()
                
                # TODO: narrow which exceptions can cause this.
                except Exception:
                    logger.error(''.join((
                        'GAO ',
                        str(self.ghid),
                        ' PUSH: failed to update object; forcibly restoring ',
                        'state w/ traceback:\n',
                        traceback.format_exc()
                    )))
                    # We had a problem, so we're going to forcibly restore the
                    # object to the last known good state. Use _pull to avoid
                    # the lock, as well as any extra baggage in "normal" pull
                    await self._pull()
                    raise
            
        else:
            raise TypeError('Static objects cannot be updated.')
            
    @push.fixture
    async def push(self):
        ''' For fixture, just pass to _push directly.
        '''
        await self._push()
    
    @public_api
    async def _push(self):
        ''' The actual "meat and bones" for pushing.
        '''
        secret = self._get_new_secret()
        packed = await self.pack_gao()
        container = await self._golcore.make_container(packed, secret)
        
        # Dynamic object
        if self.dynamic:
            binding = await self._golcore.make_binding_dyn(
                target = container.ghid,
                ghid = self.ghid,
                history = self.target_history
            )
            binding_obj = _GobdLite.from_golix(binding)
            counter = binding.counter
            reference_ghid = binding.ghid_dynamic
            
        # Static object.
        else:
            binding = await self._golcore.make_binding_stat(container.ghid)
            binding_obj = _GobsLite.from_golix(binding)
            counter = 0
            reference_ghid = container.ghid
        
        # Okay, up until now, we haven't changed any state. But now we need to
        # update state, which needs to be atomic-ish, so we're going to do some
        # funny business here.
        
        # If this gets cancelled, we don't need to worry about anything.
        await self._percore.direct_ingest(
            obj = binding_obj,
            packed = binding.packed,
            remotable = True
        )
        
        # But if we get here, we've changed state, so we need to protect stuff.
        try:
            # It we ingested the binding, we *must* also ingest the container
            # (if **at all** possible), or the binding will effectively be
            # unrecoverable
            container_ingester = asyncio.ensure_future(
                self._percore.direct_ingest(
                    obj = _GeocLite.from_golix(container),
                    packed = container.packed,
                    remotable = True
                )
            )
        
            await asyncio.shield(container_ingester)
        
        # That container ingestion is the only remaining async call within this
        # coro, so this is the only thing we need to defer cancellation on.
        # While we're at it, defer any other exceptions, too, though they will
        # likely break the object anyways.
        except Exception as exc:
            deferred_raise = exc
            
        else:
            deferred_raise = None
            
        # We created a container and binding frame, and then we successfully
        # uploaded them. Update our frame and target history accordingly and
        # commit the container secret.
        self._privateer.stage(container.ghid, secret)
        self._privateer.commit(container.ghid, localize=self._local_secret)
        # NOTE THE DISCREPANCY between the Golix dynamic binding version
        # of ghid and ours! This is recording the frame ghid.
        # I mean, for static stuff this isn't (strictly speaking) relevant,
        # but it also doesn't really hurt anything, sooo...
        self.target_history.appendleft(container.ghid)
        self._counter = counter
        
        # Do this last!
        # Update ghid if it was not defined (new object)
        self._conditional_init(
            reference_ghid,
            False,
            self._golcore.whoami
        )
        
        if deferred_raise is not None:
            raise deferred_raise
            
    @_push.fixture
    async def _push(self):
        ''' Do a conditional init if we haven't had stuff fully defined.
        '''
        self._conditional_init(
            ghid = Ghid.from_bytes(
                b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
            ),
            author = Ghid.from_bytes(
                b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
            ),
            dynamic = None
        )
    
    @public_api
    async def pull(self, notification):
        ''' Pulls state from upstream and applies it. Does not check for
        recency.
        '''
        # Is this a good idea? It is actually possible to un-delete an object,
        # if you re-bind it, soooo...
        # TODO: think darüber
        if not self.isalive:
            raise DeadObject('Cannot pull a deleted object. Create a new one.')
            
        elif self.dynamic:
            summary = await self._librarian.summarize(notification)
            # We've been deleted.
            if isinstance(summary, _GdxxLite):
                async with self._update_lock:
                    await self.apply_delete(summary)
                    logger.info(''.join((
                        'GAO at ',
                        str(self.ghid),
                        ' PULL COMPLETED; deleted.'
                    )))
                    
            # We're being updated.
            elif summary.ghid == self.ghid:
                async with self._update_lock:
                    await self._pull()
                    logger.info(''.join((
                        'GAO at ',
                        str(self.ghid),
                        ' PULL COMPLETED with updates.'
                    )))
            
            # The update did not match us. This is sufficient to verify
            # both that the update was a dynamic binding, and that there's
            # no upstream bug for us.
            else:
                raise ValueError(
                    'Update failed pull; mismatched binding ghid.'
                )
            
        else:
            raise TypeError('Static objects cannot be pulled.')
            
    @pull.fixture
    async def pull(self, notification):
        ''' For the fixture, just pass to _pull directly.
        '''
        await self._pull()
    
    @public_api
    async def _pull(self):
        ''' The actual "meat and bones" for pulling.
        
        This is what you'll extend in Dispatchable to update
        applications.
        '''
        # Make sure we have the most recent binding, regardless of what was
        # recently passed.
        binding = await self._librarian.summarize(self.ghid)
        
        # Okay, if this is calling _pull from oracle._get_object, we may be
        # going for either a dynamic binding or a static object (container).
        if isinstance(binding, _GobdLite):
            self._recover_target_secret(binding)
            
            target_vector = binding.target_vector
            dynamic = True
                    
        # Static object, so this is the actual container (and not the binding).
        elif isinstance(binding, _GeocLite):
            target_vector = [binding.ghid]
            dynamic = False
            
        # None of the above. TypeError.
        else:
            raise TypeError('Update failed pull; mismatched Golix primitive.')
        
        try:
            # Do this before the state update so it has access to self.ghid
            # If this was just pulled for the first time, init the dynamic and
            # author attributes.
            self._conditional_init(
                ghid = binding.ghid,    # For GOBS, actually the container ghid
                dynamic = dynamic,
                author = binding.author
            )
            
            state_update = asyncio.ensure_future(
                self._update_state(target_vector)
            )
            await asyncio.shield(state_update)
        
        # Ensure the state update cannot be cancelled partway through
        except asyncio.CancelledError:
            await state_update
            raise
            
    @_pull.fixture
    async def _pull(self):
        ''' Do a conditional init if we haven't had stuff fully defined.
        '''
        self._conditional_init(
            ghid = Ghid.from_bytes(
                b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
            ),
            author = Ghid.from_bytes(
                b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
            ),
            dynamic = bool(random.randint(0, 1))
        )
        
    async def _update_state(self, target_vector):
        ''' Does a failure-resistant, semi-atomic setate update.
        '''
        for target_ghid in target_vector:
            try:
                packed_state = await self._recover_container(target_ghid)
                
                # Finally, with all of the administrative stuff handled, unpack
                # the actual payload.
                await self.unpack_gao(packed_state)
                
            except asyncio.CancelledError:
                raise
                
            except Exception:
                logger.error('Exception updating GAO state. Attempting to ' +
                             'failback to stale frame w/ traceback:\n' +
                             ''.join(traceback.format_exc()))
            
            # On the first success, stop looking for a failback container.
            else:
                break
                
        # Hitting this means we failed to find a valid state. We need to raise.
        else:
            raise UnrecoverableState('No valid state found for ' +
                                     str(self.ghid))
        
    def _recover_target_secret(self, binding):
        ''' For the passed binding, ratchet the secret chain (if
        possible), staging all intermediate secrets in the process. If
        the ratchet is broken and the secret also missing, raise.
        '''
        # Also don't forget to update history. Do this first, because it
        # will create a canonical update, regardless of success in
        # unpacking.
        try:
            # Recover the new secret for the chain.
            logger.debug('Healing ratchet for ' + str(self.ghid) + ' frame ' +
                         str(binding.counter) + '...')
            self._privateer.heal_chain(
                proxy = self.ghid,
                target_vector = self._maximize_target_vector(binding),
                master_secret = self._master_secret
            )
            
        except RatchetError as exc:
            if binding.target not in self._privateer:
                raise UnknownSecret(str(binding.target)) from exc
        
        # Now, we need to be sure our local history doesn't forget about
        # our current-most-recent frame when making an update.
        finally:
            # Finally, update our target_history accordingly.
            self.target_history = collections.deque(
                binding.target_vector,
                # Make sure we have at least one historical target
                maxlen = max(2, len(binding.target_vector))
            )
            
    async def _recover_container(self, container_ghid):
        ''' From the container ghid, recover the packed state.
        '''
        secret = self._privateer.get(container_ghid)
        
        packed = await self._librarian.retrieve(container_ghid)
        
        try:
            # TODO: fix the leaky abstraction of jumping into the _identity
            unpacked = self._golcore._identity.unpack_container(packed)
            packed_state = await self._golcore.open_container(
                unpacked,
                secret
            )
        
        except SecurityError:
            self._privateer.abandon(container_ghid)
            raise
            
        else:
            self._privateer.commit(container_ghid, localize=self._local_secret)
        
        return packed_state
        
    def _maximize_target_vector(self, new_obj):
        ''' Construct the longest possible target vector, given the
        new_obj and our knowledge of the old target history.
        '''
        # We need to accommodate a possible counter is None
        old_counter = self._counter
        if old_counter is None:
            old_counter = 0
        
        offset = new_obj.counter - old_counter
        
        new_len = len(new_obj.target_vector)
        old_len = len(self.target_history)
        maximized = list(new_obj.target_vector)
        
        # If the new target vector is smaller than the offset, then the
        # old target vector is "buried" history (there are missing frames
        # between the newest old target and the oldest new target) and
        # therefore useless. Short-circuit so we don't accidentally try to
        # do something with a negative index and then IndexError.
        if new_len - offset > 0:
            for ii in range(new_len - offset, old_len):
                maximized.append(self.target_history[ii])
            
        return maximized
        
    def _advance_history(self, new_obj):
        ''' Updates our history to match the new one.
        
        DEPRECATED AND UNUSED.
        
        NOTE THAT THIS DEPENDS ON THE PERSISTER to enforce the history
        hash chain. This is only aligning the histories, and NOT looking
        at the current frames themselves.
        '''
        old_history = self.frame_history
        new_history = new_obj.history
        
        # We need to check to make sure this isn't pulling in the zeroth frame.
        if new_history:
            new_legroom = len(new_history)
            
            # Find how far offset the new_history is from the old_history.
            for offset in range(new_legroom):
                # Align the histories by checking the ii'th new element against
                # an offset ii'th old element.
                for ii in range(new_legroom - offset):
                    # The new element matches the offset old element, so don't
                    # change the offset.
                    if new_history[offset + ii] == old_history[ii]:
                        continue
                    
                    # It didn't match, so we need to up the offset by one.
                    # Short circuit the inner loop via break.
                    else:
                        break
                    
                # We found a match. Short-circuit parent loop.
                else:
                    break
        
            # We never found a match. Offset should be made into the length of
            # the entire thing. This depends on persistence system to enforce
            # the hash chain!
            else:
                offset += 1
        
        # This is pulling the zeroth frame. We have no history.
        else:
            # Make sure new_legroom is always at least 1. Otherwise, the zeroth
            # frame (which by definition has no history) will forever damn the
            # object from being pulled (until it gets updated at its origin
            # first).
            new_legroom = 1
            offset = 0
            
        # Calculate the offset as the difference between the two counters,
        # easy-peasy style.
        offset = new_obj.counter - self._counter
                    
        
        # Now, appendleft the new frame (and an empty target) until we get to
        # the most recent one -- but hold off on adding the new target so we
        # can preserve the maximum history length
        for ii in range(offset - 1, 0, -1):
            self.frame_history.appendleft(new_history[ii])
            self.target_history.appendleft(None)
            
    def _conditional_init(self, ghid, dynamic, author):
        ''' If dynamic had not been set, set both it and the author. If
        ghid has not been set, set it.
        '''
        if self._dynamic is None:
            self._dynamic = dynamic
            
        if self._author is None:
            self._author = author
            
        if self.ghid is None:
            self.ghid = ghid
        
    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.
        '''
        pass
        
    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.
        '''
        pass
        
    def __del__(self, *args, **kwargs):
        ''' Log the removal of the GAO.
        '''
        logger.info(
            'GAO ' + str(self.ghid) + ' passed out of memory and is being ' +
            'locally garbage collected by Python.'
        ) class AppToken(bytes):
    ''' Extend bytes to be a fixed-length thing.
    '''
    # The size, in bytes, of an app token
    TOKEN_LEN = 16
    
    def __init__(self, *args, **kwargs):
        ''' For simplicity, enforce length AFTER performing init.
        '''
        if len(self) != self.TOKEN_LEN:
            raise ValueError('Improper token length.')
    
    @classmethod
    def null(cls):
        ''' Return the null token (used for passing "no token" in IPC,
        and not a valid token for app usage.)
        '''
        return cls(cls.TOKEN_LEN)
        
    @classmethod
    def pseudorandom(cls):
        ''' Return a PSEUDOrandom (**NOT** secure) app token, for use in
        testing.
        '''
        return cls([random.randint(0, 255) for __ in range(cls.TOKEN_LEN)])
        
    def __repr__(self):
        ''' Add in the class name to repr.
        '''
        return type(self).__name__ + '(' + super().__repr__() + ')'
        
    def __str__(self, *args, **kwargs):
        ''' Add in the class name to repr.
        '''
        return type(self).__name__ + '(' + \
            super().__str__(*args, **kwargs) + ')' class ApiID(Ghid):
    ''' Subclass Ghid in a way that makes the API ID seem like a normal
    64-byte string.
    
    Remind me again why the hell I'm subclassing ghid for this when I'm
    ending up removing just about everything that makes it a ghid?
    '''
    
    def __init__(self, address, algo=None):
        ''' Wrap the normal Ghid creation with a forced algo.
        '''
        if len(address) != 64:
            raise ValueError('Improper API ID length.')
        
        elif algo is None:
            algo = 0
            
        elif algo != 0:
            raise ValueError('Improper API ID format.')
        
        super().__init__(algo, address)
        
    def __repr__(self):
        ''' Hide that this is a ghid.
        '''
        return type(self).__name__ + '(' + repr(self.address) + ')'
        
    @property
    def algo(self):
        return self._algo
        
    @algo.setter
    def algo(self, value):
        # Currently, ensure algo is b'\x00'
        if value == 0:
            self._algo = value
        else:
            raise ValueError('Invalid address algorithm.')
            
    @property
    def address(self):
        return self._address
            
    @address.setter
    def address(self, address):
        self._address = address
        
    @classmethod
    def pseudorandom(cls):
        return super().pseudorandom(0) 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) class NoContext:
    ''' An empty context manager.
    '''
    
    def __enter__(*args, **kwargs):
        pass
        
    def __exit__(*args, **kwargs):
        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 immutable_property(name):
    ''' Create a property that cannot be changed (nor deleted) once
    it has been defined.
    '''
    @property
    def prop(self, name=name):
        return getattr(self, name)
        
    @prop.setter
    def prop(self, value, name=name):
        try:
            existing = getattr(self, name)
        
        except AttributeError:
            existing = None
            
        if existing is not None:
            raise AttributeError('Cannot alter immutable property.')
        
        else:
            return setattr(self, name, value)
        
    return prop class DispatchError(HypergolixException, RuntimeError):
    ''' Raised when something fails with dispatch.
    '''
    pass class UnknownToken(DispatchError, ValueError):
    ''' Raised when something fails with dispatch.
    '''
    pass class ExistantAppError(DispatchError):
    ''' Raised when attempting to register a second application for the
    same connection, or the same token for a different connection.
    ''' class CatOuttaBagError(DispatchError, ValueError):
    ''' Raised when trying to make a public object private.
    '''
    pass
import logging
import collections
import weakref
import os
import abc
import traceback
import asyncio
import loopa
from loopa.utils import make_background_future
from .hypothetical import API
from .hypothetical import public_api
from .hypothetical import fixture_api
from .hypothetical import fixture_noop
from .gao import GAOCore
from .utils import AppToken
from .utils import ApiID
from .utils import WeakSetMap
from .utils import NoContext
from .utils import weak_property
from .utils import immutable_property
from .exceptions import DispatchError
from .exceptions import UnknownToken
from .exceptions import ExistantAppError
from .exceptions import CatOuttaBagError
'''
LICENSING
-------------------------------------------------

hypergolix: A python Golix client.
    Copyright (C) 2016 Muterra, Inc.
    
    Contributors
    ------------
    Nick Badger
        badg@muterra.io | badg@nickbadger.com | nickbadger.com

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the
    Free Software Foundation, Inc.,
    51 Franklin Street,
    Fifth Floor,
    Boston, MA  02110-1301 USA

------------------------------------------------------

Some notes:

'''

# External dependencies


# Intra-package dependencies





# ###############################################
# Boilerplate
# ###############################################


logger = logging.getLogger(__name__)


# Control * imports. Therefore controls what is available to toplevel
# package through __init__.py
__all__ = [
    'Dispatcher',
]

        
# ###############################################
# Lib
# ###############################################
            
            
# Identity here can be either a sender or recipient dependent upon context
_ShareLog = collections.namedtuple(
    typename = '_ShareLog',
    field_names = ('ghid', 'identity'),
)


class Dispatcher(metaclass=API):
    ''' The Dispatcher decides which objects should be delivered where.
    This is decided through either:
    
    1. An API identifier (schema) dictating general compatibility as a
        dispatchable
    2. Declaring an object to be private, at which point the Dispatcher
        will internally (though distributedly) maintain that object to
        be exclusively available to an app token.
        
    Objects, once declared as non-private, cannot be retroactively
    privatized. That cat has officially left the bag/building/rodeo.
    However, a private object can be made non-private at a later time,
    provided it has defined an API ID.
    
    Private objects do not track their owning app tokens internally.
    Instead, this is managed through a Dispatcher-internal GAO. As such,
    even a bug resulting in a leaked private object will not result in a
    leaked app token.
    
    Ideally, the dispatcher will eventually also enforce whatever user
    restrictions on sharing are desired BETWEEN INSTALLED APPLICATIONS.
    Sharing restrictions between external parties are within the purview
    of the Rolodex.
    
    TODO: support notification mechanism to push new objects to other
    concurrent hypergolix instances. See note in ipc.ipccore.send_object
    
    TODO: move most of the misc stuff into dedicated new_object and
    get_object methods within dispatch, which (for the most part) just
    pass on straight to the oracle, but add all of the extra tracking
    stuff (and so on) internally.
    '''
    _account = weak_property('__account')
    _ipc_protocol = weak_property('__ipc_protocol')

    _oracle = weak_property('__oracle')