""" aecodecs -- Convert from common Python types to Apple Event Manager types and vice-versa. """ import datetime, struct, sys from Foundation import NSAppleEventDescriptor, NSURL from . import kae __all__ = ['Codecs', 'AEType', 'AEEnum'] ###################################################################### def fourcharcode(code): """ Convert four-char code for use in NSAppleEventDescriptor methods. code : bytes -- four-char code, e.g. b'utxt' Result : int -- OSType, e.g. 1970567284 """ return struct.unpack('>I', code)[0] ####### class Codecs: """ Implements mappings for common Python types with direct AppleScript equivalents. Used by AppleScript class. """ kMacEpoch = datetime.datetime(1904, 1, 1) kUSRF = fourcharcode(kae.keyASUserRecordFields) def __init__(self): # Clients may add/remove/replace encoder and decoder items: self.encoders = { NSAppleEventDescriptor.class__(): self.packdesc, type(None): self.packnone, bool: self.packbool, int: self.packint, float: self.packfloat, bytes: self.packbytes, str: self.packstr, list: self.packlist, tuple: self.packlist, dict: self.packdict, datetime.datetime: self.packdatetime, AEType: self.packtype, AEEnum: self.packenum, } if sys.version_info.major < 3: # 2.7 compatibility self.encoders[unicode] = self.packstr self.decoders = {fourcharcode(k): v for k, v in { kae.typeNull: self.unpacknull, kae.typeBoolean: self.unpackboolean, kae.typeFalse: self.unpackboolean, kae.typeTrue: self.unpackboolean, kae.typeSInt32: self.unpacksint32, kae.typeIEEE64BitFloatingPoint: self.unpackfloat64, kae.typeUTF8Text: self.unpackunicodetext, kae.typeUTF16ExternalRepresentation: self.unpackunicodetext, kae.typeUnicodeText: self.unpackunicodetext, kae.typeLongDateTime: self.unpacklongdatetime, kae.typeAEList: self.unpackaelist, kae.typeAERecord: self.unpackaerecord, kae.typeAlias: self.unpackfile, kae.typeFSS: self.unpackfile, kae.typeFSRef: self.unpackfile, kae.typeFileURL: self.unpackfile, kae.typeType: self.unpacktype, kae.typeEnumeration: self.unpackenumeration, }.items()} def pack(self, data): """Pack Python data. data : anything -- a Python value Result : NSAppleEventDescriptor -- an AE descriptor, or error if no encoder exists for this type of data """ try: return self.encoders[data.__class__](data) # quick lookup by type/class except (KeyError, AttributeError) as e: for type, encoder in self.encoders.items(): # slower but more thorough lookup that can handle subtypes/subclasses if isinstance(data, type): return encoder(data) raise TypeError("Can't pack data into an AEDesc (unsupported type): {!r}".format(data)) def unpack(self, desc): """Unpack an Apple event descriptor. desc : NSAppleEventDescriptor Result : anything -- a Python value, or the original NSAppleEventDescriptor if no decoder is found """ decoder = self.decoders.get(desc.descriptorType()) # unpack known type if decoder: return decoder(desc) # if it's a record-like desc, unpack as dict with an extra AEType(b'pcls') key containing the desc type rec = desc.coerceToDescriptorType_(fourcharcode(kae.typeAERecord)) if rec: rec = self.unpackaerecord(rec) rec[AEType(kae.pClass)] = AEType(struct.pack('>I', desc.descriptorType())) return rec # return as-is return desc ## def _packbytes(self, desctype, data): return NSAppleEventDescriptor.descriptorWithDescriptorType_bytes_length_( fourcharcode(desctype), data, len(data)) def packdesc(self, val): return val def packnone(self, val): return NSAppleEventDescriptor.nullDescriptor() def packbool(self, val): return NSAppleEventDescriptor.descriptorWithBoolean_(int(val)) def packint(self, val): if (-2**31) <= val < (2**31): return NSAppleEventDescriptor.descriptorWithInt32_(val) else: return self.pack(float(val)) def packfloat(self, val): return self._packbytes(kae.typeFloat, struct.pack('d', val)) def packbytes(self, val): return self._packbytes(kae.typeData, val) def packstr(self, val): return NSAppleEventDescriptor.descriptorWithString_(val) def packdatetime(self, val): delta = val - self.kMacEpoch sec = delta.days * 3600 * 24 + delta.seconds return self._packbytes(kae.typeLongDateTime, struct.pack('q', sec)) def packlist(self, val): lst = NSAppleEventDescriptor.listDescriptor() for item in val: lst.insertDescriptor_atIndex_(self.pack(item), 0) return lst def packdict(self, val): record = NSAppleEventDescriptor.recordDescriptor() usrf = desctype = None for key, value in val.items(): if isinstance(key, AEType): if key.code == kae.pClass and isinstance(value, AEType): # AS packs records that contain a 'class' property by coercing the packed record to the descriptor type specified by the property's value (assuming it's an AEType) desctype = value else: record.setDescriptor_forKeyword_(self.pack(value), fourcharcode(key.code)) else: if not usrf: usrf = NSAppleEventDescriptor.listDescriptor() usrf.insertDescriptor_atIndex_(self.pack(key), 0) usrf.insertDescriptor_atIndex_(self.pack(value), 0) if usrf: record.setDescriptor_forKeyword_(usrf, self.kUSRF) if desctype: newrecord = record.coerceToDescriptorType_(fourcharcode(desctype.code)) if newrecord: record = newrecord else: # coercion failed for some reason, so pack as normal key-value pair record.setDescriptor_forKeyword_(self.pack(desctype), fourcharcode(key.code)) return record def packtype(self, val): return NSAppleEventDescriptor.descriptorWithTypeCode_(fourcharcode(val.code)) def packenum(self, val): return NSAppleEventDescriptor.descriptorWithEnumCode_(fourcharcode(val.code)) ####### def unpacknull(self, desc): return None def unpackboolean(self, desc): return desc.booleanValue() def unpacksint32(self, desc): return desc.int32Value() def unpackfloat64(self, desc): return struct.unpack('d', bytes(desc.data()))[0] def unpackunicodetext(self, desc): return desc.stringValue() def unpacklongdatetime(self, desc): return self.kMacEpoch + datetime.timedelta(seconds=struct.unpack('q', bytes(desc.data()))[0]) def unpackaelist(self, desc): return [self.unpack(desc.descriptorAtIndex_(i + 1)) for i in range(desc.numberOfItems())] def unpackaerecord(self, desc): dct = {} for i in range(desc.numberOfItems()): key = desc.keywordForDescriptorAtIndex_(i + 1) value = desc.descriptorForKeyword_(key) if key == self.kUSRF: lst = self.unpackaelist(value) for i in range(0, len(lst), 2): dct[lst[i]] = lst[i+1] else: dct[AEType(struct.pack('>I', key))] = self.unpack(value) return dct def unpacktype(self, desc): return AEType(struct.pack('>I', desc.typeCodeValue())) def unpackenumeration(self, desc): return AEEnum(struct.pack('>I', desc.enumCodeValue())) def unpackfile(self, desc): url = bytes(desc.coerceToDescriptorType_(fourcharcode(kae.typeFileURL)).data()).decode('utf8') return NSURL.URLWithString_(url).path() ####### class AETypeBase: """ Base class for AEType and AEEnum. Notes: - Hashable and comparable, so may be used as keys in dictionaries that map to AE records. """ def __init__(self, code): """ code : bytes -- four-char code, e.g. b'utxt' """ if not isinstance(code, bytes): raise TypeError('invalid code (not a bytes object): {!r}'.format(code)) elif len(code) != 4: raise ValueError('invalid code (not four bytes long): {!r}'.format(code)) self._code = code code = property(lambda self:self._code, doc="bytes -- four-char code, e.g. b'utxt'") def __hash__(self): return hash(self._code) def __eq__(self, val): return val.__class__ == self.__class__ and val.code == self._code def __ne__(self, val): return not self == val def __repr__(self): return "{}({!r})".format(self.__class__.__name__, self._code) ## class AEType(AETypeBase): """An AE type. Maps to an AppleScript type class, e.g. AEType(b'utxt') <=> 'unicode text'.""" class AEEnum(AETypeBase): """An AE enumeration. Maps to an AppleScript constant, e.g. AEEnum(b'yes ') <=> 'yes'."""