<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Channel Managers
When constructing a large graph of channels with interdependencies, it can be difficult to keep track of whether you have done a proper number of tee() calls.  Because the danger of forgetting to tee can be so grave, one can tend to over-tee, leading to memory leaks.
Also, there are patterns (like the Possible/Extant/All pattern) that are useful when setting up graphs, but, in the clutter of linear code creating channel after channel, it can become unclear when such patterns are in use or whether they are done properly.
Finally, in the spirit of the rest of flowz, it would be nice if channel construction were done lazily, strictly on an as-needed basis.
The ChannelManager aims to resolve all of those issues.  Its use is, as usual, best demonstrated before describing.
We'll start with some code borrowed from the Possible/Extant/All pattern example in chapter 4
Step1:  Now the ChannelManager code
Step2:  Note that this class is not actually a subclass of ChannelManager, but it makes use of it in two ways
Step3:  Nice.  It worked.  Here are the steps that happened
Step4:  Choosing named targets
flowz is very likely to be principally used in scripts run as periodic (e.g., nightly) processes to synthesize and analyze data coming in from external sources.  In such scripts, in can be handy to assign well-known names to some of the stages and choose the target via a script parameter.  Here is a possible pattern for doing that.
Step5:  With that in place, the main() processing of a script could capture the command-line arguments and end up calling a target like
Step6:  Or...
<ASSISTANT_TASK:>
Python Code:
def expensive_deriver(num):
    # 10 minutes pass...
    return num * 100
# Our fake durable storage holding the first 8 derived elements
storage = {num: expensive_deriver(num) for num in range(8)}
# The ExtantArtifact accessing that data
class ExampleExtantArtifact(ExtantArtifact):
    def __init__(self, num):
        super(ExampleExtantArtifact, self).__init__(self.get_me, name='ExampleExtantArtifact')
        self.num = num
    @gen.coroutine
    def get_me(self):
        raise gen.Return(storage[self.num])
Explanation: Channel Managers
When constructing a large graph of channels with interdependencies, it can be difficult to keep track of whether you have done a proper number of tee() calls.  Because the danger of forgetting to tee can be so grave, one can tend to over-tee, leading to memory leaks.
Also, there are patterns (like the Possible/Extant/All pattern) that are useful when setting up graphs, but, in the clutter of linear code creating channel after channel, it can become unclear when such patterns are in use or whether they are done properly.
Finally, in the spirit of the rest of flowz, it would be nice if channel construction were done lazily, strictly on an as-needed basis.
The ChannelManager aims to resolve all of those issues.  Its use is, as usual, best demonstrated before describing.
We'll start with some code borrowed from the Possible/Extant/All pattern example in chapter 4:
End of explanation
class GuideChannelManager(object):
    def __init__(self):
        # sets the 'channel_manager' property, which must exist on the class
        self.channel_manager = mgmt.ChannelManager()
    @mgmt.channelproperty
    def extant(self):
        print('Creating extant channel')
        return IterChannel(KeyedArtifact(i, ExampleExtantArtifact(i)) for i in sorted(storage.keys()))
    @mgmt.channelproperty
    def possible(self):
        print('Creating possible channel')
        return IterChannel(KeyedArtifact(i, DerivedArtifact(expensive_deriver, i)) for i in range(10))
    @mgmt.channelproperty
    def all(self):
        print('Creating all channel')
        return merge_keyed_channels(self.possible, self.extant)
Explanation: Now the ChannelManager code:
End of explanation
print_chans(GuideChannelManager().all, mode='get')
Explanation: Note that this class is not actually a subclass of ChannelManager, but it makes use of it in two ways:
1. It defines a channel_manager property on itself that is a private instance of a ChannelManager
2. It makes references to the @mgmt.channelproperty decorator above three methods.
Now using this class is straightforward...
End of explanation
# recreate the storage to not mess up other parts of the notebook when run out of order
storage = {num: expensive_deriver(num) for num in range(8)}
Explanation: Nice.  It worked.  Here are the steps that happened:
1. The GuideChannelManager object was instantiated, which did little other than instantiate a ChannelManager.
2. The all property was asked of the GuideChannelManager.
3. That called the all() method, which referenced the possible and extant properties.
4. Those references, in turn, called the possible() and extant() methods.
5. Each of those methods created an IterChannel and returned it.
6. The decorators on those methods captured the channels and stored them internally. Subsequent accesses of these properties would no longer call the methods, but, rather, return a tee() of their corresponding channels.
7. Control returned to the all() method where the two channels were used as input in the creation of a new channel, which is returned.
8. Again, the decorator on all() stored this channel and would tee() it on any subsequent requests for the all property.
9. The channel returned from the all property was passed to print_chans() and drained.
Very notably, no tees were performed on any of these channels.  In this configuration, they were all needed only once, so that's what this pattern did.  If hand-coded, the final channel would likely have worked with tees of the first two channels in an abundance of caution, possibly causing leaks of the original channels.
End of explanation
targets = dict()
def configure_channels():
    mgr = GuideChannelManager()
    targets['possible'] = lambda: mgr.possible
    targets['extant'] = lambda: mgr.extant
    targets['all'] = lambda: mgr.all
    targets['default'] = targets['all']
configure_channels()
def run_target(name='default'):
    print_chans(targets[name](), mode='get')
Explanation: Choosing named targets
flowz is very likely to be principally used in scripts run as periodic (e.g., nightly) processes to synthesize and analyze data coming in from external sources.  In such scripts, in can be handy to assign well-known names to some of the stages and choose the target via a script parameter.  Here is a possible pattern for doing that.
End of explanation
run_target('extant')
Explanation: With that in place, the main() processing of a script could capture the command-line arguments and end up calling a target like:
End of explanation
# Calling again to act like a fresh running of the script
configure_channels()
run_target()
# recreate the storage to not mess up other parts of the notebook when run out of order
storage = {num: expensive_deriver(num) for num in range(8)}
Explanation: Or...
End of explanation
<END_TASK>