Attachment 'emen2client.py'

Download

   1 # $Id: emen2client.py,v 1.106 2010/11/19 09:08:26 irees Exp $
   2 
   3 #!/usr/bin/env python
   4 import time
   5 import os
   6 import sys
   7 import subprocess
   8 import optparse
   9 import getpass
  10 import glob
  11 import collections
  12 import operator
  13 import fnmatch
  14 import struct
  15 import copy
  16 import shutil
  17 import urllib
  18 import urllib2
  19 import urlparse
  20 import httplib
  21 import xmlrpclib
  22 import gzip
  23 import zlib
  24 import math
  25 import StringIO
  26 import socket
  27 import tempfile
  28 import re
  29 
  30 try:
  31 	import distutils.version
  32 except:
  33 	pass
  34 
  35 __version__ = "$Revision: 1.106 $".split(":")[1][:-1].strip()
  36 USER_AGENT = "emen2client/%s"%__version__
  37 
  38 try:
  39 	import json
  40 except ImportError:
  41 	try:
  42 		import simplejson as json
  43 	except ImportError:
  44 		print """Note: No JSON module found. This is not necessary, but is recommended. Please upgrade to Python 2.6 or install simplejson, using "sudo easy_install simplejson" """
  45 		json = None
  46 
  47 try:
  48 	import EMAN2
  49 	import hashlib
  50 except ImportError:
  51 	# print "EMAN2 support not available"
  52 	EMAN2 = None
  53 
  54 
  55 try:
  56 	import xml.etree.ElementTree as ET
  57 except ImportError:
  58 	ET = None
  59 
  60 try:
  61 	import emen2.clients.jsonrpc.proxy
  62 except:
  63 	pass 
  64 
  65 	
  66 
  67 
  68 def check_output(args, **kwds):
  69 	kwds.setdefault("stdout", subprocess.PIPE)
  70 	kwds.setdefault("stderr", subprocess.STDOUT)
  71 	p = subprocess.Popen(args, **kwds)
  72 	return p.communicate()[0]
  73 
  74 
  75 
  76 
  77 ##################################
  78 # Compression schemes
  79 ##################################
  80 
  81 # Compression is handled on the fly now during upload / download with a gzip pipe
  82 
  83 # Based on http://code.activestate.com/recipes/551784/
  84 class GzipPipe(StringIO.StringIO):
  85 	"""This class implements a compression pipe suitable for asynchronous
  86 	process."""
  87 
  88 	# Size of the internal buffer
  89 	CHUNCK_SIZE = 1024*1024
  90 	COMPRESSLEVEL = 3
  91 
  92 	def __init__(self, filename=None) :
  93 		"""Streaming compression using gzip.GzipFile
  94 
  95 		@param filename source file
  96 
  97 		"""
  98 
  99 		self.filename = filename
 100 		self.source = open(self.filename, "rb")
 101 		self.source_eof = False
 102 		self.buffer = ""
 103 		self.name = self.filename + ".gz"
 104 
 105 		self.pos = 0
 106 
 107 		StringIO.StringIO.__init__(self)
 108 		#super(GzipPipe, self).__init__(self)
 109 		self.zipfile = gzip.GzipFile(filename=str(os.path.basename(filename)), mode='wb', compresslevel=self.COMPRESSLEVEL, fileobj=self)
 110 
 111 
 112 	def write(self, data):
 113 		"""Writes data to internal buffer. Do not call from outside."""
 114 		self.buffer += data
 115 
 116 
 117 	def read(self, size=-1) :
 118 		"""Calling read() on a zip pipe will suck data from the source stream.
 119 		@param	size Maximum size to read - Read whole compressed file if not specified.
 120 		@return Compressed data
 121 		"""
 122 
 123 		# Feed the zipped buffer by writing source data to the zip stream
 124 		while ((len(self.buffer) < size) or (size == -1)) and not self.source_eof:
 125 
 126 			# Get a chunk of source data
 127 			chunk = self.source.read(GzipPipe.CHUNCK_SIZE)
 128 			self.pos += len(chunk)
 129 
 130 			# Feed the source zip file (that fills the compressed buffer)
 131 			self.zipfile.write(chunk)
 132 
 133 			# End of source file ?
 134 			if (len(chunk) < GzipPipe.CHUNCK_SIZE) :
 135 				self.source_eof = True
 136 				self.zipfile.flush()
 137 				self.zipfile.close()
 138 				self.source.close()
 139 				break
 140 
 141 
 142 		# We have enough data in the buffer (or source file is EOF): Give it to the output
 143 		if size == 0:
 144 			result = ""
 145 		if size >= 1:
 146 			result = self.buffer[0:size]
 147 			self.buffer = self.buffer[size:]
 148 		else: # size < 0 : All requested
 149 			result = self.buffer
 150 			self.buffer = ""
 151 
 152 		return result
 153 
 154 
 155 
 156 ##################################
 157 # Exceptions
 158 ##################################
 159 
 160 class OverwriteError(Exception):
 161 	"""File exists, and flags are set to prevent overwriting"""
 162 
 163 
 164 
 165 ##################################
 166 # Options
 167 ##################################
 168 
 169 class DBRPCOptions(optparse.OptionParser):
 170 	"""Default options to be used by EMEN2 clients"""
 171 
 172 	def __init__(self, *args, **kwargs):
 173 		#super(DBRPCOptions, self).__init__(self, *args, **kwargs)
 174 		optparse.OptionParser.__init__(self, *args, **kwargs)
 175 
 176 		defaulthost = os.getenv("EMEN2URI", "http://ncmidb.bcm.edu")
 177 		self.add_option("--username","-U", type="string", help="Username")
 178 		self.add_option("--password","-P", type="string", help="Password (Note: specifying passwords in shell commands is not secure)")
 179 
 180 		self.add_option("--json", "-J", action="store_true", default=False, help="Enable JSON-RPC (Experimental)")
 181 
 182 		self.add_option("--host","-H",type="string",help="Host endpoint URI; default is %s"%defaulthost, default=defaulthost)
 183 		self.add_option("--ctxid","-C",type="string",help="Valid Context ID")
 184 
 185 	#def load_config(self):
 186 	#	"""Load from config file"""
 187 	#	pass
 188 
 189 
 190 
 191 ##################################
 192 # Main exported DB interface classes
 193 ##################################
 194 
 195 
 196 	
 197 class DBJSONRPCProxy(object):
 198 	_ctxid = property(lambda s: s.proxy.get_ctxid())
 199 	def __init__(self, host=None, username=None, password=None, ctxid=None):
 200 		self.proxy = emen2.clients.jsonrpc.proxy.Emen2Proxy(host)
 201 		if username:
 202 			self.proxy._login(username, password)
 203 		else:
 204 			self.proxy.set_ctxid(ctxid)
 205 		self._host = host
 206 	def __getattr__(self, name):
 207 		return getattr(self.proxy, name)
 208 	def _login(self, username=None, password=None):
 209 		self.proxy._login(username, password)
 210 	def _setContext(self, ctxid):
 211 		self.proxy.set_ctxid(ctxid)
 212 	def _getContext():
 213 		self.proxy.get_ctxid()
 214 
 215 
 216 
 217 
 218 class DBXMLRPCProxy(xmlrpclib.ServerProxy):
 219 	"""Provide a more feature-rich DB XMLRPC interface than the bare python xmlrpclib"""
 220 
 221 	def __init__(self, host=None, username=None, password=None, ctxid=None):
 222 		#super(DBXMLRPCProxy, self).__init__(host, allow_none=True)
 223 		xmlrpclib.ServerProxy.__init__(self, host, allow_none=True)
 224 
 225 		self.__username = username
 226 		self.__password = password
 227 
 228 		self._ServerProxy__allow_none = True
 229 		self._host = host
 230 		self._handler = self._ServerProxy__handler
 231 
 232 		self._setContext(ctxid)
 233 
 234 
 235 	def __wrap_request(self, method, params):
 236 		try:
 237 			ret = self._ServerProxy__request(method, params)
 238 		except xmlrpclib.Error, err:
 239 			#raise xmlrpclib.Fault(0, "Error: %s"%(err.headers.get('X-Error')))
 240 			raise ValueError, str(err.headers.get('X-Error'))
 241 
 242 		except Exception, e:
 243 			raise
 244 
 245 		return ret
 246 
 247 
 248 	def __getattr__(self, name):
 249 		# print "-> ", name
 250 		return xmlrpclib._Method(self.__wrap_request, name)
 251 
 252 
 253 	def _login(self, username=None, password=None):
 254 		ctxid = self.__wrap_request("login", (username or self.__username, password or self.__password))
 255 		self.__password = None
 256 		return self._setContext(ctxid)
 257 
 258 
 259 	def _setContext(self, ctxid):
 260 		# ian: todo: make handler a dynamically generated property? maybe not..
 261 		self._ctxid = ctxid
 262 		if self._ctxid != None:
 263 			self._ServerProxy__handler = "%s?ctxid=%s"%(self._handler, self._ctxid)
 264 		return self._ctxid
 265 
 266 
 267 	def _getContext():
 268 		return self._ctxid
 269 
 270 
 271 
 272 
 273 ##################################
 274 # File Transport
 275 ##################################
 276 
 277 class Transport(object):
 278 	def __init__(self, db=None, bdo=None, bdoid=None, param="file_binary", recid=None, newrecord=None, compress=True, filename=None, filedata=None, report=None, pos=None):
 279 		"""Needs a BDO or BDOID for download, or a filename and recid | newrecord for upload
 280 
 281 		@keyparam db DBProxy
 282 		@keyparam bdo BDO object
 283 		@keyparam bdoid BDO Id
 284 		@keyparam filename Filename to upload
 285 		@keyparam filedata Upload a string buffer instead of file reference. In this case, filename will be used for the name of the file.
 286 		@keyparam report Upload/download status report hook/callback
 287 		@keyparam compress Compress file
 288 		"""
 289 
 290 		self.db = db
 291 		self.bdo = bdo
 292 		self.bdoid = bdoid
 293 		self.filename = filename
 294 		self.filedata = filedata
 295 		self.newrecord = newrecord
 296 		self.param = param
 297 		self.recid = recid
 298 		self.compress = compress
 299 		self.pos = pos
 300 		self._report = report
 301 		self.recmap = {}
 302 
 303 
 304 	def _retryhandler(self, method, *args, **kwargs):
 305 		"""Automatic retry wrapper
 306 		@param method self._upload or self._download
 307 		"""
 308 
 309 		count = 0
 310 		ret = None
 311 		while True:
 312 			count += 1
 313 			try:
 314 				ret = method(*args, **kwargs)
 315 				break
 316 
 317 			except KeyboardInterrupt, e:
 318 				print "Keyboard Interrupt"
 319 				raise
 320 
 321 			# socket.error is recoverable, others are not recoverable!
 322 			except socket.error, e:
 323 				self.report("Network error, trying again in 10 seconds (%s attempts): %s"%(count, e), i=1)
 324 				time.sleep(10)
 325 
 326 			except Exception, e:
 327 				self.report("Unrecoverable error, aborting: %s"%(e), i=1)
 328 				raise
 329 
 330 		return ret
 331 
 332 
 333 	def report(self, msg=None, progress=None, pos=None, i=0):
 334 		try:
 335 			self._report(msg=msg, progress=progress, f=self.filename or self.bdo.get("name"), pos=pos, i=i+1)
 336 		except:
 337 			pass
 338 
 339 
 340 	def sidecar_read(self, filename=None):
 341 		filename = filename or self.filename
 342 		try:
 343 			return json.load(file(filename+".json","r"))
 344 		except:
 345 			pass
 346 
 347 
 348 	def sidecar_write(self, filename=None):
 349 		filename = filename or self.filename
 350 		try:
 351 			json.dump(self.bdo, file(filename+".json", "w"), indent=True)
 352 			self.report("Wrote sidecar: %s.json"%filename, i=1)
 353 		except:
 354 			pass
 355 
 356 
 357 	def setdb(self, db):
 358 		self.db = db
 359 
 360 
 361 	def setreport(self, report):
 362 		self._report = report
 363 
 364 
 365 	def setrecmap(self, recmap):
 366 		self.recmap = recmap
 367 		self.recid = recmap.get(self.recid, self.recid)
 368 		if self.newrecord:
 369 			self.newrecord["children"] = [recmap.get(i,i) for i in self.newrecord.get("children",[])]
 370 			self.newrecord["parents"] = [recmap.get(i,i) for i in self.newrecord.get("parents",[])]
 371 
 372 
 373 	def action(self, *args, **kwargs):
 374 		pass
 375 
 376 
 377 
 378 class DownloadTransport(Transport):
 379 
 380 	def action(self, overwrite=False, rename=False, sidecar=False, *args, **kwargs):
 381 		"""Download BDO to local disk
 382 
 383 		@keyparam overwrite Overwrite an existing file
 384 		@keyparam rename If file exists, rename file to avoid overwriting
 385 
 386 		"""
 387 
 388 		self.report(pos=self.pos)
 389 
 390 		try:
 391 			# Check that we can download the file, and then get it
 392 			self._download_checkfile(overwrite=overwrite, rename=rename)
 393 			self.report("Downloading %s -> %s..."%(self.bdo.get("filename"), self.filename), i=1)
 394 
 395 			ret = self._retryhandler(self._download)
 396 
 397 			if sidecar:
 398 				self.sidecar_write()
 399 
 400 		except OverwriteError, e:
 401 			# Or report that we can't download it
 402 			self.report("Skipping file: %s"%e, i=1)
 403 			return self.filename
 404 
 405 		return self.filename
 406 
 407 
 408 
 409 
 410 	def _download_checkfile(self, overwrite=False, rename=False):
 411 		"""Check if we can write the file, and if to use compression or not. See download() for params"""
 412 
 413 		filename = os.path.basename(self.bdo.get("filename")) or self.bdo.get("name")
 414 
 415 		fsplit = os.path.splitext(filename)
 416 		if self.compress and fsplit[-1] == ".gz":
 417 			compress = True
 418 			filename = fsplit[0]
 419 		else:
 420 			compress = False
 421 
 422 		# this may raise OverwriteError Exception, which should be caught by caller
 423 		if os.path.exists(filename) and not overwrite:
 424 			if rename:
 425 				filename = "duplicate.%s:%s"%(self.bdo.get("recid"), filename)
 426 			else:
 427 				raise OverwriteError, "File exists: %s"%filename
 428 
 429 		self.filename = filename
 430 		self.compress = compress
 431 
 432 
 433 
 434 	def _download(self):
 435 		"""Actual download."""
 436 
 437 		# Setup connection
 438 		ctxid = urllib.urlencode({"ctxid":self.db._ctxid})
 439 
 440 		# ian: changed .netloc to [1] because it caused failure on python 2.4
 441 		host = urlparse.urlparse(self.db._host)[1] # .netloc
 442 		path = "/download/%s?%s"%(self.bdo.get("name"), ctxid)
 443 
 444 		# Connect
 445 		http = httplib.HTTPConnection(host)
 446 		http.request("GET", path)
 447 		resp = http.getresponse()
 448 		print(resp.getheader('content-length'))
 449 		clength = float(resp.getheader('content-length'))
 450 
 451 		# Download and pipe through gzip
 452 		stdout = None
 453 		if self.compress:
 454 			stdout = open(self.filename, "wb")
 455 			gz = subprocess.Popen(["gzip","-d"], stdout=stdout, stdin=subprocess.PIPE)
 456 			fio = gz.stdin
 457 		else:
 458 			fio = open(self.filename,"wb")
 459 
 460 		chunksize = 1024*1024
 461 		outlen = 0
 462 		while outlen < clength:
 463 			chunk = resp.read(chunksize)
 464 			outlen += len(chunk)
 465 			fio.write(chunk)
 466 			self.report(progress=(outlen/clength))
 467 
 468 
 469 		fio.close()
 470 		if stdout: stdout.close()
 471 		http.close()
 472 
 473 		return self.filename
 474 
 475 
 476 
 477 
 478 class UploadTransport(Transport):
 479 	"""Transfer files from/to DB"""
 480 
 481 	########################
 482 	# Upload methods
 483 	########################
 484 
 485 	def action(self):
 486 		"""Handler provides records and files to put in DB"""
 487 
 488 		self.report(pos=self.pos)
 489 
 490 		s = self.sidecar_read()
 491 		if s != None:
 492 			self.report("File already exists in database. Check record %s."%s.get("recid"), i=1)
 493 			self.bdo = s
 494 			self.recid = s.get('recid')
 495 			return
 496 
 497 		ret = self._retryhandler(self._upload)
 498 		self.sidecar_write()
 499 		return ret
 500 
 501 
 502 
 503 	def _upload(self, mimetype=None):
 504 		"""Implements Transfer-Encoding:Chunked over a PUT request, with optional gzip pipe streaming. See upload() for arguments."""
 505 
 506 		# Upload a string buffer as a file
 507 		if self.filedata:
 508 			filesize = len(self.filedata)
 509 			fio = StringIO.StringIO(self.filedata)
 510 			filename = self.filename
 511 
 512 
 513 		# Or upload a file from disk
 514 		else:
 515 			# Get file size for progress bar
 516 			filesize = float(os.stat(self.filename).st_size)
 517 
 518 			# Are we compressing this file?
 519 			if self.compress:
 520 				fio = GzipPipe(filename=self.filename)
 521 			else:
 522 				fio = open(self.filename, "rb")
 523 
 524 			filename = os.path.basename(fio.name)
 525 
 526 
 527 		if filesize == 0:
 528 			self.report("Warning: Uploading empty file!")
 529 			filesize = 1
 530 
 531 
 532 		# Prepare connection info
 533 		# recid = recid or self.newrecord.get('recid')
 534 		qs = {
 535 			"param": self.param,
 536 			"ctxid": self.db._ctxid,
 537 			"filename": filename
 538 		}
 539 
 540 
 541 		# If rec is set, with json support, use one-step newrecord/putbinary
 542 		if self.newrecord:
 543 			tmprecid = self.newrecord.get("recid")
 544 			if json:
 545 				qs["newrecord"] = json.dumps(self.newrecord)
 546 				self.recid = None
 547 			else:
 548 				self.report("Warning: JSON support is strongly recommended for upload; using workaround..")
 549 				newrec = self.db.putrecord(self.newrecord)
 550 				self.recid = newrec.get("recid")
 551 
 552 
 553 		if self.newrecord == None and self.recid == None:
 554 			raise ValueError, "No new record or target recid specified for upload"
 555 
 556 		# ._put
 557 		qs = urllib.urlencode(qs)
 558 		host = self.db._host.split("://")[-1]
 559 		path = "/upload/%s?%s"%(self.recid or "", qs)
 560 
 561 		# Set headers
 562 		headers = {"User-Agent":USER_AGENT}
 563 		headers["Transfer-Encoding"] = "chunked"
 564 		if self.compress:
 565 			mimetype = "application/x-gzip"
 566 		if mimetype:
 567 			headers["Content-Type"] = mimetype
 568 
 569 
 570 		# Open connection
 571 		http = httplib.HTTPConnection(host)
 572 		http.request("PUT", path, headers=headers)
 573 
 574 		t = time.time()
 575 
 576 		# Upload in chunks
 577 		chunksize = 1024*1024
 578 
 579 		while True:
 580 			try:
 581 				chunk = fio.read(chunksize)
 582 				cs = len(chunk)
 583 				http.send("%X\r\n"%(cs))
 584 				http.send(chunk)
 585 				http.send("\r\n")
 586 				self.report(progress=(fio.tell()/filesize))
 587 
 588 			except socket.error:
 589 				if fio: fio.close()
 590 				raise
 591 
 592 			if not chunk:
 593 				if fio: fio.close()
 594 				break
 595 
 596 		# Responses..
 597 		resp = http.getresponse()
 598 		status, reason, response = resp.status, resp.reason, resp.read()
 599 		http.close()
 600 
 601 		if status not in range(200, 300):
 602 			raise httplib.HTTPException, "Bad response code: %s: %s"%(status, reason)
 603 
 604 		kbsec = (filesize / (time.time() - t))/1024
 605 
 606 		if json:
 607 			response = json.loads(response)
 608 			self.bdo = response
 609 			self.recid = self.bdo.get("recid")
 610 			self.report("Done. Uploaded %s to record %s @ %0.2f kb/sec"%(self.filename, self.bdo.get("recid"), kbsec), i=1)
 611 			if self.newrecord:
 612 				self.recmap[tmprecid] = self.bdo.get("recid")
 613 
 614 		else:
 615 			self.report("Done. Uploaded %s @ %0.2f kb/sec"%(self.filename, kbsec), i=1)
 616 
 617 		return response
 618 
 619 
 620 
 621 
 622 class NewRecordTransport(Transport):
 623 	"""This class is similar to UploadTransport, but doesn't upload a file, just creates a record"""
 624 
 625 	def action(self, *args, **kwargs):
 626 		tmprecid = self.newrecord.get('recid')
 627 		newrec = self.db.putrecord(self.newrecord)
 628 		self.recmap[tmprecid] = newrec.get('recid')
 629 
 630 		# The return isn't a BDO, but it will do..
 631 		self.recid = newrec.get('recid')
 632 		self.bdo = {'recid': self.recid}
 633 
 634 
 635 
 636 
 637 ##################################
 638 # Base FileHandler
 639 ##################################
 640 
 641 class FileHandler(object):
 642 
 643 	request_params = []
 644 
 645 	def __init__(self, db=None, recid=None, filename=None, filedata=None, rectype=None, applyparams=None, pos=None, options=None, **kwargs):
 646 		"""Provide special support for different filetypes
 647 
 648 		@keyparam db DBProxy
 649 		@keyparam recid Record ID for download or upload
 650 		@keyparam filename Filename for upload
 651 		@keyparam rectype Record type for default-setting record creation
 652 		@keyparam applyparams Parameters to overlay on new records (see: CCDHandler)
 653 		@keyparam pos Display this FileHandler's position in the overall queue, tuple (position, total)
 654 		@keyparam options Options. See UploadController and DownloadController OptionParsers for details.
 655 		"""
 656 
 657 		self.db = db
 658 
 659 		# target recid for upload or download
 660 		self.recid = recid
 661 
 662 		# filename for upload..
 663 		self.filename = filename
 664 		self.filedata = filedata
 665 		self.bdos = []
 666 
 667 		# new records to apply to uploads
 668 		self.rectype = rectype
 669 		self.applyparams = applyparams or {}
 670 
 671 		# default and passed options
 672 		self.options = {
 673 			"recurse": -1,
 674 			"overwrite": False,
 675 			"rename": False,
 676 			"compress": True,
 677 		}
 678 
 679 		if options:
 680 			self.options.update(options)
 681 
 682 		if kwargs:
 683 			self.options.update(kwargs)
 684 
 685 		# internal attributes
 686 		self.tmpfiles = []
 687 		self.checked = False
 688 		self.quiet = False
 689 		self.pos = pos
 690 		self.progress = 0.0
 691 		self.listeners = []
 692 
 693 		self.init()
 694 
 695 
 696 	def init(self):
 697 		pass
 698 
 699 
 700 
 701 	######################
 702 	# Upload
 703 	######################
 704 
 705 	def upload(self):
 706 		"""Start the upload process for this handler"""
 707 
 708 		ret = collections.defaultdict(list)
 709 		recmap = {}
 710 
 711 		self.report(pos=self.pos, f=self.filename)
 712 		self.report("Preparing for upload", i=1)
 713 
 714 		self.prepare_upload()
 715 		files = self.get_upload_items()
 716 
 717 		self.report("Checking and committing records", i=1)
 718 
 719 
 720 		# Check records, call newrecord to properly init, validate, set permissions, etc.
 721 		# If a particular record doesn't validate, will throw exception
 722 		for f in files:
 723 			if f.newrecord:
 724 				f.newrecord = self._newrecordinit(newrecord=f.newrecord)
 725 								
 726 
 727 		# Upload other files/newrecords
 728 		filecount = len(files)
 729 		for count, f in enumerate(files):
 730 
 731 			f.setdb(self.db)
 732 			f.setreport(self.report)
 733 			f.setrecmap(recmap)
 734 
 735 			target_recid = f.recid
 736 
 737 			if filecount > 1:
 738 				f.pos = (count, filecount)
 739 
 740 			f.action()
 741 			recmap[target_recid] = f.recid
 742 			ret[f.filename] = f.bdo
 743 
 744 		self.cleanup()
 745 		return ret
 746 
 747 
 748 	def get_upload_items(self):
 749 		"""Returns records and files to upload"""
 750 		f = UploadTransport(recid=self.recid, filename=self.filename)
 751 		return [f]
 752 
 753 
 754 	def prepare_upload(self):
 755 		"""Do any required prep work before files can be uploaded"""
 756 		self.check_db()
 757 
 758 
 759 	def _newrecordinit(self, newrecord):
 760 		"""Initialize and validate a new record"""
 761 
 762 		tmprec = self.db.newrecord(newrecord.get('rectype'), self.recid)
 763 		tmprec.update(newrecord)
 764 		self.db.validaterecord(tmprec)
 765 		return tmprec
 766 
 767 
 768 
 769 	######################
 770 	# Download
 771 	######################
 772 
 773 	def download(self):
 774 		"""Start the download process for this handler"""
 775 
 776 		overwrite = self.options.get("overwrite")
 777 		rename = self.options.get("rename")
 778 		compress = self.options.get("compress")
 779 		sidecar = self.options.get("sidecar")
 780 
 781 		self.report(pos=self.pos, f=self.recid)
 782 		self.report("Checking for items to download", i=1)
 783 
 784 		recs, bdos = self.get_download_items()
 785 
 786 		bdolen = len(bdos)
 787 
 788 		if bdos:
 789 			self.report("Found %s items in %s records"%(bdolen, len(recs)), i=1)
 790 		else:
 791 			self.report("Found no downloadable items in %s records"%len(recs), i=1)
 792 			return
 793 
 794 		ret = {}
 795 
 796 		for count, bdo in enumerate(bdos):
 797 			u = DownloadTransport(db=self.db, bdo=bdo, report=self.report, compress=self.options.get("compress"), pos=(count, bdolen))
 798 			u.action(overwrite=overwrite, rename=rename, sidecar=sidecar)
 799 			ret[u.filename] = u.bdo
 800 
 801 		self.download_postprocess(recs, ret)
 802 
 803 		return ret
 804 
 805 
 806 
 807 	def download_postprocess(self, recs, ret):
 808 		pass
 809 
 810 
 811 
 812 	def get_download_items(self):
 813 		"""Find child recods of self.recid and get their BDOs"""
 814 
 815 		#recs = self.db.getchildren(self.recid, "record", self.options.get("recurse"), None, True, True)
 816 		recs = self.db.getchildren(self.recid, self.options.get("recurse"))
 817 		recs.append(self.recid)
 818 		bdos = self.db.getbinary(recs)
 819 		return recs, bdos
 820 
 821 
 822 
 823 	########################
 824 	# Status updates
 825 	########################
 826 
 827 	def addlistener(self, callback):
 828 		self.listeners.append(callback)
 829 
 830 
 831 	def report(self, msg=None, progress=None, f=None, pos=None, i=0):
 832 
 833 		for listener in self.listeners:
 834 			listener(msg=msg, progress=progress, f=f)
 835 
 836 		if self.quiet:
 837 			return
 838 
 839 		# Normally suppress this...
 840 		if progress != None and msg == None:
 841 			return
 842 
 843 		if not msg:
 844 			if pos:
 845 				print "\n%s%s of %s: %s"%("\t"*i, pos[0]+1, pos[1], f)
 846 			else:
 847 				print "\n%s%s"%("\t"*i, f)
 848 
 849 		else:
 850 			print "%s%s"%("\t"*i, msg)
 851 
 852 
 853 	########################
 854 	# Utility methods
 855 	########################
 856 
 857 
 858 	def _shortsleep(self):
 859 		time.sleep(10)
 860 
 861 
 862 	def check_db(self):
 863 		pass
 864 
 865 
 866 	########################
 867 	# Overrideable methods for upload control
 868 	########################
 869 
 870 	def cleanup(self):
 871 		for i in self.tmpfiles:
 872 			try:
 873 				self.report("Cleanup temp file: %s"%i, i=1)
 874 				os.unlink(i)
 875 			except Exception, e:
 876 				self.report("Warning: Could not remove %s: %s"%(i, e), i=1)
 877 
 878 
 879 
 880 
 881 
 882 
 883 #################################
 884 # Filetype-specific handlers
 885 ##################################
 886 
 887 
 888 class NewRecordFileHandler(FileHandler):
 889 	def get_upload_items(self):
 890 		newrecord = {}
 891 		newrecord["recid"] = -1
 892 		newrecord["rectype"] = self.rectype
 893 		newrecord["parents"] = [self.recid]
 894 		return [UploadTransport(newrecord=newrecord, filename=self.filename, param="file_binary_image")]
 895 
 896 
 897 
 898 class VolumeHandler(NewRecordFileHandler):
 899 	def get_upload_items(self):
 900 		newrecord = {}
 901 		newrecord["recid"] = -1
 902 		newrecord["title_structure"] = self.filename
 903 		newrecord["rectype"] = self.rectype
 904 		newrecord["parents"] = [self.recid]
 905 		return [UploadTransport(newrecord=newrecord, filename=self.filename, param="file_binary_image")]
 906 
 907 
 908 
 909 class BoxHandler(FileHandler):
 910 	def get_upload_items(self):
 911 		newrecord = {}
 912 		newrecord["recid"] = -1
 913 		newrecord["rectype"] = "box"
 914 		newrecord["parents"] = [self.recid]
 915 		newrecord.update(self.applyparams)
 916 		return [UploadTransport(newrecord=newrecord, filename=self.filename, filedata=self.filedata, compress=False, param="box")]
 917 
 918 
 919 
 920 class ParticleHandler(FileHandler):
 921 	def get_upload_items(self):
 922 		newrecord = {}
 923 		newrecord["recid"] = -1
 924 		newrecord["rectype"] = "particles"
 925 		newrecord["parents"] = [self.recid]
 926 		return [UploadTransport(newrecord=newrecord, filename=self.filename, compress=False, param="ptcl")]
 927 
 928 
 929 
 930 class GridImagingFileHandler(FileHandler):
 931 	def check_db(self):
 932 		gi = self.db.getrecord(self.recid)
 933 
 934 		if gi.get("rectype") != "grid_imaging":
 935 			raise Exception, "This action may only be used with grid_imaging sessions!"
 936 
 937 		microscopy = self.db.getparents(self.recid, 1, ["microscopy"])
 938 		if not microscopy:
 939 			self.report("\tWARNING! No microscopy record present for grid_imaging session!")
 940 			#raise Exception, "No microscopy record present for grid_imaging session!"
 941 
 942 
 943 
 944 class MicrographHandler(GridImagingFileHandler):
 945 	"""Creates a placeholder for a scan to be uploaded in the future"""
 946 
 947 	def get_upload_items(self):
 948 		newrecord = {}
 949 		newrecord["recid"] = -1
 950 		newrecord["rectype"] = "micrograph"
 951 		newrecord["id_micrograph"] = os.path.basename(self.filename)
 952 		newrecord.update(self.applyparams)
 953 		newrecord["parents"] = [self.recid]
 954 		return [NewRecordTransport(newrecord=newrecord, filename=self.filename)]
 955 	
 956 
 957 
 958 class JADASHandler(GridImagingFileHandler):
 959 	"""Creates a placeholder for a scan to be uploaded in the future"""
 960 
 961 
 962 	def get_upload_items(self):
 963 		# This is run for a .tif file produced by JADAS. Find the associated .xml files, load them, map as many
 964 		# parameters as possible, and attach the raw xml file.
 965 		jadas_params, foundfiles = self.load_jadas_xml()
 966 		newrecord = {}
 967 		newrecord["recid"] = -100
 968 		newrecord["rectype"] = "ccd_jadas"
 969 		newrecord["id_micrograph"] = os.path.basename(self.filename)
 970 		newrecord.update(self.applyparams)
 971 		newrecord.update(jadas_params)
 972 		newrecord["parents"] = [self.recid]
 973 		
 974 		files = [UploadTransport(recid=-100, filename=self.filename, param='file_binary_image')]
 975 		files[0].newrecord = newrecord
 976 		
 977 		for i in foundfiles:
 978 			files.append(UploadTransport(recid=-100, filename=i, compress=False))
 979 				
 980 		return files
 981 
 982 
 983 	def load_jadas_xml(self):
 984 		# find related XML files, according to JADAS naming conventions
 985 		# take off the .tif, because the xml could be either file.tif_metadata.xml or file_metadata.xml
 986 		foundfiles = []
 987 		ret = {}		
 988 		for xmlfile in glob.glob('%s_*.xml'%self.filename) + glob.glob('%s_*.xml'%self.filename.replace('.tif','')):
 989 			print "Attempting to load ", xmlfile
 990 			try:
 991 				e = ET.parse(xmlfile)
 992 				root = e.getroot()
 993 				# There should be a loader for each root tag type, e.g. TemParameter -> map_jadas_TemParameter
 994 				loader = getattr(self, 'map_jadas_%s'%root.tag, None)
 995 				if loader:
 996 					ret.update(loader(root))
 997 					foundfiles.append(xmlfile)
 998 			except Exception, e:
 999 				print "Could not load %s: %s"%(xmlfile, e)
1000 
1001 		return ret, foundfiles
1002 
1003 
1004 
1005 	def map_jadas_TemParameter(self, root):
1006 		"""One of these long, ugly, metadata-mapping methods"""
1007 		ret = {}
1008 		# Defocus
1009 		ret['defocus_absdac'] = root.find('Defocus/defocus').get('absDac')
1010 		ret['defocus_realphysval'] = root.find('Defocus/defocus').get('relPhisVal')
1011 		ret['intendeddefocus_valinnm'] = root.find('Defocus/intendedDefocus').get('valInNm')
1012 		d = root.find('Defocus/intendedDefocus').get('valInNm')
1013 		if d != None:
1014 			d = float(d) / 1000.0
1015 			ret['ctf_defocus_set'] = d
1016 
1017 		# Eos
1018 		ret['eos_brightdarkmode'] = root.find('Eos/eos').get('brightDarkMode')
1019 		ret['eos_darklevel'] = root.find('Eos/eos').get('darkLevel')
1020 		ret['eos_stiglevel'] = root.find('Eos/eos').get('stigLevel')
1021 		ret['eos_temasidmode'] = root.find('Eos/eos').get('temAsidMode')
1022 		ret['eos_htlevel'] = root.find('Eos/eos').get('htLevel')
1023 		ret['eos_imagingmode'] = root.find('Eos/eos').get('imagingMode')
1024 		ret['eos_magcamindex'] = root.find('Eos/eos').get('magCamIndex')
1025 		ret['eos_spectrummode'] = root.find('Eos/eos').get('spectrumMode')
1026 		ret['eos_illuminationmode'] = root.find('Eos/eos').get('illuminationMode')
1027 		ret['eos_spot'] = root.find('Eos/eos').get('spot')
1028 		ret['eos_alpha'] = root.find('Eos/eos').get('alpha')
1029 
1030 		# Lens
1031 		ret['lens_cl1dac'] = root.find('Lens/lens').get('cl1Dac')
1032 		ret['lens_cl2dac'] = root.find('Lens/lens').get('cl2Dac')
1033 		ret['lens_cl3dac'] = root.find('Lens/lens').get('cl3Dac')
1034 		ret['lens_cmdac'] = root.find('Lens/lens').get('cmDac')
1035 		ret['lens_il1dac'] = root.find('Lens/lens').get('il1Dac')
1036 		ret['lens_il2dac'] = root.find('Lens/lens').get('il2Dac')
1037 		ret['lens_il3dac'] = root.find('Lens/lens').get('il3Dac')
1038 		ret['lens_il4dac'] = root.find('Lens/lens').get('il4Dac')
1039 		ret['lens_pl1dac'] = root.find('Lens/lens').get('pl1Dac')
1040 		ret['lens_pl2dac'] = root.find('Lens/lens').get('pl2Dac')
1041 		ret['lens_pl3dac'] = root.find('Lens/lens').get('pl3Dac')
1042 		
1043 		# Def
1044 		ret['def_gunshiftx'] = root.find('Def/def').get('gunShiftX')
1045 		ret['def_gunshifty'] = root.find('Def/def').get('gunShiftY')
1046 		ret['def_guntiltx'] = root.find('Def/def').get('gunTiltX')
1047 		ret['def_guntilty'] = root.find('Def/def').get('gunTiltY')
1048 		ret['def_beamshiftx'] = root.find('Def/def').get('beamShiftX')
1049 		ret['def_beamshifty'] = root.find('Def/def').get('beamShiftY')
1050 		ret['def_beamtiltx'] = root.find('Def/def').get('beamTiltX')
1051 		ret['def_beamtilty'] = root.find('Def/def').get('beamTiltY')			
1052 		ret['def_clstigx'] = root.find('Def/def').get('clStigX')
1053 		ret['def_clstigy'] = root.find('Def/def').get('clStigY')
1054 		ret['def_olstigx'] = root.find('Def/def').get('olStigX')
1055 		ret['def_olstigy'] = root.find('Def/def').get('olStigY')
1056 		ret['def_ilstigx'] = root.find('Def/def').get('ilStigX')
1057 		ret['def_ilstigy'] = root.find('Def/def').get('ilStigY')
1058 		ret['def_imageshiftx'] = root.find('Def/def').get('imageShiftX')
1059 		ret['def_imageshifty'] = root.find('Def/def').get('imageShiftY')
1060 		ret['def_plax'] = root.find('Def/def').get('plaX')
1061 		ret['def_play'] = root.find('Def/def').get('plaY')
1062 		
1063 		# HT
1064 		ret['ht_ht'] = root.find('HT/ht').get('ht')
1065 		ret['ht_energyshift'] = root.find('HT/ht').get('energyShift')
1066 		
1067 		# MDS
1068 		ret['mds_mdsmode'] = root.find('MDS/mds').get('mdsMode')
1069 		ret['mds_blankingdef'] = root.find('MDS/mds').get('blankingDef')
1070 		ret['mds_defx'] = root.find('MDS/mds').get('defX')
1071 		ret['mds_defy'] = root.find('MDS/mds').get('defY')
1072 		ret['mds_blankingtype'] = root.find('MDS/mds').get('blankingType')
1073 		ret['mds_blankingtime'] = root.find('MDS/mds').get('blankingTime')
1074 		ret['mds_shutterdelay'] = root.find('MDS/mds').get('shutterDelay')
1075 		
1076 		# Photo
1077 		ret['photo_exposuremode'] = root.find('PHOTO/photo').get('exposureMode')
1078 		ret['photo_manualexptime'] = root.find('PHOTO/photo').get('manualExpTime')
1079 		ret['photo_filmtext'] = root.find('PHOTO/photo').get('filmText')
1080 		ret['photo_filmnumber'] = root.find('PHOTO/photo').get('filmNumber')
1081 		
1082 		# GonioPos
1083 		ret['goniopos_x'] = root.find('GonioPos/gonioPos').get('x')
1084 		ret['goniopos_y'] = root.find('GonioPos/gonioPos').get('y')
1085 		ret['goniopos_z'] = root.find('GonioPos/gonioPos').get('z')
1086 		ret['goniopos_tiltx'] = root.find('GonioPos/gonioPos').get('tiltX')
1087 		ret['goniopos_rotortilty'] = root.find('GonioPos/gonioPos').get('rotOrTiltY')
1088 
1089 		return ret
1090 		
1091 		
1092 		
1093 	def map_jadas_DigitalCameraParameter(self, root):
1094 		attrmap = {
1095 			'CameraName': 'ccd_id',
1096 			'AreaTop': 'digicamprm_areatop',
1097 			'AreaBottom': 'digicamprm_areabottom',
1098 			'AreaLeft': 'digicamprm_arealeft',
1099 			'AreaRight': 'digicamprm_arearight',
1100 			'Exposure': 'time_exposure_tem',
1101 			'Binning': 'binning',
1102 			'PreIrradiation': 'digicamcond_preirradiation',
1103 			'BlankingTime': 'digicamcond_blankingtime',
1104 			'BlankBeam': 'digicamcond_blankbeam',
1105 			'CloseScreen': 'digicamcond_closescreen',
1106 			'DataFormat': 'digicamcond_dataformat'		
1107 		}
1108 
1109 		ret = {}
1110 		for i in root.findall('*/tagCamPrm'):
1111 			param = attrmap.get(i.get('tagAttrName'))
1112 			value = i.get('tagAttrVal')
1113 			if param != None and value != None:
1114 				ret[param] = value		
1115 
1116 		return ret
1117 		
1118 		
1119 		
1120 	def map_jadas_IntensityBasedHoleSelection(self, root):
1121 		ret = {}
1122 		return ret
1123 		
1124 			
1125 
1126 
1127 class CCDHandler(GridImagingFileHandler):
1128 
1129 	request_params = [
1130 		'tem_magnification_set',
1131 		'ctf_defocus_set',
1132 		'angstroms_per_pixel',
1133 		'time_exposure_tem',
1134 		'tem_dose_rate',
1135 		'current_screen',
1136 		'ice_type',
1137 		'assess_ice_thick',
1138 		'assess_ice_comments',
1139 		'assess_image_quality',
1140 		'ccd_id',
1141 		'status_energy_filter',
1142 		'binning'
1143 		]
1144 
1145 
1146 	def get_upload_items(self):
1147 		newrecord = {}
1148 		newrecord["recid"] = -1
1149 		newrecord["rectype"] = "ccd"
1150 		newrecord["id_ccd_frame"] = os.path.basename(self.filename)
1151 		newrecord.update(self.applyparams)
1152 		newrecord["parents"] = [self.recid]
1153 		# read metadata
1154 		return [UploadTransport(newrecord=newrecord, filename=self.filename, param="file_binary_image")]
1155 
1156 
1157 
1158 
1159 class ScanHandler(GridImagingFileHandler):
1160 
1161 	request_params = [
1162 		'scanner_film',
1163 		'scanner_cartridge',
1164 		'scan_average',
1165 		'nikon_gain',
1166 		'scan_step',
1167 		'angstroms_per_pixel'
1168 		]
1169 
1170 
1171 	def get_upload_items(self):
1172 		print "\tChecking for existing micrograph..."
1173 		idmap = collections.defaultdict(set)
1174 		mc = self.db.getchildren(self.recid, 1, "micrograph")
1175 		mc = self.db.getrecord(mc)
1176 		for rec in mc:
1177 			i = rec.get('id_micrograph', '').strip().lower()
1178 			idmap[i].add(rec.get('recid'))
1179 
1180 
1181 
1182 		# Ugly.
1183 		match = self.filename.split(".")[0].strip().lower()
1184 		matches = idmap[match]
1185 
1186 		if len(idmap[match]) == 0:
1187 			print "\tCould not find micrograph for %s -- creating new micrograph."%match
1188 
1189 			mrec = {}
1190 			mrec["recid"] = -1
1191 			mrec["rectype"] = "micrograph"
1192 			mrec["parents"] = [self.recid]
1193 			mrec["id_micrograph"] = match
1194 
1195 			newrecord = {}
1196 			newrecord["recid"] = -2
1197 			newrecord["rectype"] = self.rectype
1198 			newrecord["parents"] = [-1]
1199 			newrecord.update(self.applyparams)
1200 
1201 			m = NewRecordTransport(newrecord=mrec)
1202 			s = UploadTransport(newrecord=newrecord, filename=self.filename, param="file_binary_image")
1203 
1204 			sidecar = s.sidecar_read()
1205 			if sidecar:
1206 				print "\tThis scan already appears to be uploaded! Check record ID %s"%sidecar.get('recid')
1207 				return []
1208 
1209 			return [m, s]
1210 
1211 
1212 		elif len(idmap[match]) == 1:
1213 			matches = matches.pop()
1214 			print "\tFound match for %s: %s"%(match, matches)
1215 			newrecord = {}
1216 			newrecord["recid"] = -1
1217 			newrecord["rectype"] = self.rectype
1218 			newrecord["parents"] = [matches]
1219 			newrecord.update(self.applyparams)
1220 
1221 			return [UploadTransport(newrecord=newrecord, filename=self.filename, param="file_binary_image")]
1222 
1223 
1224 		elif len(idmap[match]) > 1:
1225 			print "\tAmbiguous matches for %s: %s -- skipping"%(match, matches)
1226 			return []
1227 
1228 
1229 
1230 
1231 
1232 class StackHandler(GridImagingFileHandler):
1233 
1234 	# See notes at bottom of file
1235 	header_labels = [
1236 		# X,Y,Z size
1237 		['stack_size_nx', 'i', 0],
1238 		['stack_size_ny', 'i', 0],
1239 		['stack_size_nz', 'i', 0],
1240 		## Mode: mode, 0 = unsigned byte, 1 = short int, 2 = float, 3 = short*2, 4 = float*2
1241 		['stack_data_mode', 'i', 0],
1242 		## Starting point of sub image: nxstart, nystart, nzstart
1243 		['stack_start_nx', 'i', 0],
1244 		['stack_start_ny', 'i', 0],
1245 		['stack_start_nz', 'i', 0],
1246 		## Grid Size: mx, my, mz
1247 		['stack_size_mx', 'i', 0],
1248 		['stack_size_my', 'i', 0],
1249 		['stack_size_mz', 'i', 0],
1250 		## Cell size: xlen, ylen, zlen... pixel spacing = xlen/mx
1251 		['stack_size_xlen', 'i', 0],
1252 		['stack_size_ylen', 'i', 0],
1253 		['stack_size_zlen', 'i', 0],
1254 		## Cell angles: alpha, beta, gamma
1255 		['stack_angle_alpha', 'f', 0.0],
1256 		['stack_angle_beta', 'f', 0.0],
1257 		['stack_angle_gamma', 'f', 0.0],
1258 		## Map column. Ignored by IMOD: mapc, mapr, maps. (1=x, 2=y, 3=z)
1259 		['stack_map_mapc', 'i', 0],
1260 		['stack_map_mapr', 'i', 0],
1261 		['stack_map_maps', 'i', 0],
1262 		## Minimum pixel value, amin
1263 		['stack_pixel_min', 'f', 0.0],
1264 		## Maximum pixel value, amax
1265 		['stack_pixel_max', 'f', 0.0],
1266 		## Mean pixel value, amean
1267 		['stack_pixel_mean', 'f', 0.0],
1268 		## Image type, ispg
1269 		['stack_data_ispg', 'h', 0],
1270 		## Space group number, nsymbt
1271 		['stack_data_nsymbt', 'h', 0],
1272 		## Number of bytes in extended header, next
1273 		['stack_data_extheadersize', 'i', 0],
1274 		## Creator ID, creatid
1275 		['stack_data_creatid', 'h', 0],
1276 		# 30 bytes of unused data: See explanation above
1277 		['stack_unused', '30s', ''],
1278 		## Number of bytes per section (SerialEM format), nint
1279 		['stack_data_bytespersection', 'h', 0],
1280 		## Flags for which types of short data (SerialEM format), nreal. (See above documentation.)
1281 		['stack_data_extheaderflags', 'h', 0],
1282 		# 28 bytes of unused data: See explanation above
1283 		['stack_unused2', '28s', ''],
1284 		['stack_data_idtype', 'h', 0],
1285 		['stack_data_lens', 'h', 0],
1286 		['stack_data_nd1', 'h', 0],
1287 		['stack_data_nd2', 'h', 0],
1288 		['stack_data_vd1', 'h', 0],
1289 		['stack_data_vd2', 'h', 0],
1290 		['stack_data_tiltangles_orig', 'fff', [0.0, 0.0, 0.0]],
1291 		['stack_data_tiltangles_current', 'fff', [0.0, 0.0, 0.0]],
1292 		['stack_data_xorg', 'f', 0.0],
1293 		['stack_data_yorg', 'f', 0.0],
1294 		['stack_data_zorg', 'f', 0.0],
1295 		['stack_data_cmap', '4s', ''],
1296 		# big/little endian flag
1297 		['stack_data_stamp', '4s', ''],
1298 		['stack_data_rms', 'f', 0.0],
1299 		['stack_data_nlabl', 'i', 0],
1300 		# spliced seperately: see after
1301 		['stack_data_labels', '80s'*10, '']
1302 	]
1303 
1304 
1305 	extheader_flags = {
1306 		1: {
1307 			'pack': 'h',
1308 			'load': lambda x:[x[0] / 100.0],
1309 			'dump': lambda x:[x[0] * 100],
1310 			'dest': ['specimen_tilt'],
1311 			},
1312 
1313 		2: {
1314 			'pack': 'hhh',
1315 			'load': lambda x:[x],
1316 			'dump': lambda x:[x],
1317 			'dest': ['stack_data_montage']
1318 			},
1319 
1320 		4: {
1321 			'pack': 'hh',
1322 			'load': lambda x:[x[0] / 25.0 , x[1] / 25.0],
1323 			'dump': lambda x:[x[0] * 25   , x[1] * 25],
1324 			'dest': ['stack_stagepos_x', 'stack_stagepos_y']
1325 			},
1326 
1327 		8: {
1328 			'pack': 'h',
1329 			'load': lambda x:[x[0] / 10.0],
1330 			'dump': lambda x:[x[0] * 10],
1331 			'dest': ['tem_magnification_set']
1332 			},
1333 
1334 		16: {
1335 			'pack': 'h',
1336 			'load': lambda x:[x[0] / 25000.0],
1337 			'dump': lambda x:[x[0] * 25000],
1338 			'dest': ['stack_intensity']
1339 			},
1340 
1341 		32: {
1342 			'pack': 'hh',
1343 			'load': lambda x:[sign(x[0])*(math.fabs(x[0])*256+(math.fabs(x[1])%256))*2**(sign(x[1])*(int(math.fabs(x[1]))/256))],
1344 			'dump': lambda x:[0,0],
1345 			'dest': ['stack_dose']
1346 			}
1347 	}
1348 
1349 
1350 
1351 	def download_postprocess(self, recids, ret):
1352 
1353 		rec = self.db.getrecord(self.recid)
1354 		print "Rebuilding .st file. It is generally a good idea to run this download in a new directory."
1355 
1356 		workfile = rec.get('stack_filename')
1357 		if os.access(workfile, os.F_OK):
1358 			raise ValueError, "File %s already exists -- not going to overwrite. Exiting."
1359 
1360 		f = open(workfile, "wb")
1361 
1362 		print "Making header"
1363 		a = self._make_header(rec)
1364 		f.write(a)
1365 
1366 		print "Making extheader"
1367 		slices = self.db.getrecord(sorted(self.db.getchildren(self.recid, 1, "stackimage"), reverse=True))
1368 
1369 
1370 		hl = rec.get('stack_data_extheaderflags')
1371 		hs = rec.get('stack_data_extheadersize')
1372 		b = self._make_extheader(slices, stack_data_extheaderflags=hl)
1373 		padsize = hs - len(b)
1374 		f.write(b)
1375 		print "\t...size was %s, padding by %s to %s / %s"%(len(b), padsize, len(b)+padsize, hs)
1376 		f.write("\0"*padsize)
1377 
1378 		print "Adding images to stack"
1379 
1380 		reti = {}
1381 		for k,v in ret.items():
1382 			reti[v.get("recid")] = k
1383 
1384 		for rec in slices:
1385 			fn = reti.get(rec.get("recid"))
1386 			print fn
1387 			fnopen = open(fn, "rb")
1388 			fnopen.seek(1024)
1389 			data = fnopen.read()
1390 			f.write(data)
1391 			print "\t...wrote %s bytes"%len(data)
1392 			fnopen.close()
1393 			os.unlink(fn)
1394 
1395 		f.close()
1396 
1397 
1398 
1399 	def init(self):
1400 		pass
1401 
1402 
1403 	def prepare_upload(self):
1404 		try:
1405 			self.header = self.get_header()
1406 			self.slices = self.get_extheader()
1407 			self.update_maxangles()
1408 			self.header['stack_filename'] = self.filename
1409 		except Exception, inst:
1410 			raise ValueError, "This does not appear to be a SerielEM stack: %s"%inst
1411 
1412 
1413 		# Extract headers for backup, unstack file and compress, add to tmpfiles to be removed after upload
1414 		self.check_db()
1415 
1416 		self.tmpdir = tempfile.mkdtemp()
1417 
1418 		self.raw_headers = self.backup_headers()
1419 		self.tmpfiles.extend(self.raw_headers)
1420 
1421 		self.unstacked_files = self.unstack()
1422 		self.tmpfiles.extend(self.unstacked_files)
1423 
1424 
1425 	def get_upload_items(self):
1426 		"""Get basic records and files to put in database. These will be filled out more fully with db.newrecord, etc."""
1427 
1428 		stackrec = copy.copy(self.header)
1429 		stackrec["recid"] = -100
1430 		stackrec["parents"] = [self.recid]
1431 
1432 		files = [UploadTransport(recid=-100, filename=i, compress=False) for i in self.raw_headers]
1433 		files[0].newrecord = stackrec
1434 
1435 		for offset, (s, s_filename) in enumerate(zip(self.slices, self.unstacked_files)):
1436 			s["recid"] = -(offset+101)
1437 			s["parents"] = [-100]
1438 			files.append(UploadTransport(newrecord=s, filename=s_filename, param="file_binary_image"))
1439 
1440 		return files
1441 
1442 
1443 	########
1444 	# Stack specific methods, mostly header related
1445 
1446 	def update_maxangles(self):
1447 		if self.slices[0].has_key("specimen_tilt"):
1448 			tilts = [i['specimen_tilt'] for i in self.slices]
1449 
1450 		self.header["stack_maxangle"] = max(tilts)
1451 		self.header["stack_minangle"] = min(tilts)
1452 
1453 
1454 	def get_header(self):
1455 		"""Read an IMOD/SerialEM/MRC stack header"""
1456 		f = open(self.filename,"rb")
1457 		h = f.read(1024)
1458 		f.close()
1459 
1460 		return self._handle_header(h)
1461 
1462 
1463 	def get_extheader(self):
1464 		if not self.header:
1465 			self.header = self.get_header()
1466 
1467 		if not self.header["stack_data_extheadersize"]:
1468 			return []
1469 
1470 		f = open(self.filename, "rb")
1471 		h = f.read(1024)
1472 		eh = f.read(self.header["stack_data_extheadersize"])
1473 		f.close()
1474 
1475 		return self._handle_extheader(eh)
1476 
1477 
1478 	def backup_headers(self):
1479 		fin = open(self.filename, 'rb')
1480 		raw_header = fin.read(1024)
1481 		raw_extheader = fin.read(self.header["stack_data_extheadersize"])
1482 		fin.close()
1483 
1484 		ret = []
1485 
1486 		header_outfile = "%s/%s.header"%(self.tmpdir, os.path.basename(self.filename))
1487 
1488 		fout = open(header_outfile, 'wb')
1489 		fout.write(raw_header)
1490 		fout.close()
1491 		ret.append(header_outfile)
1492 
1493 		if raw_extheader:
1494 			extheader_outfile = "%s/%s.header"%(self.tmpdir, os.path.basename(self.filename))
1495 			fout = open(extheader_outfile, 'wb')
1496 			fout.write(raw_extheader)
1497 			fout.close()
1498 			ret.append(extheader_outfile)
1499 
1500 		return ret
1501 
1502 
1503 	def unstack(self, slices=None):
1504 		"""Unstack slices into new files.
1505 		@keyparam slices Single slice or list of slices to unstack; default is all.
1506 		@return Filenames of unstacked files
1507 		"""
1508 
1509 		raw_header = self._make_unstacked_header()
1510 		header_offset, size = self._get_offset_and_size()
1511 		fstack = open(self.filename, "rb")
1512 
1513 		ret = []
1514 
1515 		if slices and not hasattr(slices, "__iter__"):
1516 			slices = [slices]
1517 		if not slices:
1518 			slices = range(len(self.slices))
1519 
1520 		for n in slices:
1521 			read_offset = header_offset + (size*n)
1522 
1523 			fout_name = "%s/%s.%s.mrc"%(self.tmpdir, os.path.basename(self.filename), n)
1524 
1525 			self.report("Unstacking frame %s into %s"%(n, fout_name), i=1)
1526 
1527 			# check_filenames = [fout_name]
1528 			# check_filenames.append(fout_name+".gz")
1529 			# if not filter(None, map(os.path.exists, check_filenames)):
1530 
1531 			#ret.append(fout_name)
1532 			#continue
1533 
1534 			fout = open(fout_name, "wb")
1535 			fout.write(raw_header)
1536 			fstack.seek(read_offset)
1537 
1538 			blen = 1024*1024*8
1539 			for buffer_size in [blen]*(size/blen)+[size%blen]:
1540 				copy_buffer = fstack.read(buffer_size)
1541 				fout.write(copy_buffer)
1542 
1543 			fout.close()
1544 
1545 			ret.append(fout_name)
1546 
1547 		fstack.close()
1548 		return ret
1549 
1550 
1551 	def stack(self, outfile=None):
1552 		pass
1553 
1554 
1555 	def _get_offset_and_size(self):
1556 		# 4    int     mode;      Types of pixel in image.  Values used by IMOD:
1557 		# 	 0 = unsigned bytes,
1558 		# 	 1 = signed short integers (16 bits),
1559 		# 	 2 = float,
1560 		# 	 3 = short * 2, (used for complex data)
1561 		# 	 4 = float * 2, (used for complex data)
1562 		#                          6 = unsigned 16-bit integers (non-standard)
1563 		# 	16 = unsigned char * 3 (for rgb data, non-standard)
1564 
1565 		header_offset = 1024 + self.header["stack_data_extheadersize"]
1566 		depth = 2
1567 		dmode = self.header['stack_data_mode']
1568 		if dmode in [0]:
1569 			depth = 1
1570 		elif dmode in [2, 3]:
1571 			depth = 4
1572 		elif dmode in [4]:
1573 			depth = 8
1574 		elif dmode in [16]:
1575 			depth = 3
1576 
1577 		slicesize = self.header["stack_size_nx"] * self.header["stack_size_ny"] * depth
1578 
1579 		return header_offset, slicesize
1580 
1581 
1582 	def _handle_header(self, h):
1583 		"""Extract data from header string (1024 bytes) and process"""
1584 
1585 		d={}
1586 		offset = 0
1587 
1588 		for dest, format, default in self.header_labels:
1589 			size = struct.calcsize(format)
1590 			value = struct.unpack(format, h[offset:offset+size])
1591 			if len(value) == 1: d[dest] = value[0]
1592 			else: d[dest] = value
1593 			offset += size
1594 
1595 		# Process data labels
1596 		n = d["stack_data_nlabl"]
1597 		d["stack_data_labels"] = [i.strip() for i in d["stack_data_labels"][:n]]+[""]*(10-n)
1598 
1599 		# Include rectype
1600 		d["rectype"] = "stack"
1601 		d["recid"] = -1
1602 
1603 		# This guy gives us trouble
1604 		d['stack_data_stamp'] = d['stack_data_stamp'].partition('\x00')[0]
1605 
1606 		# Delete unused items
1607 		try:
1608 			del d["stack_unused"]
1609 			del d["stack_unused2"]
1610 		except:
1611 			pass
1612 
1613 		return d
1614 
1615 
1616 	def _make_unstacked_header(self):
1617 
1618 		h = copy.deepcopy(self.header)
1619 		for i in ['stack_size_nz', 'stack_size_zlen']:
1620 			h[i] = 1
1621 		for i in ['stack_data_extheadersize', 'stack_data_bytespersection', 'stack_data_extheaderflags']:
1622 			h[i] = 0
1623 
1624 		rawheader = self._make_header(h)
1625 		return rawheader
1626 
1627 
1628 	def _make_header(self, header):
1629 		rawheader = []
1630 
1631 		header["stack_unused"] = ""
1632 		header["stack_unused2"] = ""
1633 
1634 		for dest, format, default in self.header_labels:
1635 			value = header.get(dest, default)
1636 			size = struct.calcsize(format)
1637 			if hasattr(value,"__iter__"):
1638 				value = struct.pack(format, *value)
1639 			else:
1640 				value = struct.pack(format, value)
1641 			# print dest, format, header.get(dest), value
1642 			rawheader.append(value)
1643 
1644 		return "".join(rawheader)
1645 
1646 
1647 	def _make_extheader(self, slices, stack_data_extheaderflags=0):
1648 		eh = []
1649 
1650 		# ian: todo: calculate keys based on info in DB instead of extheaderflags
1651 		keys = self._extheader_getkeys(stack_data_extheaderflags)
1652 
1653 		for s in slices:
1654 			for key in keys:
1655 				_k = self.extheader_flags.get(key)
1656 
1657 				value = [s[i] for i in _k['dest']]
1658 				value = _k['dump'](value)
1659 				if hasattr(value, "__iter__"):
1660 					value = struct.pack(_k['pack'], *value)
1661 				else:
1662 					value = struct.pack(_k['pack'], value)
1663 
1664 				# print key, _k, [s[i] for i in _k['dest']]
1665 				eh.append(value)
1666 
1667 		return "".join(eh)
1668 
1669 
1670 	def _handle_extheader(self, eh):
1671 		"""Process extended header"""
1672 
1673 		d = self.header
1674 		ed = []
1675 		keys = self._extheader_getkeys(d["stack_data_extheaderflags"])
1676 
1677 		offset = 0
1678 		for i in range(0, d["stack_size_nz"]):
1679 			sslice = {}
1680 			for key in keys:
1681 				_k = self.extheader_flags.get(key)
1682 				size = struct.calcsize(_k['pack'])
1683 				# print "Consuming %s bytes (%s:%s) for %s"%(size, i+offset, i+offset+size, _k['dest'])
1684 				value = struct.unpack(_k['pack'], eh[offset: offset+size])
1685 				value = _k['load'](value)
1686 
1687 				for _i in zip(_k['dest'], value):
1688 					sslice[_i[0]] = _i[1]
1689 
1690 				offset += size
1691 
1692 			sslice["rectype"] = "stackimage"
1693 
1694 			ed.append(sslice)
1695 
1696 		return ed
1697 
1698 
1699 	def _extheader_getkeys(self, flags):
1700 		keys = []
1701 		for i in sorted(self.extheader_flags.keys()):
1702 			if flags & i:
1703 				keys.append(i)
1704 		return keys
1705 
1706 
1707 
1708 
1709 
1710 ##################################
1711 # Application controllers
1712 ##################################
1713 
1714 
1715 class EMEN2ClientController(object):
1716 	def __init__(self, args=None, options=None):
1717 		"""EMEN2 Client. DBRPCProxy available as 'db' attr after login."""
1718 		self.options = options
1719 		self.args = None
1720 		self.parser = None
1721 
1722 		self.db = None
1723 
1724 		self.setparser()
1725 		if args != None:
1726 			self.parse_args(args)
1727 
1728 
1729 	def setparser(self):
1730 		self.parser = DBRPCOptions()
1731 		self.setparser_add_option()
1732 
1733 
1734 	def setparser_add_option(self):
1735 		pass
1736 
1737 
1738 	def parse_args(self, inargs):
1739 		self.options, self.args = self.parser.parse_args(inargs)
1740 		self.check_args()
1741 
1742 
1743 	def check_args(self):
1744 		pass
1745 
1746 
1747 	def run(self):
1748 		pass
1749 
1750 
1751 	def login(self):
1752 		self.options.username = self.options.username or raw_input("Username: ")
1753 		self.options.password = self.options.password or getpass.getpass("Password: ")
1754 
1755 		if self.options.json:
1756 			self.db = DBJSONRPCProxy(host=self.options.host)
1757 		else:
1758 			self.db = DBXMLRPCProxy(host=self.options.host)
1759 		self.checkversion()
1760 		try:
1761 			self.ctxid = self.db._login(username=self.options.username, password=self.options.password)
1762 		except Exception, e:
1763 			print "Unable to login: %s"%(e)
1764 			sys.exit(0)
1765 
1766 
1767 	def checkversion(self):
1768 
1769 		v = self.db.checkversion("emen2client")
1770 
1771 		if distutils.version.StrictVersion(__version__) < distutils.version.StrictVersion(v):
1772 			print """
1773 Note: emen2client version %s is available; installed version is %s.
1774 
1775 EMAN2 now includes emen2client.py, so you may download a new nightly build of EMAN2,
1776 	http://ncmi.bcm.edu/ncmi/
1777 
1778 ...or visit the EMEN2 wiki and download emen2client.py directly:
1779 	http://blake.grid.bcm.edu/emanwiki/EMEN2/emen2client.py
1780 
1781 """%(v, __version__)
1782 
1783 		else:
1784 			print "emen2client version %s is up to date"%(__version__)
1785 
1786 
1787 
1788 
1789 
1790 
1791 class UploadController(EMEN2ClientController):
1792 	def setparser_add_option(self):
1793 		self.parser.add_option("--noninteractive","-q", action="store_true", help="Do not prompt for parameter values")
1794 		self.parser.add_option("--force", "-f", action="store_true", help="Force re-upload even if a sidecar is found")
1795 		self.parser.add_option("--metafile", action="store_true", dest="metafile", help="Attempt to read JAMES/JADAS metadata files (default)", default=True)
1796 		self.parser.add_option("--no-metafile", action="store_false", dest="metafile", help="Ignore metadata files")
1797 
1798 		usage = """%prog upload [options] <record type> <recid> <files to upload>
1799 
1800 Record type can be any valid database protocol.
1801 Some record types have special, application-specific handlers, e.g.:
1802 
1803 	ccd			CCD Frames
1804 	scan		Scanned micrographs
1805 	stack		Tomograms
1806 
1807 Other values, e.g. "volume", will create child records of that type, with 1 file per child record.
1808 
1809 Alternatively, you can use "none" for record type and the files will be attached directly to the specified record ID.
1810 
1811 		"""
1812 
1813 		self.parser.set_usage(usage)
1814 
1815 
1816 	def upload(self):
1817 		if self.action not in map_rectype_handler:
1818 			try:
1819 				rd = self.db.getrecorddef(self.action)
1820 			except:
1821 				print "Invalid record type: %s"%self.action
1822 				return
1823 
1824 
1825 		print ""
1826 		print "%s Files to upload:"%len(self.filenames)
1827 		for i in self.filenames:
1828 			print "\t",i
1829 
1830 		handlerclass = gethandler(rectype=self.action) # or NewRecordFileHandler
1831 		applyparams = self._get_param_values(handlerclass.request_params)
1832 
1833 		# There isn't a dict(options)...
1834 		options = {}
1835 		for k,v in self.options.__dict__.items():
1836 			options[k]=v
1837 
1838 
1839 		dbts = []
1840 		filecount = len(self.filenames)
1841 		for count, filename in enumerate(self.filenames):
1842 			dbt = handlerclass(
1843 				db=self.db,
1844 				rectype=self.action,
1845 				filename=filename,
1846 				recid=self.recid,
1847 				applyparams=applyparams,
1848 				pos=(count,filecount),
1849 				options=options
1850 			)
1851 			dbt.upload()
1852 
1853 
1854 
1855 	def check_args(self):
1856 		self.action = self.args[0]
1857 
1858 		try:
1859 			self.recid = int(self.args[1])
1860 		except:
1861 			raise Exception, "Record ID required as argument after upload action"
1862 
1863 
1864 		self.filenames = reduce(operator.concat, map(glob.glob, self.args[2:]))
1865 
1866 		if not self.filenames:
1867 			raise Exception, "No files specified"
1868 
1869 
1870 	def run(self):
1871 		self.login()
1872 		self.upload()
1873 
1874 
1875 	#########################
1876 	# Ugly console input code
1877 	#########################
1878 
1879 	def _get_param_values(self, params):
1880 
1881 		ret = {}
1882 		if self.options.noninteractive:
1883 			return ret
1884 
1885 		pds = self.db.getparamdef(params)
1886 		pds = dict([[i.get('name'), i] for i in pds])
1887 
1888 
1889 		for param in params:
1890 			pd = pds.get(param)
1891 			ret[param] = self._get_param_value_console(pd)
1892 		return ret
1893 
1894 
1895 
1896 	def _get_param_value_console(self, param):
1897 		name = param.get('name')
1898 
1899 		print "\n----- %s -----"%name
1900 		print "\tDescription: %s"%param.get('desc_long')
1901 		if param.get('defaultunits'):
1902 			print "\tUnits: %s"%param.get('defaultunits')
1903 
1904 
1905 		punits = param.get('defaultunits') or ''
1906 		lines = None
1907 		selections = []
1908 
1909 		try:
1910 			lines = [i[0] for i in self.db.findvalue(name, '', True, False, 5)]
1911 		except Exception, inst:
1912 			print inst
1913 			pass
1914 
1915 		count = 0
1916 		if param.get('choices'):
1917 			print "\n\tSuggested values:"
1918 			for choice in param.get('choices', []):
1919 				print "\t\t%s) %s"%(count, choice)
1920 				selections.append(choice)
1921 				count += 1
1922 
1923 		if param.get('vartype') != "choice" and lines:
1924 			if param.get('choices'):
1925 				print "\n\tOther common values:"
1926 			else:
1927 				print "\n\tCommon values:"
1928 
1929 			for line in lines:
1930 				print "\t\t%s) %s"%(count, line)
1931 				selections.append(line)
1932 				count += 1
1933 
1934 		print "\n\t\t%s) None or N/A"%count
1935 		selections.append('')
1936 		count += 1
1937 
1938 		if param.get('vartype') != "choice":
1939 			print "\t\t%s) Enter a different not listed above"%count
1940 
1941 
1942 		while True:
1943 			inp = raw_input("\n\tSelection (0-%s): "%count)
1944 			try:
1945 				inp = int(inp)
1946 
1947 				if inp == count:
1948 					inp = raw_input("\tValue: ")
1949 				else:
1950 					inp = selections[inp]
1951 			except:
1952 				continue
1953 
1954 			break
1955 
1956 		return inp
1957 
1958 
1959 
1960 
1961 
1962 class DownloadController(EMEN2ClientController):
1963 
1964 	def setparser_add_option(self):
1965 		self.parser.add_option("--recurse", type="int",help="Recursion level",default=3)
1966 		self.parser.add_option("--overwrite", "-o", action="store_true", help="Overwrite existing files (default is to skip)", default=False)
1967 		self.parser.add_option("--rename", "-r", action="store_true", help="If a file already exists, save with format 'duplicate.recid:original_filename.dm3'", default=False)
1968 		self.parser.add_option("--sidecar", "-s", action="store_true", help="Include sidecar file with EMEN2 metadata in JSON format")
1969 		self.parser.add_option("--gzip", action="store_true", dest="compress", help="Decompress gzip'd files. Requires gzip in path (default)", default=True)
1970 		self.parser.add_option("--no-gzip", action="store_false", dest="compress", help="Do not decompress gzip'd files.")
1971 
1972 		usage = """%prog download [options] <recid> [filename-pattern]"""
1973 
1974 		self.parser.set_usage(usage)
1975 
1976 		# self.parser.add_option("--convert","-c",action="store_true",help="Convert files to mrc",default=0)
1977 		# self.parser.add_option("--invert","-i",action="store_true",help="Invert contrast",default=0)
1978 
1979 
1980 	def check_args(self):
1981 
1982 		if self.options.sidecar and not json:
1983 			print "Warning: sidecar support requires JSON"
1984 			self.options.sidecar = False
1985 
1986 		if len(self.args) < 1:
1987 			raise  optparse.OptionError, "Record ID Required"
1988 
1989 		self.recids = [int(i) for i in self.args]
1990 		self.patterns = []
1991 
1992 		#try:
1993 		#	self.recid = int(self.args[0])
1994 		#	self.patterns = self.args[1:] or []
1995 		#except Exception, inst:
1996 		#	raise  optparse.OptionError, "Record ID Required"
1997 
1998 
1999 	def run(self):
2000 		self.check_args()
2001 		self.login()
2002 
2003 		options = {}
2004 		for k,v in self.options.__dict__.items():
2005 			options[k] = v
2006 
2007 		for recid in self.recids:
2008 			rec = self.db.getrecord(recid)
2009 			handler = gethandler(rectype=rec.get('rectype'))
2010 			dbt = handler(db=self.db, recid=recid, options=options, pos=(0,1))
2011 			dbt.download()
2012 
2013 
2014 		return
2015 
2016 
2017 
2018 
2019 
2020 
2021 
2022 class SyncController(EMEN2ClientController):
2023 
2024 	def setparser_add_option(self):
2025 
2026 		self.parser.add_option("--check", action="store_true", help="Do not upload anything; just check file mappings")
2027 		self.parser.add_option("--ctf", action="store_true", help="Upload CTF Parameters")
2028 		self.parser.add_option("--boxes", action="store_true", help="Upload Boxes")
2029 		self.parser.add_option("--eman1", action="store_true", help="Look in EMAN1-style files instead of EMAN2 project database")
2030 		self.parser.add_option("--ptcls", action="store_true", help="Upload Particle Sets")
2031 		self.parser.add_option("--confirm", action="store_true", help="Request confirmation of mappings before proceeding")
2032 		self.parser.add_option("--clip_filename", type="string", help="Remove a substr from source filenames to aid matching, e.g. _filt_ptcls")
2033 		self.parser.add_option("--match", type="string", help="Restrict particle sets to this substring, e.g. for quick testing")
2034 		#self.parser.add_option("--check_boxsize", type="int", help="Check if a micrograph has been shrunk; if box_size < check_boxsize, zoom by box_size / check_boxsize")
2035 		self.parser.add_option("--shrink_factor", type="float", help="Specify a shrink factor (e.g. 0.25 if the boxed micrograph was reduced by a factor of 4)", default=1.0)
2036 
2037 
2038 		usage = """%prog sync [options] <project record ID>
2039 
2040 This program will upload CTF parameters, boxes, and particle sets into an EMEN2 database. Because filenames are not always globally unique, you must specify a base project that will be searched for files.
2041 
2042 If run with "--check", it will only test to see if it can make the correct file mappings between the local EMAN2 project and the remote database. Nothing will be written. This is a good way to test to see if the correct mappings can be made before you attempt to commit any changes.
2043 
2044 		"""
2045 
2046 		self.parser.set_usage(usage)
2047 
2048 
2049 	def check_args(self):
2050 		try:
2051 			self.project = int(self.args[0])
2052 		except:
2053 			raise Exception, "Project record ID required"
2054 
2055 		if EMAN2 == None:
2056 			raise Exception, "EMAN2 support is required"
2057 
2058 		if self.options.clip_filename:
2059 			# print "Debug: Clipping '%s' from filenames"%self.options.clip_filename
2060 			test = "filename_ok_filt.mrc"
2061 			test_clip = test.replace(self.options.clip_filename or '', "")
2062 			# print "\t%s -> %s"%(test, test_clip)
2063 
2064 
2065 
2066 	def run(self):
2067 
2068 		self.sources = set()
2069 		self.source_bdo = {}
2070 		self.source_ctf = {}
2071 		self.source_boxes = {}
2072 		self.source_box_size = {}
2073 		self.source_quality = {}
2074 
2075 		self.tmpdirs = []
2076 		self.projrecs = []
2077 
2078 		# Check arguments and auth
2079 		self.check_args()
2080 		self.login()
2081 
2082 		# Actually read EMAN1 or EMAN2 CTF/boxes
2083 		if self.options.eman1:
2084 			self.readEMAN1()
2085 		else:
2086 			self.readEMAN2()
2087 
2088 		# Check project and get remote records
2089 		self.getremoteinfo()
2090 
2091 		# Make mappings
2092 		self.findbinary()
2093 
2094 		# Upload
2095 		if self.options.ctf:
2096 			self.uploadctf()
2097 
2098 		if self.options.boxes:
2099 			self.uploadboxes()
2100 
2101 		if self.options.ptcls:
2102 			self.uploadptcls()
2103 
2104 		self.cleanup()
2105 
2106 
2107 	def cleanup(self):
2108 		if not self.tmpdirs:
2109 			return
2110 
2111 		print "You will want to remove the following tmp directories. They are not automatically removed to prevent accidental file loss in the event of a bug or other error."
2112 		for k in self.tmpdirs:
2113 			print k
2114 
2115 
2116 
2117 	def getremoteinfo(self):
2118 		# Since filenames are not guaranteed unique, restrict to a project...
2119 		print "\nChecking project %s on %s"%(self.project, self.db._host)
2120 
2121 		self.projrecs = self.db.getchildren(self.project, -1, ["ccd","scan"])
2122 		print "\tFound %s ccds/scans in remote project"%len(self.projrecs)
2123 
2124 		self.bdos = self.db.getbinary(self.projrecs)
2125 		print "\tFound %s binaries"%len(self.bdos)
2126 
2127 
2128 
2129 	def readEMAN1(self):
2130 		if self.options.ctf:
2131 			self.readEMAN1ctf()
2132 		if self.options.boxes:
2133 			self.readEMAN1boxes()
2134 
2135 
2136 	def readEMAN1ctf(self):
2137 		try:
2138 			f = open("ctfparm.txt", "r")
2139 			r = [i.strip() for i in f.readlines()]
2140 			f.close()
2141 		except:
2142 			print "No ctfparm.txt present; could not load EMAN1 CTF parameters"
2143 			return
2144 
2145 		indexes = {
2146 			"ctf_defocus_measured": 0, # multiply by -1
2147 			"ctf_bfactor": 3,
2148 			"tem_voltage": 10,
2149 			"aberration_spherical": 11,
2150 			"angstroms_per_pixel": 12,
2151 			"ctf_astig_angle": 2 ,
2152 			"ctf_astig_defocus_diff": 1,
2153 			"ctf_ampcont": 5, # multiply by 100
2154 		}
2155 
2156 		for i in r:
2157 			try:
2158 				source, params = i.split("\t")
2159 				params = params.split(",")
2160 
2161 				# it seems that if there are 14 params, astig params are inserted as 1,2
2162 				shift = 0
2163 				if len(params) == 14:
2164 					shift = 2
2165 
2166 				ctf = EMAN2.EMAN2Ctf()
2167 				ctf.defocus = float(params[0]) * -1
2168 				ctf.bfactor = float(params[1+shift])
2169 				ctf.ampcont = float(params[3+shift]) * 100
2170 
2171 				# print "defocus %s, bfactor %s, ampcont %s"%(ctf.defocus, ctf.bfactor, ctf.ampcont)
2172 				self.source_ctf[source] = ctf
2173 			except:
2174 				print "Unable to parse CTF parameters, skipping"
2175 
2176 
2177 	def readEMAN1boxes(self):
2178 		boxes = glob.glob("*.box")
2179 
2180 		if not boxes:
2181 			print "No EMAN1 .box files found"
2182 			return
2183 
2184 		print "Found EMAN1 boxes: %s"%boxes
2185 
2186 		for box in boxes:
2187 			try:
2188 				source = os.path.splitext(box)[0]
2189 				box_size, coords = self.readEMAN1box(box)
2190 			except:
2191 				print "Could not load data from box %s, skipping"%box
2192 				continue
2193 
2194 			self.source_box_size[source] = box_size
2195 			self.source_boxes[source] = coords
2196 
2197 
2198 	def readEMAN1box(self, box):
2199 		f = open(box, "r")
2200 		r = [i.split() for i in f.readlines()]
2201 		f.close()
2202 
2203 		coords = []
2204 		for b in r:
2205 			box_size = int(b[2])
2206 			xc = (int(b[0]) + (box_size/2)) / self.options.shrink_factor
2207 			yc = (int(b[1]) + (box_size/2)) / self.options.shrink_factor
2208 			coords.append([int(xc),int(yc)])
2209 
2210 		box_size = int(box_size / self.options.shrink_factor)
2211 
2212 		return box_size, coords
2213 
2214 
2215 	def readEMAN2(self):
2216 
2217 		###############
2218 		# Find all the raw images, particle sets, and CTF parameters in the local DB
2219 		###############
2220 
2221 		print "\nOpening EMAN2 local project"
2222 
2223 		projdb = EMAN2.db_open_dict("bdb:project")
2224 		ptclsets = projdb.get("global.spr_ptcls_dict", {})
2225 		e2ctfparms = EMAN2.db_open_dict("bdb:e2ctf.parms")
2226 		total = len(ptclsets)
2227 
2228 		# Read the EMAN2 managed particle sets
2229 		for count, (k,v) in enumerate(ptclsets.items()):
2230 			ref = v.get('Phase flipped') or v.get('Original Data')
2231 
2232 			if not ref:
2233 				print "No particles found for %s, skipping"%k
2234 				continue
2235 
2236 			print "%s of %s: Getting info for particle set %s from %s"%(count+1, total, k, ref)
2237 
2238 			d = EMAN2.db_open_dict(ref)
2239 
2240 			coords = []
2241 			maxrec = d['maxrec']
2242 			if maxrec == None:
2243 				print "No particles in %s, skipping"%(k)
2244 				d.close()
2245 				continue
2246 
2247 
2248 			# Get info from first particle in stack
2249 			ptcl = d[0]
2250 
2251 			try: ctf = ptcl.get_attr("ctf")
2252 			except: ctf = None
2253 
2254 			try: box_size = int(ptcl.get_attr("nx") / self.options.shrink_factor)
2255 			except: box_size = None
2256 
2257 			# Mangle source name if necessary
2258 			try:
2259 				source = os.path.basename(ptcl.get_attr('ptcl_source_image'))
2260 			except:
2261 				source = k
2262 				source = source.split("_ptcl")[0]
2263 				print "\tUnable to get source image %s, using %s for the filename search"%(k,k)
2264 
2265 
2266 			# Try to read boxes from particle headers
2267 			if self.options.boxes:
2268 				for i in range(maxrec+1):
2269 					dptcl = d[i]
2270 					try:
2271 						x, y = dptcl.get_attr('ptcl_source_coord')
2272 						x /= self.options.shrink_factor
2273 						y /= self.options.shrink_factor
2274 						coords.append([int(x), int(y)])
2275 					except:
2276 						coords.append(None)
2277 
2278 
2279 				if None in coords:
2280 					print "\tSome particles for %s did not specify coordinates"%k
2281 					coords = []
2282 					# self.options.boxes = False
2283 
2284 				print "Got box_size %s and coords %s"%(box_size, coords)
2285 
2286 
2287 
2288 			# Get alternative CTF and quality from e2ctfit settings
2289 			ctf2, quality = self.readEMAN2e2ctfparms(e2ctfparms, k)
2290 			if not ctf and ctf2:
2291 				print "\tUsing CTF parameters from bdb:e2ctf.parms#%s"%k
2292 				ctf = ctf2
2293 
2294 			if ctf:
2295 				self.source_ctf[source] = ctf
2296 				print "Got CTF: defocus %s, B-factor %s"%(ctf.defocus, ctf.bfactor)
2297 			else:
2298 				print "\tNo CTF for %s"%k
2299 
2300 			if box_size and coords:
2301 				self.source_box_size[source] = box_size
2302 				self.source_boxes[source] = coords
2303 
2304 			if quality:
2305 				self.source_quality[source] = quality
2306 
2307 			d.close()
2308 
2309 
2310 		# If we can't find any EMAN2 managed particle sets, at least check e2ctf.parms for any CTF params
2311 		if not ptclsets:
2312 			print "No EMAN2 managed particle sets found; checking e2ctf.parms for CTF instead"
2313 			# self.options.boxes = False
2314 
2315 			for k,v in e2ctfparms.items():
2316 				ctf, quality = self.readEMAN2e2ctfparms(e2ctfparms, k)
2317 				source = k.split("_ptcl")[0]
2318 
2319 				if ctf:
2320 					self.source_ctf[source] = ctf
2321 				if quality:
2322 					self.source_quality[source] = quality
2323 
2324 		print "\n%s Files in local project: "%len(self.source_ctf), self.source_ctf.keys()
2325 
2326 		projdb.close()
2327 		e2ctfparms.close()
2328 
2329 
2330 
2331 	def readEMAN2e2ctfparms(self, e2ctfparms, item):
2332 		ctf2 = None
2333 		quality = None
2334 
2335 		ctf_str = e2ctfparms.get(item)
2336 		if ctf_str:
2337 			try:
2338 				ctf2 = EMAN2.EMAN2Ctf()
2339 				ctf2.from_string(ctf_str[0])
2340 				quality = ctf_str[-1]
2341 			except:
2342 				print "\tProblem reading CTF from bdb:e2ctf.parms for %s"%item
2343 
2344 		return ctf2, quality
2345 
2346 
2347 	def __filematch(self, i, j):
2348 		if i in j: return True
2349 
2350 
2351 	def findbinary(self):
2352 
2353 		###############
2354 		# For each file in the local project, search for a matching BDO and record ID in the database
2355 		###############
2356 
2357 		self.sources = set(self.source_ctf.keys() + self.source_boxes.keys())
2358 		total = len(self.sources)
2359 
2360 		bdosbyfilename = dict([[i["filename"], i] for i in self.bdos])
2361 		filenames = bdosbyfilename.keys()
2362 
2363 		ambiguous = {}
2364 		nomatches = {}
2365 
2366 		# ian: new style: I fixed getbinary, so just do one big BDO lookup instead of many db.findbinary
2367 
2368 		print "remote filenames?:"
2369 		print filenames
2370 
2371 		for count, source in enumerate(self.sources):
2372 			print "\n%s of %s: Looking up file %s in project %s"%(count+1, total, source, self.project)
2373 
2374 			q = source.replace(self.options.clip_filename or '', "")
2375 			gzipstrip = ".gz" in source
2376 
2377 			# ian: replace this with functools.partial
2378 			# matches = map(bdosbyfilename.get, filter(lambda x:q in x.split("."), filenames))
2379 			matches = []
2380 			for i in filenames:
2381 				i2 = i
2382 				if gzipstrip:
2383 					i2 = i.replace('.gz','')
2384 				#if q in i2:
2385 				if self.__filematch(q, i2):
2386 					matches.append(bdosbyfilename.get(i))
2387 
2388 
2389 			if len(matches) > 1:
2390 				print "\tAmbiguous match for %s: %s"%(source, [match["filename"] for match in matches])
2391 				ambiguous[source] = matches
2392 				continue
2393 
2394 
2395 			if len(matches) == 0:
2396 				print "\tNo matches for %s"%source
2397 				nomatches[source] = []
2398 				continue
2399 
2400 
2401 			match = matches[0]
2402 			print "\tFound %s in record %s, matching filename is %s"%(match["name"], match["recid"], match["filename"])
2403 
2404 			self.source_bdo[source] = match
2405 
2406 
2407 		print "\n\nSuccessful Local-Remote Mappings:"
2408 		for k,v in self.source_bdo.items():
2409 			print "\t%s -> %s (recid %s)"%(k, v["filename"], v["recid"])
2410 
2411 
2412 		if ambiguous:
2413 			print "\n\nAmbiguous matches:"
2414 			for k,v in ambiguous.items():
2415 				print "\t%s -> %s"%(k, v)
2416 		if nomatches:
2417 			print "\n\nNo matches:"
2418 			for k in nomatches:
2419 				print "\t%s"%k
2420 
2421 
2422 		if self.options.confirm:
2423 			answer = raw_input("\nContinue with these mappings Y/N? ")
2424 			if not answer.lower() in ["y","yes"]:
2425 				print "Mappings rejected; SyncController exiting"
2426 				# ian: do this in a better way, but not a harsh sys.exit
2427 				self.options.ctf = False
2428 				self.options.boxes = False
2429 
2430 
2431 	def uploadctf(self):
2432 
2433 		###############
2434 		# Now that we have found all source images, CTF parameters, and database references, prepare to save in DB.
2435 		###############
2436 
2437 		recs = [i["recid"] for i in self.source_bdo.values()]
2438 		recs = self.db.getrecord(recs)
2439 		recs = dict([(i["recid"],i) for i in recs])
2440 		putrecs = []
2441 
2442 		print "\nGot %s records for CTF parameter upload"%len(recs)
2443 
2444 		for source, match in self.source_bdo.items():
2445 
2446 			ctf = self.source_ctf.get(source)
2447 			quality = self.source_quality.get(source)
2448 
2449 			if not ctf:
2450 				print "No CTF found for %s"%source
2451 				continue
2452 
2453 			rec = recs[match["recid"]]
2454 
2455 			if rec.get("ctf_defocus_measured") == ctf.defocus and rec.get("ctf_bfactor") == ctf.bfactor and rec.get("ctf_ampcont") == ctf.ampcont and rec.get("assess_image_quality") == quality:
2456 				print "%s already has current CTF parameters"%source
2457 				continue
2458 
2459 			rec["ctf_defocus_measured"] = ctf.defocus
2460 			rec["ctf_bfactor"] = ctf.bfactor
2461 			rec["ctf_ampcont"] = ctf.ampcont
2462 
2463 			if quality != None:
2464 				rec["assess_image_quality"] = quality
2465 
2466 			putrecs.append(rec)
2467 
2468 		###############
2469 		# Store / upload
2470 		###############
2471 
2472 		print "\nCommitting %s updated records with changed CTF..."%(len(putrecs))
2473 
2474 		self.db.putrecord(putrecs)
2475 
2476 
2477 
2478 	def uploadboxes(self):
2479 
2480 		print "\nPreparing to upload boxes..."
2481 		newboxes = []
2482 
2483 		for source, boxes in self.source_boxes.items():
2484 
2485 			bdo = self.source_bdo.get(source)
2486 			box_size = self.source_box_size[source]
2487 
2488 			if not bdo:
2489 				print "\tNo bdo for source %s..."%source
2490 				continue
2491 
2492 			recid = bdo.get("recid")
2493 
2494 			# Check remote site for existing boxes
2495 			remoteboxes = self.db.getchildren(recid, -1, ["box"])
2496 
2497 			if len(remoteboxes) == 1:
2498 				print "\tUpdating existing box record"
2499 				newbox = self.db.getrecord(remoteboxes.pop())
2500 
2501 			else:
2502 				if len(remoteboxes) > 1:
2503 					print "\tNote: more than one box record already specified!"
2504 
2505 				print "\tCreating new box record"
2506 				newbox = self.db.newrecord("box", recid)
2507 
2508 			print "\t%s / %s has %s boxes with box size %s"%(source, recid, len(boxes), box_size)
2509 
2510 			newbox["box_coords"] = boxes
2511 			newbox["box_size"] = box_size
2512 
2513 			newboxes.append(newbox)
2514 
2515 			# print "-----"
2516 			# print newbox
2517 			# print "-----"
2518 
2519 		print "\tCommitting %s box records"%(len(newboxes))
2520 		newrecs = self.db.putrecord(newboxes)
2521 		# print "\t... %s"%", ".join([i.get('recid') for i in newrecs])
2522 
2523 
2524 
2525 	def uploadptcls(self):
2526 
2527 		print "\nUploading particles..."
2528 
2529 		print "Not implemented yet"
2530 
2531 		for source in self.source_ctf.keys():
2532 			pass
2533 
2534 
2535 
2536 
2537 
2538 
2539 ##################################
2540 # Mappings
2541 ##################################
2542 
2543 map_filename_rectype = {
2544 	".st":"stack",
2545 	".dm3":"ccd",
2546 	".tif":"jadas"
2547 }
2548 
2549 map_rectype_handler = {
2550 	"ccd": CCDHandler,
2551 	"stack": StackHandler,
2552 	"scan": ScanHandler,
2553 	"volume": VolumeHandler,
2554 	"micrograph": MicrographHandler,
2555 	"jadas": JADASHandler,
2556 	"none": FileHandler
2557 }
2558 
2559 
2560 def gethandler(filename=None, rectype=None):
2561 	if not rectype:
2562 		ext = os.path.splitext(filename)[-1]
2563 		rectype = map_filename_rectype.get(ext)
2564 	return map_rectype_handler.get(rectype, FileHandler)
2565 
2566 
2567 
2568 
2569 ##################################
2570 # Exported utility methods
2571 ##################################
2572 
2573 def opendb(host='http://ncmidb.bcm.edu', username=None, password=None, json=False):
2574 	username = username or raw_input("Username: ")
2575 	password = password or getpass.getpass("Password: ")
2576 	if json:
2577 		db = DBJSONRPCProxy(host=host)
2578 	else:
2579 		db = DBXMLRPCProxy(host=host)
2580 	db._login(username=username, password=password)
2581 	return db
2582 
2583 
2584 def sign(a):
2585 	if a>0: return 1
2586 	return -1
2587 
2588 
2589 
2590 
2591 ##################################
2592 # Main()
2593 ##################################
2594 
2595 
2596 def print_help():
2597 	print """%s <action>
2598 
2599 Actions available: upload, download
2600 
2601 For detailed help: %s <action> --help
2602 
2603 	"""%(sys.argv[0],sys.argv[0])
2604 
2605 
2606 
2607 
2608 def main():
2609 	parser = DBRPCOptions()
2610 
2611 	controllers = {
2612 		"download":DownloadController,
2613 		"upload":UploadController,
2614 		"sync":SyncController
2615 	}
2616 
2617 	try:
2618 		action = sys.argv[1]
2619 	except:
2620 		action = "help"
2621 
2622 	if action in ["-h","--help","help"]:
2623 		return print_help()
2624 
2625 	try:
2626 		if len(sys.argv) == 2:
2627 			sys.argv.append("-h")
2628 		controller = controllers.get(action)(args=sys.argv[2:])
2629 
2630 	except Exception, inst:
2631 		print "Error: %s"%inst
2632 		sys.exit(0)
2633 
2634 	controller.run()
2635 
2636 
2637 
2638 if __name__ == "__main__":
2639 	main()

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.

You are not allowed to attach a file to this page.