github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/lib/python/camli/schema.py (about) 1 #!/usr/bin/env python 2 # 3 # Camlistore uploader client for Python. 4 # 5 # Copyright 2011 Google Inc. 6 # 7 # Licensed under the Apache License, Version 2.0 (the "License"); 8 # you may not use this file except in compliance with the License. 9 # You may obtain a copy of the License at 10 # 11 # http://www.apache.org/licenses/LICENSE-2.0 12 # 13 # Unless required by applicable law or agreed to in writing, software 14 # distributed under the License is distributed on an "AS IS" BASIS, 15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 # See the License for the specific language governing permissions and 17 # limitations under the License. 18 # 19 """Schema blob library for Camlistore.""" 20 21 __author__ = 'Brett Slatkin (bslatkin@gmail.com)' 22 23 import datetime 24 import re 25 import simplejson 26 27 __all__ = [ 28 'Error', 'DecodeError', 'SchemaBlob', 'FileCommon', 'File', 29 'Directory', 'Symlink', 'decode'] 30 31 32 class Error(Exception): 33 """Base class for exceptions in this module.""" 34 35 class DecodeError(Error): 36 """Could not decode the supplied schema blob.""" 37 38 39 # Maps 'camliType' to SchemaBlob sub-classes. 40 _TYPE_TO_CLASS = {} 41 42 43 def _camel_to_python(name): 44 """Converts camelcase to Python case.""" 45 return re.sub(r'([a-z]+)([A-Z])', r'\1_\2', name).lower() 46 47 48 class _SchemaMeta(type): 49 """Meta-class for schema blobs.""" 50 51 def __init__(cls, name, bases, dict): 52 required_fields = set() 53 optional_fields = set() 54 json_to_python = {} 55 python_to_json = {} 56 serializers = {} 57 58 def map_name(field): 59 if field.islower(): 60 return field 61 python_name = _camel_to_python(field) 62 json_to_python[field] = python_name 63 python_to_json[python_name] = field 64 return python_name 65 66 for klz in bases + (cls,): 67 if hasattr(klz, '_json_to_python'): 68 json_to_python.update(klz._json_to_python) 69 if hasattr(klz, '_python_to_json'): 70 python_to_json.update(klz._python_to_json) 71 72 if hasattr(klz, 'required_fields'): 73 for field in klz.required_fields: 74 field = map_name(field) 75 assert field not in required_fields, (klz, field) 76 assert field not in optional_fields, (klz, field) 77 required_fields.add(field) 78 79 if hasattr(klz, 'optional_fields'): 80 for field in klz.optional_fields: 81 field = map_name(field) 82 assert field not in required_fields, (klz, field) 83 assert field not in optional_fields, (klz, field) 84 optional_fields.add(field) 85 86 if hasattr(klz, '_serializers'): 87 for field, value in klz._serializers.iteritems(): 88 field = map_name(field) 89 assert (field in required_fields or 90 field in optional_fields), (klz, field) 91 if not isinstance(value, _FieldSerializer): 92 serializers[field] = value(field) 93 else: 94 serializers[field] = value 95 96 setattr(cls, 'required_fields', frozenset(required_fields)) 97 setattr(cls, 'optional_fields', frozenset(optional_fields)) 98 setattr(cls, '_serializers', serializers) 99 setattr(cls, '_json_to_python', json_to_python) 100 setattr(cls, '_python_to_json', python_to_json) 101 if hasattr(cls, 'type'): 102 _TYPE_TO_CLASS[cls.type] = cls 103 104 105 class SchemaBlob(object): 106 """Base-class for schema blobs. 107 108 Each sub-class should have these fields: 109 type: Required value of 'camliType'. 110 required_fields: Set of required field names. 111 optional_fields: Set of optional field names. 112 _serializers: Dictionary mapping field names to the _FieldSerializer 113 sub-class to use for serializing/deserializing the field's value. 114 """ 115 116 __metaclass__ = _SchemaMeta 117 118 required_fields = frozenset([ 119 'camliVersion', 120 'camliType', 121 ]) 122 optional_fields = frozenset([ 123 'camliSigner', 124 'camliSig', 125 ]) 126 _serializers = {} 127 128 def __init__(self, blobref): 129 """Initializer. 130 131 Args: 132 blobref: The blobref of the schema blob. 133 """ 134 self.blobref = blobref 135 self.unexpected_fields = {} 136 137 @property 138 def all_fields(self): 139 """Returns the set of all potential fields for this blob.""" 140 all_fields = set() 141 all_fields.update(self.required_fields) 142 all_fields.update(self.optional_fields) 143 all_fields.update(self.unexpected_fields) 144 return all_fields 145 146 def decode(self, blob_bytes, parsed=None): 147 """Decodes a schema blob's bytes and unmarshals its fields. 148 149 Args: 150 blob_bytes: String with the bytes of the blob. 151 parsed: If not None, an already parsed version of the blob bytes. When 152 set, the blob_bytes argument is ignored. 153 154 Raises: 155 DecodeError if the blob_bytes are bad or the parsed blob is missing 156 required fields. 157 """ 158 for field in self.all_fields: 159 if hasattr(self, field): 160 delattr(self, field) 161 162 if parsed is None: 163 try: 164 parsed = simplejson.loads(blob_bytes) 165 except simplejson.JSONDecodeError, e: 166 raise DecodeError('Could not parse JSON. %s: %s' % (e.__class__, e)) 167 168 for json_name, value in parsed.iteritems(): 169 name = self._json_to_python.get(json_name, json_name) 170 if not (name in self.required_fields or name in self.optional_fields): 171 self.unexpected_fields[name] = value 172 continue 173 serializer = self._serializers.get(name) 174 if serializer: 175 value = serializer.from_json(value) 176 setattr(self, name, value) 177 178 for name in self.required_fields: 179 if not hasattr(self, name): 180 raise DecodeError('Missing required field: %s' % name) 181 182 def encode(self): 183 """Encodes a schema blob's bytes and marshals its fields. 184 185 Returns: 186 A UTF-8-encoding plain string containing the encoded blob bytes. 187 """ 188 out = {} 189 for python_name in self.all_fields: 190 if not hasattr(self, python_name): 191 continue 192 value = getattr(self, python_name) 193 serializer = self._serializers.get(python_name) 194 if serializer: 195 value = serializer.to_json(value) 196 json_name = self._python_to_json.get(python_name, python_name) 197 out[json_name] = value 198 return simplejson.dumps(out) 199 200 ################################################################################ 201 # Serializers for converting JSON fields to/from Python values 202 203 class _FieldSerializer(object): 204 """Serializes a named field's value to and from JSON.""" 205 206 def __init__(self, name): 207 """Initializer. 208 209 Args: 210 name: The name of the field. 211 """ 212 self.name = name 213 214 def from_json(self, value): 215 """Converts the JSON format of the field to the Python type. 216 217 Args: 218 value: The JSON value. 219 220 Returns: 221 The Python value. 222 """ 223 raise NotImplemented('Must implement from_json') 224 225 def to_json(self, value): 226 """Converts the Python field value to the JSON format of the field. 227 228 Args: 229 value: The Python value. 230 231 Returns: 232 The JSON formatted-value. 233 """ 234 raise NotImplemented('Must implement to_json') 235 236 237 class _DateTimeSerializer(_FieldSerializer): 238 """Formats ISO 8601 strings to/from datetime.datetime instances.""" 239 240 def from_json(self, value): 241 if '.' in value: 242 iso, micros = value.split('.') 243 micros = int((micros[:-1] + ('0' * 6))[:6]) 244 else: 245 iso, micros = value[:-1], 0 246 247 when = datetime.datetime.strptime(iso, '%Y-%m-%dT%H:%M:%S') 248 return when + datetime.timedelta(microseconds=micros) 249 250 def to_json(self, value): 251 return value.isoformat() + 'Z' 252 253 ################################################################################ 254 # Concrete Schema Blobs 255 256 class FileCommon(SchemaBlob): 257 """Common base-class for all unix-y files.""" 258 259 required_fields = frozenset([]) 260 optional_fields = frozenset([ 261 'fileName', 262 'fileNameBytes', 263 'unixPermission', 264 'unixOwnerId', 265 'unixGroupId', 266 'unixGroup', 267 'unixXattrs', 268 'unixMtime', 269 'unixCtime', 270 'unixAtime', 271 ]) 272 _serializers = { 273 'unixMtime': _DateTimeSerializer, 274 'unixCtime': _DateTimeSerializer, 275 'unixAtime': _DateTimeSerializer, 276 } 277 278 279 class File(FileCommon): 280 """A file.""" 281 282 type = 'file' 283 required_fields = frozenset([ 284 'size', 285 'contentParts', 286 ]) 287 optional_fields = frozenset([ 288 'inodeRef', 289 ]) 290 _serializers = {} 291 292 293 class Directory(FileCommon): 294 """A directory.""" 295 296 type = 'directory' 297 required_fields = frozenset([ 298 'entries', 299 ]) 300 optional_fields = frozenset([]) 301 _serializers = {} 302 303 304 class Symlink(FileCommon): 305 """A symlink.""" 306 307 type = 'symlink' 308 required_fields = frozenset([]) 309 optional_fields = frozenset([ 310 'symlinkTarget', 311 'symlinkTargetBytes', 312 ]) 313 _serializers = {} 314 315 316 ################################################################################ 317 # Helper methods 318 319 def decode(blobref, blob_bytes): 320 """Decode any schema blob, validating all required fields for its time.""" 321 try: 322 parsed = simplejson.loads(blob_bytes) 323 except simplejson.JSONDecodeError, e: 324 raise DecodeError('Could not parse JSON. %s: %s' % (e.__class__, e)) 325 326 if 'camliType' not in parsed: 327 raise DecodeError('Could not find "camliType" field.') 328 329 camli_type = parsed['camliType'] 330 blob_class = _TYPE_TO_CLASS.get(camli_type) 331 if blob_class is None: 332 raise DecodeError( 333 'Could not find SchemaBlob sub-class for camliType=%r' % camli_type) 334 335 schema_blob = blob_class(blobref) 336 schema_blob.decode(None, parsed=parsed) 337 return schema_blob