pvl/irk.py
changeset 224 ed410776effd
parent 223 6842794c20e8
child 225 3c2d0dd42045
equal deleted inserted replaced
223:6842794c20e8 224:ed410776effd
     1 """
       
     2     Irker client.
       
     3 """
       
     4 
       
     5 import pvl.syslog.file # for stdin
       
     6 import pvl.socket # for tcp
       
     7 
       
     8 import optparse, sys
       
     9 
       
    10 import logging; log = logging.getLogger('pvl.irk')
       
    11 
       
    12 import json
       
    13 
       
    14 def parser (parser, connect='tcp://localhost/', target=None) :
       
    15     """
       
    16         Optparse option group.
       
    17     """
       
    18 
       
    19     irker = optparse.OptionGroup(parser, 'Irker output')
       
    20     
       
    21     irker.add_option('--irker', metavar='URL',  default=connect,
       
    22             help="Irker daemon URL")
       
    23 
       
    24     irker.add_option('--irker-notice',          action='store_true',
       
    25             help="Use irker NOTICE")
       
    26 
       
    27     irker.add_option('--irker-part',            action='store_true',
       
    28             help="Use irker PART")
       
    29 
       
    30     return irker
       
    31 
       
    32 def apply (options) :
       
    33     """
       
    34         Return Irker (XXX: target) from options.
       
    35     """
       
    36     
       
    37     # None -> stdout
       
    38     return Irker(options.irker, options) # options.irker_*
       
    39 
       
    40 class IrkError (Exception) :
       
    41     """
       
    42         Irk write error.
       
    43     """
       
    44 
       
    45 class Irk (object) :
       
    46     """
       
    47         Irker JSON connection speaks JSON over a stream.
       
    48 
       
    49         TODO: timeouts?
       
    50     """
       
    51 
       
    52     PORT = 6659
       
    53 
       
    54     @classmethod
       
    55     def connect (cls, url) :
       
    56         """
       
    57             Connect to given URL string, or None -> stdout
       
    58         """
       
    59 
       
    60         if not url :
       
    61             # no read
       
    62             return cls(pvl.syslog.file.File(sys.stdout), recv=False)
       
    63 
       
    64         else :
       
    65             sock = pvl.socket.connect(url, port=cls.PORT)
       
    66 
       
    67             # just to make things a bit more exciting... and we really don't want to be blocking on our output..
       
    68             sock.setblocking(False)
       
    69             
       
    70             return cls(
       
    71                     pvl.socket.WriteStream(sock, buffer=None),
       
    72                     pvl.socket.ReadStream(sock)
       
    73             )
       
    74 
       
    75     def __init__ (self, send, recv=None) :
       
    76         """
       
    77             Use given file-like object (write, flush, fileno) for output.
       
    78         """
       
    79 
       
    80         self.send = send
       
    81         self.recv = recv
       
    82         
       
    83         log.debug("%s <-> %s", send, recv)
       
    84 
       
    85     def fileno (self) :
       
    86         """
       
    87             Return fd. Useful for detecting error conditions (connection lost).
       
    88 
       
    89             Only valid if self.recv is True.
       
    90         """
       
    91 
       
    92         return self.recv.fileno()
       
    93 
       
    94     def __call__ (self, **opts) :
       
    95         """
       
    96             Send given json.
       
    97 
       
    98             Raises IrkError on write EOF.
       
    99 
       
   100             XXX: Raises socket.error/IOError on write errors?
       
   101         """
       
   102 
       
   103         log.debug("%s", opts)
       
   104         
       
   105         try :
       
   106             # write line + flush
       
   107             self.send(json.dumps(opts))
       
   108 
       
   109         except EOFError as ex :
       
   110             # XXX: also socket.error etc?
       
   111             raise IrkError("%s: send eof: %s" % (self, ex))
       
   112         
       
   113         # XXX: self.send.flush()
       
   114 
       
   115     def __iter__ (self) :
       
   116         """
       
   117             Yield JSON inputs from source.
       
   118         """
       
   119 
       
   120         if not self.recv :
       
   121             # never going to be anything
       
   122             return
       
   123 
       
   124         for line in self.recv :
       
   125             # XXX: error handling?
       
   126             yield json.loads(line)
       
   127 
       
   128 class IrkerTarget (object) :
       
   129     """
       
   130         A channel on an Irk connection.
       
   131             
       
   132         Raises IrkError if irk(..) fails.
       
   133     """
       
   134 
       
   135     def __init__ (self, irker, target, notice=None, part=None) :
       
   136         self.irker = irker
       
   137         self.target = target
       
   138 
       
   139         self._notice = notice
       
   140         self._part = part
       
   141         
       
   142     def join (self) :
       
   143         log.info("%s", self)
       
   144         self.irker(to=str(self), privmsg='')
       
   145 
       
   146     def privmsg (self, *args) :
       
   147         for arg in args :
       
   148             log.info("%s: %s", self, arg)
       
   149             self.irker(to=str(self), privmsg=arg)
       
   150 
       
   151     def notice (self, *args) :
       
   152         for arg in args :
       
   153             log.info("%s: %s", self, arg)
       
   154             self.irker(to=str(self), notice=arg)
       
   155 
       
   156     def part (self, msg='') :
       
   157         log.info("%s: %s", self, msg)
       
   158 
       
   159         if self._part :
       
   160             self.irker(to=str(self), part=msg)
       
   161         else :
       
   162             log.warn("%s: no --irker-part", self)
       
   163 
       
   164     def __call__ (self, *args) :
       
   165         # default msg policy
       
   166         if self._notice :
       
   167             return self.notice(*args)
       
   168         else :
       
   169             return self.privmsg(*args)
       
   170 
       
   171     def __str__ (self) :
       
   172         return self.target
       
   173 
       
   174 class Irker (object) :
       
   175     """
       
   176         Reconnecting Irk.
       
   177     """
       
   178 
       
   179     def __init__ (self, url=None, options=None) :
       
   180         """
       
   181             url         - irker to connect to
       
   182             options     - irker_* configs
       
   183         """
       
   184 
       
   185         self.url = url
       
   186         self.targets = {}
       
   187         self.options = options
       
   188         
       
   189         self.connect()
       
   190     
       
   191     def connect (self) :
       
   192         """
       
   193             Connect, and fix up our targets.
       
   194         """
       
   195 
       
   196         self.irk = Irk.connect(self.url)
       
   197 
       
   198         # rejoin
       
   199         for target in self.targets.itervalues() :
       
   200             target.join()
       
   201     
       
   202     def __call__ (self, **opts) :
       
   203         """
       
   204             Send on current irker connection.
       
   205 
       
   206             Raises IrkError if irk(..) fails.
       
   207 
       
   208             TODO: handle errors and reconnect?
       
   209             """
       
   210 
       
   211         self.irk(**opts)
       
   212 
       
   213     def target (self, target, join=True) :
       
   214         """
       
   215             Bind to given target URL, returning an IrkerTarget for sending messages.
       
   216         """
       
   217 
       
   218         if target not in self.targets :
       
   219             self.targets[target] = IrkerTarget(self, target, 
       
   220                     notice  = self.options and self.options.irker_notice,
       
   221                     part    = self.options and self.options.irker_part,
       
   222             )
       
   223 
       
   224             if join :
       
   225                 self.targets[target].join()
       
   226             
       
   227         return self.targets[target]
       
   228     
       
   229     __getitem__ = target
       
   230 
       
   231     def __delitem__ (self, target) :
       
   232         """
       
   233             Unbind given target URL.
       
   234         """
       
   235 
       
   236         target = self.targets.pop(target)
       
   237         target.part()
       
   238 
       
   239     def __iter__ (self) :
       
   240         return iter(self.targets)