Commits

beroe committed fd12565

First commit of database scripts

Comments (0)

Files changed (5)

crossref_parse.py

+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+""" Using a basic file format, return the CrossREF XML for a citation list
+    Runs curl in a subprocess so only best on OSX or maybe linux
+"""
+
+import sys
+import re
+import subprocess
+
+# Most of thse are hard to find. Not "working" examples
+
+RefList = """
+Brown, R. M., Ferner, J. W. & Diesmos, A. C. Herpetologica 53, 357–373 (1997).
+Webb, R. G. Herpetologica 34, 422–425 (1978).
+Inger, R. F. Fieldiana Zool. 33, 183–531 (1954).
+Savage, J. M. The Amphibians and Reptiles of Costa Rica (Univ. Chicago. Press, 2002).
+Merrill, E. D. Science 101, 401 (1945).
+Diesmos, A. C., Brown, R. M. & Gee, G. V. A. Sylvatrop 13, 63–80 (2003).
+Taylor, E. H., Leonard, A. B., Smith, H. M. & Pisani, G. R. Monogr. Mus. Nat. Hist. Univ. Kansas 4, 1–160 (1975).
+Taylor, E. H. The Caecilians of the World (Univ. Kansas Press, 1968).
+Brown, R. M. et al. Check List 8, 469–490 (2012).
+Brown, R. M., Siler, C. D., Diesmos, A. C. & Alcala, A. C. Herpetol. Monogr. 23, 1–44 (2009).
+"""
+URL_Template = 'curl -s "http://www.crossref.org/openurl/?title={0}&date={1}&volume={2}&spage={3}&pid=demo@practicalcomputing.org&redirect=false&format=unixref"'
+
+Dashes = r"\xe2\x80\x93" # weird unicode dash from web copy/paste...
+RemoveAuthors = r"^.*([A-Z]\. |al. )"
+GrabYear = r" \((\d+)\)\.?"
+Volume_Pages = u"(\d+), (\d+)-?\d*$"
+
+
+DEBUG = False
+
+for Line in RefList.split("\n"):
+	print >> sys.stderr, "#"*50
+	Line = re.sub(Dashes,"-",Line.rstrip())
+	if DEBUG: print >> sys.stderr, Line
+	Line = re.sub(RemoveAuthors,"",Line)
+	if DEBUG: print >> sys.stderr, Line
+	YearGr = re.search(GrabYear,Line)
+	if YearGr:
+		Year = YearGr.group(1)
+		if DEBUG: print >> sys.stderr, Year
+		Line = re.sub(GrabYear,"",Line)
+		if DEBUG: print >> sys.stderr, "LINE:",Line
+		VolumeGr = re.search(Volume_Pages,Line)
+		if VolumeGr:
+			Volume,StartPg = VolumeGr.group(1,2)
+			if DEBUG: print >> sys.stderr, Volume,StartPg
+			Journal = re.sub(Volume_Pages,"",Line).strip().replace(" ","%20")
+			if DEBUG: print >> sys.stderr, Journal
+			Query = URL_Template.format(Journal,Year,Volume,StartPg)
+			if DEBUG: print >> sys.stderr, Query
+			RefString = subprocess.check_output(Query, stderr=subprocess.STDOUT,shell=True) 
+			print Query
+			print RefString

flickr_search2.py

+#!/usr/bin/python
+"""
+(Based on a search script by Tamara Berg and James Hays)
+Generates an HTML table of images, link, location from flickr 
+on search terms and within a bounding box. Use -term to exclude
+certain terms.
+
+Very preliminary version w/o user interface...
+Uses flickrapi2.py which is also available here:
+    https://bitbucket.org/beroe/mbari-public/src/master/database/flickrapi2.py?at=master
+
+Usage: 
+1. Add your flickr API key information
+2. Edit the query string and mybb variables
+3. Run and capture output:
+
+  flickr_search2.py > myresults.html
+
+Version 0.9 - Steve Haddock [haddock at mbari dot org]
+
+"""
+
+import sys, time, socket
+from flickrapi2 import FlickrAPI
+from datetime import datetime
+
+socket.setdefaulttimeout(30)  #30 second time out on sockets before they throw
+#an exception.  I've been having trouble with urllib.urlopen hanging in the 
+#flickr API.  This will show up as exceptions.IOError.
+
+#the time out needs to be pretty long, it seems, because the flickr servers can be slow
+#to respond to our big searches.
+
+############################################################
+####  CHANGE THESE VALUES FOR SEARCH TERMS AND BOUNDING BOX
+############################################################
+#form the query string.
+
+query_string = "jellyfish -aquarium"
+
+# bounding box in lat/lon
+# west, south, east, north
+mybb = "73, -13, 126, 13" #indonesia
+# mybb = "60, 0, 100, 10" #
+
+
+###########################################################################
+# flickr auth information:
+# change these to your flickr api keys and secret
+# you can request one here:
+# http://www.flickr.com/services/apps/create/apply
+flickrAPIKey = "INSERTAPIKEYHERE"  # API key
+flickrSecret = "INSERTSECRETKEYHERE"                  # shared "secret"
+
+
+# make a new FlickrAPI instance
+fapi = FlickrAPI(flickrAPIKey, flickrSecret)
+
+try:
+	rsp = fapi.photos_search(api_key=flickrAPIKey,
+							ispublic="1",
+							media="photos",
+							per_page="250", 
+							page="1",
+							has_geo = "1", 
+							bbox=mybb,
+							extras = "tags, original_format, license, geo, date_taken, date_upload, o_dims, views",
+							text=query_string
+							#accuracy="6", #6 is region level.  most things seem 10 or better.
+							#min_upload_date=str(mintime),
+							#max_upload_date=str(maxtime))
+							##min_taken_date=str(datetime.fromtimestamp(mintime)),
+							##max_taken_date=str(datetime.fromtimestamp(maxtime))
+							)
+	#we want to catch these failures somehow and keep going.
+	time.sleep(1)
+	fapi.testFailure(rsp)
+	total_images = rsp.photos[0]['total']
+	# sys.stderr.write("Found {0} images\n".format(total_images)) 
+
+except KeyboardInterrupt:
+	sys.stderr.write('Keyboard exception while querying for images, exiting\n')
+	raise
+except:
+	print rsp
+	raise
+	#print type(inst)     # the exception instance
+	#print inst.args      # arguments stored in .args
+	#print inst           # __str__ allows args to printed directly
+	sys.stderr.write ('Exception encountered while querying for images\n')
+	
+i = getattr(rsp,'photos',None)
+current_image_num = 0
+
+### Print info in tabular format
+
+# do you want to print the table?
+printout = False
+
+
+if printout:
+	for b in rsp.photos[0].photo:
+		if b!=None:
+			outstring =  b['latitude'].encode("ascii","replace") 
+			outstring += '\t'+  b['longitude'].encode("ascii","replace") 
+			outstring += '\t'+  b['datetaken'].encode("ascii","replace")
+			outstring += '\t'+ b['title'].encode("ascii","replace")
+			MyURL = 'http://farm3.static.flickr.com/' + b['server'] + '/'+ b['id'] + "_" + b['secret'] + '.jpg'
+			outstring += '\t' + MyURL
+			print outstring
+			current_image_num = current_image_num + 1;
+		
+
+#### Print the html format
+
+print '''
+
+
+<html>
+<body>
+<code>
+<table border="1">
+  <tr>
+    <th>Lat</th>
+    <th>Lon</th>
+    <th>Date</th>
+    <th>Img</th>
+  </tr>
+'''
+current_image_num = 0;
+
+for b in rsp.photos[0].photo:
+	if b!=None:
+		MyURL = 'http://farm3.static.flickr.com/' + b['server'] + '/'+ b['id'] + "_" + b['secret'] + '.jpg'
+		outline = '\t<tr><td>%s</td><td>%s</td><td>%s</td><td><img src="%s" width = "300"></td></tr>' \
+  		%(b['latitude'],b['longitude'],b['datetaken'],MyURL)
+		print outline
+		current_image_num = current_image_num + 1;
+		
+print '''</table>
+</code>
+</body>
+</html>'''
+sys.stderr.write("Showing {0} out of {1} images...\n".format(str(current_image_num),total_images)) 
+
+
+
+#!/usr/bin/python
+#
+# Flickr API implementation
+#
+# Inspired largely by Michele Campeotto's flickrclient and Aaron Swartz'
+# xmltramp... but I wanted to get a better idea of how python worked in
+# those regards, so I mostly worked those components out for myself.
+#
+# http://micampe.it/things/flickrclient
+# http://www.aaronsw.com/2002/xmltramp/
+#
+# Release 1: initial release
+# Release 2: added upload functionality
+# Release 3: code cleanup, convert to doc strings
+# Release 4: better permission support
+# Release 5: converted into fuller-featured "flickrapi"
+# Release 6: fix upload sig bug (thanks Deepak Jois), encode test output
+# Release 7: fix path construction, Manish Rai Jain's improvements, exceptions
+# Release 8: change API endpoint to "api.flickr.com"
+#
+# Work by (or inspired by) Manish Rai Jain <manishrjain@gmail.com>:
+#
+#    improved error reporting, proper multipart MIME boundary creation,
+#    use of urllib2 to allow uploads through a proxy, upload accepts
+#    raw data as well as a filename
+#
+# Copyright 2005 Brian "Beej Jorgensen" Hall <beej@beej.us>
+#
+#    This work is licensed under the Creative Commons
+#    Attribution License.  To view a copy of this license,
+#    visit http://creativecommons.org/licenses/by/2.5/ or send
+#    a letter to Creative Commons, 543 Howard Street, 5th
+#    Floor, San Francisco, California, 94105, USA.
+#
+# This license says that I must be credited for any derivative works.
+# You do not need to credit me to simply use the FlickrAPI classes in
+# your Python scripts--you only need to credit me if you're taking this
+# FlickrAPI class and modifying it or redistributing it.
+#
+# Previous versions of this API were granted to the public domain.
+# You're free to use those as you please.
+#
+# Beej Jorgensen, Maintainer, November 2005
+# beej@beej.us
+#
+
+import sys
+# got rid of this in favor of hashlib - S.Haddock
+# import md5
+import hashlib
+import string
+import urllib
+import urllib2
+import mimetools
+import httplib
+import os.path
+import xml.dom.minidom
+
+########################################################################
+# Exceptions
+########################################################################
+
+class UploadException(Exception):
+	pass
+
+########################################################################
+# XML functionality
+########################################################################
+
+#-----------------------------------------------------------------------
+class XMLNode:
+	"""XMLNode -- generic class for holding an XML node
+
+	xmlStr = \"\"\"<xml foo="32">
+	<name bar="10">Name0</name>
+	<name bar="11" baz="12">Name1</name>
+	</xml>\"\"\"
+
+	f = XMLNode.parseXML(xmlStr)
+
+	print f.elementName              # xml
+	print f['foo']                   # 32
+	print f.name                     # [<name XMLNode>, <name XMLNode>]
+	print f.name[0].elementName      # name
+	print f.name[0]["bar"]           # 10
+	print f.name[0].elementText      # Name0
+	print f.name[1].elementName      # name
+	print f.name[1]["bar"]           # 11
+	print f.name[1]["baz"]           # 12
+
+	"""
+
+	def __init__(self):
+		"""Construct an empty XML node."""
+		self.elementName=""
+		self.elementText=""
+		self.attrib={}
+		self.xml=""
+
+	def __setitem__(self, key, item):
+		"""Store a node's attribute in the attrib hash."""
+		self.attrib[key] = item
+
+	def __getitem__(self, key):
+		"""Retrieve a node's attribute from the attrib hash."""
+                try:
+		  return self.attrib[key]
+                except:
+                  return "null"
+	#-----------------------------------------------------------------------
+	#@classmethod
+	def parseXML(cls, xmlStr, storeXML=False):
+		"""Convert an XML string into a nice instance tree of XMLNodes.
+
+		xmlStr -- the XML to parse
+		storeXML -- if True, stores the XML string in the root XMLNode.xml
+
+		"""
+
+		def __parseXMLElement(element, thisNode):
+			"""Recursive call to process this XMLNode."""
+			thisNode.elementName = element.nodeName
+
+			#print element.nodeName
+
+			# add element attributes as attributes to this node
+			for i in range(element.attributes.length):
+				an = element.attributes.item(i)
+				thisNode[an.name] = an.nodeValue
+
+			for a in element.childNodes:
+				if a.nodeType == xml.dom.Node.ELEMENT_NODE:
+
+					child = XMLNode()
+					try:
+						list = getattr(thisNode, a.nodeName)
+					except AttributeError:
+						setattr(thisNode, a.nodeName, [])
+
+					# add the child node as an attrib to this node
+					list = getattr(thisNode, a.nodeName);
+					#print "appending child: %s to %s" % (a.nodeName, thisNode.elementName)
+					list.append(child);
+
+					__parseXMLElement(a, child)
+
+				elif a.nodeType == xml.dom.Node.TEXT_NODE:
+					thisNode.elementText += a.nodeValue
+			
+			return thisNode
+
+		dom = xml.dom.minidom.parseString(xmlStr)
+
+		# get the root
+		rootNode = XMLNode()
+		if storeXML: rootNode.xml = xmlStr
+
+		return __parseXMLElement(dom.firstChild, rootNode)
+	parseXML = classmethod(parseXML)
+
+########################################################################
+# Flickr functionality
+########################################################################
+
+#-----------------------------------------------------------------------
+class FlickrAPI:
+	"""Encapsulated flickr functionality.
+
+	Example usage:
+
+	  flickr = FlickrAPI(flickrAPIKey, flickrSecret)
+	  rsp = flickr.auth_checkToken(api_key=flickrAPIKey, auth_token=token)
+
+	"""
+	flickrHost = "api.flickr.com"
+	flickrRESTForm = "/services/rest/"
+	flickrAuthForm = "/services/auth/"
+	flickrUploadForm = "/services/upload/"
+
+	#-------------------------------------------------------------------
+	def __init__(self, apiKey, secret):
+		"""Construct a new FlickrAPI instance for a given API key and secret."""
+		self.apiKey = apiKey
+		self.secret = secret
+
+		self.__handlerCache={}
+
+	#-------------------------------------------------------------------
+	def __sign(self, data):
+		"""Calculate the flickr signature for a set of params.
+
+		data -- a hash of all the params and values to be hashed, e.g.
+		        {"api_key":"AAAA", "auth_token":"TTTT"}
+
+		"""
+		dataName = self.secret
+		keys = data.keys()
+		keys.sort()
+		for a in keys: dataName += (a + data[a])
+		#print dataName
+		# changed this to avoid md5 lib
+		hash = hashlib.md5()
+		hash.update(dataName)
+		return hash.hexdigest()
+
+	#-------------------------------------------------------------------
+	def __getattr__(self, method, **arg):
+		"""Handle all the flickr API calls.
+		
+		This is Michele Campeotto's cleverness, wherein he writes a
+		general handler for methods not defined, and assumes they are
+		flickr methods.  He then converts them to a form to be passed as
+		the method= parameter, and goes from there.
+
+		http://micampe.it/things/flickrclient
+
+		My variant is the same basic thing, except it tracks if it has
+		already created a handler for a specific call or not.
+
+		example usage:
+
+			flickr.auth_getFrob(api_key="AAAAAA")
+			rsp = flickr.favorites_getList(api_key=flickrAPIKey, \\
+				auth_token=token)
+
+		"""
+
+		if not self.__handlerCache.has_key(method):
+			def handler(_self = self, _method = method, **arg):
+				_method = "flickr." + _method.replace("_", ".")
+				url = "http://" + FlickrAPI.flickrHost + \
+					FlickrAPI.flickrRESTForm
+				arg["method"] = _method
+				postData = urllib.urlencode(arg) + "&api_sig=" + \
+					_self.__sign(arg)
+				#print "--url---------------------------------------------"
+				#print url
+				#print "--postData----------------------------------------"
+				#print postData
+				f = urllib.urlopen(url, postData)
+				data = f.read()
+				#print "--response----------------------------------------"
+				#print data
+				f.close()
+				return XMLNode.parseXML(data, True)
+
+			self.__handlerCache[method] = handler;
+
+		return self.__handlerCache[method]
+	
+	#-------------------------------------------------------------------
+	def __getAuthURL(self, perms, frob):
+		"""Return the authorization URL to get a token.
+
+		This is the URL the app will launch a browser toward if it
+		needs a new token.
+			
+		perms -- "read", "write", or "delete"
+		frob -- picked up from an earlier call to FlickrAPI.auth_getFrob()
+
+		"""
+
+		data = {"api_key": self.apiKey, "frob": frob, "perms": perms}
+		data["api_sig"] = self.__sign(data)
+		return "http://%s%s?%s" % (FlickrAPI.flickrHost, \
+			FlickrAPI.flickrAuthForm, urllib.urlencode(data))
+
+	#-------------------------------------------------------------------
+	def upload(self, filename=None, jpegData=None, **arg):
+		"""Upload a file to flickr.
+
+		Be extra careful you spell the parameters correctly, or you will
+		get a rather cryptic "Invalid Signature" error on the upload!
+
+		Supported parameters:
+
+		One of filename or jpegData must be specified by name when 
+		calling this method:
+
+		filename -- name of a file to upload
+		jpegData -- array of jpeg data to upload
+
+		api_key
+		auth_token
+		title
+		description
+		tags -- space-delimited list of tags, "tag1 tag2 tag3"
+		is_public -- "1" or "0"
+		is_friend -- "1" or "0"
+		is_family -- "1" or "0"
+
+		"""
+
+		if filename == None and jpegData == None or \
+			filename != None and jpegData != None:
+
+			raise UploadException("filename OR jpegData must be specified")
+
+		# verify key names
+		for a in arg.keys():
+			if a != "api_key" and a != "auth_token" and a != "title" and \
+				a != "description" and a != "tags" and a != "is_public" and \
+				a != "is_friend" and a != "is_family":
+
+				sys.stderr.write("FlickrAPI: warning: unknown parameter " \
+					"\"%s\" sent to FlickrAPI.upload\n" % (a))
+		
+		arg["api_sig"] = self.__sign(arg)
+		url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrUploadForm
+
+		# construct POST data
+		boundary = mimetools.choose_boundary()
+		body = ""
+
+		# required params
+		for a in ('api_key', 'auth_token', 'api_sig'):
+			body += "--%s\r\n" % (boundary)
+			body += "Content-Disposition: form-data; name=\""+a+"\"\r\n\r\n"
+			body += "%s\r\n" % (arg[a])
+
+		# optional params
+		for a in ('title', 'description', 'tags', 'is_public', \
+			'is_friend', 'is_family'):
+
+			if arg.has_key(a):
+				body += "--%s\r\n" % (boundary)
+				body += "Content-Disposition: form-data; name=\""+a+"\"\r\n\r\n"
+				body += "%s\r\n" % (arg[a])
+
+		body += "--%s\r\n" % (boundary)
+		body += "Content-Disposition: form-data; name=\"photo\";"
+		body += " filename=\"%s\"\r\n" % filename
+		body += "Content-Type: image/jpeg\r\n\r\n"
+
+		#print body
+
+		if filename != None:
+			fp = file(filename, "rb")
+			data = fp.read()
+			fp.close()
+		else:
+			data = jpegData
+
+		postData = body.encode("utf_8") + data + \
+			("--%s--" % (boundary)).encode("utf_8")
+
+		request = urllib2.Request(url)
+		request.add_data(postData)
+		request.add_header("Content-Type", \
+			"multipart/form-data; boundary=%s" % boundary)
+		response = urllib2.urlopen(request)
+		rspXML = response.read()
+
+		return XMLNode.parseXML(rspXML)
+
+
+	#-----------------------------------------------------------------------
+	#@classmethod
+	def testFailure(cls, rsp, exit=True):
+		"""Exit app if the rsp XMLNode indicates failure."""
+		if rsp['stat'] == "fail":
+			sys.stderr.write("%s\n" % (cls.getPrintableError(rsp)))
+			if exit: sys.exit(1)
+        testFailure = classmethod(testFailure)
+
+	#-----------------------------------------------------------------------
+	#@classmethod
+	def getPrintableError(cls, rsp):
+		"""Return a printed error message string."""
+		return "%s: error %s: %s" % (rsp.elementName, \
+			cls.getRspErrorCode(rsp), cls.getRspErrorMsg(rsp))
+        getPrintableError = classmethod(getPrintableError)
+
+	#-----------------------------------------------------------------------
+	#@classmethod
+        def getRspErrorCode(cls, rsp):
+		"""Return the error code of a response, or 0 if no error."""
+		if rsp['stat'] == "fail":
+			return rsp.err[0]['code']
+
+		return 0
+        getRspErrorCode = classmethod(getRspErrorCode)
+
+	#-----------------------------------------------------------------------
+	#@classmethod
+	def getRspErrorMsg(cls, rsp):
+		"""Return the error message of a response, or "Success" if no error."""
+		if rsp['stat'] == "fail":
+			return rsp.err[0]['msg']
+
+		return "Success"
+        getRspErrorMsg = classmethod(getRspErrorMsg)
+
+	#-----------------------------------------------------------------------
+	def __getCachedTokenPath(self):
+		"""Return the directory holding the app data."""
+		return os.path.expanduser(os.path.sep.join(["~", ".flickr", \
+			self.apiKey]))
+
+	#-----------------------------------------------------------------------
+	def __getCachedTokenFilename(self):
+		"""Return the full pathname of the cached token file."""
+		return os.path.sep.join([self.__getCachedTokenPath(), "auth.xml"])
+
+	#-----------------------------------------------------------------------
+	def __getCachedToken(self):
+		"""Read and return a cached token, or None if not found.
+
+		The token is read from the cached token file, which is basically the
+		entire RSP response containing the auth element.
+		"""
+
+		try:
+			f = file(self.__getCachedTokenFilename(), "r")
+			
+			data = f.read()
+			f.close()
+
+			rsp = XMLNode.parseXML(data)
+
+			return rsp.auth[0].token[0].elementText
+
+		except IOError:
+			return None
+
+	#-----------------------------------------------------------------------
+	def __setCachedToken(self, xml):
+		"""Cache a token for later use.
+
+		The cached tag is stored by simply saving the entire RSP response
+		containing the auth element.
+
+		"""
+
+		path = self.__getCachedTokenPath()
+		if not os.path.exists(path):
+			os.makedirs(path)
+
+		f = file(self.__getCachedTokenFilename(), "w")
+		f.write(xml)
+		f.close()
+
+
+	#-----------------------------------------------------------------------
+	def getToken(self, perms="read", browser="lynx"):
+		"""Get a token either from the cache, or make a new one from the
+		frob.
+
+		This first attempts to find a token in the user's token cache on
+		disk.
+		
+		If that fails (or if the token is no longer valid based on
+		flickr.auth.checkToken) a new frob is acquired.  The frob is
+		validated by having the user log into flickr (with lynx), and
+		subsequently a valid token is retrieved.
+
+		The newly minted token is then cached locally for the next run.
+
+		perms--"read", "write", or "delete"
+		browser--whatever browser should be used in the system() call
+
+		"""
+		
+		# see if we have a saved token
+		token = self.__getCachedToken()
+
+		# see if it's valid
+		if token != None:
+			rsp = self.auth_checkToken(api_key=self.apiKey, auth_token=token)
+			if rsp['stat'] != "ok":
+				token = None
+			else:
+				# see if we have enough permissions
+				tokenPerms = rsp.auth[0].perms[0].elementText
+				if tokenPerms == "read" and perms != "read": token = None
+				elif tokenPerms == "write" and perms == "delete": token = None
+
+		# get a new token if we need one
+		if token == None:
+			# get the frob
+			rsp = self.auth_getFrob(api_key=self.apiKey)
+			self.testFailure(rsp)
+
+			frob = rsp.frob[0].elementText
+
+			# validate online
+			os.system("%s '%s'" % (browser, self.__getAuthURL(perms, frob)))
+
+			# get a token
+			rsp = self.auth_getToken(api_key=self.apiKey, frob=frob)
+			self.testFailure(rsp)
+
+			token = rsp.auth[0].token[0].elementText
+
+			# store the auth info for next time
+			self.__setCachedToken(rsp.xml)
+
+		return token
+
+########################################################################
+# App functionality
+########################################################################
+
+def main(argv):
+	# flickr auth information:
+	# Amy's key
+	flickrAPIKey = "8b7d872755275e492d1c1945b924d3ad"  # API key
+	flickrSecret = "43b36dd53c5c453a"                  # shared "secret"
+
+	# make a new FlickrAPI instance
+	fapi = FlickrAPI(flickrAPIKey, flickrSecret)
+
+	# do the whole whatever-it-takes to get a valid token:
+	token = fapi.getToken(browser="firefox")
+
+	# get my favorites
+	rsp = fapi.favorites_getList(api_key=flickrAPIKey,auth_token=token)
+	fapi.testFailure(rsp)
+
+	# and print them
+	for a in rsp.photos[0].photo:
+		print "%10s: %s" % (a['id'], a['title'].encode("ascii", "replace"))
+
+	# upload the file foo.jpg
+	#rsp = fapi.upload(filename="foo.jpg", \
+	#	api_key=flickrAPIKey, auth_token=token, \
+	#	title="This is the title", description="This is the description", \
+	#	tags="tag1 tag2 tag3", is_public="1")
+	#if rsp == None:
+	#	sys.stderr.write("can't find file\n")
+	#else:
+	#	fapi.testFailure(rsp)
+
+	return 0
+
+# run the main if we're not being imported:
+if __name__ == "__main__": sys.exit(main(sys.argv))
+

vars_image_move.py

+#! /usr/bin/env python
+
+"""
+
+Finds all frame-grab files in three vehicle-named subdirectories 
+of the current directory, and renames them by adding
+Taxon_Vehicle_directory to the front, moving them to the 
+<Destination> folder. This folder should be at the same level
+as the vehicle-named folders.
+
+Usage: 
+
+* CD to the folder that has the vehicle folders, 
+* Create a folder at that same level to serve as the destination
+
+* Run:
+     vars_image_move.py TaxonName DestinationName
+
+
+Version 2.1 - Aug 2013 - added parameters for TaxonName and DestinationName
+Version 2.0 - June 2013
+"""
+
+import os, sys, glob
+
+
+if len(sys.argv) < 3:
+	print __doc__
+else:
+	Taxon = sys.argv[1]
+	Destination = sys.argv[2].rstrip("/")
+# Name of folder in current directory at same level as vehicles
+# Destination = 'Periphyllopsis'
+# Taxon = "Periphyllopsis"
+	DEBUG = False
+
+	Extension="jpg"
+
+	BaseDir = os.getcwd()
+	VehList = ["Doc Ricketts", "Tiburon", "Ventana"]
+
+	for Veh in VehList:
+		NewPath = os.path.join(BaseDir,Veh)
+		if not os.path.exists(NewPath):
+			sys.stderr.write("####\n### No folder for vehicle {}\n####\n".format(Veh))
+		else:
+			os.chdir(NewPath)
+			# sys.stderr.write("IN {} DIRECTORY...\n".format(os.getcwd()))
+			Vehicle = Veh[0:3]
+			MyCwd = os.getcwd().split("/")[-1]
+			# e.g. Ventana
+			if DEBUG:
+				print MyCwd
+
+
+			ImageDirList = os.popen('ls -F  | grep \/','r').read().split()
+
+			# e.g. images
+			if DEBUG:
+				print "ImageDir:",ImageDirList
+
+
+			for SubDir in ImageDirList:
+				#e.g. 3602
+				if SubDir.startswith("stills"):
+					# sys.stderr.write("IN STILLS DIRECTORY...\n")
+					DirList = [o.replace("stills/","") for o in glob.glob('stills/*/*') if os.path.isdir(o)]
+				else:
+					DirList = os.popen('ls -F  %s | grep \/' % SubDir,'r').read().split()
+				if DEBUG:
+					print "** In SubDir: DirList:",DirList
+			
+				for Direct in DirList:
+					# 	InDirList = os.popen('ls -F %s/%s/*/*| grep \/' % (SubDir, Direct),'r').read().split()
+					# else: 
+					# 	InDirList = os.popen('ls -F  %s/%s/* | grep \/' % (SubDir, Direct),'r').read().split()
+					# if DEBUG:
+					# 	print "InDirList:",InDirList
+					if SubDir.startswith("stills"):
+						Direct = Direct.replace(":","/").replace("//","/")
+						# print >> sys.stderr, "SUBDIR: {}".format(SubDir)
+						# print >> sys.stderr, "DIRECT",Direct
+					
+					print "Directory", Direct,'-----------------'
+					ListCommand = 'ls ' + SubDir + Direct +'/*.'+Extension
+					if DEBUG:
+						print >> sys.stderr, "LIST COMMAND: ", ListCommand
+					FileList=os.popen(ListCommand).read().split()
+					if len(FileList)==0:
+						sys.stderr.write( "No files found in " + ListCommand + "\n")
+					else:
+						if DEBUG:
+							print "yes", FileList
+						for PathName in FileList:
+							PathName = PathName.replace("//","/")
+							if SubDir.startswith("stills"):
+								FileName= PathName.split('/')[-1]
+								Directs = "_".join(PathName.split('/')[1:3] )
+								EndName = Directs + '-' + FileName
+								if DEBUG:
+									print >> sys.stderr, "ListCommand: ", ListCommand
+								# EndName = PathName.replace("/","_").replace(":","_")	
+							else:
+								if DEBUG:
+									print "Pathname", PathName
+								FileName=PathName.split('/')[-1]
+								if (FileName) and FileName[0]==('_'):
+									FileName=FileName[1:]
+								#print FileName
+								EndName = Direct[:-1] + '-' + FileName
+							NewName= '../' + Destination + '/' + Taxon+'_' + Vehicle + '_' + EndName
+							command = 'cp ' + PathName + " " + NewName
+							print command
+							if not DEBUG:
+								os.popen(command,'r')
+						if not DEBUG:
+							os.popen('chmod 644 ../' + Destination + '/*.'+ Extension ,'r')
+	os.chdir(BaseDir)		
+

vars_retrieve_concept.py

+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Will find either a concept or a list of samples from a dive.
+
+To find a concept run with a COMMA-separated list of concept names (no space).
+
+To find samples from a dive, run with the appropriate flag as true,
+and send in a space- or comma-delimited list of dive numbers.
+NOTE: Comma-separated lists cannot have spaces too...
+
+You can search in three ways:
+ -d : get dive summary
+ -s : get samples collected during dive
+ -c : get all annotations of concepts for all dives
+ -a : get all associations between two or more groups, separated by +
+       secret option: put 1 at the end for full table (nothing is summary)
+ -k : get all species for a higher taxon (takes only one name)
+       secret option: put 1 at the end for quoted csv, 2 for full table
+ 
+  
+Usage: 
+	{0} -s D422 R500
+	{0} -d D422,v360 
+	{0} -c Aulacoctena,"Bathyctena chuni"
+	{0} -d v{{2770..2880}}    # get a dive summary for all dives between those two numbers
+	{0} -k Narcomed, Scyphozo # All species for both Narcos and Scyphos 
+	{0} -a Narcome, Scyphoz + Amphipod # Associations between Narcos and Amphipods
+	
+version 1.47: Changed order of fields in output, added field number for cutting
+version 1.46: Cleaned up output and improved library installation instructions
+version 1.45: Added associations and reformatted output
+version 1.4: Added extraction of species from higher taxon
+version 1.3: Handle sample numbers in V3875-D5 format
+version 1.2: Flag to perform different queries at run time
+version 1.1: Parses the V2132 format. Can also take dive number w/o vehicle
+  [haddock at MBARI dot org]
+Requires: pymssql module, freetds-dev, and unixODBC 
+Installation instructions are a comment in the source code below...
+
+"""
+# main() code is at the bottom
+# INSTALLATION: pre-requisites for OSX, in this order
+#     Command Line Tools from https://developer.apple.com/downloads/ 
+#     Install HomeBrew with this exact command: 
+#         ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"
+#     Install pip with this exact command:
+#         sudo easy_install pip
+#     brew install unixodbc
+#     brew install --with-unixodbc freetds
+#     sudo pip install pymssql
+
+import time
+import sys
+import pymssql
+
+
+
+def parsedivenumbers(Dive,shortname=False):
+	Dive = Dive.strip()
+	DiveDict = dict(zip(list("TVRD"),["Tiburon","Ventana","Doc Ricketts","Doc Ricketts"]))
+	if shortname:
+		DiveDict = dict(zip(list("TVRD"),["tibr","vnta","docr","docr"]))
+	if "-" in Dive:
+		DFields = Dive.split("-")
+		Dive = DFields[0]
+		Sample = DFields[1]
+	else:
+		Sample = ""
+	if Dive[0].isdigit():
+		Veh= "%"
+		Num = Dive.strip()
+	else:
+		Veh = DiveDict[Dive[0].upper()]
+		Num = Dive[1:].strip()
+	return Veh,Num,Sample
+
+def getsampleinfo(SampleID):
+	"""Incomplete?"""
+	DQuery = "SELECT DISTINCT SampleRefName, DescriptionComment from MBARI_Samples.dbo.Sample where SampleID < 1000; "
+
+	execquery(DQuery, mydatabase="EXPD")
+
+def getconceptfromKB(higher_taxa,style=1):
+	""" Retrieve all subordinate taxonomic concepts from the knowledgebase
+ Style 1 (default) is quoted and comma delimited. 
+ Style 0 is a list with one concept per line
+ Style 2 is the full table with concept numbers and higher-order classes
+ """
+	ConString = """ parent_name LIKE '%{}%' """
+	Splits = higher_taxa.split(",")
+	
+	if len(Splits) > 1:
+		ConList = Splits
+	else:
+		ConList = [higher_taxa]
+	SearchNames = ""
+	outstr=""
+	NumCons = 0
+	delimit=["\n",", ",""][style] # zero gets newline
+	for Con in ConList:
+		Constraint = ConString.format(Con.strip())
+		# print "CONSTRAINT: ", Constraint
+	
+# Using knowledgebase (VARS_KB), select all the species for a group
+		Cquery = """
+		WITH org_name AS (
+	        SELECT DISTINCT
+	            parent.id AS parent_id, parentname.ConceptName as parent_name,
+	            child.id AS child_id, childname.ConceptName as child_name
+	        FROM
+	            Concept parent RIGHT OUTER JOIN 
+	            Concept child ON child.ParentConceptID_FK = parent.id LEFT OUTER JOIN
+	            ConceptName childname ON childname.ConceptID_FK = child.id LEFT OUTER JOIN
+	            ConceptName parentname ON parentname.ConceptID_FK = parent.id
+	        WHERE
+	            childname.NameType = 'Primary' AND
+	            parentname.NameType = 'Primary' ), 
+	jn AS (   SELECT            parent_id, parent_name, child_id, child_name FROM org_name 
+			WHERE ({}) 
+			UNION ALL SELECT C.parent_id, C.parent_name, C.child_id, C.child_name FROM jn AS p 
+			JOIN org_name AS C ON C.parent_id = p.child_id ) 
+	SELECT DISTINCT jn.parent_id, jn.parent_name, jn.child_id, jn.child_name 
+	FROM jn ORDER BY 1;
+	""".format(Constraint)
+
+		NumRecs, SpeciesList = execquery(Cquery, mydatabase="VARS_KB")
+		SearchNames += " and ".join(Con)
+		NumCons += NumRecs
+		sys.stderr.write("## Found %d concepts for query of %s\n"%(NumRecs,Con))
+		if style == 2:
+			outstr = SpeciesList
+			break
+		try:
+			# Get the full name of the query to include it too, e.g. Chrysaora and not just speciesprint
+			if style == 1:
+				FirstName = "'{}'".format(SpeciesList.split("\t")[1])
+				SpNames = [ "'{}'".format(F.split("\t")[3]) for F in SpeciesList.rstrip().split("\n")]
+			else:
+				FirstName = SpeciesList.split("\t")[1]
+				SpNames = [ F.split("\t")[3] for F in SpeciesList.rstrip().split("\n")]
+		except IndexError:
+			# sys.stderr.write("No lower concepts found for {}\n".format(higher_taxa))
+			if style == 1:
+				FirstName = "'{}'".format(Con)
+			else:
+				FirstName = Con
+			SpNames = []
+			
+		if outstr:
+			head=delimit
+		else:
+			head=""
+		outstr += head + delimit.join([ FirstName ] + SpNames)
+
+	return NumCons,outstr
+	
+def findassociation(conceptstrings):
+	try:
+		HostString,AssocString = " ".join(conceptstrings).split("+")
+		HostList = HostString.split()
+		AssocList = AssocString.split()
+	except ValueError:
+		sys.exit("** To search for associations, provide two or more concepts separated by a plus\n")
+	
+	""" Make your own custom query """
+	# print "HostString",  (getconceptfromKB([HostString]))
+	# print "AssocString", AssocList
+	Outstr=""
+	TotalNum=0
+	
+	
+	# This returns a tuple with number and string...
+	HostCons = getconceptfromKB(HostString)[1]
+	AssocCons = getconceptfromKB(AssocString)[1]
+	sys.stderr.write("Finding associations for {0}... \n                    WITH {1}...\n".format(HostCons[:100],AssocCons[:100]))
+	
+	# Need to search both ways: fish annotated with jelly and jelly annotated with fish...
+	for Flipper in ([HostCons,AssocCons],[AssocCons,HostCons]):
+		FromCon = """ ann.ConceptName IN ({}) """.format(Flipper[0])
+		# ToCon = """ ann.ToConcept IN ({},'Marine organism') """.format(Flipper[1])
+		ToCon = """ ( (ann.ToConcept IN ({0}) ) OR (REPLACE(ann.Associations,'-',' ' ) IN ({0}) ) ) """.format(Flipper[1])
+		# FromCon = """ ann.ConceptName IN ({}) """.format("'Chrysaora'")
+		# ToCon = """ ann.ToConcept IN ({}) """.format("'Doryteuthis opalescens'")
+		# Take all args as a string, split on commas (genus species have spaces)
+	
+		Aquery = """ SELECT
+		ann.RovName, ann.DiveNumber,ann.DEPTH, ann.ConceptName,ann.Latitude, ann.Longitude, ann.Temperature, ann.Oxygen, ann.Salinity,
+		ann.RecordedDate, ann.Image, ann.TapeTimeCode, ann.Zoom,
+		ann.ObservationID_FK AS obsid, ann.LinkValue, ann.linkName,
+		ann.VideoArchiveSetID_FK AS vasid, ann.ShipName,
+		ann.Associations,ann.videoArchiveName, ann.CameraDirection,
+		ann.ChiefScientist, ann.FieldWidth, ann.Light, ann.Notes,
+		ann.Observer, ann.ToConcept
+		FROM Annotations AS ann
+		WHERE (
+		( ({0}) AND ({1}) ) 
+		AND ( ann.linkName LIKE '%association%')   
+		) """.format(FromCon,ToCon)
+		# print "query: ",Aquery
+		# AND (( ann.linkName LIKE '%association%') OR (ann.linkName LIKE '%commensal%'))  
+
+		NumFound,ResultStr = execquery(Aquery)
+
+		TotalNum += NumFound 
+		Outstr += ResultStr
+
+	Fields = """RovName\tDiveNumber\tDEPTH\tConceptName\tLatitude\tLongitude\tTemperature\tOxygen\tSalinity\t\
+	RecordedDate\tImage\tEpochSecs\tTapeTimeCode\tZoom\tObservationID_FK\tLinkValue\tlinkName\t\
+	VideoArchiveSetID_FK\tShipName\tObservationID_FK\tAssociations\tvideoArchiveName\tCameraDirection\t\
+	ChiefScientist\tFieldWidth\tNotes\tObserver\tAssocConcept\n"""
+	L = Fields.split("\t")
+	while '' in L:
+			L.remove('')
+	NumFields = "\t".join(["".join([str(i+1),j]) for i,j in enumerate(L)])
+	
+	# head = """RovName	DiveNumber	Depth	ConceptName	Latitude	Longitude	Temperature	Oxygen	EpochSecs	Salinity	RecordedDate	Image	TapeTimeCode	Zoom	ObservationID_FK	LinkValue	linkName	VideoArchiveSetID_FK	ShipName	Associations	videoArchiveName	CameraDirection	ChiefScientist	FieldWidth	Light	Notes	Observer	AssocConcept"""
+	# print Fields
+	# sys.stderr.write("## Found %d associations for %s...\n" % (TotalNum,conceptstrings))
+
+	return TotalNum, NumFields + Outstr
+	
+def getsamples(DiveList):
+	
+	# removed Image, but add back in if you want Image URL
+	# for epoch secs add this back in: DateDiff(ss, '01/01/70', RecordedDate) AS Esecs,
+	#DiveList = DiveListAsString.split(",")
+	SQuery = """
+	SELECT DISTINCT
+	      CONVERT(varchar(22), RecordedDate, 120) as DateTime24,
+	      RovName, DiveNumber, ConceptName, Associations,
+	      Depth, Latitude, Longitude, TapeTimeCode, ISNULL(Observer, '') AS Observer,
+	      AnnotationMode, ObservationID_FK, LinkName, LinkValue
+	FROM  dbo.Annotations
+	WHERE (RovName like '{0}') AND (DiveNumber = {1})
+	AND (LinkName LIKE '%sample-reference%') {2}
+	ORDER BY TapeTimeCode, DateTime24 ; """
+	#sys.stderr.write("Finding samples...\n")
+	SHead = "DateTime24	RovName	DiveNumber	ConceptName	Associations	Depth	Latitude	Longitude	TapeTimeCode	Observer	AnnotationMode	ObservationID_FK	SampleRefName"
+	if __name__== "__main__":
+		print SHead
+	NumFound = 0
+	TotalFound = 0
+	AllOut = ""
+	if "," in DiveList[0]:
+		DiveList = DiveList[0].split(",") 
+	for Dive in DiveList:
+		ROVName,DiveNum,Sample = parsedivenumbers(Dive)
+		if Sample:
+			SampleString = "AND (LinkValue LIKE '%{}') ".format(Sample.lower())
+		else:
+			SampleString = ""
+		NumFound, Outstr = execquery(SQuery.format(ROVName, DiveNum, SampleString), mydatabase="VARS")
+		TotalFound += NumFound
+		AllOut += Outstr
+	return AllOut
+	sys.stderr.write("## Found %d samples for the query...\n" % (TotalFound))
+
+	
+def getdivesummary(DiveList):
+	# removed Image, but add back in if you want Image URL
+	# for epoch secs add this back in: DateDiff(ss, '01/01/70', RecordedDate) AS Esecs,
+	#DiveList = DiveListAsString.split(",")
+	SQuery = """
+	SELECT DISTINCT
+		  chiefscientist, shipname, rovname,  divenumber, avgrovlat, avgrovlon, divestartdtg, maxpressure
+	FROM  dbo.DiveSummary
+	WHERE (RovName like '{0}') AND (DiveNumber = {1})
+	"""
+
+	SHead = "ChiefScientist	ShipName	RovName	DiveNumber	AvgROVLatitude	AvgROVLongitude	DiveStartTime	MaxPressure"
+	print SHead
+	if "," in DiveList[0]:
+		DiveList = DiveList[0].split(",") 
+	NumFound = 0
+	TotalFound = 0
+	for Dive in DiveList:
+		ROVName,DiveNum,S = parsedivenumbers(Dive, shortname = True)
+		# print SQuery.format(ROVName,DiveNum)
+		NumFound,Outstr = execquery(query = SQuery.format(ROVName, DiveNum), mydatabase="EXPD")
+		TotalFound +=NumFound
+		print Outstr.rstrip("\n")
+	sys.stderr.write("## Found %s dive summaries...\n" % (TotalFound))
+
+def findconcept(conceptstrings):
+	ConString = """ ann.ConceptName like '%%%s%%' """
+	# Take all args as a string, split on commas (genus species have spaces)
+	Splits = " ".join(conceptstrings).split(",")
+	ConList = [ConString % (Con.strip()) for Con in Splits]
+	Constraint = "( %s )" % (" OR ".join(ConList))
+	# Concept = " ".join(sys.argv[1:])
+	query = """ SELECT
+	ann.RovName, ann.DiveNumber,ann.DEPTH, ann.ConceptName,ann.Latitude, ann.Longitude, ann.Temperature, ann.Oxygen, ann.Salinity,
+	ann.RecordedDate, ann.Image, ann.TapeTimeCode, ann.Zoom,
+	ann.ObservationID_FK AS obsid, ann.LinkValue, ann.linkName,
+	ann.VideoArchiveSetID_FK AS vasid, ann.ShipName,
+	ann.Associations,ann.videoArchiveName, ann.CameraDirection,
+	ann.ChiefScientist, ann.FieldWidth, ann.Light, ann.Notes,
+	ann.Observer,ann.ToConcept
+	FROM
+	Annotations AS ann
+	where
+	%s """ % Constraint
+
+	Fields = """RovName\tDiveNumber\tDEPTH\tConceptName\tLatitude\tLongitude\tTemperature\tOxygen\tSalinity\t\
+	RecordedDate\tImage\tEpochSecs\tTapeTimeCode\tZoom\tObservationID_FK\tLinkValue\tlinkName\t\
+	VideoArchiveSetID_FK\tShipName\tObservationID_FK\tAssociations\tvideoArchiveName\tCameraDirection\t\
+	ChiefScientist\tFieldWidth\tNotes\tObserver\tToConcept\n"""
+
+	L = Fields.split("\t")
+	while '' in L:
+			L.remove('')
+	NumFields = "\t".join(["".join([str(i+1),j]) for i,j in enumerate(L)])
+	sys.stderr.write("Finding all annotations for %s...\n" % conceptstrings)
+	# head = """RovName	DiveNumber	Depth	ConceptName	Latitude	Longitude	Temperature	Oxygen	EpochSecs	Salinity	RecordedDate	Image	TapeTimeCode	Zoom	ObservationID_FK	LinkValue	linkName	VideoArchiveSetID_FK	ShipName	Associations	videoArchiveName	CameraDirection	ChiefScientist	FieldWidth	Light	Notes	Observer	ToConcept"""
+	print NumFields
+	NumFound,Outstr = execquery(query)
+	print Outstr
+	sys.stderr.write("## Found %d annotations for %s...\n" % (NumFound,conceptstrings))
+
+def execquery(query, mydatabase="VARS"):
+	serverlookup = {
+		"EXPD"	 : "solstice.shore.mbari.org",
+		"VARS"	 : "equinox.shore.mbari.org",
+		"VARS_KB": "equinox.shore.mbari.org"
+	}
+
+	# grab the right server name, depending on what database is being used...
+	servername = serverlookup[mydatabase]
+	username = "GETUSERNAMEFROMBRIAN"
+	pw = "GETPASSWORDFROMBRIAN"
+
+
+	# Try the ** operator with the parameters as a dictionary
+	"""config = {
+  'user': 'scott',
+  'password': 'tiger',
+  'host': '127.0.0.1',
+  'database': 'employees',
+  'raise_on_warnings': True,
+}
+cnx = mysql.connector.connect(**config)
+cnx.close()
+"""
+
+	conn = pymssql.connect(host=servername, user=username, password=pw, database=mydatabase, as_dict=False)
+	cur = conn.cursor()
+
+	#Write all records to one big file:
+	# to write to a different file per dive, uncomment the "write to individual files lines below..."
+
+	NumRecords=0
+
+	cur.execute(query)
+	outstr=""
+	for row in cur:
+		NumRecords += 1
+		#### THIS HAS TO BE FIXED??
+		# date_time = row[8]
+		# try:
+		# 	epoch= time.mktime(date_time.timetuple())
+		# except:
+		# 	epoch = 999
+		# #epoch = int(time.mktime(time.strptime(date_time, timepattern)))
+		strlist = ["%s"%(x) for x in row]
+		# strlist.insert(8,str(epoch) )
+		#print "%s\t%s\t%s" % (row[7],row[8],row[9])
+		#print "\t".join(strlist)
+		outstr += "\t".join(strlist)
+		outstr += "\n"
+		#print epoch
+		#print "\t".join(row)
+		#print "ID=%d, Name=%s" % (row['id'], row['name'])
+
+	conn.close()
+	return NumRecords,outstr
+
+def summarizeassoc(instring):
+	""" Split the associations to a subset of pairs"""
+
+	allstr = instring.split("\n")[1:]
+	OutSet = {}
+	for Line in allstr:
+		Values = Line.split("\t")
+		if len(Values)>25:
+			OutKey = Values[2] + " + " + Values[26]
+			OutSet[OutKey] = OutSet.get(OutKey,0) + 1
+	OK = sorted(OutSet.keys())
+	for k in OK:
+		print "%8d  %s" % (OutSet[k],k)
+	
+### START OF PROGRAM
+def main():
+	
+	if len(sys.argv)<=1:
+		sys.stderr.write(__doc__.format(sys.argv[0].split("/")[-1]))
+	else:
+		if sys.argv[1] == "-c":
+			findconcept(sys.argv[2:])
+			if len(sys.argv) > 3:
+				sys.stderr.write("** Concepts should be comma-separated or surrounded in quotes.\n Space-separated lists will not work as expected")
+		elif sys.argv[1] == "-s":
+			
+			print( getsamples(sys.argv[2:]).rstrip("\n") )
+		elif sys.argv[1] == "-d":
+			getdivesummary(sys.argv[2:])
+		elif sys.argv[1] == "-k":
+			if sys.argv[-1] in ("1","2"):
+				print( getconceptfromKB(" ".join(sys.argv[2:-1]), style=int(sys.argv[-1]))[1])
+			else:
+				print( getconceptfromKB(" ".join(sys.argv[2:]), style=0)[1])
+		elif sys.argv[1] == "-a":
+			style = 0
+			if sys.argv[-1] in ("1","2"):
+				assocnum, assocstr = findassociation(sys.argv[2:-1]) 
+				style = 1
+			else:
+				assocnum, assocstr = findassociation(sys.argv[2:]) 
+
+			sys.stderr.write("\n## Found %d annotations for %s (and vice versa)...\n" % (assocnum," ".join(sys.argv[2:]) ))
+			if style:
+				print assocstr
+			else:
+				summarizeassoc(assocstr)
+			
+		else:
+			sys.stderr.write(__doc__.format(sys.argv[0]))
+
+if __name__ == "__main__":
+	main()
+
+"""
+DiveSummary in expd has the following fields:
+COLUMN_NAME
+avgrovlat
+avgrovlon
+avgshiplat
+avgshiplon
+chiefscientist
+ctdconfigid
+ctdlastupdate
+ctdlastwho
+ctdlastwhy
+ctdo2optodecount
+ctdo2sbecount
+ctdpcount
+ctdplotsent
+ctdscount
+ctdtcount
+ctdxmisscount
+diveenddtg
+diveid
+divenumber
+divestartdtg
+expdid
+id
+maxpressure
+maxrovlat
+maxrovlon
+maxshiplat
+maxshiplon
+minrovlat
+minrovlon
+minshiplat
+minshiplon
+rovid
+rovname
+shipid
+shipname
+"""
+