Commits

Chris Mutel committed bf33172 Merge

Merged bw2package branch

Comments (0)

Files changed (16)

 Changelog
 *********
 
+0.12.1 (2014-02-04)
+===================
+
+New BW2Package format
+---------------------
+
+The new BW2Package is not specific to databases or methods, but should work for any data store that implements the DataStore API. This allows for normalization, weighting, regionalization, and others, and makes it easy to backup and restore.
+
 0.12 (2014-02-04)
 =================
 

bw2data/__init__.py

 # -*- coding: utf-8 -*
-__version__ = (0, 12)
+__version__ = (0, 12, 1)
 
 from ._config import config
 from .meta import databases, methods, mapping, reset_meta, geomapping, \

bw2data/data_store.py

 # -*- coding: utf-8 -*
 from . import config
-from .errors import UnknownObject
+from .errors import UnknownObject, MissingIntermediateData
 from .utils import safe_filename
 import numpy as np
 import os
         new_obj.process()
         return new_obj
 
+    def backup(self):
+        """Save a backup to ``backups`` folder.
+
+        Returns:
+            File path of backup.
+
+        """
+        from .io import BW2Package
+        return BW2Package.export_obj(self)
+
     def write(self, data):
         """Serialize intermediate data to disk.
 
         Need some metaprogramming because class methods have `self` injected automatically."""
         self.validator(data)
         return True
-
-    def backup(self):
-        """Backup data to compressed JSON file"""
-        raise NotImplementedError
-

bw2data/database.py

         ('col', np.uint32),
     ]
 
-
-    def backup(self):
-        """Save a backup to ``backups`` folder.
-
-        Returns:
-            File path of backup.
-
-        """
-        from .io import BW2PackageExporter
-        return BW2PackageExporter.export_database(self.name,
-            folder="backups", extra_string="." + str(int(time()))
-            )
-
     def copy(self, name):
         """Make a copy of the database.
 

bw2data/errors.py

 
 class UnknownObject(StandardError):
     pass
+
+
+class UnsafeData(StandardError):
+    """bw2package data comes from a class that isn't recognized by Brightway2"""
+    pass
+
+
+class InvalidPackage(StandardError):
+    """bw2package data doesn't validate"""
+    pass

bw2data/io/__init__.py

-from .bw2package import BW2PackageExporter, BW2PackageImporter, \
-    download_biosphere, download_methods
+from .bw2package import BW2Package, download_biosphere, download_methods
 from .export_gexf import DatabaseToGEXF, DatabaseSelectionToGEXF, keyword_to_gephi_graph
 from .import_ecospold import Ecospold1Importer
 from .import_method import EcospoldImpactAssessmentImporter

bw2data/io/bw2package.py

 # -*- coding: utf-8 -*
-from .. import Database, databases, config, JsonWrapper, methods, Method
+from .. import config
+from ..serialization import JsonWrapper, JsonSanitizer
 from ..logs import get_logger
-from ..utils import database_hash, download_file
+from ..utils import download_file
+from ..errors import UnsafeData, InvalidPackage
+from ..validate import bw2package_validator
+from voluptuous import Invalid
 from time import time
-import bz2
 import os
-import warnings
 
 
-class BW2PackageExporter(object):
+class BW2Package(object):
+    """This is a format for saving objects which implement the :ref:`datastore` API. Data is stored as a BZip2-compressed file of JSON data. This archive format is compatible across Python versions, and is, at least in theory, programming-language agnostic.
+
+    Validation is done with ``bw2data.validate.bw2package_validator``.
+
+    The data format is:
+
+    .. code-block:: python
+
+        {
+            'metadata': {},  # Dictionary of metadata to be written to metadata-store.
+            'name': basestring,  # Name of object
+            'class': {  # Data on the undying class. A new class is instantiated base on these strings. See _create_class.
+                'module': basestring,  # e.g. "bw2data.database"
+                'name': basestring  # e.g. "Database"
+            },
+            'unrolled_dict': bool,  # Flag indicating if dictionary keys needed to be modified for JSON
+            'data': object  # Object data, e.g. LCIA method or LCI database
+        }
+
+    Perfect roundtrips between machines are not guaranteed:
+        * All lists are converted to tuples (because JSON does not distinguish between lists and tuples).
+        * Absolute filepaths in metadata would be specific to a certain computer and user.
+
+    .. note:: This class does not need to be instantiated, as all its methods are ``classmethods``, i.e. do ``BW2Package.import_obj("foo")`` instead of ``BW2Package().import_obj("foo")``
+
+    """
+    APPROVED = {
+        'bw2data',
+        'bw2regional',
+        'bw2calc'
+    }
+
     @classmethod
-    def _prepare_method(cls, name):
-        data = {
-            "metadata": methods[name],
-            "cfs": [{
-                "database": o[0][0],
-                "code": o[0][1],
-                "amount": o[1],
-                "location": o[2],
-            } for o in Method(name).load()]
+    def _get_class_metadata(cls, obj):
+        return {
+            'module': obj.__class__.__module__,
+            'name': obj.__class__.__name__
         }
-        data["metadata"]["name"] = name
+
+    @classmethod
+    def _is_valid_package(cls, data):
+        try:
+            bw2package_validator(data)
+            return True
+        except Invalid:
+            return False
+
+    @classmethod
+    def _is_whitelisted(cls, metadata):
+        return metadata['module'].split(".")[0] in cls.APPROVED
+
+    @classmethod
+    def _create_class(cls, metadata, apply_whitelist=True):
+        if apply_whitelist and not cls._is_whitelisted(metadata):
+            raise UnsafeData("{}.{} not a whitelisted class name".format(
+                metadata['module'], metadata['name']
+            ))
+        exec("from {} import {}".format(metadata['module'], metadata['name']))
+        return locals()[metadata['name']]
+
+    @classmethod
+    def _prepare_obj(cls, obj):
+        return {
+            'metadata': obj.metadata[obj.name],
+            'name': obj.name,
+            'class': cls._get_class_metadata(obj),
+            'data': obj.load()
+        }
+
+    @classmethod
+    def _load_obj(cls, data, whitelist=True):
+        if not cls._is_valid_package(data):
+            raise InvalidPackage
+        data['class'] = cls._create_class(data['class'], whitelist)
         return data
 
     @classmethod
-    def export_ia_method(cls, name, folder="export"):
-        filepath = os.path.join(config.request_dir(folder),
-            ".".join(name) + ".bw2iapackage")
-        with bz2.BZ2File(filepath, "w") as f:
-            f.write(JsonWrapper.dumps([cls._prepare_method(name)]))
+    def _create_obj(cls, data):
+        instance = data['class'](data['name'])
+
+        if data['name'] not in instance.metadata:
+            instance.register(**data['metadata'])
+        else:
+            instance.backup()
+            instance.metadata[data['name']] = data['metadata']
+
+        instance.write(data['data'])
+        instance.process()
+        return instance
+
+    @classmethod
+    def _write_file(cls, filepath, data):
+        JsonWrapper.dump_bz2(
+            JsonSanitizer.sanitize(data), filepath
+        )
+
+    @classmethod
+    def export_objs(cls, objs, filename, folder="export"):
+        """Export a list of objects. Can have heterogeneous types.
+
+        Args:
+            * *objs* (list): List of objects to export.
+            * *filename* (str): Name of file to create.
+            * *folder* (str, optional): Folder to create file in. Default is ``export``.
+
+        Returns:
+            Filepath of created file.
+
+        """
+        filepath = os.path.join(
+            config.request_dir(folder),
+            filename + u".bw2package"
+        )
+        cls._write_file(filepath, [cls._prepare_obj(o) for o in objs])
         return filepath
 
     @classmethod
-    def export_all_methods(cls, folder="export"):
-        filepath = os.path.join(config.request_dir(folder),
-            "methods.bw2iapackage")
-        with bz2.BZ2File(filepath, "w") as f:
-            f.write(JsonWrapper.dumps(
-                [cls._prepare_method(name) for name in methods.list]
-                ))
+    def export_obj(cls, obj, filename=None, folder="export"):
+        """Export an object.
+
+        Args:
+            * *obj* (object): Object to export.
+            * *filename* (str, optional): Name of file to create. Default is ``obj.name``.
+            * *folder* (str, optional): Folder to create file in. Default is ``export``.
+
+        Returns:
+            Filepath of created file.
+
+        """
+        if filename is None:
+            filename = obj.name
+        filepath = os.path.join(
+            config.request_dir(folder),
+            filename + u".bw2package"
+        )
+        cls._write_file(filepath, cls._prepare_obj(obj))
         return filepath
 
     @classmethod
-    def export_database(cls, name, include_dependencies=False, **kwargs):
-        assert name in databases, "Can't find this database"
+    def load_file(cls, filepath, whitelist=True):
+        """Load a bw2package file with one or more objects. Does not create new objects.
 
-        extra_string = kwargs.get("extra_string", "")
-        folder = kwargs.get("folder", "export")
+        Args:
+            * *filepath* (str): Path of file to import
+            * *whitelist* (bool): Apply whitelist to allowed types. Default is ``True``.
 
-        if include_dependencies:
-            for dependency in databases[name]["depends"]:
-                assert dependency in databases, \
-                    "Can't find dependent database %s" % dependency
-            to_export = [name] + databases[name]["depends"]
-            filename = name + extra_string + ".fat.bw2package"
+        Returns the loaded data in the bw2package dict data format, with the following changes:
+            * ``"class"`` is an actual class.
+
+        """
+        raw_data = JsonSanitizer.load(JsonWrapper.load_bz2(filepath))
+        if isinstance(raw_data, dict):
+            return cls._load_obj(raw_data)
         else:
-            to_export = [name]
-            filename = name + extra_string + ".bw2package"
-        filepath = os.path.join(config.request_dir(folder), filename)
-        with bz2.BZ2File(filepath, "w") as f:
-            f.write(JsonWrapper.dumps({db_name: {
-                "metadata": databases[db_name],
-                "data": {k[1]: v for k, v in Database(db_name).load().iteritems()}
-                } for db_name in to_export}))
-        return filepath
+            return [cls._load_obj(o) for o in raw_data]
 
     @classmethod
-    def export(cls, name, include_dependencies=False):
-        if isinstance(name, (tuple, list)):
-            return cls.export_ia_method(name)
-        elif isinstance(name, basestring):
-            return cls.export_database(name, include_dependencies)
+    def import_file(cls, filepath, whitelist=True):
+        """Import bw2package file, and create the loaded objects, including registering, writing, and processing the created objects.
+
+        Args:
+            * *filepath* (str): Path of file to import
+            * *whitelist* (bool): Apply whitelist to allowed types. Default is ``True``.
+
+        Returns:
+            Created object or list of created objects.
+
+        """
+        loaded = cls.load_file(filepath, whitelist)
+        if isinstance(loaded, dict):
+            return cls._create_obj(loaded)
         else:
-            raise ValueError("Unknown input data")
-
-
-class BW2PackageImporter(object):
-    @classmethod
-    def importer(cls, filepath, overwrite=False):
-        if overwrite:
-            raise NotImplementedError
-        if filepath.split(".")[-1] == "bw2package":
-            return cls.import_database(filepath, overwrite)
-        elif filepath.split(".")[-1] == "bw2iapackage":
-            return cls.import_method(filepath, overwrite)
-        else:
-            raise ValueError("Unknown input data")
-
-    @classmethod
-    def import_database(cls, filepath, overwrite):
-        logger = get_logger("io-performance.log")
-
-        if overwrite:
-            raise NotImplementedError
-        with bz2.BZ2File(filepath) as f:
-            start = time()
-
-            package_data = JsonWrapper.loads(f.read())
-
-            logger.info("Loading BW2Package database (len %s): %.4g" % (len(package_data), time() - start))
-
-        with warnings.catch_warnings():
-            warnings.simplefilter("ignore")
-            for name, data in package_data.iteritems():
-                start = time()
-
-                db_data = dict([((name, key), value) for key, value in \
-                    data["data"].iteritems()])
-                if name in databases:
-                    raise ValueError("Database %s already exists" % name)
-                metadata = data["metadata"]
-                database = Database(name)
-                database.register(
-                    format=metadata["from format"],
-                    depends=metadata["depends"],
-                    num_processes=metadata["number"],
-                    version=metadata["version"]
-                    )
-                database.write(db_data)
-                database.process()
-
-                logger.info("Processing BW2Package database (len %s): %.4g" % (len(db_data), time() - start))
-
-    @classmethod
-    def import_method(cls, filepath, overwrite):
-        logger = get_logger("io-performance.log")
-
-        if overwrite:
-            raise NotImplementedError
-        with bz2.BZ2File(filepath) as f:
-            start = time()
-
-            package_data = JsonWrapper.loads(f.read())
-
-            logger.info("Loading BW2Package method (len %s): %.4g" % (len(package_data), time() - start))
-
-        with warnings.catch_warnings():
-            warnings.simplefilter("ignore")
-            start = time()
-            for data in package_data:
-
-                name = tuple(data["metadata"]["name"])
-                if name in methods:
-                    raise ValueError("Duplicate method")
-                method = Method(name)
-                method.register(
-                    unit=data["metadata"]["unit"],
-                    description=data["metadata"]["description"],
-                    num_cfs=data["metadata"]["num_cfs"]
-                )
-                method.write([
-                    [(o["database"], o["code"]), o["amount"], o["location"]
-                ] for o in data["cfs"]])
-                method.process()
-
-            logger.info("Processing BW2Package methods (len %s): %.4g" % (len(package_data), time() - start))
+            return [cls._create_obj(o) for o in loaded]
 
 
 def download_biosphere():
     logger = get_logger("io-performance.log")
     start = time()
-    filepath = download_file("biosphere.bw2package")
+    filepath = download_file("biosphere-new.bw2package")
     logger.info("Downloading biosphere package: %.4g" % (time() - start))
     start = time()
-    BW2PackageImporter.importer(filepath)
+    BW2Package.import_file(filepath)
     logger.info("Importing biosphere package: %.4g" % (time() - start))
 
 
 def download_methods():
     logger = get_logger("io-performance.log")
     start = time()
-    filepath = download_file("methods.bw2iapackage")
+    filepath = download_file("methods-new.bw2package")
     logger.info("Downloading methods package: %.4g" % (time() - start))
     start = time()
-    BW2PackageImporter.importer(filepath)
+    BW2Package.import_file(filepath)
     logger.info("Importing methods package: %.4g" % (time() - start))

bw2data/serialization.py

 # -*- coding: utf-8 -*-
 from . import config
 from time import time
+import bz2
 import os
 import random
 try:
 class JsonWrapper(object):
     @classmethod
     def dump(self, data, file):
-        with open(file, "w") as f:
+        with open(file, "wb") as f:
             if anyjson:
                 f.write(anyjson.serialize(data))
             else:
                 json.dump(data, f, indent=2)
 
     @classmethod
+    def dump_bz2(self, data, filepath):
+        with bz2.BZ2File(filepath, "wb") as f:
+            f.write(JsonWrapper.dumps(data))
+
+    @classmethod
     def load(self, file):
         if anyjson:
             return anyjson.deserialize(open(file).read())
             return json.load(open(file))
 
     @classmethod
+    def load_bz2(self, filepath):
+        return JsonWrapper.loads(bz2.BZ2File(filepath).read())
+
+    @classmethod
     def dumps(self, data):
         if anyjson:
             return anyjson.serialize(data)
             return json.loads(data)
 
 
+class JsonSanitizer(object):
+    @classmethod
+    def sanitize(cls, data):
+        if isinstance(data, tuple):
+            return {
+                '__tuple__': True,
+                'data': [cls.sanitize(x) for x in data]
+            }
+        elif isinstance(data, dict):
+            return {
+                '__dict__': True,
+                'keys': [cls.sanitize(x) for x in data.keys()],
+                'values': [cls.sanitize(x) for x in data.values()]
+            }
+        elif isinstance(data, list):
+            return [cls.sanitize(x) for x in data]
+        else:
+            return data
+
+    @classmethod
+    def load(cls, data):
+        if isinstance(data, dict):
+            if "__tuple__" in data:
+                return tuple([cls.load(x) for x in data['data']])
+            elif "__dict__" in data:
+                return dict(zip(
+                    [cls.load(x) for x in data['keys']],
+                    [cls.load(x) for x in data['values']]
+                ))
+            else:
+                raise ValueError
+        elif isinstance(data, list):
+            return [cls.load(x) for x in data]
+        else:
+            return data
+
+
 class SerializedDict(object):
     """Base class for dictionary that can be serlialized to of unserialized from disk. Uses JSON as its storage format. Has most of the methods of a dictionary.
 
     def __str__(self):
         return unicode(self).encode('utf-8')
 
+    def __unicode__(self):
+        return u"Brightway2 serialized dictionary with {} entries".format(len(self))
+
     def __delitem__(self, name):
         del self.data[name]
         self.flush()

bw2data/tests/__init__.py

 # -*- coding: utf-8 -*-
+from .array import ArrayProxyTest, ListArrayProxyTest
 from .base import BW2DataTest
-from .array import ArrayProxyTest, ListArrayProxyTest
 from .config import ConfigTest
+from .data_store import DataStoreTestCase
 from .database import DatabaseTest
-from .data_store import DataStoreTestCase
 from .geo import GeoTest
 from .ia import IADSTest, MethodTest, WeightingTest, NormalizationTest
+from .packaging import BW2PackageTest
+from .serialization import JsonSantizierTestCase
 from .simapro import SimaProImportTest
 from .sparse import SparseMatrixProxyTest
+from .updates import UpdatesTest
 from .utils import UtilsTest
-from .updates import UpdatesTest
 from .validation import ValidationTestCase

bw2data/tests/packaging.py

+# -*- coding: utf-8 -*-
+from . import BW2DataTest
+from .. import Database, config, databases
+from ..data_store import DataStore
+from ..errors import UnsafeData, InvalidPackage
+from ..io import BW2Package
+from ..serialization import SerializedDict
+from fixtures import food, biosphere
+import copy
+import fractions
+import json
+
+
+class MockMetadata(SerializedDict):
+    filename = "mock-meta.json"
+
+mocks = MockMetadata()
+
+
+class MockDS(DataStore):
+    """Mock DataStore for testing"""
+    metadata = mocks
+    validator = lambda x, y: True
+    dtype_fields = []
+
+    def process_data(self, row):
+        return (), 0
+
+
+class BW2PackageTest(BW2DataTest):
+    def extra_setup(self):
+        mocks.__init__()
+
+    def test_class_metadata(self):
+        class_metadata = {
+            'module': 'bw2data.tests.packaging',
+            'name': 'MockDS',
+        }
+        self.assertEqual(
+            BW2Package._get_class_metadata(MockDS('foo')),
+            class_metadata
+        )
+
+    def test_validation(self):
+        good_dict = {
+            'metadata': {'foo': 'bar'},
+            'name': 'Johnny',
+            'class': {
+                'module': 'some',
+                'name': 'thing'
+            },
+            'data': {}
+        }
+        self.assertTrue(BW2Package._is_valid_package(good_dict))
+        d = copy.deepcopy(good_dict)
+        d['name'] = ()
+        self.assertTrue(BW2Package._is_valid_package(d))
+        for key in ['metadata', 'name', 'data']:
+            d = copy.deepcopy(good_dict)
+            del d[key]
+            self.assertFalse(BW2Package._is_valid_package(d))
+
+    def test_whitelist(self):
+        good_class_metadata = {
+            'module': 'bw2data.tests.packaging',
+            'name': 'MockDS',
+        }
+        bad_class_metadata = {
+            'module': 'some.package',
+            'name': 'Foo',
+        }
+        self.assertTrue(BW2Package._is_whitelisted(good_class_metadata))
+        self.assertFalse(BW2Package._is_whitelisted(bad_class_metadata))
+
+    def test_create_class_whitelist(self):
+        bad_class_metadata = {
+            'module': 'some.package',
+            'name': 'Foo',
+        }
+        with self.assertRaises(UnsafeData):
+            BW2Package._create_class(bad_class_metadata)
+        with self.assertRaises(ImportError):
+            BW2Package._create_class(bad_class_metadata, False)
+
+    def test_create_class(self):
+        class_metadata = {
+            'module': 'collections',
+            'name': 'Counter'
+        }
+        cls = BW2Package._create_class(class_metadata, False)
+        import collections
+        self.assertEqual(cls, collections.Counter)
+        class_metadata = {
+            'module': 'bw2data.database',
+            'name': 'Database'
+        }
+        cls = BW2Package._create_class(class_metadata, False)
+        self.assertEqual(cls, Database)
+
+    def test_load_obj(self):
+        test_data = {
+            'metadata': {'foo': 'bar'},
+            'name': ['Johnny', 'B', 'Good'],
+            'class': {
+                'module': 'fractions',
+                'name': 'Fraction'
+            },
+            'data': {}
+        }
+        after = BW2Package._load_obj(copy.deepcopy(test_data), False)
+        for key in test_data:
+            self.assertTrue(key in after)
+        with self.assertRaises(InvalidPackage):
+            BW2Package._load_obj({})
+        self.assertEqual(after['class'], fractions.Fraction)
+
+    def test_create_obj(self):
+        mock_data = {
+            'class': {'module': 'bw2data.tests.packaging', 'name': 'MockDS'},
+            'metadata': {'circle': 'square'},
+            'data': [],
+            'name': 'Wilhelm'
+        }
+        data = BW2Package._load_obj(mock_data)
+        obj = BW2Package._create_obj(data)
+        self.assertTrue(isinstance(obj, MockDS))
+        self.assertTrue("Wilhelm" in mocks)
+        self.assertEqual(mocks['Wilhelm'], {'circle': 'square'})
+        self.assertEqual(MockDS("Wilhelm").load(), [])
+
+    def test_roundtrip_obj(self):
+        obj = MockDS("Slick Al")
+        obj.register()
+        obj.write(["a boring string", {'foo': 'bar'}, (1,2,3)])
+        fp = BW2Package.export_obj(obj)
+        obj.deregister()
+        del obj
+        self.assertFalse('Slick Al' in mocks)
+        obj = BW2Package.import_file(fp)
+        self.assertTrue('Slick Al' in mocks)
+        self.assertTrue(isinstance(obj, MockDS))
+        self.assertEqual(obj.load(), ["a boring string", {'foo': 'bar'}, (1,2,3)])
+
+    def test_roundtrip_objs(self):
+        pass

bw2data/tests/serialization.py

+from ..serialization import JsonSanitizer
+import unittest
+
+
+class JsonSantizierTestCase(unittest.TestCase):
+    def test_tuple(self):
+        self.assertEqual(
+            JsonSanitizer.sanitize((1,)),
+            {'__tuple__': True, 'data': [1]}
+        )
+        self.assertEqual(
+            JsonSanitizer.load({'__tuple__': True, 'data': [1]}),
+            (1,)
+        )
+
+    def test_dict(self):
+        self.assertEqual(
+            JsonSanitizer.sanitize({1: 2}),
+            {'__dict__': True, 'keys': [1], 'values': [2]}
+        )
+        self.assertEqual(
+            JsonSanitizer.load({'__dict__': True, 'keys': [1], 'values': [2]}),
+            {1: 2}
+        )
+
+    def test_nested(self):
+        input_data = [
+            {(1, 2): "foo"},
+            ["bar", (5, 6)],
+            {},
+            tuple([]),
+            ((7,),)
+        ]
+        expected = [
+            {'__dict__': True, 'keys': [{'__tuple__': True, 'data': [1, 2]}], 'values': ["foo"]},
+            ["bar", {'__tuple__': True, 'data': [5, 6]}],
+            {'__dict__': True, 'keys': [], 'values': []},
+            {'__tuple__': True, 'data': []},
+            {'__tuple__': True, 'data': [{'__tuple__': True, 'data': [7]}]},
+        ]
+        self.assertEqual(JsonSanitizer.sanitize(input_data), expected)
+        self.assertEqual(JsonSanitizer.load(expected), input_data)

bw2data/validate.py

 normalization_validator = Schema([
     [valid_tuple, maybe_uncertainty]
 ])
+
+bw2package_validator = Schema({
+    Required('metadata'): {basestring: object},
+    Required('name'): Any(basestring, tuple, list),
+    'class': {
+        Required('module'): basestring,
+        Required('name'): basestring,
+        "unrolled dict": bool,
+    },
+    'unrolled_dict': bool,
+    Required('data'): object
+})
 # The short X.Y version.
 version = '0.12'
 # The full version, including alpha/beta/rc tags.
-release = '0.12'
+release = '0.12.1'
 
 import sys
 from os.path import abspath, dirname
 
 Both the data and metadata objects *store* data, and provide easy ways to save and load data.
 
+.. _metadata-store:
+
 Metadata stores
 ---------------
 
 BW2Package
 ==========
 
-Brightway2 has its own data format for efficient saving, loading, and transfer. Read more at the `Brightway2 documentation <http://brightway2.readthedocs.org/en/latest/key-concepts.html#data-interchange>`_.
+Brightway2 has its own data format for archiving data which is both efficient and compatible across operating systems and programming languages. This is the default backup format for Brightway2 :ref:`datastore` objects.
 
 .. note:: **imports** and **exports** are supported.
 
-
-.. autoclass:: bw2data.io.BW2PackageImporter
-    :members:
-
-.. autoclass:: bw2data.io.BW2PackageExporter
+.. autoclass:: bw2data.io.BW2Package
     :members:
 
 Ecospold1
 
 setup(
     name='bw2data',
-    version="0.12",
+    version="0.12.1",
     packages=packages,
     author="Chris Mutel",
     author_email="cmutel@gmail.com",