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