4 srfsh.py - provides a basic shell for issuing OpenSRF requests
10 - runs <count> opensrf.math requests and prints the average time
12 request <service> <method> [<param1>, <param2>, ...]
13 - performs an opensrf request
14 - parameters are JSON strings
17 - Queries the router. Query options: services service-stats service-nodes
19 introspect <service> [<api_name_prefix>]
20 - List API calls for a service.
21 - api_name_prefix is a bare string or JSON string.
24 - sets an environment variable
27 - Returns the value for the environment variable
29 Environment variables:
30 SRFSH_OUTPUT_NET_OBJ_KEYS = true - If a network object is array-encoded and key registry exists for the object type, annotate the object with field names
33 SRFSH_OUTPUT_FORMAT_JSON = true - Use JSON pretty printer
34 = false - Print raw JSON
36 SRFSH_OUTPUT_PAGED = true - Paged output. Uses "less -EX"
37 = false - Output is not paged
39 SRFSH_LOCALE = <locale> - request responses to be returned in locale <locale> if available
42 import os, sys, time, readline, atexit, re, pydoc, traceback
43 import osrf.json, osrf.system, osrf.ses, osrf.conf, osrf.log, osrf.net
48 def __init__(self, script_file=None):
51 self.output_buffer = ''
53 # true if invoked with a script file
54 self.reading_script = False
56 # multi-request sessions
57 self.active_session = None
59 # default opensrf request timeout
62 # map of command name to handler
66 self.open_script(script_file)
67 self.reading_script = True
69 # map of router sub-commands to router API calls
70 self.router_command_map = {
71 'services' : 'opensrf.router.info.class.list',
72 'service-stats' : 'opensrf.router.info.stats.class.node.all',
73 'service-nodes' : 'opensrf.router.info.stats.class.all'
76 # seed the tab completion word bank
77 self.tab_complete_words = self.router_command_map.keys() + [
86 # add the default commands
87 for command in ['request', 'router', 'help', 'set',
88 'get', 'math_bench', 'introspect', 'connect', 'disconnect' ]:
90 self.add_command(command = command, handler = getattr(Srfsh, 'handle_' + command))
92 # for compat w/ srfsh.c
93 self.add_command(command = 'open', handler = Srfsh.handle_connect)
94 self.add_command(command = 'close', handler = Srfsh.handle_disconnect)
96 def open_script(self, script_file):
97 ''' Opens the script file and redirects the contents to STDIN for reading. '''
100 script = open(script_file, 'r')
101 os.dup2(script.fileno(), sys.stdin.fileno())
104 self.report_error("Error opening script file '%s': %s" % (script_file, str(e)))
109 ''' Main listen loop. '''
114 self.setup_readline()
119 self.report("", True)
120 line = raw_input("srfsh# ")
125 if re.search('^\s*#', line): # ignore lines starting with #
128 if str.lower(line) == 'exit' or str.lower(line) == 'quit':
131 parts = str.split(line)
132 command = parts.pop(0)
134 if command not in self.command_map:
135 self.report("unknown command: '%s'\n" % command)
138 self.command_map[command](self, parts)
140 except EOFError: # ctrl-d
143 except KeyboardInterrupt: # ctrl-c
147 self.report("%s\n" % traceback.format_exc())
151 def handle_connect(self, parts):
152 ''' Opens a connected session to an opensrf service '''
155 self.report("usage: connect <service>")
158 service = parts.pop(0)
160 if self.active_session:
161 if self.active_session['service'] == service:
162 return # use the existing active session
164 # currently, we only support one active session at a time
165 self.handle_disconnect([self.active_session['service']])
167 self.active_session = {
168 'ses' : osrf.ses.ClientSession(service, locale = self.__get_locale()),
172 self.active_session['ses'].connect()
174 def handle_disconnect(self, parts):
175 ''' Disconnects the currently active session. '''
178 self.report("usage: disconnect <service>")
181 service = parts.pop(0)
183 if self.active_session:
184 if self.active_session['service'] == service:
185 self.active_session['ses'].disconnect()
186 self.active_session['ses'].cleanup()
187 self.active_session = None
189 self.report_error("There is no open connection for service '%s'" % service)
191 def handle_introspect(self, parts):
192 ''' Introspect an opensrf service. '''
195 self.report("usage: introspect <service> [api_prefix]\n")
198 service = parts.pop(0)
199 args = [service, 'opensrf.system.method']
203 if api_pfx[0] != '"': # json-encode if necessary
204 api_pfx = '"%s"' % api_pfx
209 return self.handle_request(args)
212 def handle_router(self, parts):
213 ''' Send requests to the router. '''
216 self.report("usage: router <query>\n")
221 if query not in self.router_command_map:
222 self.report("router query options: %s\n" % ','.join(self.router_command_map.keys()))
225 return self.handle_request(['router', self.router_command_map[query]])
227 def handle_set(self, parts):
228 ''' Set env variables to control srfsh behavior. '''
231 pattern = re.compile('(.*)=(.*)').match(cmd)
232 key = pattern.group(1)
233 val = pattern.group(2)
234 self.set_var(key, val)
235 self.report("%s = %s\n" % (key, val))
237 def handle_get(self, parts):
238 ''' Returns environment variable value '''
240 self.report("%s=%s\n" % (parts[0], self.get_var(parts[0])))
245 def handle_help(self, foo):
246 ''' Prints help info '''
249 def handle_request(self, parts):
250 ''' Performs an OpenSRF request and reports the results. '''
253 self.report("usage: request <service> <api_name> [<param1>, <param2>, ...]\n")
258 service = parts.pop(0)
259 method = parts.pop(0)
260 locale = self.__get_locale()
261 jstr = '[%s]' % "".join(parts)
265 params = osrf.json.to_object(jstr)
267 self.report("Error parsing JSON: %s\n" % jstr)
271 if self.active_session and self.active_session['service'] == service:
272 # if we have an open connection to the same service, use it
273 ses = self.active_session['ses']
276 ses = osrf.ses.ClientSession(service, locale=locale)
280 req = ses.request2(method, tuple(params))
288 resp = req.recv(timeout=self.timeout)
289 except osrf.net.XMPPNoRecipient:
290 self.report("Unable to communicate with %s\n" % service)
292 except osrf.ex.OSRFServiceException, e:
293 self.report("Server exception occurred: %s" % e)
296 total = time.time() - start
300 content = resp.content()
302 if content is not None:
303 last_content = content
304 if self.get_var('SRFSH_OUTPUT_NET_OBJ_KEYS') == 'true':
305 self.report("Received Data: %s\n" % osrf.json.debug_net_object(content))
307 if self.get_var('SRFSH_OUTPUT_FORMAT_JSON') == 'true':
308 self.report("Received Data: %s\n" % osrf.json.pprint(osrf.json.to_json(content)))
310 self.report("Received Data: %s\n" % osrf.json.to_json(content))
316 self.report("\n" + '-'*60 + "\n")
317 self.report("Total request time: %f\n" % total)
318 self.report('-'*60 + "\n")
323 def handle_math_bench(self, parts):
324 ''' Sends a series of request to the opensrf.math service and collects timing stats. '''
326 count = int(parts.pop(0))
327 ses = osrf.ses.ClientSession('opensrf.math')
330 for cnt in range(100):
332 sys.stdout.write('.')
334 sys.stdout.write( str( cnt / 10 ) )
337 for cnt in range(count):
339 starttime = time.time()
340 req = ses.request('add', 1, 2)
341 resp = req.recv(timeout=2)
342 endtime = time.time()
344 if resp.content() == 3:
345 sys.stdout.write("+")
347 times.append( endtime - starttime )
349 print "What happened? %s" % str(resp.content())
352 if not ( (cnt + 1) % 100):
353 print ' [%d]' % (cnt + 1)
359 print "\naverage time %f" % (total / len(times))
364 def setup_readline(self):
365 ''' Initialize readline history and tab completion. '''
367 class SrfshCompleter(object):
369 def __init__(self, words):
373 def complete(self, prefix, index):
375 if prefix != self.prefix:
379 # find all words that start with this prefix
380 self.matching_words = [
381 w for w in self.words if w.startswith(prefix)
384 if len(self.matching_words) == 0:
387 if len(self.matching_words) == 1:
388 return self.matching_words[0]
390 # re-print the prompt w/ all of the possible word completions
391 sys.stdout.write('\n%s\nsrfsh# %s' %
392 (' '.join(self.matching_words), readline.get_line_buffer()))
396 completer = SrfshCompleter(tuple(self.tab_complete_words))
397 readline.parse_and_bind("tab: complete")
398 readline.set_completer(completer.complete)
400 histfile = os.path.join(self.get_var('HOME'), ".srfsh_history")
402 readline.read_history_file(histfile)
405 atexit.register(readline.write_history_file, histfile)
407 readline.set_completer_delims(readline.get_completer_delims().replace('-',''))
410 def do_connect(self):
411 ''' Connects this instance to the OpenSRF network. '''
413 osrf.ses.Session.ingress('srfsh')
414 file = os.path.join(self.get_var('HOME'), ".srfsh.xml")
415 osrf.system.System.connect(config_file=file, config_context='srfsh')
417 def add_command(self, **kwargs):
418 ''' Adds a new command to the supported srfsh commands.
420 Command is also added to the tab-completion word bank.
423 command : the command name
424 handler : reference to a two-argument function.
425 Arguments are Srfsh instance and command arguments.
428 command = kwargs['command']
429 self.command_map[command] = kwargs['handler']
430 self.tab_complete_words.append(command)
433 def load_plugins(self):
434 ''' Load plugin modules from the srfsh configuration file '''
437 plugins = osrf.conf.get('plugins.plugin')
441 if not isinstance(plugins, list):
444 for plugin in plugins:
445 module = plugin['module']
446 init = plugin.get('init', 'load')
447 self.report("Loading module %s..." % module, True, True)
450 mod = __import__(module, fromlist=' ')
451 getattr(mod, init)(self, plugin)
452 self.report("OK.\n", True, True)
455 self.report_error("Error importing plugin '%s' : %s\n" % (module, traceback.format_exc()))
458 ''' Disconnects from opensrf. '''
459 osrf.system.System.net_disconnect()
461 def report_error(self, msg):
462 ''' Log to stderr. '''
463 sys.stderr.write("%s\n" % msg)
466 def report(self, text, flush=False, no_page=False):
467 ''' Logs to the pager or stdout, depending on env vars and context '''
469 if self.reading_script or no_page or self.get_var('SRFSH_OUTPUT_PAGED') != 'true':
470 sys.stdout.write(text)
474 self.output_buffer += text
476 if flush and self.output_buffer != '':
477 pipe = os.popen('less -EX', 'w')
478 pipe.write(self.output_buffer)
480 self.output_buffer = ''
483 ''' Set defaults for environment variables. '''
485 if not self.get_var('SRFSH_OUTPUT_NET_OBJ_KEYS'):
486 self.set_var('SRFSH_OUTPUT_NET_OBJ_KEYS', 'false')
488 if not self.get_var('SRFSH_OUTPUT_FORMAT_JSON'):
489 self.set_var('SRFSH_OUTPUT_FORMAT_JSON', 'true')
491 if not self.get_var('SRFSH_OUTPUT_PAGED'):
492 self.set_var('SRFSH_OUTPUT_PAGED', 'true')
494 # XXX Do we need to differ between LANG and LC_MESSAGES?
495 if not self.get_var('SRFSH_LOCALE'):
496 self.set_var('SRFSH_LOCALE', self.get_var('LC_ALL'))
498 def set_var(self, key, val):
499 ''' Sets an environment variable's value. '''
500 os.environ[key] = val
502 def get_var(self, key):
503 ''' Returns an environment variable's value. '''
504 return os.environ.get(key, '')
506 def __get_locale(self):
508 Return the defined locale for this srfsh session.
510 A locale in OpenSRF is currently defined as a [a-z]{2}-[A-Z]{2} pattern.
511 This function munges the LC_ALL setting to conform to that pattern; for
512 example, trimming en_CA.UTF-8 to en-CA.
515 >>> shell = srfsh.Srfsh()
516 >>> shell.set_var('SRFSH_LOCALE', 'zz-ZZ')
517 >>> print shell.__get_locale()
519 >>> shell.set_var('SRFSH_LOCALE', 'en_CA.UTF-8')
520 >>> print shell.__get_locale()
524 env_locale = self.get_var('SRFSH_LOCALE')
526 pattern = re.compile(r'^\s*([a-z]+)[^a-zA-Z]([A-Z]+)').search(env_locale)
527 lang = pattern.group(1)
528 region = pattern.group(2)
529 locale = "%s-%s" % (lang, region)
535 if __name__ == '__main__':
536 script = sys.argv[1] if len(sys.argv) > 1 else None
537 Srfsh(script).main_loop()