def smart_escape(source, unsafe=r"([^a-zA-Z0-9_.\-\/:]+)"): def pack(m): return to_bytes('%' + "%".join([ "%02X" % x for x in struct.unpack('B' * len(m.group(1)), m.group(1)) ]).upper()) return to_string(re.sub(to_bytes(unsafe), pack, to_bytes(source)))
def call_api(method, uri, params, **options): prefix = options.pop( "upload_prefix", cloudinary.config().upload_prefix) or "https://api.cloudinary.com" cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name) if not cloud_name: raise Exception("Must supply cloud_name") api_key = options.pop("api_key", cloudinary.config().api_key) if not api_key: raise Exception("Must supply api_key") api_secret = options.pop("api_secret", cloudinary.config().api_secret) if not cloud_name: raise Exception("Must supply api_secret") data = to_bytes(urlencode(params)) api_url = "/".join([prefix, "v1_1", cloud_name] + uri) request = urllib2.Request(api_url, data) # Add authentication byte_value = to_bytes('%s:%s' % (api_key, api_secret)) encoded_value = base64.encodebytes( byte_value) if PY3 else base64.encodestring(byte_value) base64string = to_string(encoded_value).replace('\n', '') request.add_header("Authorization", "Basic %s" % base64string) request.add_header("User-Agent", cloudinary.USER_AGENT) request.get_method = lambda: method.upper() kw = {} if 'timeout' in options: kw['timeout'] = options['timeout'] try: response = urllib2.urlopen(request, **kw) body = response.read() except HTTPError: e = sys.exc_info()[1] exception_class = EXCEPTION_CODES.get(e.code) if exception_class: response = e body = response.read() else: raise GeneralError( "Server returned unexpected status code - %d - %s" % (e.code, e.read())) except socket.error: e = sys.exc_info()[1] raise GeneralError("Socket Error: %s" % (str(e))) try: body = to_string(body) result = json.loads(body) except Exception: # Error is parsing json e = sys.exc_info()[1] raise GeneralError( "Error parsing server response (%d) - %s. Got - %s" % (response.code, body, e)) if "error" in result: exception_class = exception_class or Exception raise exception_class(result["error"]["message"]) return Response(result, response)
def _strify(s): if s is None: return None elif isinstance(s, bytes): return s else: try: return to_bytes(s) except AttributeError: return to_bytes(str(s))
def encode(self, boundary): """Returns the string encoding of this parameter""" if self.value is None: value = self.fileobj.read() else: value = self.value if re.search(to_bytes("^--%s$" % re.escape(boundary)), value, re.M): raise ValueError("boundary found in encoded string") return to_bytes(self.encode_hdr(boundary)) + value + b"\r\n"
def call_api(method, uri, params, **options): prefix = options.pop("upload_prefix", cloudinary.config().upload_prefix) or "https://api.cloudinary.com" cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name) if not cloud_name: raise Exception("Must supply cloud_name") api_key = options.pop("api_key", cloudinary.config().api_key) if not api_key: raise Exception("Must supply api_key") api_secret = options.pop("api_secret", cloudinary.config().api_secret) if not cloud_name: raise Exception("Must supply api_secret") data = to_bytes(urlencode(params)) api_url = "/".join([prefix, "v1_1", cloud_name] + uri) request = urllib2.Request(api_url, data) # Add authentication byte_value = to_bytes('%s:%s' % (api_key, api_secret)) encoded_value = base64.encodebytes(byte_value) if PY3 else base64.encodestring(byte_value) base64string = to_string(encoded_value).replace('\n', '') request.add_header("Authorization", "Basic %s" % base64string) request.add_header("User-Agent", cloudinary.USER_AGENT) request.get_method = lambda: method.upper() kw = {} if 'timeout' in options: kw['timeout'] = options['timeout'] try: response = urllib2.urlopen(request, **kw) body = response.read() except socket.error: e = sys.exc_info()[1] raise GeneralError("Socket Error: %s" % (str(e))) except urllib2.HTTPError: e = sys.exc_info()[1] exception_class = EXCEPTION_CODES.get(e.code) if exception_class: response = e body = response.read() else: raise GeneralError("Server returned unexpected status code - %d - %s" % (e.code, e.read())) try: body = to_string(body) result = json.loads(body) except Exception: # Error is parsing json e = sys.exc_info()[1] raise GeneralError("Error parsing server response (%d) - %s. Got - %s" % (response.code, body, e)) if "error" in result: exception_class = exception_class or Exception raise exception_class(result["error"]["message"]) return Response(result, response)
def smart_escape(source, unsafe=r"([^a-zA-Z0-9_.\-\/:]+)"): """ Based on ruby's CGI::unescape. In addition does not escape / : :param source: Source string to escape :param unsafe: Unsafe characters :return: Escaped string """ def pack(m): return to_bytes('%' + "%".join( ["%02X" % x for x in struct.unpack('B' * len(m.group(1)), m.group(1))] ).upper()) return to_string(re.sub(to_bytes(unsafe), pack, to_bytes(source)))
def next(self): """generator function to yield multipart/form-data representation of parameters""" if self.param_iter is not None: try: block = advance_iterator(self.param_iter) self.current += len(block) if self.cb: self.cb(self.p, self.current, self.total) return block except StopIteration: self.p = None self.param_iter = None if self.i is None: raise StopIteration elif self.i >= len(self.params): self.param_iter = None self.p = None self.i = None block = to_bytes("--%s--\r\n" % self.boundary) self.current += len(block) if self.cb: self.cb(self.p, self.current, self.total) return block self.p = self.params[self.i] self.param_iter = self.p.iter_encode(self.boundary) self.i += 1 return advance_iterator(self)
def __init__(self, name, value=None, filename=None, filetype=None, filesize=None, fileobj=None, cb=None): self.name = Header(name).encode() self.value = _strify(value) if filename is None: self.filename = None else: if PY3: byte_filename = filename.encode("ascii", "xmlcharrefreplace") self.filename = to_string(byte_filename) encoding = 'unicode_escape' else: if isinstance(filename, unicode): # Encode with XML entities self.filename = filename.encode("ascii", "xmlcharrefreplace") else: self.filename = str(filename) encoding = 'string_escape' self.filename = self.filename.encode(encoding).replace( to_bytes('"'), to_bytes('\\"')) self.filetype = _strify(filetype) self.filesize = filesize self.fileobj = fileobj self.cb = cb if self.value is not None and self.fileobj is not None: raise ValueError("Only one of value or fileobj may be specified") if fileobj is not None and filesize is None: # Try and determine the file size try: self.filesize = os.fstat(fileobj.fileno()).st_size except (OSError, AttributeError, UnsupportedOperation): try: fileobj.seek(0, 2) self.filesize = fileobj.tell() fileobj.seek(0) except Exception: raise ValueError("Could not determine filesize")
def cloudinary_url(source, **options): original_source = source type = options.pop("type", "upload") if type == 'fetch': options["fetch_format"] = options.get("fetch_format", options.pop("format", None)) transformation, options = generate_transformation_string(**options) resource_type = options.pop("resource_type", "image") version = options.pop("version", None) format = options.pop("format", None) cdn_subdomain = options.pop("cdn_subdomain", cloudinary.config().cdn_subdomain) secure_cdn_subdomain = options.pop("secure_cdn_subdomain", cloudinary.config().secure_cdn_subdomain) cname = options.pop("cname", cloudinary.config().cname) shorten = options.pop("shorten", cloudinary.config().shorten) cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name or None) if cloud_name is None: raise ValueError("Must supply cloud_name in tag or in configuration") secure = options.pop("secure", cloudinary.config().secure) private_cdn = options.pop("private_cdn", cloudinary.config().private_cdn) secure_distribution = options.pop("secure_distribution", cloudinary.config().secure_distribution) sign_url = options.pop("sign_url", cloudinary.config().sign_url) api_secret = options.pop("api_secret", cloudinary.config().api_secret) url_suffix = options.pop("url_suffix", None) use_root_path = options.pop("use_root_path", cloudinary.config().use_root_path) auth_token = options.pop("auth_token", None) if auth_token is not False: auth_token = merge(cloudinary.config().auth_token, auth_token) if (not source) or type == "upload" and re.match(r'^https?:', source): return original_source, options resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten) source, source_to_sign = finalize_source(source, format, url_suffix) if source_to_sign.find("/") >= 0 \ and not re.match(r'^https?:/', source_to_sign) \ and not re.match(r'^v[0-9]+', source_to_sign) \ and not version: version = "1" if version: version = "v" + str(version) transformation = re.sub(r'([^:])/+', r'\1/', transformation) signature = None if sign_url and not auth_token: to_sign = "/".join(__compact([transformation, source_to_sign])) signature = "s--" + to_string( base64.urlsafe_b64encode(hashlib.sha1(to_bytes(to_sign + api_secret)).digest())[0:8]) + "--" prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution) source = "/".join(__compact([prefix, resource_type, type, signature, transformation, version, source])) if sign_url and auth_token: path = urlparse(source).path token = cloudinary.auth_token.generate( **merge(auth_token, {"url": path})) source = "%s?%s" % (source, token) return source, options
def base64url_encode(data): """ Url safe version of urlsafe_b64encode with stripped `=` sign at the end. :param data: input data :return: Base64 URL safe encoded string """ return to_string(base64.urlsafe_b64encode(to_bytes(data)))
def compute_hex_hash(s): """ Compute hash and convert the result to HEX string :param s: string to process :return: HEX string """ return hashlib.sha1(to_bytes(s)).hexdigest()
def __init__(self, name, value=None, filename=None, filetype=None, filesize=None, fileobj=None, cb=None): self.name = Header(name).encode() self.value = _strify(value) if filename is None: self.filename = None else: if PY3: byte_filename = filename.encode("ascii", "xmlcharrefreplace") self.filename = to_string(byte_filename) encoding = 'unicode_escape' else: if isinstance(filename, unicode): # Encode with XML entities self.filename = filename.encode("ascii", "xmlcharrefreplace") else: self.filename = str(filename) encoding = 'string_escape' self.filename = self.filename.encode(encoding).replace(to_bytes('"'), to_bytes('\\"')) self.filetype = _strify(filetype) self.filesize = filesize self.fileobj = fileobj self.cb = cb if self.value is not None and self.fileobj is not None: raise ValueError("Only one of value or fileobj may be specified") if fileobj is not None and filesize is None: # Try and determine the file size try: self.filesize = os.fstat(fileobj.fileno()).st_size except (OSError, AttributeError, UnsupportedOperation): try: fileobj.seek(0, 2) self.filesize = fileobj.tell() fileobj.seek(0) except: raise ValueError("Could not determine filesize")
def iter_encode(self, boundary, blocksize=4096): """Yields the encoding of this parameter If self.fileobj is set, then blocks of ``blocksize`` bytes are read and yielded.""" total = self.get_size(boundary) current = 0 if self.value is not None: block = self.encode(boundary) current += len(block) yield block if self.cb: self.cb(self, current, total) else: block = to_bytes(self.encode_hdr(boundary)) current += len(block) yield block if self.cb: self.cb(self, current, total) last_block = to_bytearray("") encoded_boundary = "--%s" % encode_and_quote(boundary) boundary_exp = re.compile( to_bytes("^%s$" % re.escape(encoded_boundary)), re.M) while True: block = self.fileobj.read(blocksize) if not block: current += 2 yield to_bytes("\r\n") if self.cb: self.cb(self, current, total) break last_block += block if boundary_exp.search(last_block): raise ValueError("boundary found in file data") last_block = last_block[-len(to_bytes(encoded_boundary)) - 2:] current += len(block) yield block if self.cb: self.cb(self, current, total)
def iter_encode(self, boundary, blocksize=4096): """Yields the encoding of this parameter If self.fileobj is set, then blocks of ``blocksize`` bytes are read and yielded.""" total = self.get_size(boundary) current = 0 if self.value is not None: block = self.encode(boundary) current += len(block) yield block if self.cb: self.cb(self, current, total) else: block = to_bytes(self.encode_hdr(boundary)) current += len(block) yield block if self.cb: self.cb(self, current, total) last_block = to_bytearray("") encoded_boundary = "--%s" % encode_and_quote(boundary) boundary_exp = re.compile(to_bytes("^%s$" % re.escape(encoded_boundary)), re.M) while True: block = self.fileobj.read(blocksize) if not block: current += 2 yield to_bytes("\r\n") if self.cb: self.cb(self, current, total) break last_block += block if boundary_exp.search(last_block): raise ValueError("boundary found in file data") last_block = last_block[-len(to_bytes(encoded_boundary))-2:] current += len(block) yield block if self.cb: self.cb(self, current, total)
def compute_hex_hash(s, algorithm=SIGNATURE_SHA1): """ Computes string hash using specified algorithm and return HEX string representation of hash. :param s: String to compute hash for :param algorithm: The name of algorithm to use for computing hash :return: HEX string of computed hash value """ try: hash_fn = signature_algorithms[algorithm] except KeyError: raise ValueError('Unsupported hash algorithm: {}'.format(algorithm)) return hash_fn(to_bytes(s)).hexdigest()
def smart_escape(string): pack = lambda m: to_bytes('%' + "%".join(["%02X" % x for x in struct.unpack('B'*len(m.group(1)), m.group(1))]).upper()) return to_string(re.sub(to_bytes(r"([^a-zA-Z0-9_.\-\/:]+)"), pack, to_bytes(string)))
def cloudinary_url(source, **options): original_source = source type = options.pop("type", "upload") if type == 'fetch': options["fetch_format"] = options.get("fetch_format", options.pop("format", None)) transformation, options = generate_transformation_string(**options) resource_type = options.pop("resource_type", "image") version = options.pop("version", None) format = options.pop("format", None) cdn_subdomain = options.pop("cdn_subdomain", cloudinary.config().cdn_subdomain) cname = options.pop("cname", cloudinary.config().cname) shorten = options.pop("shorten", cloudinary.config().shorten) cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name or None) if cloud_name == None: raise ValueError("Must supply cloud_name in tag or in configuration") secure = options.pop("secure", cloudinary.config().secure) private_cdn = options.pop("private_cdn", cloudinary.config().private_cdn) secure_distribution = options.pop("secure_distribution", cloudinary.config().secure_distribution) sign_url = options.pop("sign_url", cloudinary.config().sign_url) api_secret = options.pop("api_secret", cloudinary.config().api_secret) if (not source) or ((type == "upload" or type=="asset") and re.match(r'^https?:', source)): return (original_source, options) if re.match(r'^https?:', source): source = smart_escape(source) else: source = unquote(source) if not PY3: source = source.decode('utf8') source = smart_escape(source) if format: source = source + "." + format if cloud_name.startswith("/"): prefix = "/res" + cloud_name else: shared_domain = not private_cdn if secure: if not secure_distribution or secure_distribution == cloudinary.OLD_AKAMAI_SHARED_CDN: secure_distribution = cloud_name + "-res.cloudinary.com" if private_cdn else cloudinary.SHARED_CDN shared_domain = shared_domain or secure_distribution == cloudinary.SHARED_CDN prefix = "https://" + secure_distribution else: subdomain = "a" + str((zlib.crc32(to_bytearray(source)) & 0xffffffff)%5 + 1) + "." if cdn_subdomain else "" if cname: host = cname elif private_cdn: host = cloud_name + "-res.cloudinary.com" else: host = "res.cloudinary.com" prefix = "http://" + subdomain + host if shared_domain: prefix += "/" + cloud_name if shorten and resource_type == "image" and type == "upload": resource_type = "iu" type = "" if source.find("/") >= 0 and not re.match(r'^https?:/', source) and not re.match(r'^v[0-9]+', source) and not version: version = "1" rest = "/".join(filter(lambda x: x, [transformation, "v" + str(version) if version else "", source])) if sign_url: signature = to_string(base64.urlsafe_b64encode( hashlib.sha1(to_bytes(rest + api_secret)).digest() )[0:8]) rest = "s--%(signature)s--/%(rest)s" % {"signature": signature, "rest": rest} components = [prefix, resource_type, type, rest] source = re.sub(r'([^:])/+', r'\1/', "/".join(components)) return (source, options)
def api_sign_request(params_to_sign, api_secret): to_sign = "&".join(sorted([(k+"="+(",".join(v) if isinstance(v, list) else str(v))) for k, v in params_to_sign.items() if v])) return hashlib.sha1(to_bytes(to_sign + api_secret)).hexdigest()
def api_sign_request(params_to_sign, api_secret): params = [(k + "=" + (",".join(v) if isinstance(v, list) else str(v))) for k, v in params_to_sign.items() if v] to_sign = "&".join(sorted(params)) return hashlib.sha1(to_bytes(to_sign + api_secret)).hexdigest()
class PhotoUnsignedDirectForm(PhotoForm): upload_preset_name = "sample_" + hashlib.sha1( to_bytes(cloudinary.config().api_key + cloudinary.config().api_secret)).hexdigest()[0:10] image = CloudinaryUnsignedJsFileField(upload_preset_name)
def _strify(s): if s is None: return None return to_bytes(s)
def smart_escape(string): pack = lambda m: to_bytes('%' + "%".join( ["%02X" % x for x in struct.unpack('B' * len(m.group(1)), m.group(1))]).upper()) return to_string( re.sub(to_bytes(r"([^a-zA-Z0-9_.\-\/:]+)"), pack, to_bytes(string)))
def pack(m): return to_bytes('%' + "%".join(["%02X" % x for x in struct.unpack('B' * len(m.group(1)), m.group(1))]).upper())
def call_api(action, params, **options): try: file_io = None return_error = options.get("return_error") if options.get("unsigned"): params = utils.cleanup_params(params) else: params = utils.sign_request(params, options) param_list = [] for k, v in params.items(): if isinstance(v, list): for vv in v: param_list.append((k+"[]", vv)) elif v: param_list.append((k, v)) api_url = utils.cloudinary_api_url(action, **options) global _initialized if not _initialized: _initialized = True # Register the streaming http handlers with urllib2 register_openers() datagen = to_bytes("") headers = {} if "file" in options: file = options["file"] if not isinstance(file, string_types): datagen, headers = multipart_encode({'file': file}) elif not re.match(r'^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$', file): file_io = open(file, "rb") datagen, headers = multipart_encode({'file': file_io}) else: param_list.append(("file", file)) request = urllib2.Request(api_url + "?" + urlencode(param_list), datagen, headers) request.add_header("User-Agent", cloudinary.USER_AGENT) kw = {} if 'timeout' in options: kw['timeout'] = options['timeout'] code = 200 try: response = urllib2.urlopen(request, **kw).read() except socket.error: e = sys.exc_info()[1] raise Error("Socket error: %s" % str(e)) except urllib2.HTTPError: e = sys.exc_info()[1] if not e.code in [200, 400, 500]: raise Error("Server returned unexpected status code - %d - %s" % (e.code, e.read())) code = e.code response = e.read() try: result = json.loads(to_string(response)) except Exception: e = sys.exc_info()[1] # Error is parsing json raise Error("Error parsing server response (%d) - %s. Got - %s", code, response, e) if "error" in result: if return_error: result["error"]["http_code"] = code else: raise Error(result["error"]["message"]) return result finally: if file_io: file_io.close()
def call_api(action, params, **options): try: file_io = None return_error = options.get("return_error") if options.get("unsigned"): params = utils.cleanup_params(params) else: params = utils.sign_request(params, options) param_list = [] for k, v in params.items(): if isinstance(v, list): for vv in v: param_list.append((k + "[]", vv)) elif v: param_list.append((k, v)) api_url = utils.cloudinary_api_url(action, **options) global _initialized if not _initialized: _initialized = True # Register the streaming http handlers with urllib2 register_openers() datagen = to_bytes("") headers = {} if "file" in options: file = options["file"] if not isinstance(file, string_types): datagen, headers = multipart_encode({'file': file}) elif not re.match( r'^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$', file): file_io = open(file, "rb") datagen, headers = multipart_encode({'file': file_io}) else: param_list.append(("file", file)) request = urllib2.Request(api_url + "?" + urlencode(param_list), datagen, headers) request.add_header("User-Agent", cloudinary.USER_AGENT) code = 200 try: response = urllib2.urlopen(request).read() except urllib2.HTTPError: e = sys.exc_info()[1] if not e.code in [200, 400, 500]: raise Error( "Server returned unexpected status code - %d - %s" % (e.code, e.read())) code = e.code response = e.read() try: result = json.loads(to_string(response)) except Exception: e = sys.exc_info()[1] # Error is parsing json raise Error("Error parsing server response (%d) - %s. Got - %s", code, response, e) if "error" in result: if return_error: result["error"]["http_code"] = response.code else: raise Error(result["error"]["message"]) return result finally: if file_io: file_io.close()
def cloudinary_url(source, **options): original_source = source patch_fetch_format(options) type = options.pop("type", "upload") transformation, options = generate_transformation_string(**options) resource_type = options.pop("resource_type", "image") force_version = options.pop("force_version", cloudinary.config().force_version) if force_version is None: force_version = True version = options.pop("version", None) format = options.pop("format", None) cdn_subdomain = options.pop("cdn_subdomain", cloudinary.config().cdn_subdomain) secure_cdn_subdomain = options.pop("secure_cdn_subdomain", cloudinary.config().secure_cdn_subdomain) cname = options.pop("cname", cloudinary.config().cname) shorten = options.pop("shorten", cloudinary.config().shorten) cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name or None) if cloud_name is None: raise ValueError("Must supply cloud_name in tag or in configuration") secure = options.pop("secure", cloudinary.config().secure) private_cdn = options.pop("private_cdn", cloudinary.config().private_cdn) secure_distribution = options.pop("secure_distribution", cloudinary.config().secure_distribution) sign_url = options.pop("sign_url", cloudinary.config().sign_url) api_secret = options.pop("api_secret", cloudinary.config().api_secret) url_suffix = options.pop("url_suffix", None) use_root_path = options.pop("use_root_path", cloudinary.config().use_root_path) auth_token = options.pop("auth_token", None) long_url_signature = options.pop("long_url_signature", cloudinary.config().long_url_signature) signature_algorithm = options.pop("signature_algorithm", cloudinary.config().signature_algorithm) if auth_token is not False: auth_token = merge(cloudinary.config().auth_token, auth_token) if (not source) or type == "upload" and re.match(r'^https?:', source): return original_source, options resource_type, type = finalize_resource_type( resource_type, type, url_suffix, use_root_path, shorten) source, source_to_sign = finalize_source(source, format, url_suffix) if not version and force_version \ and source_to_sign.find("/") >= 0 \ and not re.match(r'^https?:/', source_to_sign) \ and not re.match(r'^v[0-9]+', source_to_sign): version = "1" if version: version = "v" + str(version) else: version = None transformation = re.sub(r'([^:])/+', r'\1/', transformation) signature = None if sign_url and not auth_token: to_sign = "/".join(__compact([transformation, source_to_sign])) if long_url_signature: # Long signature forces SHA256 signature_algorithm = SIGNATURE_SHA256 chars_length = LONG_URL_SIGNATURE_LENGTH else: chars_length = SHORT_URL_SIGNATURE_LENGTH if signature_algorithm not in signature_algorithms: raise ValueError("Unsupported signature algorithm '{}'".format(signature_algorithm)) hash_fn = signature_algorithms[signature_algorithm] signature = "s--" + to_string( base64.urlsafe_b64encode( hash_fn(to_bytes(to_sign + api_secret)).digest())[0:chars_length]) + "--" prefix = unsigned_download_url_prefix( source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution) source = "/".join(__compact( [prefix, resource_type, type, signature, transformation, version, source])) if sign_url and auth_token: path = urlparse(source).path token = cloudinary.auth_token.generate(**merge(auth_token, {"url": path})) source = "%s?%s" % (source, token) return source, options
def encode_and_quote(data): if data is None: return None return quote_plus(to_bytes(data))
def smart_escape(source,unsafe = r"([^a-zA-Z0-9_.\-\/:]+)"): def pack(m): return to_bytes('%' + "%".join(["%02X" % x for x in struct.unpack('B' * len(m.group(1)), m.group(1))]).upper()) return to_string(re.sub(to_bytes(unsafe), pack, to_bytes(source)))
def pack(m): return to_bytes('%' + "%".join([ "%02X" % x for x in struct.unpack('B' * len(m.group(1)), m.group(1)) ]).upper())