mirror of
https://github.com/kennethreitz/tablib.git
synced 2026-06-05 15:00:19 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f17ccf445 | |||
| 9b3268f0ad | |||
| f386ef8ac8 | |||
| e8f5e023c4 | |||
| 81445aeec8 | |||
| f94a236122 | |||
| bfbb7c626f | |||
| be0f77f9ee | |||
| 3b44349090 | |||
| 04a16afa58 | |||
| a8632125dc | |||
| ccf2ebcde2 | |||
| 2c60ce9233 | |||
| 649c7e8bb7 | |||
| 2d3dc5ef71 | |||
| efc516f366 | |||
| b2a51fd941 | |||
| d54d70bc22 | |||
| 391ad61bef | |||
| 99a45814d1 | |||
| fad3546614 | |||
| 7ba2849829 | |||
| 7ec0f2ef07 | |||
| bd470684a4 | |||
| dbcea81c17 | |||
| 49dc4a249e | |||
| 7cd82f956f | |||
| 13c3e537fd | |||
| f913853cae | |||
| ea1de420a3 | |||
| d0c8df95a3 | |||
| bb4e97f8aa | |||
| ffaeb64639 | |||
| f31ec562b4 | |||
| 68d7204b2d | |||
| 52db1ddc3e | |||
| 4755020dd7 | |||
| 5468dd7e67 | |||
| 8673710ddb | |||
| f01cf184d4 | |||
| 1482ca4a19 | |||
| 93c6c39581 | |||
| a0cb44cc43 | |||
| b2cd061773 | |||
| 876b849950 |
+4
-2
@@ -4,8 +4,8 @@ dist/*
|
||||
MANIFEST
|
||||
|
||||
# python skin
|
||||
.pyc
|
||||
.pyo
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# osx noise
|
||||
.DS_Store
|
||||
@@ -15,3 +15,5 @@ profile
|
||||
.idea
|
||||
.idea/*
|
||||
|
||||
# vi noise
|
||||
*.swp
|
||||
|
||||
+21
@@ -1,6 +1,27 @@
|
||||
History
|
||||
=======
|
||||
|
||||
0.7.0 (2010-09-20)
|
||||
------------------
|
||||
|
||||
* Renamed DataBook Databook for consistiency.
|
||||
* Export properties changed to methods (XLS filename / StringIO bug).
|
||||
* Optional Dataset.xls(path='filename') support (for writing on windows).
|
||||
* Added utf-8 on the worksheet level.
|
||||
|
||||
|
||||
0.6.4 (2010-09-19)
|
||||
------------------
|
||||
|
||||
* Updated unicode export for XLS.
|
||||
* More exhaustive unit tests.
|
||||
|
||||
|
||||
0.6.3 (2010-09-14)
|
||||
------------------
|
||||
* Added Dataset.append() support for columns.
|
||||
|
||||
|
||||
0.6.2 (2010-09-13)
|
||||
------------------
|
||||
* Fixed Dataset.append() error on empty dataset.
|
||||
|
||||
+15
-10
@@ -31,11 +31,11 @@ Usage
|
||||
|
||||
Populate fresh data files: ::
|
||||
|
||||
headers = ('first_name', 'last_name', 'gpa')
|
||||
headers = ('first_name', 'last_name')
|
||||
|
||||
data = [
|
||||
('John', 'Adams', 90),
|
||||
('George', 'Washington', 67)
|
||||
('John', 'Adams'),
|
||||
('George', 'Washington')
|
||||
]
|
||||
|
||||
data = tablib.Dataset(*data, headers=headers)
|
||||
@@ -43,7 +43,11 @@ Populate fresh data files: ::
|
||||
|
||||
Intelligently add new rows: ::
|
||||
|
||||
>>> data.append(('Henry', 'Ford', 83))
|
||||
>>> data.append(('Henry', 'Ford'))
|
||||
|
||||
Intelligently add new columns: ::
|
||||
|
||||
>>> data.append(col=('age', 90, 67, 83))
|
||||
|
||||
Slice rows: ::
|
||||
|
||||
@@ -66,7 +70,7 @@ JSON!
|
||||
+++++
|
||||
::
|
||||
|
||||
>>> print data.json
|
||||
>>> print data.json()
|
||||
[
|
||||
{
|
||||
"last_name": "Adams",
|
||||
@@ -85,7 +89,7 @@ YAML!
|
||||
+++++
|
||||
::
|
||||
|
||||
>>> print data.yaml
|
||||
>>> print data.yaml()
|
||||
- {age: 90, first_name: John, last_name: Adams}
|
||||
- {age: 83, first_name: Henry, last_name: Ford}
|
||||
|
||||
@@ -93,7 +97,7 @@ CSV...
|
||||
++++++
|
||||
::
|
||||
|
||||
>>> print data.csv
|
||||
>>> print data.csv()
|
||||
first_name,last_name,age
|
||||
John,Adams,90
|
||||
Henry,Ford,83
|
||||
@@ -102,8 +106,8 @@ EXCEL!
|
||||
++++++
|
||||
::
|
||||
|
||||
>>> open('people.xls').write(data.xls)
|
||||
|
||||
>>> data.xls('people.xls')
|
||||
|
||||
It's that easy.
|
||||
|
||||
|
||||
@@ -122,7 +126,7 @@ Or, if you absolutely must: ::
|
||||
Contribute
|
||||
----------
|
||||
|
||||
If you'd like to contribute, simply fork `the repository`_, commit your changes, and send a pull request. Make sure you add yourself to AUTHORS_.
|
||||
If you'd like to contribute, simply fork `the repository`_, commit your changes to the **develop** branch (or branch off of it), and send a pull request. Make sure you add yourself to AUTHORS_.
|
||||
|
||||
|
||||
Roadmap
|
||||
@@ -133,6 +137,7 @@ Roadmap
|
||||
- Auto-detect import format
|
||||
- Add possible other exports (SQL?)
|
||||
- Possibly plugin-ify format architecture
|
||||
- Ability to assign types to rows (set, regex=, &c.)
|
||||
- Plugin support
|
||||
|
||||
.. _`the repository`: http://github.com/kennethreitz/tablib
|
||||
|
||||
@@ -18,7 +18,7 @@ if sys.argv[-1] == "publish":
|
||||
|
||||
setup(
|
||||
name='tablib',
|
||||
version='0.6.2',
|
||||
version='0.7.0',
|
||||
description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)',
|
||||
long_description=open('README.rst').read() + '\n\n' +
|
||||
open('HISTORY.rst').read(),
|
||||
|
||||
+85
-37
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
import csv
|
||||
import cStringIO
|
||||
import StringIO
|
||||
import random
|
||||
|
||||
import simplejson as json
|
||||
@@ -21,8 +21,8 @@ from helpers import *
|
||||
# __all__ = ['Dataset', 'DataBook']
|
||||
|
||||
__name__ = 'tablib'
|
||||
__version__ = '0.6.1'
|
||||
__build__ = 0x000601
|
||||
__version__ = '0.7.0'
|
||||
__build__ = 0x000700
|
||||
__author__ = 'Kenneth Reitz'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2010 Kenneth Reitz'
|
||||
@@ -54,7 +54,7 @@ class Dataset(object):
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
if is_string(key):
|
||||
if isinstance(key, basestring):
|
||||
if key in self.headers:
|
||||
pos = self.headers.index(key) # get 'key' index from each data
|
||||
return [row[pos] for row in self._data]
|
||||
@@ -80,10 +80,15 @@ class Dataset(object):
|
||||
return '<dataset object>'
|
||||
|
||||
|
||||
def _validate(self, row=None, safety=False):
|
||||
def _validate(self, row=None, col=None, safety=False):
|
||||
"""Assures size of every row in dataset is of proper proportions."""
|
||||
if row:
|
||||
is_valid = (len(row) == self.width) if self.width else True
|
||||
elif col:
|
||||
if self.headers:
|
||||
is_valid = (len(col) - 1) == self.height
|
||||
else:
|
||||
is_valid = (len(col) == self.height) if self.height else True
|
||||
else:
|
||||
is_valid = all((len(x)== self.width for x in self._data))
|
||||
|
||||
@@ -130,33 +135,39 @@ class Dataset(object):
|
||||
"""Headers property."""
|
||||
return self.__headers
|
||||
|
||||
|
||||
@headers.setter
|
||||
def headers(self, collection):
|
||||
"""Validating headers setter."""
|
||||
self._validate(collection)
|
||||
self.__headers = collection
|
||||
|
||||
if collection:
|
||||
try:
|
||||
self.__headers = list(collection)
|
||||
except TypeError, why:
|
||||
raise TypeError
|
||||
else:
|
||||
self.__headers = None
|
||||
|
||||
|
||||
@property
|
||||
def dict(self):
|
||||
"""Returns python dict of Dataset."""
|
||||
return self._package()
|
||||
|
||||
@property
|
||||
|
||||
def json(self):
|
||||
"""Returns JSON representation of Dataset."""
|
||||
return json.dumps(self.dict)
|
||||
|
||||
|
||||
@property
|
||||
def yaml(self):
|
||||
"""Returns YAML representation of Dataset."""
|
||||
return yaml.dump(self.dict)
|
||||
|
||||
|
||||
@property
|
||||
def csv(self):
|
||||
"""Returns CSV representation of Dataset."""
|
||||
stream = cStringIO.StringIO()
|
||||
stream = StringIO.StringIO()
|
||||
_csv = csv.writer(stream)
|
||||
|
||||
for row in self._package(dicts=False):
|
||||
@@ -165,34 +176,58 @@ class Dataset(object):
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
@property
|
||||
def xls(self):
|
||||
def xls(self, path=None):
|
||||
"""Returns XLS representation of Dataset."""
|
||||
stream = cStringIO.StringIO()
|
||||
|
||||
wb = xlwt.Workbook()
|
||||
wb = xlwt.Workbook(encoding='utf8')
|
||||
ws = wb.add_sheet(self.title if self.title else 'Tabbed Dataset')
|
||||
|
||||
for i, row in enumerate(self._package(dicts=False)):
|
||||
for j, col in enumerate(row):
|
||||
ws.write(i, j, str(col))
|
||||
ws.write(i, j, col)
|
||||
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
if path:
|
||||
wb.save(path)
|
||||
return True
|
||||
else:
|
||||
stream = StringIO.StringIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
def append(self, row):
|
||||
def append(self, row=None, col=None):
|
||||
"""Adds a row to the end of Dataset"""
|
||||
self._validate(row)
|
||||
self._data.append(tuple(row))
|
||||
if row:
|
||||
self._validate(row)
|
||||
self._data.append(tuple(row))
|
||||
elif col:
|
||||
self._validate(col=col)
|
||||
|
||||
if self.headers:
|
||||
# pop the first item off, add to headers
|
||||
self.headers.append(col[0])
|
||||
col = col[1:]
|
||||
|
||||
if self.height and self.width:
|
||||
|
||||
for i, row in enumerate(self._data):
|
||||
_row = list(row)
|
||||
_row.append(col[i])
|
||||
self._data[i] = tuple(_row)
|
||||
else:
|
||||
self._data = [tuple([row]) for row in col]
|
||||
|
||||
|
||||
def index(self, i, row):
|
||||
def insert(self, i, row=None, col=None):
|
||||
"""Inserts a row at given position in Dataset"""
|
||||
self._validate(row)
|
||||
self._data.insert(i, tuple(row))
|
||||
if row:
|
||||
self._validate(row)
|
||||
self._data.insert(i, tuple(row))
|
||||
elif col:
|
||||
pass
|
||||
|
||||
|
||||
class DataBook(object):
|
||||
class Databook(object):
|
||||
"""A book of Dataset objects.
|
||||
Currently, this exists only for XLS workbook support.
|
||||
"""
|
||||
@@ -200,12 +235,14 @@ class DataBook(object):
|
||||
def __init__(self, sets=[]):
|
||||
self._datasets = sets
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
return '<%s databook>' % (self.title.lower())
|
||||
except AttributeError:
|
||||
return '<databook object>'
|
||||
|
||||
|
||||
def add_sheet(self, dataset):
|
||||
"""Add given dataset ."""
|
||||
if type(dataset) is Dataset:
|
||||
@@ -213,6 +250,7 @@ class DataBook(object):
|
||||
else:
|
||||
raise InvalidDatasetType
|
||||
|
||||
|
||||
def _package(self):
|
||||
collector = []
|
||||
for dset in self._datasets:
|
||||
@@ -222,48 +260,58 @@ class DataBook(object):
|
||||
))
|
||||
return collector
|
||||
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""The number of the Datasets within DataBook."""
|
||||
return len(self._datasets)
|
||||
|
||||
|
||||
@property
|
||||
def xls(self):
|
||||
def xls(self, path=None):
|
||||
"""Returns XLS representation of DataBook."""
|
||||
|
||||
stream = cStringIO.StringIO()
|
||||
wb = xlwt.Workbook()
|
||||
|
||||
for dset in self._datasets:
|
||||
ws = wb.add_sheet(dset.title if dset.title else 'Tabbed Dataset %s' % (int(random.random() * 100000000)))
|
||||
wb = xlwt.Workbook(encoding='utf8')
|
||||
|
||||
for i, dset in enumerate(self._datasets):
|
||||
ws = wb.add_sheet(dset.title if dset.title else 'Sheet%s' % (i))
|
||||
|
||||
#for row in self._package(dicts=False):
|
||||
for i, row in enumerate(dset._package(dicts=False)):
|
||||
for j, col in enumerate(row):
|
||||
ws.write(i, j, str(col))
|
||||
ws.write(i, j, col)
|
||||
|
||||
|
||||
|
||||
if path:
|
||||
wb.save(path)
|
||||
return True
|
||||
else:
|
||||
stream = cStringIO.StringIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""Returns JSON representation of Databook."""
|
||||
|
||||
return json.dumps(self._package())
|
||||
|
||||
@property
|
||||
|
||||
def yaml(self):
|
||||
"""Returns YAML representation of Databook."""
|
||||
|
||||
return yaml.dump(self._package())
|
||||
|
||||
|
||||
|
||||
|
||||
class InvalidDatasetType(Exception):
|
||||
"Only Datasets can be added to a DataBook"
|
||||
|
||||
|
||||
class InvalidDimensions(Exception):
|
||||
"Invalid size"
|
||||
|
||||
|
||||
class UnsupportedFormat(NotImplementedError):
|
||||
"Format is not supported"
|
||||
|
||||
+3
-7
@@ -10,16 +10,12 @@ class Struct(object):
|
||||
self.__dict__.update(entries)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
return getattr(self, key, None)
|
||||
|
||||
|
||||
def piped():
|
||||
"""Returns piped input via stdin, else False"""
|
||||
"""Returns piped input via stdin, else False."""
|
||||
with sys.stdin as stdin:
|
||||
# TTY is only way to detect if stdin contains data
|
||||
return stdin.read() if not stdin.isatty() else None
|
||||
|
||||
|
||||
def is_string(obj):
|
||||
"""Tests if an object is a string"""
|
||||
|
||||
return True if type(obj).__name__ == 'str' else False
|
||||
Regular → Executable
+163
-17
@@ -1,40 +1,186 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Tests for tablib."""
|
||||
|
||||
import unittest
|
||||
|
||||
import tablib
|
||||
|
||||
|
||||
class TablibTestCase(unittest.TestCase):
|
||||
"""Tablib test cases."""
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_empty_append(self):
|
||||
|
||||
"""Create simple data set with headers."""
|
||||
global data
|
||||
data = tablib.Dataset()
|
||||
|
||||
new_row = (1,2,3)
|
||||
self.headers = ('first_name', 'last_name', 'gpa')
|
||||
self.john = ('John', 'Adams', 90)
|
||||
self.george = ('George', 'Washington', 67)
|
||||
self.tom = ('Thomas', 'Jefferson', 50)
|
||||
|
||||
self.founders = tablib.Dataset(headers=self.headers)
|
||||
self.founders.append(self.john)
|
||||
self.founders.append(self.george)
|
||||
self.founders.append(self.tom)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
"""Teardown."""
|
||||
pass
|
||||
|
||||
|
||||
def test_empty_append(self):
|
||||
"""Verify append() correctly adds tuple with no headers."""
|
||||
new_row = (1, 2, 3)
|
||||
data.append(new_row)
|
||||
|
||||
# Verify width/data
|
||||
self.assertTrue(data.width == len(new_row))
|
||||
self.assertTrue(data[0] == new_row)
|
||||
|
||||
|
||||
def test_empty_append_with_headers(self):
|
||||
|
||||
data = tablib.Dataset()
|
||||
|
||||
"""Verify append() correctly detects mismatch of number of
|
||||
headers and data.
|
||||
"""
|
||||
data.headers = ['first', 'second']
|
||||
new_row = (1,2,3,4)
|
||||
|
||||
new_row = (1, 2, 3, 4)
|
||||
|
||||
self.assertRaises(tablib.InvalidDimensions, data.append, new_row)
|
||||
|
||||
# def test_adding_header with (self):
|
||||
|
||||
|
||||
|
||||
def test_add_column(self):
|
||||
"""Verify adding column works with/without headers."""
|
||||
|
||||
data.append(['kenneth'])
|
||||
data.append(['bessie'])
|
||||
|
||||
new_col = ['reitz', 'monke']
|
||||
|
||||
data.append(col=new_col)
|
||||
|
||||
self.assertEquals(data[0], ('kenneth', 'reitz'))
|
||||
self.assertEquals(data.width, 2)
|
||||
|
||||
# With Headers
|
||||
data.headers = ('fname', 'lname')
|
||||
new_col = ['age', 21, 22]
|
||||
data.append(col=new_col)
|
||||
|
||||
self.assertEquals(data[new_col[0]], new_col[1:])
|
||||
|
||||
|
||||
def test_add_column_no_data_no_headers(self):
|
||||
"""Verify adding new column with no headers."""
|
||||
|
||||
new_col = ('reitz', 'monke')
|
||||
|
||||
data.append(col=new_col)
|
||||
|
||||
self.assertEquals(data[0], tuple([new_col[0]]))
|
||||
self.assertEquals(data.width, 1)
|
||||
self.assertEquals(data.height, len(new_col))
|
||||
|
||||
|
||||
def test_add_column_no_data_with_headers(self):
|
||||
"""Verify adding new column with headers."""
|
||||
|
||||
data.headers = ('first', 'last')
|
||||
|
||||
new_col = ('age',)
|
||||
data.append(col=new_col)
|
||||
|
||||
self.assertEquals(len(data.headers), 3)
|
||||
self.assertEquals(data.width, 3)
|
||||
|
||||
new_col = ('foo', 'bar')
|
||||
|
||||
self.assertRaises(tablib.InvalidDimensions, data.append, col=new_col)
|
||||
|
||||
|
||||
def test_header_slicing(self):
|
||||
"""Verify slicing by headers."""
|
||||
|
||||
self.assertEqual(self.founders['first_name'],
|
||||
[self.john[0], self.george[0], self.tom[0]])
|
||||
self.assertEqual(self.founders['last_name'],
|
||||
[self.john[1], self.george[1], self.tom[1]])
|
||||
self.assertEqual(self.founders['gpa'],
|
||||
[self.john[2], self.george[2], self.tom[2]])
|
||||
|
||||
|
||||
def test_data_slicing(self):
|
||||
"""Verify slicing by data."""
|
||||
|
||||
# Slice individual rows
|
||||
self.assertEqual(self.founders[0], self.john)
|
||||
self.assertEqual(self.founders[:1], [self.john])
|
||||
self.assertEqual(self.founders[1:2], [self.george])
|
||||
self.assertEqual(self.founders[-1], self.tom)
|
||||
self.assertEqual(self.founders[3:], [])
|
||||
|
||||
# Slice multiple rows
|
||||
self.assertEqual(self.founders[:], [self.john, self.george, self.tom])
|
||||
self.assertEqual(self.founders[0:2], [self.john, self.george])
|
||||
self.assertEqual(self.founders[1:3], [self.george, self.tom])
|
||||
self.assertEqual(self.founders[2:], [self.tom])
|
||||
|
||||
|
||||
def test_delete(self):
|
||||
"""Verify deleting from dataset works."""
|
||||
|
||||
# Delete from front of object
|
||||
del self.founders[0]
|
||||
self.assertEqual(self.founders[:], [self.george, self.tom])
|
||||
|
||||
# Verify dimensions, width should NOT change
|
||||
self.assertEqual(self.founders.height, 2)
|
||||
self.assertEqual(self.founders.width, 3)
|
||||
|
||||
# Delete from back of object
|
||||
del self.founders[1]
|
||||
self.assertEqual(self.founders[:], [self.george])
|
||||
|
||||
# Verify dimensions, width should NOT change
|
||||
self.assertEqual(self.founders.height, 1)
|
||||
self.assertEqual(self.founders.width, 3)
|
||||
|
||||
# Delete from invalid index
|
||||
self.assertRaises(IndexError, self.founders.__delitem__, 3)
|
||||
|
||||
|
||||
def test_csv_export(self):
|
||||
"""Verify exporting dataset object as CSV."""
|
||||
|
||||
# Build up the csv string with headers first, followed by each row
|
||||
csv = ''
|
||||
for col in self.headers:
|
||||
csv += col + ','
|
||||
|
||||
csv = csv.strip(',') + '\r\n'
|
||||
|
||||
for founder in self.founders:
|
||||
for col in founder:
|
||||
csv += str(col) + ','
|
||||
csv = csv.strip(',') + '\r\n'
|
||||
|
||||
self.assertEqual(csv, self.founders.csv())
|
||||
|
||||
|
||||
def test_unicode_append(self):
|
||||
"""Passes in a single unicode charecter and exports."""
|
||||
|
||||
new_row = ('å', 'é')
|
||||
data.append(new_row)
|
||||
|
||||
data.json()
|
||||
data.yaml()
|
||||
data.csv()
|
||||
data.xls()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user