#!/usr/bin/env python # # Copyright (C) 2010 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This module is used for version 2 of the Google Data APIs. __author__ = 'j.s@google.com (Jeff Scudder)' """Provides classes and methods for working with JSON-C. This module is experimental and subject to backwards incompatible changes. Jsonc: Class which represents JSON-C data and provides pythonic member access which is a bit cleaner than working with plain old dicts. parse_json: Converts a JSON-C string into a Jsonc object. jsonc_to_string: Converts a Jsonc object into a string of JSON-C. """ try: import simplejson except ImportError: try: # Try to import from django, should work on App Engine from django.utils import simplejson except ImportError: # Should work for Python2.6 and higher. import json as simplejson def _convert_to_jsonc(x): """Builds a Jsonc objects which wraps the argument's members.""" if isinstance(x, dict): jsonc_obj = Jsonc() # Recursively transform all members of the dict. # When converting a dict, we do not convert _name items into private # Jsonc members. for key, value in x.iteritems(): jsonc_obj._dict[key] = _convert_to_jsonc(value) return jsonc_obj elif isinstance(x, list): # Recursively transform all members of the list. members = [] for item in x: members.append(_convert_to_jsonc(item)) return members else: # Return the base object. return x def parse_json(json_string): """Converts a JSON-C string into a Jsonc object. Args: json_string: str or unicode The JSON to be parsed. Returns: A new Jsonc object. """ return _convert_to_jsonc(simplejson.loads(json_string)) def parse_json_file(json_file): return _convert_to_jsonc(simplejson.load(json_file)) def jsonc_to_string(jsonc_obj): """Converts a Jsonc object into a string of JSON-C.""" return simplejson.dumps(_convert_to_object(jsonc_obj)) def prettify_jsonc(jsonc_obj, indentation=2): """Converts a Jsonc object to a pretified (intented) JSON string.""" return simplejson.dumps(_convert_to_object(jsonc_obj), indent=indentation) def _convert_to_object(jsonc_obj): """Creates a new dict or list which has the data in the Jsonc object. Used to convert the Jsonc object to a plain old Python object to simplify conversion to a JSON-C string. Args: jsonc_obj: A Jsonc object to be converted into simple Python objects (dicts, lists, etc.) Returns: Either a dict, list, or other object with members converted from Jsonc objects to the corresponding simple Python object. """ if isinstance(jsonc_obj, Jsonc): plain = {} for key, value in jsonc_obj._dict.iteritems(): plain[key] = _convert_to_object(value) return plain elif isinstance(jsonc_obj, list): plain = [] for item in jsonc_obj: plain.append(_convert_to_object(item)) return plain else: return jsonc_obj def _to_jsonc_name(member_name): """Converts a Python style member name to a JSON-C style name. JSON-C uses camelCaseWithLower while Python tends to use lower_with_underscores so this method converts as follows: spam becomes spam spam_and_eggs becomes spamAndEggs Args: member_name: str or unicode The Python syle name which should be converted to JSON-C style. Returns: The JSON-C style name as a str or unicode. """ characters = [] uppercase_next = False for character in member_name: if character == '_': uppercase_next = True elif uppercase_next: characters.append(character.upper()) uppercase_next = False else: characters.append(character) return ''.join(characters) class Jsonc(object): """Represents JSON-C data in an easy to access object format. To access the members of a JSON structure which looks like this: { "data": { "totalItems": 800, "items": [ { "content": { "1": "rtsp://v5.cache3.c.youtube.com/CiILENy.../0/0/0/video.3gp" }, "viewCount": 220101, "commentCount": 22, "favoriteCount": 201 } ] }, "apiVersion": "2.0" } You would do the following: x = gdata.core.parse_json(the_above_string) # Gives you 800 x.data.total_items # Should be 22 x.data.items[0].comment_count # The apiVersion is '2.0' x.api_version To create a Jsonc object which would produce the above JSON, you would do: gdata.core.Jsonc( api_version='2.0', data=gdata.core.Jsonc( total_items=800, items=[ gdata.core.Jsonc( view_count=220101, comment_count=22, favorite_count=201, content={ '1': ('rtsp://v5.cache3.c.youtube.com' '/CiILENy.../0/0/0/video.3gp')})])) or x = gdata.core.Jsonc() x.api_version = '2.0' x.data = gdata.core.Jsonc() x.data.total_items = 800 x.data.items = [] # etc. How it works: The JSON-C data is stored in an internal dictionary (._dict) and the getattr, setattr, and delattr methods rewrite the name which you provide to mirror the expected format in JSON-C. (For more details on name conversion see _to_jsonc_name.) You may also access members using getitem, setitem, delitem as you would for a dictionary. For example x.data.total_items is equivalent to x['data']['totalItems'] (Not all dict methods are supported so if you need something other than the item operations, then you will want to use the ._dict member). You may need to use getitem or the _dict member to access certain properties in cases where the JSON-C syntax does not map neatly to Python objects. For example the YouTube Video feed has some JSON like this: "content": {"1": "rtsp://v5.cache3.c.youtube.com..."...} You cannot do x.content.1 in Python, so you would use the getitem as follows: x.content['1'] or you could use the _dict member as follows: x.content._dict['1'] If you need to create a new object with such a mapping you could use. x.content = gdata.core.Jsonc(_dict={'1': 'rtsp://cache3.c.youtube.com...'}) """ def __init__(self, _dict=None, **kwargs): json = _dict or {} for key, value in kwargs.iteritems(): if key.startswith('_'): object.__setattr__(self, key, value) else: json[_to_jsonc_name(key)] = _convert_to_jsonc(value) object.__setattr__(self, '_dict', json) def __setattr__(self, name, value): if name.startswith('_'): object.__setattr__(self, name, value) else: object.__getattribute__( self, '_dict')[_to_jsonc_name(name)] = _convert_to_jsonc(value) def __getattr__(self, name): if name.startswith('_'): object.__getattribute__(self, name) else: try: return object.__getattribute__(self, '_dict')[_to_jsonc_name(name)] except KeyError: raise AttributeError( 'No member for %s or [\'%s\']' % (name, _to_jsonc_name(name))) def __delattr__(self, name): if name.startswith('_'): object.__delattr__(self, name) else: try: del object.__getattribute__(self, '_dict')[_to_jsonc_name(name)] except KeyError: raise AttributeError( 'No member for %s (or [\'%s\'])' % (name, _to_jsonc_name(name))) # For container methods pass-through to the underlying dict. def __getitem__(self, key): return self._dict[key] def __setitem__(self, key, value): self._dict[key] = value def __delitem__(self, key): del self._dict[key]