mirror of
https://github.com/kennethreitz/tablib.git
synced 2026-06-05 15:00:19 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 993af5b0b4 | |||
| 0accb4c437 | |||
| 0821716983 | |||
| 660990b6b0 | |||
| 6152d995f0 | |||
| 0ea6d706a9 | |||
| 00d8ab0b37 | |||
| 06c2326dc0 | |||
| fa30ea858d | |||
| 4de2e17984 | |||
| 52b64757b7 | |||
| 5ff4a55ae6 | |||
| ce7d887adc | |||
| 57a535f577 | |||
| 357a5594c5 | |||
| f61b8d8926 | |||
| 22a193dafb | |||
| b539e96697 | |||
| 626a062747 | |||
| 9d2f7d6999 | |||
| a9d9671b7f | |||
| f1046cd13e | |||
| d21bd10908 | |||
| c26159d48f | |||
| 34fe72305e | |||
| d94420d968 | |||
| 51a720b21c | |||
| 20f51d0bc1 | |||
| 87d15a1529 | |||
| 08a6759520 | |||
| 205403d377 | |||
| 9858539c87 | |||
| 201d8d9910 | |||
| fede4a4f13 | |||
| a76933edd5 | |||
| 3197e59b25 | |||
| 1f000f2f2c | |||
| 7879fef65a | |||
| b8bff1190e | |||
| d77aba6210 | |||
| bf6e5c2e78 | |||
| 088b916bab | |||
| e4ac50260e | |||
| 7347d07624 | |||
| c9027b446c | |||
| 825de0193b | |||
| 78b483d39e | |||
| 8f09789d40 | |||
| e0e75ed43c | |||
| bdc84255a8 | |||
| b3c7145c40 | |||
| 44f43516a5 | |||
| e0a40577fd | |||
| 0329eb6168 | |||
| 4c3dc847b0 | |||
| 89fbd54b00 | |||
| 067dc769dc | |||
| 1726c1cf37 | |||
| debe77e432 |
@@ -0,0 +1,30 @@
|
||||
name: Docs and lint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
env:
|
||||
- TOXENV: docs
|
||||
- TOXENV: lint
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade tox
|
||||
|
||||
- name: Tox
|
||||
run: tox
|
||||
env: ${{ matrix.env }}
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [3.5, 3.6, 3.7]
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade tox
|
||||
python -m pip install -e .
|
||||
|
||||
- name: Tox tests
|
||||
shell: bash
|
||||
# Drop the dot: py3.7-tests -> py37-tests
|
||||
run: |
|
||||
tox -e py`echo ${{ matrix.python-version }} | tr -d .`-tests
|
||||
@@ -1,6 +0,0 @@
|
||||
[settings]
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
@@ -0,0 +1,25 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v1.25.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ["--py3-plus"]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-isort
|
||||
rev: v4.3.21
|
||||
hooks:
|
||||
- id: isort
|
||||
additional_dependencies: [toml]
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.4.2
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
+24
-8
@@ -1,14 +1,30 @@
|
||||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
- 3.6
|
||||
- 3.7
|
||||
- 3.8
|
||||
cache: pip
|
||||
dist: xenial
|
||||
cache:
|
||||
pip: true
|
||||
directories:
|
||||
- $HOME/.cache/pre-commit
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: 3.8
|
||||
env: TOXENV=docs
|
||||
- python: 3.8
|
||||
env: TOXENV=lint
|
||||
- python: 3.8
|
||||
- python: 3.7
|
||||
- python: 3.6
|
||||
|
||||
install: travis_retry pip install tox-travis
|
||||
|
||||
script: tox
|
||||
after_success: bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
after_success:
|
||||
- |
|
||||
if [[ "$TOXENV" != "docs" && "$TOXENV" != "lint" ]]; then
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
fi
|
||||
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: jazzband
|
||||
|
||||
@@ -8,6 +8,7 @@ Here is a list of passed and present much-appreciated contributors:
|
||||
Benjamin Wohlwend
|
||||
Bruno Soares
|
||||
Claude Paroz
|
||||
Daniel Santos
|
||||
Erik Youngren
|
||||
Hugo van Kemenade
|
||||
Iuri de Silvio
|
||||
@@ -24,6 +25,7 @@ Here is a list of passed and present much-appreciated contributors:
|
||||
Mark Walling
|
||||
Mathias Loesch
|
||||
Mike Waldner
|
||||
Peyman Salehi
|
||||
Rabin Nankhwa
|
||||
Tommy Anthony
|
||||
Tsuyoshi Hombashi
|
||||
|
||||
+21
@@ -1,5 +1,26 @@
|
||||
# History
|
||||
|
||||
## 1.0.0 (2020-01-13)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- Dropped Python 2 support
|
||||
- Dependencies are now all optional. To install `tablib` as before with all
|
||||
possible supported formats, run `pip install tablib[all]`
|
||||
|
||||
### Improvements
|
||||
|
||||
- Formats can now be dynamically registered through the
|
||||
`tablib.formats.registry.refister` API (#256).
|
||||
- Tablib methods expecting data input (`detect_format`, `import_set`,
|
||||
`Dataset.load`, `Databook.load`) now accepts file-like objects in addition
|
||||
to raw strings and bytestrings (#440).
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Fixed a crash when exporting an empty string with the ReST format (#368)
|
||||
- Error cells from imported .xls files contain now the error string (#202)
|
||||
|
||||
## 0.14.0 (2019-10-19)
|
||||
|
||||
### Deprecations
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
recursive-include docs *
|
||||
recursive-include tests *
|
||||
include pytest.ini tox.ini .isort.cfg .coveragerc HISTORY.md README.md LICENSE AUTHORS
|
||||
prune docs/_build
|
||||
prune *.pyc
|
||||
prune __pycache__
|
||||
@@ -1,8 +1,13 @@
|
||||
# Tablib: format-agnostic tabular dataset library
|
||||
|
||||
[](https://jazzband.co/)
|
||||
[](https://travis-ci.org/jazzband/tablib)
|
||||
[](https://pypi.org/project/tablib/)
|
||||
[](https://pypi.org/project/tablib/)
|
||||
[](https://pypistats.org/packages/tablib)
|
||||
[](https://travis-ci.org/jazzband/tablib)
|
||||
[](https://github.com/jazzband/tablib/actions)
|
||||
[](https://codecov.io/gh/jazzband/tablib)
|
||||
[](LICENSE)
|
||||
|
||||
_____ ______ ___________ ______
|
||||
__ /_______ ____ /_ ___ /___(_)___ /_
|
||||
@@ -29,149 +34,12 @@ Output formats supported:
|
||||
Note that tablib *purposefully* excludes XML support. It always will. (Note: This is a
|
||||
joke. Pull requests are welcome.)
|
||||
|
||||
Tablib documentation is graciously hosted on https://tablib.readthedocs.io
|
||||
|
||||
## Overview
|
||||
|
||||
`tablib.Dataset()`
|
||||
|
||||
A Dataset is a table of tabular data.
|
||||
It may or may not have a header row.
|
||||
They can be build and manipulated as raw Python datatypes (Lists of tuples|dictionaries).
|
||||
Datasets can be imported from JSON, YAML, DBF, and CSV;
|
||||
they can be exported to XLSX, XLS, ODS, JSON, YAML, DBF, CSV, TSV, and HTML.
|
||||
|
||||
`tablib.Databook()`
|
||||
|
||||
A Databook is a set of Datasets.
|
||||
The most common form of a Databook is an Excel file with multiple spreadsheets.
|
||||
Databooks can be imported from JSON and YAML;
|
||||
they can be exported to XLSX, XLS, ODS, JSON, and YAML.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Populate fresh data files:
|
||||
|
||||
```python
|
||||
headers = ('first_name', 'last_name')
|
||||
|
||||
data = [
|
||||
('John', 'Adams'),
|
||||
('George', 'Washington')
|
||||
]
|
||||
|
||||
data = tablib.Dataset(*data, headers=headers)
|
||||
```
|
||||
|
||||
Intelligently add new rows:
|
||||
|
||||
```python
|
||||
>>> data.append(('Henry', 'Ford'))
|
||||
```
|
||||
|
||||
Intelligently add new columns:
|
||||
|
||||
```python
|
||||
>>> data.append_col((90, 67, 83), header='age')
|
||||
```
|
||||
|
||||
Slice rows:
|
||||
|
||||
```python
|
||||
>>> print(data[:2])
|
||||
[('John', 'Adams', 90), ('George', 'Washington', 67)]
|
||||
```
|
||||
|
||||
Slice columns by header:
|
||||
|
||||
```python
|
||||
>>> print(data['first_name'])
|
||||
['John', 'George', 'Henry']
|
||||
```
|
||||
|
||||
Easily delete rows:
|
||||
|
||||
```python
|
||||
>>> del data[1]
|
||||
```
|
||||
|
||||
|
||||
## Exports
|
||||
|
||||
Drumroll please...........
|
||||
|
||||
### JSON!
|
||||
|
||||
```python
|
||||
>>> print(data.export('json'))
|
||||
[
|
||||
{
|
||||
"last_name": "Adams",
|
||||
"age": 90,
|
||||
"first_name": "John"
|
||||
},
|
||||
{
|
||||
"last_name": "Ford",
|
||||
"age": 83,
|
||||
"first_name": "Henry"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### YAML!
|
||||
|
||||
```python
|
||||
>>> print(data.export('yaml'))
|
||||
- {age: 90, first_name: John, last_name: Adams}
|
||||
- {age: 83, first_name: Henry, last_name: Ford}
|
||||
```
|
||||
|
||||
### CSV...
|
||||
|
||||
```python
|
||||
>>> print(data.export('csv'))
|
||||
first_name,last_name,age
|
||||
John,Adams,90
|
||||
Henry,Ford,83
|
||||
```
|
||||
|
||||
### EXCEL!
|
||||
|
||||
```python
|
||||
>>> with open('people.xls', 'wb') as f:
|
||||
... f.write(data.export('xls'))
|
||||
```
|
||||
|
||||
### DBF!
|
||||
|
||||
```python
|
||||
>>> with open('people.dbf', 'wb') as f:
|
||||
... f.write(data.export('dbf'))
|
||||
```
|
||||
|
||||
### Pandas DataFrame!
|
||||
|
||||
```python
|
||||
>>> print(data.export('df')):
|
||||
first_name last_name age
|
||||
0 John Adams 90
|
||||
1 Henry Ford 83
|
||||
```
|
||||
|
||||
It's that easy.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
To install tablib, simply:
|
||||
|
||||
```console
|
||||
$ pip install tablib[pandas]
|
||||
```
|
||||
It is also available in the ``docs`` directory of the source distribution.
|
||||
|
||||
Make sure to check out [Tablib on PyPI](https://pypi.org/project/tablib/)!
|
||||
|
||||
|
||||
## Contribute
|
||||
|
||||
Please see the [contributing guide](https://github.com/jazzband/tablib/blob/master/.github/CONTRIBUTING.md).
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Release checklist
|
||||
|
||||
Jazzband guidelines: https://jazzband.co/about/releases
|
||||
|
||||
* [ ] Get master to the appropriate code release state.
|
||||
[Travis CI](https://travis-ci.org/jazzband/tablib)
|
||||
should pass on master.
|
||||
[](https://travis-ci.org/jazzband/tablib)
|
||||
|
||||
* [ ] Check [HISTORY.md](https://github.com/jazzband/tablib/blob/master/HISTORY.md),
|
||||
update version number and release date
|
||||
|
||||
* [ ] Tag with version number and push tag, for example:
|
||||
```bash
|
||||
git tag -a v0.14.0 -m v0.14.0
|
||||
git push --tags
|
||||
```
|
||||
|
||||
* [ ] Once Travis CI has built and uploaded distributions, check files at
|
||||
[Jazzband](https://jazzband.co/projects/tablib) and release to
|
||||
[PyPI](https://pypi.org/pypi/tablib)
|
||||
|
||||
* [ ] Check installation:
|
||||
```bash
|
||||
pip uninstall -y tablib && pip install -U tablib
|
||||
```
|
||||
|
||||
* [ ] Create new GitHub release: https://github.com/jazzband/tablib/releases/new
|
||||
* Tag: Pick existing tag "v0.14.0"
|
||||
@@ -1,18 +0,0 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
"backports.csv" = "*"
|
||||
odfpy = "*"
|
||||
openpyxl = ">=2.4.0"
|
||||
pandas = "*"
|
||||
xlrd = "*"
|
||||
xlwt = "*"
|
||||
PyYAML = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
+11
-8
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Tablib documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Oct 5 15:25:21 2010.
|
||||
@@ -23,7 +22,11 @@ from pkg_resources import get_distribution
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode']
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'
|
||||
]
|
||||
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
@@ -38,8 +41,8 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Tablib'
|
||||
copyright = u'2019 Jazzband'
|
||||
project = 'Tablib'
|
||||
copyright = '2019 Jazzband'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
@@ -181,8 +184,8 @@ htmlhelp_basename = 'Tablibdoc'
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'Tablib.tex', u'Tablib Documentation',
|
||||
u'Jazzband', 'manual'),
|
||||
('index', 'Tablib.tex', 'Tablib Documentation',
|
||||
'Jazzband', 'manual'),
|
||||
]
|
||||
|
||||
latex_use_modindex = False
|
||||
@@ -222,6 +225,6 @@ latex_use_parts = True
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'tablib', u'Tablib Documentation',
|
||||
[u'Jazzband'], 1)
|
||||
('index', 'tablib', 'Tablib Documentation',
|
||||
['Jazzband'], 1)
|
||||
]
|
||||
|
||||
+22
-15
@@ -90,32 +90,36 @@ Tablib features a micro-framework for adding format support.
|
||||
The easiest way to understand it is to use it.
|
||||
So, let's define our own format, named *xxx*.
|
||||
|
||||
1. Write a new format interface.
|
||||
From version 1.0, Tablib formats are class-based and can be dynamically
|
||||
registered.
|
||||
|
||||
:class:`tablib.core` follows a simple pattern for automatically utilizing your format throughout Tablib.
|
||||
Function names are crucial.
|
||||
|
||||
Example **tablib/formats/_xxx.py**: ::
|
||||
1. Write your custom format class::
|
||||
|
||||
class MyXXXFormatClass:
|
||||
title = 'xxx'
|
||||
|
||||
def export_set(dset):
|
||||
@classmethod
|
||||
def export_set(cls, dset):
|
||||
....
|
||||
# returns string representation of given dataset
|
||||
|
||||
def export_book(dbook):
|
||||
@classmethod
|
||||
def export_book(cls, dbook):
|
||||
....
|
||||
# returns string representation of given databook
|
||||
|
||||
def import_set(dset, in_stream):
|
||||
@classmethod
|
||||
def import_set(cls, dset, in_stream):
|
||||
...
|
||||
# populates given Dataset with given datastream
|
||||
|
||||
def import_book(dbook, in_stream):
|
||||
@classmethod
|
||||
def import_book(cls, dbook, in_stream):
|
||||
...
|
||||
# returns Databook instance
|
||||
|
||||
def detect(stream):
|
||||
@classmethod
|
||||
def detect(cls, stream):
|
||||
...
|
||||
# returns True if given stream is parsable as xxx
|
||||
|
||||
@@ -124,15 +128,18 @@ So, let's define our own format, named *xxx*.
|
||||
If the format excludes support for an import/export mechanism (*e.g.*
|
||||
:class:`csv <tablib.Dataset.csv>` excludes
|
||||
:class:`Databook <tablib.Databook>` support),
|
||||
simply don't define the respective functions.
|
||||
simply don't define the respective class methods.
|
||||
Appropriate errors will be raised.
|
||||
|
||||
2. Add your new format module to the :class:`tablib.formats.available` tuple.
|
||||
2. Register your class::
|
||||
|
||||
3. Add a mock property to the :class:`Dataset <tablib.Dataset>` class with verbose `reStructured Text`_ docstring.
|
||||
This alleviates IDE confusion, and allows for pretty auto-generated Sphinx_ documentation.
|
||||
from tablib.formats import registry
|
||||
|
||||
4. Write respective :ref:`tests <testing>`.
|
||||
registry.register('xxx', MyXXXFormatClass())
|
||||
|
||||
3. From then on, you should be able to use your new custom format as if it were
|
||||
a built-in Tablib format, e.g. using ``dataset.export('xxx')`` will use the
|
||||
``MyXXXFormatClass.export_set`` method.
|
||||
|
||||
.. _testing:
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
.. _formats:
|
||||
|
||||
=======
|
||||
Formats
|
||||
=======
|
||||
|
||||
Tablib supports a wide variety of different tabular formats, both for input and
|
||||
output. Moreover, you can :ref:`register your own formats <newformats>`.
|
||||
|
||||
cli
|
||||
===
|
||||
|
||||
The ``cli`` format is currently export-only. The exports produce a representation
|
||||
table suited to a terminal.
|
||||
|
||||
When exporting to a CLI you can pass the table format with the ``tablefmt``
|
||||
parameter, the supported formats are::
|
||||
|
||||
>>> import tabulate
|
||||
>>> list(tabulate._table_formats)
|
||||
['simple', 'plain', 'grid', 'fancy_grid', 'github', 'pipe', 'orgtbl',
|
||||
'jira', 'presto', 'psql', 'rst', 'mediawiki', 'moinmoin', 'youtrack',
|
||||
'html', 'latex', 'latex_raw', 'latex_booktabs', 'tsv', 'textile']
|
||||
|
||||
For example::
|
||||
|
||||
dataset.export("cli", tablefmt="github")
|
||||
dataset.export("cli", tablefmt="grid")
|
||||
|
||||
This format is optional, install Tablib with ``pip install tablib[cli]`` to
|
||||
make the format available.
|
||||
|
||||
csv
|
||||
===
|
||||
|
||||
When you import CSV data, you can specify if the first line of your data source
|
||||
is headers with the ``headers`` boolean parameter (defaults to ``True``)::
|
||||
|
||||
import tablib
|
||||
|
||||
tablib.import_set(your_data_stream, format='csv', headers=False)
|
||||
|
||||
When exporting with the ``csv`` format, the top row will contain headers, if
|
||||
they have been set. Otherwise, the top row will contain the first row of the
|
||||
dataset.
|
||||
|
||||
When importing a CSV data source or exporting a dataset as CSV, you can pass any
|
||||
parameter supported by the :py:func:`csv.reader` and :py:func:`csv.writer`
|
||||
functions. For example::
|
||||
|
||||
tablib.import_set(your_data_stream, format='csv', dialect='unix')
|
||||
|
||||
dataset.export('csv', delimiter=' ', quotechar='|')
|
||||
|
||||
.. admonition:: Line endings
|
||||
|
||||
Exporting uses \\r\\n line endings by default so, make sure to include
|
||||
``newline=''`` otherwise you will get a blank line between each row
|
||||
when you open the file in Excel::
|
||||
|
||||
with open('output.csv', 'w', newline='') as f:
|
||||
f.write(dataset.export('csv'))
|
||||
|
||||
If you do not do this, and you export the file on Windows, your
|
||||
CSV file will open in Excel with a blank line between each row.
|
||||
|
||||
dbf
|
||||
===
|
||||
|
||||
Import/export using the dBASE_ format.
|
||||
|
||||
.. admonition:: Binary Warning
|
||||
|
||||
The ``dbf`` format contains binary data, so make sure to write in binary
|
||||
mode::
|
||||
|
||||
with open('output.dbf', 'wb') as f:
|
||||
f.write(dataset.export('dbf')
|
||||
|
||||
.. _dBASE: https://en.wikipedia.org/wiki/DBase
|
||||
|
||||
df (DataFrame)
|
||||
==============
|
||||
|
||||
Import/export using the pandas_ DataFrame format. This format is optional,
|
||||
install Tablib with ``pip install tablib[pandas]`` to make the format available.
|
||||
|
||||
.. _pandas: https://pandas.pydata.org/
|
||||
|
||||
html
|
||||
====
|
||||
|
||||
The ``html`` format is currently export-only. The exports produce an HTML page
|
||||
with the data in a ``<table>``. If headers have been set, they will be used as
|
||||
table headers.
|
||||
|
||||
This format is optional, install Tablib with ``pip install tablib[html]`` to
|
||||
make the format available.
|
||||
|
||||
jira
|
||||
====
|
||||
|
||||
The ``jira`` format is currently export-only. Exports format the dataset
|
||||
according to the Jira table syntax::
|
||||
|
||||
||heading 1||heading 2||heading 3||
|
||||
|col A1|col A2|col A3|
|
||||
|col B1|col B2|col B3|
|
||||
|
||||
json
|
||||
====
|
||||
|
||||
Import/export using the JSON_ format. If headers have been set, a JSON list of
|
||||
objects will be returned. If no headers have been set, a JSON list of lists
|
||||
(rows) will be returned instead.
|
||||
|
||||
Import assumes (for now) that headers exist.
|
||||
|
||||
.. _JSON: http://json.org/
|
||||
|
||||
latex
|
||||
=====
|
||||
|
||||
Import/export using the LaTeX_ format. This format is export-only.
|
||||
If a title has been set, it will be exported as the table caption.
|
||||
|
||||
.. _LaTeX: https://www.latex-project.org/
|
||||
|
||||
ods
|
||||
===
|
||||
|
||||
Export data in OpenDocument Spreadsheet format. The ``ods`` format is currently
|
||||
export-only.
|
||||
|
||||
This format is optional, install Tablib with ``pip install tablib[ods]`` to
|
||||
make the format available.
|
||||
|
||||
.. admonition:: Binary Warning
|
||||
|
||||
:class:`Dataset.ods` contains binary data, so make sure to write in binary mode::
|
||||
|
||||
with open('output.ods', 'wb') as f:
|
||||
f.write(data.ods)
|
||||
|
||||
rst
|
||||
===
|
||||
|
||||
Export data as a reStructuredText_ table representation of a dataset. The
|
||||
``rst`` format is export-only.
|
||||
|
||||
Exporting returns a simple table if the text in the first column is never
|
||||
wrapped, otherwise returns a grid table::
|
||||
|
||||
>>> from tablib import Dataset
|
||||
>>> bits = ((0, 0), (1, 0), (0, 1), (1, 1))
|
||||
>>> data = Dataset()
|
||||
>>> data.headers = ['A', 'B', 'A and B']
|
||||
>>> for a, b in bits:
|
||||
... data.append([bool(a), bool(b), bool(a * b)])
|
||||
>>> table = data.export('rst')
|
||||
>>> table.split('\\n') == [
|
||||
... '===== ===== =====',
|
||||
... ' A B A and',
|
||||
... ' B ',
|
||||
... '===== ===== =====',
|
||||
... 'False False False',
|
||||
... 'True False False',
|
||||
... 'False True False',
|
||||
... 'True True True ',
|
||||
... '===== ===== =====',
|
||||
... ]
|
||||
True
|
||||
|
||||
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
|
||||
|
||||
tsv
|
||||
===
|
||||
|
||||
A variant of the csv_ format with tabulators as fields separators.
|
||||
|
||||
xls
|
||||
===
|
||||
|
||||
Import/export data in Legacy Excel Spreadsheet representation.
|
||||
|
||||
This format is optional, install Tablib with ``pip install tablib[xls]`` to
|
||||
make the format available.
|
||||
|
||||
.. note::
|
||||
|
||||
XLS files are limited to a maximum of 65,000 rows. Use xlsx_ to avoid this
|
||||
limitation.
|
||||
|
||||
.. admonition:: Binary Warning
|
||||
|
||||
The ``xls`` file format is binary, so make sure to write in binary mode::
|
||||
|
||||
with open('output.xls', 'wb') as f:
|
||||
f.write(data.export('xls'))
|
||||
|
||||
xlsx
|
||||
====
|
||||
|
||||
Import/export data in Excel 07+ Spreadsheet representation.
|
||||
|
||||
This format is optional, install Tablib with ``pip install tablib[xlsx]`` to
|
||||
make the format available.
|
||||
|
||||
.. admonition:: Binary Warning
|
||||
|
||||
The ``xlsx`` file format is binary, so make sure to write in binary mode::
|
||||
|
||||
with open('output.xlsx', 'wb') as f:
|
||||
f.write(data.export('xlsx'))
|
||||
|
||||
yaml
|
||||
====
|
||||
|
||||
Import/export data in the YAML_ format.
|
||||
When exporting, if headers have been set, a YAML list of objects will be
|
||||
returned. If no headers have been set, a YAML list of lists (rows) will be
|
||||
returned instead.
|
||||
|
||||
Import assumes (for now) that headers exist.
|
||||
|
||||
This format is optional, install Tablib with ``pip install tablib[yaml]`` to
|
||||
make the format available.
|
||||
|
||||
.. _YAML: https://yaml.org
|
||||
+6
-1
@@ -1,7 +1,7 @@
|
||||
.. Tablib documentation master file, created by
|
||||
sphinx-quickstart on Tue Oct 5 15:25:21 2010.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
contain the root ``toctree`` directive.
|
||||
|
||||
Tablib: Pythonic Tabular Datasets
|
||||
=================================
|
||||
@@ -98,6 +98,11 @@ This part of the documentation, which is mostly prose, begins with some backgrou
|
||||
|
||||
tutorial
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
formats
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
+20
-2
@@ -19,14 +19,32 @@ Of course, the recommended way to install Tablib is with `pip <https://pip.pypa.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install tablib[pandas]
|
||||
$ pip install tablib
|
||||
|
||||
You can also choose to install more dependencies to have more import/export
|
||||
formats available:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install tablib[xlsx]
|
||||
|
||||
Or all possible formats:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install tablib[all]
|
||||
|
||||
which is equivalent to:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install tablib[html, pandas, ods, xls, xlsx, yaml]
|
||||
|
||||
-------------------
|
||||
Download the Source
|
||||
-------------------
|
||||
|
||||
You can also install tablib from source.
|
||||
You can also install Tablib from source.
|
||||
The latest release (|version|) is available from GitHub.
|
||||
|
||||
* tarball_
|
||||
|
||||
+6
-27
@@ -6,7 +6,7 @@ Introduction
|
||||
This part of the documentation covers all the interfaces of Tablib.
|
||||
Tablib is a format-agnostic tabular dataset library, written in Python.
|
||||
It allows you to Pythonically import, export, and manipulate tabular data sets.
|
||||
Advanced features include segregation, dynamic columns, tags / filtering, and
|
||||
Advanced features include segregation, dynamic columns, tags/filtering, and
|
||||
seamless format import/export.
|
||||
|
||||
|
||||
@@ -23,31 +23,13 @@ Tablib was developed with a few :pep:`20` idioms in mind.
|
||||
|
||||
All contributions to Tablib should keep these important rules in mind.
|
||||
|
||||
.. mit:
|
||||
|
||||
MIT License
|
||||
-----------
|
||||
|
||||
A large number of open source projects you find today are `GPL Licensed`_.
|
||||
While the GPL has its time and place, it should most certainly not be your
|
||||
go-to license for your next open source project.
|
||||
|
||||
A project that is released as GPL cannot be used in any commercial product
|
||||
without the product itself also being offered as open source. The MIT, BSD, and
|
||||
ISC licenses are great alternatives to the GPL that allow your open-source
|
||||
software to be used in proprietary, closed-source software.
|
||||
|
||||
Tablib is released under terms of `The MIT License`_.
|
||||
|
||||
.. _`GPL Licensed`: https://opensource.org/licenses/gpl-license.php
|
||||
.. _`The MIT License`: https://opensource.org/licenses/mit-license.php
|
||||
|
||||
|
||||
.. _license:
|
||||
|
||||
Tablib License
|
||||
--------------
|
||||
|
||||
Tablib is released under terms of `The MIT License`_.
|
||||
|
||||
Copyright 2017 Kenneth Reitz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
@@ -68,17 +50,14 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
.. _`The MIT License`: https://opensource.org/licenses/mit-license.php
|
||||
|
||||
.. _pythonsupport:
|
||||
|
||||
Pythons Supported
|
||||
-----------------
|
||||
|
||||
At this time, the following Python versions are officially supported:
|
||||
Python 3.5+ is officially supported.
|
||||
|
||||
* CPython 2.7
|
||||
* CPython 3.5
|
||||
* CPython 3.6
|
||||
* CPython 3.7
|
||||
Now, go :ref:`install Tablib <install>`.
|
||||
|
||||
Now, go :ref:`Install Tablib <install>`.
|
||||
|
||||
+31
-11
@@ -35,7 +35,7 @@ You can now start filling this :class:`Dataset <tablib.Dataset>` object with dat
|
||||
|
||||
.. admonition:: Example Context
|
||||
|
||||
From here on out, if you see ``data``, assume that it's a fresh
|
||||
From here on out, if you see ``data``, assume that it's a fresh
|
||||
:class:`Dataset <tablib.Dataset>` object.
|
||||
|
||||
|
||||
@@ -106,10 +106,18 @@ Importing Data
|
||||
--------------
|
||||
Creating a :class:`tablib.Dataset` object by importing a pre-existing file is simple. ::
|
||||
|
||||
imported_data = Dataset().load(open('data.csv').read())
|
||||
with open('data.csv', 'r') as fh:
|
||||
imported_data = Dataset().load(fh)
|
||||
|
||||
This detects what sort of data is being passed in, and uses an appropriate formatter to do the import. So you can import from a variety of different file types.
|
||||
|
||||
.. admonition:: Source without headers
|
||||
|
||||
When the format is :class:`csv <Dataset.csv>`, :class:`tsv <Dataset.tsv>`, :class:`dbf <Dataset.dbf>`, :class:`xls <Dataset.xls>` or :class:`xlsx <Dataset.xlsx>`, and the data source does not have headers, the import should be done as follows ::
|
||||
|
||||
with open('data.csv', 'r') as fh:
|
||||
imported_data = Dataset().load(fh, headers=False)
|
||||
|
||||
--------------
|
||||
Exporting Data
|
||||
--------------
|
||||
@@ -200,7 +208,7 @@ Delete a range of rows::
|
||||
Advanced Usage
|
||||
==============
|
||||
|
||||
This part of the documentation services to give you an idea that are otherwise hard to extract from the :ref:`API Documentation <api>`
|
||||
This part of the documentation services to give you an idea that are otherwise hard to extract from the :ref:`API Documentation <api>`.
|
||||
|
||||
And now for something completely different.
|
||||
|
||||
@@ -287,24 +295,36 @@ Let's tag some students. ::
|
||||
students.headers = ['first', 'last']
|
||||
|
||||
students.rpush(['Kenneth', 'Reitz'], tags=['male', 'technical'])
|
||||
students.rpush(['Daniel', 'Dupont'], tags=['male', 'creative' ])
|
||||
students.rpush(['Bessie', 'Monke'], tags=['female', 'creative'])
|
||||
|
||||
Now that we have extra meta-data on our rows, we can easily filter our :class:`Dataset`. Let's just see Male students. ::
|
||||
Now that we have extra meta-data on our rows, we can easily filter our :class:`Dataset`. Let's just see Female students. ::
|
||||
|
||||
>>> students.filter(['female']).yaml
|
||||
- {first: Bessie, Last: Monke}
|
||||
|
||||
>>> students.filter(['male']).yaml
|
||||
- {first: Kenneth, Last: Reitz}
|
||||
By default, when you pass a list of tags you get filter type or. ::
|
||||
|
||||
>>> students.filter(['female', 'creative']).yaml
|
||||
- {first: Daniel, Last: Dupont}
|
||||
- {first: Bessie, Last: Monke}
|
||||
|
||||
Using chaining you can get a filter type and. ::
|
||||
|
||||
>>> students.filter(['female']).filter(['creative']).yaml
|
||||
- {first: Bessie, Last: Monke}
|
||||
|
||||
It's that simple. The original :class:`Dataset` is untouched.
|
||||
|
||||
Open an Excel Workbook and read first sheet
|
||||
-------------------------------------------
|
||||
|
||||
To open an Excel 2007 and later workbook with a single sheet (or a workbook with multiple sheets but you just want the first sheet), use the following:
|
||||
Open an Excel 2007 and later workbook with a single sheet (or a workbook with multiple sheets but you just want the first sheet). ::
|
||||
|
||||
data = tablib.Dataset()
|
||||
data.xlsx = open('my_excel_file.xlsx', 'rb').read()
|
||||
print(data)
|
||||
data = tablib.Dataset()
|
||||
with open('my_excel_file.xlsx', 'rb') as fh:
|
||||
data.load(fh, 'xlsx')
|
||||
print(data)
|
||||
|
||||
Excel Workbook With Multiple Sheets
|
||||
------------------------------------
|
||||
@@ -321,7 +341,7 @@ All we have to do is add them to a :class:`Databook` object... ::
|
||||
... and export to Excel just like :class:`Datasets <Dataset>`. ::
|
||||
|
||||
with open('students.xls', 'wb') as f:
|
||||
f.write(book.xls)
|
||||
f.write(book.export('xls'))
|
||||
|
||||
The resulting ``students.xls`` file will contain a separate spreadsheet for each :class:`Dataset` object in the :class:`Databook`.
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[tool.isort]
|
||||
force_grid_wrap = 0
|
||||
include_trailing_comma = true
|
||||
known_third_party = ["MarkupPy", "odf", "openpyxl", "pkg_resources", "setuptools", "tablib", "xlrd", "xlwt", "yaml"]
|
||||
line_length = 88
|
||||
multi_line_output = 3
|
||||
use_parentheses = true
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
[pytest]
|
||||
norecursedirs = .git .*
|
||||
addopts = -rsxX --showlocals --tb=native --cov=tablib --cov-report xml --cov-report term --cov-report html
|
||||
addopts = -rsxX --showlocals --tb=native --cov=tablib --cov=tests --cov-report xml --cov-report term --cov-report html
|
||||
python_paths = .
|
||||
|
||||
@@ -1,37 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
install = [
|
||||
'odfpy',
|
||||
'openpyxl>=2.4.0',
|
||||
'backports.csv;python_version<"3.0"',
|
||||
'markuppy',
|
||||
'xlrd',
|
||||
'xlwt',
|
||||
'pyyaml',
|
||||
]
|
||||
|
||||
|
||||
setup(
|
||||
name='tablib',
|
||||
use_scm_version=True,
|
||||
setup_requires=['setuptools_scm'],
|
||||
description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)',
|
||||
long_description=(open('README.md').read() + '\n\n' +
|
||||
open('HISTORY.md').read()),
|
||||
long_description=(
|
||||
open('README.md').read() + '\n\n' + open('HISTORY.md').read()
|
||||
),
|
||||
long_description_content_type="text/markdown",
|
||||
author='Kenneth Reitz',
|
||||
author_email='me@kennethreitz.org',
|
||||
maintainer='Jazzband',
|
||||
maintainer_email='roadies@jazzband.co',
|
||||
url='https://tablib.readthedocs.io',
|
||||
project_urls={
|
||||
"Documentation": "https://tablib.readthedocs.io",
|
||||
"Source": "https://github.com/jazzband/tablib",
|
||||
},
|
||||
packages=find_packages(where="src"),
|
||||
package_dir={"": "src"},
|
||||
license='MIT',
|
||||
@@ -41,17 +29,22 @@ setup(
|
||||
'Natural Language :: English',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
],
|
||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
|
||||
install_requires=install,
|
||||
python_requires='>=3.5',
|
||||
extras_require={
|
||||
'all': ['markuppy', 'odfpy', 'openpyxl>=2.4.0', 'pandas', 'pyyaml', 'tabulate', 'xlrd', 'xlwt'],
|
||||
'cli': ['tabulate'],
|
||||
'html': ['markuppy'],
|
||||
'ods': ['odfpy'],
|
||||
'pandas': ['pandas'],
|
||||
'xls': ['xlrd', 'xlwt'],
|
||||
'xlsx': ['openpyxl>=2.4.0'],
|
||||
'yaml': ['pyyaml'],
|
||||
},
|
||||
)
|
||||
|
||||
+10
-5
@@ -1,9 +1,14 @@
|
||||
""" Tablib. """
|
||||
from pkg_resources import get_distribution, DistributionNotFound
|
||||
|
||||
from tablib.core import (
|
||||
Databook, Dataset, detect_format, import_set, import_book,
|
||||
InvalidDatasetType, InvalidDimensions, UnsupportedFormat
|
||||
from pkg_resources import DistributionNotFound, get_distribution
|
||||
from tablib.core import ( # noqa: F401
|
||||
Databook,
|
||||
Dataset,
|
||||
InvalidDatasetType,
|
||||
InvalidDimensions,
|
||||
UnsupportedFormat,
|
||||
detect_format,
|
||||
import_book,
|
||||
import_set,
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
tablib.compat
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Tablib compatiblity module.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
is_py3 = (sys.version_info[0] > 2)
|
||||
|
||||
|
||||
if is_py3:
|
||||
from io import StringIO
|
||||
from statistics import median
|
||||
from itertools import zip_longest as izip_longest
|
||||
import csv
|
||||
import tablib.packages.dbfpy3 as dbfpy
|
||||
|
||||
unicode = str
|
||||
xrange = range
|
||||
|
||||
else:
|
||||
from StringIO import StringIO
|
||||
from tablib.packages.statistics import median
|
||||
from itertools import izip_longest
|
||||
from backports import csv
|
||||
import tablib.packages.dbfpy as dbfpy
|
||||
|
||||
unicode = unicode
|
||||
xrange = xrange
|
||||
|
||||
from MarkupPy import markup # Kept temporarily to avoid breaking existing imports
|
||||
+77
-312
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
tablib.core
|
||||
~~~~~~~~~~~
|
||||
@@ -13,10 +12,15 @@ from collections import OrderedDict
|
||||
from copy import copy
|
||||
from operator import itemgetter
|
||||
|
||||
from tablib import formats
|
||||
|
||||
from tablib.compat import unicode
|
||||
|
||||
from tablib.exceptions import (
|
||||
HeadersNeeded,
|
||||
InvalidDatasetIndex,
|
||||
InvalidDatasetType,
|
||||
InvalidDimensions,
|
||||
UnsupportedFormat,
|
||||
)
|
||||
from tablib.formats import registry
|
||||
from tablib.utils import normalize_input
|
||||
|
||||
__title__ = 'tablib'
|
||||
__author__ = 'Kenneth Reitz'
|
||||
@@ -25,7 +29,7 @@ __copyright__ = 'Copyright 2017 Kenneth Reitz. 2019 Jazzband.'
|
||||
__docformat__ = 'restructuredtext'
|
||||
|
||||
|
||||
class Row(object):
|
||||
class Row:
|
||||
"""Internal Row object. Mainly used for filtering."""
|
||||
|
||||
__slots__ = ['_row', 'tags']
|
||||
@@ -43,9 +47,6 @@ class Row(object):
|
||||
def __repr__(self):
|
||||
return repr(self._row)
|
||||
|
||||
def __getslice__(self, i, j):
|
||||
return self._row[i:j]
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self._row[i]
|
||||
|
||||
@@ -66,7 +67,8 @@ class Row(object):
|
||||
return slots
|
||||
|
||||
def __setstate__(self, state):
|
||||
for (k, v) in list(state.items()): setattr(self, k, v)
|
||||
for (k, v) in list(state.items()):
|
||||
setattr(self, k, v)
|
||||
|
||||
def rpush(self, value):
|
||||
self.insert(0, value)
|
||||
@@ -96,7 +98,7 @@ class Row(object):
|
||||
def has_tag(self, tag):
|
||||
"""Returns true if current row contains tag."""
|
||||
|
||||
if tag == None:
|
||||
if tag is None:
|
||||
return False
|
||||
elif isinstance(tag, str):
|
||||
return (tag in self.tags)
|
||||
@@ -104,7 +106,7 @@ class Row(object):
|
||||
return bool(len(set(tag) & set(self.tags)))
|
||||
|
||||
|
||||
class Dataset(object):
|
||||
class Dataset:
|
||||
"""The :class:`Dataset` object is the heart of Tablib. It provides all core
|
||||
functionality.
|
||||
|
||||
@@ -119,7 +121,7 @@ class Dataset(object):
|
||||
|
||||
|
||||
Setting columns is similar. The column data length must equal the
|
||||
current height of the data and headers must be set ::
|
||||
current height of the data and headers must be set. ::
|
||||
|
||||
data = tablib.Dataset()
|
||||
data.headers = ('first_name', 'last_name')
|
||||
@@ -151,8 +153,6 @@ class Dataset(object):
|
||||
|
||||
"""
|
||||
|
||||
_formats = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._data = list(Row(arg) for arg in args)
|
||||
self.__headers = None
|
||||
@@ -167,15 +167,13 @@ class Dataset(object):
|
||||
|
||||
self.title = kwargs.get('title')
|
||||
|
||||
self._register_formats()
|
||||
|
||||
def __len__(self):
|
||||
return self.height
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, (str, unicode)):
|
||||
if isinstance(key, str):
|
||||
if key in self.headers:
|
||||
pos = self.headers.index(key) # get 'key' index from each data
|
||||
pos = self.headers.index(key) # get 'key' index from each data
|
||||
return [row[pos] for row in self._data]
|
||||
else:
|
||||
raise KeyError
|
||||
@@ -191,7 +189,7 @@ class Dataset(object):
|
||||
self._data[key] = Row(value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
if isinstance(key, (str, unicode)):
|
||||
if isinstance(key, str):
|
||||
|
||||
if key in self.headers:
|
||||
|
||||
@@ -213,15 +211,15 @@ class Dataset(object):
|
||||
except AttributeError:
|
||||
return '<dataset object>'
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
result = []
|
||||
|
||||
# Add unicode representation of headers.
|
||||
# Add str representation of headers.
|
||||
if self.__headers:
|
||||
result.append([unicode(h) for h in self.__headers])
|
||||
result.append([str(h) for h in self.__headers])
|
||||
|
||||
# Add unicode representation of rows.
|
||||
result.extend(list(map(unicode, row)) for row in self._data)
|
||||
# Add str representation of rows.
|
||||
result.extend(list(map(str, row)) for row in self._data)
|
||||
|
||||
lens = [list(map(len, row)) for row in result]
|
||||
field_lens = list(map(max, zip(*lens)))
|
||||
@@ -234,30 +232,16 @@ class Dataset(object):
|
||||
|
||||
return '\n'.join(format_string.format(*row) for row in result)
|
||||
|
||||
def __str__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
# ---------
|
||||
# Internals
|
||||
# ---------
|
||||
|
||||
@classmethod
|
||||
def _register_formats(cls):
|
||||
"""Adds format properties."""
|
||||
for fmt in formats.available:
|
||||
try:
|
||||
try:
|
||||
setattr(cls, fmt.title, property(fmt.export_set, fmt.import_set))
|
||||
setattr(cls, 'get_%s' % fmt.title, fmt.export_set)
|
||||
setattr(cls, 'set_%s' % fmt.title, fmt.import_set)
|
||||
cls._formats[fmt.title] = (fmt.export_set, fmt.import_set)
|
||||
except AttributeError:
|
||||
setattr(cls, fmt.title, property(fmt.export_set))
|
||||
setattr(cls, 'get_%s' % fmt.title, fmt.export_set)
|
||||
cls._formats[fmt.title] = (fmt.export_set, None)
|
||||
def _get_in_format(self, fmt_key, **kwargs):
|
||||
return registry.get_format(fmt_key).export_set(self, **kwargs)
|
||||
|
||||
except AttributeError:
|
||||
cls._formats[fmt.title] = (None, None)
|
||||
def _set_in_format(self, fmt_key, in_stream, **kwargs):
|
||||
in_stream = normalize_input(in_stream)
|
||||
return registry.get_format(fmt_key).import_set(self, in_stream, **kwargs)
|
||||
|
||||
def _validate(self, row=None, col=None, safety=False):
|
||||
"""Assures size of every row in dataset is of proper proportions."""
|
||||
@@ -269,7 +253,7 @@ class Dataset(object):
|
||||
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))
|
||||
is_valid = all(len(x) == self.width for x in self._data)
|
||||
|
||||
if is_valid:
|
||||
return True
|
||||
@@ -419,18 +403,23 @@ class Dataset(object):
|
||||
def load(self, in_stream, format=None, **kwargs):
|
||||
"""
|
||||
Import `in_stream` to the :class:`Dataset` object using the `format`.
|
||||
`in_stream` can be a file-like object, a string, or a bytestring.
|
||||
|
||||
:param \\*\\*kwargs: (optional) custom configuration to the format `import_set`.
|
||||
"""
|
||||
|
||||
stream = normalize_input(in_stream)
|
||||
if not format:
|
||||
format = detect_format(in_stream)
|
||||
format = detect_format(stream)
|
||||
|
||||
fmt = registry.get_format(format)
|
||||
if not hasattr(fmt, 'import_set'):
|
||||
raise UnsupportedFormat('Format {} cannot be imported.'.format(format))
|
||||
|
||||
export_set, import_set = self._formats.get(format, (None, None))
|
||||
if not import_set:
|
||||
raise UnsupportedFormat('Format {0} cannot be imported.'.format(format))
|
||||
raise UnsupportedFormat('Format {} cannot be imported.'.format(format))
|
||||
|
||||
import_set(self, in_stream, **kwargs)
|
||||
fmt.import_set(self, stream, **kwargs)
|
||||
return self
|
||||
|
||||
def export(self, format, **kwargs):
|
||||
@@ -439,204 +428,11 @@ class Dataset(object):
|
||||
|
||||
:param \\*\\*kwargs: (optional) custom configuration to the format `export_set`.
|
||||
"""
|
||||
export_set, import_set = self._formats.get(format, (None, None))
|
||||
if not export_set:
|
||||
raise UnsupportedFormat('Format {0} cannot be exported.'.format(format))
|
||||
fmt = registry.get_format(format)
|
||||
if not hasattr(fmt, 'export_set'):
|
||||
raise UnsupportedFormat('Format {} cannot be exported.'.format(format))
|
||||
|
||||
return export_set(self, **kwargs)
|
||||
|
||||
# -------
|
||||
# Formats
|
||||
# -------
|
||||
|
||||
@property
|
||||
def xls():
|
||||
"""A Legacy Excel Spreadsheet representation of the :class:`Dataset` object, with :ref:`separators`. Cannot be set.
|
||||
|
||||
.. note::
|
||||
|
||||
XLS files are limited to a maximum of 65,000 rows. Use :class:`Dataset.xlsx` to avoid this limitation.
|
||||
|
||||
.. admonition:: Binary Warning
|
||||
|
||||
:class:`Dataset.xls` contains binary data, so make sure to write in binary mode::
|
||||
|
||||
with open('output.xls', 'wb') as f:
|
||||
f.write(data.xls)
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def xlsx():
|
||||
"""An Excel '07+ Spreadsheet representation of the :class:`Dataset` object, with :ref:`separators`. Cannot be set.
|
||||
|
||||
.. admonition:: Binary Warning
|
||||
|
||||
:class:`Dataset.xlsx` contains binary data, so make sure to write in binary mode::
|
||||
|
||||
with open('output.xlsx', 'wb') as f:
|
||||
f.write(data.xlsx)
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def ods():
|
||||
"""An OpenDocument Spreadsheet representation of the :class:`Dataset` object, with :ref:`separators`. Cannot be set.
|
||||
|
||||
.. admonition:: Binary Warning
|
||||
|
||||
:class:`Dataset.ods` contains binary data, so make sure to write in binary mode::
|
||||
|
||||
with open('output.ods', 'wb') as f:
|
||||
f.write(data.ods)
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def csv():
|
||||
"""A CSV representation of the :class:`Dataset` object. The top row will contain
|
||||
headers, if they have been set. Otherwise, the top row will contain
|
||||
the first row of the dataset.
|
||||
|
||||
A dataset object can also be imported by setting the :class:`Dataset.csv` attribute. ::
|
||||
|
||||
data = tablib.Dataset()
|
||||
data.csv = 'age, first_name, last_name\\n90, John, Adams'
|
||||
|
||||
Import assumes (for now) that headers exist.
|
||||
|
||||
.. admonition:: Binary Warning for Python 2
|
||||
|
||||
:class:`Dataset.csv` uses \\r\\n line endings by default so, in Python 2, make
|
||||
sure to write in binary mode::
|
||||
|
||||
with open('output.csv', 'wb') as f:
|
||||
f.write(data.csv)
|
||||
|
||||
If you do not do this, and you export the file on Windows, your
|
||||
CSV file will open in Excel with a blank line between each row.
|
||||
|
||||
.. admonition:: Line endings for Python 3
|
||||
|
||||
:class:`Dataset.csv` uses \\r\\n line endings by default so, in Python 3, make
|
||||
sure to include newline='' otherwise you will get a blank line between each row
|
||||
when you open the file in Excel::
|
||||
|
||||
with open('output.csv', 'w', newline='') as f:
|
||||
f.write(data.csv)
|
||||
|
||||
If you do not do this, and you export the file on Windows, your
|
||||
CSV file will open in Excel with a blank line between each row.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def tsv():
|
||||
"""A TSV representation of the :class:`Dataset` object. The top row will contain
|
||||
headers, if they have been set. Otherwise, the top row will contain
|
||||
the first row of the dataset.
|
||||
|
||||
A dataset object can also be imported by setting the :class:`Dataset.tsv` attribute. ::
|
||||
|
||||
data = tablib.Dataset()
|
||||
data.tsv = 'age\tfirst_name\tlast_name\\n90\tJohn\tAdams'
|
||||
|
||||
Import assumes (for now) that headers exist.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def yaml():
|
||||
"""A YAML representation of the :class:`Dataset` object. If headers have been
|
||||
set, a YAML list of objects will be returned. If no headers have
|
||||
been set, a YAML list of lists (rows) will be returned instead.
|
||||
|
||||
A dataset object can also be imported by setting the :class:`Dataset.yaml` attribute: ::
|
||||
|
||||
data = tablib.Dataset()
|
||||
data.yaml = '- {age: 90, first_name: John, last_name: Adams}'
|
||||
|
||||
Import assumes (for now) that headers exist.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def df():
|
||||
"""A DataFrame representation of the :class:`Dataset` object.
|
||||
|
||||
A dataset object can also be imported by setting the :class:`Dataset.df` attribute: ::
|
||||
|
||||
data = tablib.Dataset()
|
||||
data.df = DataFrame(np.random.randn(6,4))
|
||||
|
||||
Import assumes (for now) that headers exist.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def json():
|
||||
"""A JSON representation of the :class:`Dataset` object. If headers have been
|
||||
set, a JSON list of objects will be returned. If no headers have
|
||||
been set, a JSON list of lists (rows) will be returned instead.
|
||||
|
||||
A dataset object can also be imported by setting the :class:`Dataset.json` attribute: ::
|
||||
|
||||
data = tablib.Dataset()
|
||||
data.json = '[{"age": 90, "first_name": "John", "last_name": "Adams"}]'
|
||||
|
||||
Import assumes (for now) that headers exist.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def html():
|
||||
"""A HTML table representation of the :class:`Dataset` object. If
|
||||
headers have been set, they will be used as table headers.
|
||||
|
||||
..notice:: This method can be used for export only.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def dbf():
|
||||
"""A dBASE representation of the :class:`Dataset` object.
|
||||
|
||||
A dataset object can also be imported by setting the
|
||||
:class:`Dataset.dbf` attribute. ::
|
||||
|
||||
# To import data from an existing DBF file:
|
||||
data = tablib.Dataset()
|
||||
data.dbf = open('existing_table.dbf', mode='rb').read()
|
||||
|
||||
# to import data from an ASCII-encoded bytestring:
|
||||
data = tablib.Dataset()
|
||||
data.dbf = '<bytestring of tabular data>'
|
||||
|
||||
.. admonition:: Binary Warning
|
||||
|
||||
:class:`Dataset.dbf` contains binary data, so make sure to write in binary mode::
|
||||
|
||||
with open('output.dbf', 'wb') as f:
|
||||
f.write(data.dbf)
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def latex():
|
||||
"""A LaTeX booktabs representation of the :class:`Dataset` object. If a
|
||||
title has been set, it will be exported as the table caption.
|
||||
|
||||
.. note:: This method can be used for export only.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def jira():
|
||||
"""A Jira table representation of the :class:`Dataset` object.
|
||||
|
||||
.. note:: This method can be used for export only.
|
||||
"""
|
||||
pass
|
||||
return fmt.export_set(self, **kwargs)
|
||||
|
||||
# ----
|
||||
# Rows
|
||||
@@ -824,9 +620,9 @@ class Dataset(object):
|
||||
each cell value.
|
||||
"""
|
||||
|
||||
if isinstance(col, unicode):
|
||||
if isinstance(col, str):
|
||||
if col in self.headers:
|
||||
col = self.headers.index(col) # get 'key' index from each data
|
||||
col = self.headers.index(col) # get 'key' index from each data
|
||||
else:
|
||||
raise KeyError
|
||||
|
||||
@@ -855,7 +651,7 @@ class Dataset(object):
|
||||
sorted.
|
||||
"""
|
||||
|
||||
if isinstance(col, (str, unicode)):
|
||||
if isinstance(col, str):
|
||||
|
||||
if not self.headers:
|
||||
raise HeadersNeeded
|
||||
@@ -992,13 +788,13 @@ class Dataset(object):
|
||||
if cols is None:
|
||||
cols = list(self.headers)
|
||||
|
||||
#filter out impossible rows and columns
|
||||
# filter out impossible rows and columns
|
||||
rows = [row for row in rows if row in range(self.height)]
|
||||
cols = [header for header in cols if header in self.headers]
|
||||
|
||||
_dset = Dataset()
|
||||
|
||||
#filtering rows and columns
|
||||
# filtering rows and columns
|
||||
_dset.headers = list(cols)
|
||||
|
||||
_dset._data = []
|
||||
@@ -1017,20 +813,12 @@ class Dataset(object):
|
||||
return _dset
|
||||
|
||||
|
||||
class Databook(object):
|
||||
class Databook:
|
||||
"""A book of :class:`Dataset` objects.
|
||||
"""
|
||||
|
||||
_formats = {}
|
||||
|
||||
def __init__(self, sets=None):
|
||||
|
||||
if sets is None:
|
||||
self._datasets = list()
|
||||
else:
|
||||
self._datasets = sets
|
||||
|
||||
self._register_formats()
|
||||
self._datasets = sets or []
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
@@ -1042,21 +830,6 @@ class Databook(object):
|
||||
"""Removes all :class:`Dataset` objects from the :class:`Databook`."""
|
||||
self._datasets = []
|
||||
|
||||
@classmethod
|
||||
def _register_formats(cls):
|
||||
"""Adds format properties."""
|
||||
for fmt in formats.available:
|
||||
try:
|
||||
try:
|
||||
setattr(cls, fmt.title, property(fmt.export_book, fmt.import_book))
|
||||
cls._formats[fmt.title] = (fmt.export_book, fmt.import_book)
|
||||
except AttributeError:
|
||||
setattr(cls, fmt.title, property(fmt.export_book))
|
||||
cls._formats[fmt.title] = (fmt.export_book, None)
|
||||
|
||||
except AttributeError:
|
||||
cls._formats[fmt.title] = (None, None)
|
||||
|
||||
def sheets(self):
|
||||
return self._datasets
|
||||
|
||||
@@ -1078,8 +851,8 @@ class Databook(object):
|
||||
|
||||
for dset in self._datasets:
|
||||
collector.append(dict_pack(
|
||||
title = dset.title,
|
||||
data = dset._package(ordered=ordered)
|
||||
title=dset.title,
|
||||
data=dset._package(ordered=ordered)
|
||||
))
|
||||
return collector
|
||||
|
||||
@@ -1091,18 +864,20 @@ class Databook(object):
|
||||
def load(self, in_stream, format, **kwargs):
|
||||
"""
|
||||
Import `in_stream` to the :class:`Databook` object using the `format`.
|
||||
`in_stream` can be a file-like object, a string, or a bytestring.
|
||||
|
||||
:param \\*\\*kwargs: (optional) custom configuration to the format `import_book`.
|
||||
"""
|
||||
|
||||
stream = normalize_input(in_stream)
|
||||
if not format:
|
||||
format = detect_format(in_stream)
|
||||
format = detect_format(stream)
|
||||
|
||||
export_book, import_book = self._formats.get(format, (None, None))
|
||||
if not import_book:
|
||||
raise UnsupportedFormat('Format {0} cannot be loaded.'.format(format))
|
||||
fmt = registry.get_format(format)
|
||||
if not hasattr(fmt, 'import_book'):
|
||||
raise UnsupportedFormat('Format {} cannot be loaded.'.format(format))
|
||||
|
||||
import_book(self, in_stream, **kwargs)
|
||||
fmt.import_book(self, stream, **kwargs)
|
||||
return self
|
||||
|
||||
def export(self, format, **kwargs):
|
||||
@@ -1111,50 +886,40 @@ class Databook(object):
|
||||
|
||||
:param \\*\\*kwargs: (optional) custom configuration to the format `export_book`.
|
||||
"""
|
||||
export_book, import_book = self._formats.get(format, (None, None))
|
||||
if not export_book:
|
||||
raise UnsupportedFormat('Format {0} cannot be exported.'.format(format))
|
||||
fmt = registry.get_format(format)
|
||||
if not hasattr(fmt, 'export_book'):
|
||||
raise UnsupportedFormat('Format {} cannot be exported.'.format(format))
|
||||
|
||||
return export_book(self, **kwargs)
|
||||
return fmt.export_book(self, **kwargs)
|
||||
|
||||
|
||||
def detect_format(stream):
|
||||
"""Return format name of given stream."""
|
||||
for fmt in formats.available:
|
||||
"""Return format name of given stream (file-like object, string, or bytestring)."""
|
||||
stream = normalize_input(stream)
|
||||
fmt_title = None
|
||||
for fmt in registry.formats():
|
||||
try:
|
||||
if fmt.detect(stream):
|
||||
return fmt.title
|
||||
fmt_title = fmt.title
|
||||
break
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
if hasattr(stream, 'seek'):
|
||||
stream.seek(0)
|
||||
return fmt_title
|
||||
|
||||
|
||||
def import_set(stream, format=None, **kwargs):
|
||||
"""Return dataset of given stream."""
|
||||
"""Return dataset of given stream (file-like object, string, or bytestring)."""
|
||||
|
||||
return Dataset().load(stream, format, **kwargs)
|
||||
return Dataset().load(normalize_input(stream), format, **kwargs)
|
||||
|
||||
|
||||
def import_book(stream, format=None, **kwargs):
|
||||
"""Return dataset of given stream."""
|
||||
"""Return dataset of given stream (file-like object, string, or bytestring)."""
|
||||
|
||||
return Databook().load(stream, format, **kwargs)
|
||||
return Databook().load(normalize_input(stream), format, **kwargs)
|
||||
|
||||
|
||||
class InvalidDatasetType(Exception):
|
||||
"Only Datasets can be added to a DataBook"
|
||||
|
||||
|
||||
class InvalidDimensions(Exception):
|
||||
"Invalid size"
|
||||
|
||||
|
||||
class InvalidDatasetIndex(Exception):
|
||||
"Outside of Dataset size"
|
||||
|
||||
|
||||
class HeadersNeeded(Exception):
|
||||
"Header parameter must be given when appending a column in this Dataset."
|
||||
|
||||
|
||||
class UnsupportedFormat(NotImplementedError):
|
||||
"Format is not supported"
|
||||
registry.register_builtins()
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
class InvalidDatasetType(Exception):
|
||||
"Only Datasets can be added to a DataBook"
|
||||
|
||||
|
||||
class InvalidDimensions(Exception):
|
||||
"Invalid size"
|
||||
|
||||
|
||||
class InvalidDatasetIndex(Exception):
|
||||
"Outside of Dataset size"
|
||||
|
||||
|
||||
class HeadersNeeded(Exception):
|
||||
"Header parameter must be given when appending a column in this Dataset."
|
||||
|
||||
|
||||
class UnsupportedFormat(NotImplementedError):
|
||||
"Format is not supported"
|
||||
+131
-17
@@ -1,21 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - formats
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
from functools import partialmethod
|
||||
from importlib import import_module
|
||||
from importlib.util import find_spec
|
||||
|
||||
from . import _csv as csv
|
||||
from . import _json as json
|
||||
from . import _xls as xls
|
||||
from . import _yaml as yaml
|
||||
from . import _tsv as tsv
|
||||
from . import _html as html
|
||||
from . import _xlsx as xlsx
|
||||
from . import _ods as ods
|
||||
from . import _dbf as dbf
|
||||
from . import _latex as latex
|
||||
from . import _df as df
|
||||
from . import _rst as rst
|
||||
from . import _jira as jira
|
||||
from tablib.exceptions import UnsupportedFormat
|
||||
from tablib.utils import normalize_input
|
||||
|
||||
# xlsx before as xls (xlrd) can also read xlsx
|
||||
available = (json, xlsx, xls, yaml, csv, dbf, tsv, html, jira, latex, ods, df, rst)
|
||||
from ._csv import CSVFormat
|
||||
from ._json import JSONFormat
|
||||
from ._tsv import TSVFormat
|
||||
|
||||
uninstalled_format_messages = {
|
||||
"cli": {"package_name": "tabulate package", "extras_name": "cli"},
|
||||
"df": {"package_name": "pandas package", "extras_name": "pandas"},
|
||||
"html": {"package_name": "MarkupPy package", "extras_name": "html"},
|
||||
"ods": {"package_name": "odfpy package", "extras_name": "ods"},
|
||||
"xls": {"package_name": "odfpy and xlwt packages", "extras_name": "ods"},
|
||||
"xlsx": {"package_name": "openpyxl package", "extras_name": "xlsx"},
|
||||
"yaml": {"package_name": "pyyaml package", "extras_name": "yaml"},
|
||||
}
|
||||
|
||||
|
||||
def load_format_class(dotted_path):
|
||||
try:
|
||||
module_path, class_name = dotted_path.rsplit('.', 1)
|
||||
return getattr(import_module(module_path), class_name)
|
||||
except (ValueError, AttributeError) as err:
|
||||
raise ImportError("Unable to load format class '{}' ({})".format(dotted_path, err))
|
||||
|
||||
|
||||
class FormatDescriptorBase:
|
||||
def __init__(self, key, format_or_path):
|
||||
self.key = key
|
||||
self._format_path = None
|
||||
if isinstance(format_or_path, str):
|
||||
self._format = None
|
||||
self._format_path = format_or_path
|
||||
else:
|
||||
self._format = format_or_path
|
||||
|
||||
def ensure_format_loaded(self):
|
||||
if self._format is None:
|
||||
self._format = load_format_class(self._format_path)
|
||||
|
||||
|
||||
class ImportExportBookDescriptor(FormatDescriptorBase):
|
||||
def __get__(self, obj, cls, **kwargs):
|
||||
self.ensure_format_loaded()
|
||||
return self._format.export_book(obj, **kwargs)
|
||||
|
||||
def __set__(self, obj, val):
|
||||
self.ensure_format_loaded()
|
||||
return self._format.import_book(obj, normalize_input(val))
|
||||
|
||||
|
||||
class ImportExportSetDescriptor(FormatDescriptorBase):
|
||||
def __get__(self, obj, cls, **kwargs):
|
||||
self.ensure_format_loaded()
|
||||
return self._format.export_set(obj, **kwargs)
|
||||
|
||||
def __set__(self, obj, val):
|
||||
self.ensure_format_loaded()
|
||||
return self._format.import_set(obj, normalize_input(val))
|
||||
|
||||
|
||||
class Registry:
|
||||
_formats = OrderedDict()
|
||||
|
||||
def register(self, key, format_or_path):
|
||||
from tablib.core import Databook, Dataset
|
||||
|
||||
# Create Databook.<format> read or read/write properties
|
||||
setattr(Databook, key, ImportExportBookDescriptor(key, format_or_path))
|
||||
|
||||
# Create Dataset.<format> read or read/write properties,
|
||||
# and Dataset.get_<format>/set_<format> methods.
|
||||
setattr(Dataset, key, ImportExportSetDescriptor(key, format_or_path))
|
||||
try:
|
||||
setattr(Dataset, 'get_%s' % key, partialmethod(Dataset._get_in_format, key))
|
||||
setattr(Dataset, 'set_%s' % key, partialmethod(Dataset._set_in_format, key))
|
||||
except AttributeError:
|
||||
setattr(Dataset, 'get_%s' % key, partialmethod(Dataset._get_in_format, key))
|
||||
|
||||
self._formats[key] = format_or_path
|
||||
|
||||
def register_builtins(self):
|
||||
# Registration ordering matters for autodetection.
|
||||
self.register('json', JSONFormat())
|
||||
# xlsx before as xls (xlrd) can also read xlsx
|
||||
if find_spec('openpyxl'):
|
||||
self.register('xlsx', 'tablib.formats._xlsx.XLSXFormat')
|
||||
if find_spec('xlrd') and find_spec('xlwt'):
|
||||
self.register('xls', 'tablib.formats._xls.XLSFormat')
|
||||
if find_spec('yaml'):
|
||||
self.register('yaml', 'tablib.formats._yaml.YAMLFormat')
|
||||
self.register('csv', CSVFormat())
|
||||
self.register('tsv', TSVFormat())
|
||||
if find_spec('odf'):
|
||||
self.register('ods', 'tablib.formats._ods.ODSFormat')
|
||||
self.register('dbf', 'tablib.formats._dbf.DBFFormat')
|
||||
if find_spec('MarkupPy'):
|
||||
self.register('html', 'tablib.formats._html.HTMLFormat')
|
||||
self.register('jira', 'tablib.formats._jira.JIRAFormat')
|
||||
self.register('latex', 'tablib.formats._latex.LATEXFormat')
|
||||
if find_spec('pandas'):
|
||||
self.register('df', 'tablib.formats._df.DataFrameFormat')
|
||||
self.register('rst', 'tablib.formats._rst.ReSTFormat')
|
||||
if find_spec('tabulate'):
|
||||
self.register('cli', 'tablib.formats._cli.CLIFormat')
|
||||
|
||||
def formats(self):
|
||||
for key, frm in self._formats.items():
|
||||
if isinstance(frm, str):
|
||||
self._formats[key] = load_format_class(frm)
|
||||
yield self._formats[key]
|
||||
|
||||
def get_format(self, key):
|
||||
if key not in self._formats:
|
||||
if key in uninstalled_format_messages:
|
||||
raise UnsupportedFormat(
|
||||
"The '{key}' format is not available. You may want to install the "
|
||||
"{package_name} (or `pip install tablib[{extras_name}]`).".format(
|
||||
**uninstalled_format_messages[key], key=key
|
||||
)
|
||||
)
|
||||
raise UnsupportedFormat("Tablib has no format '%s' or it is not registered." % key)
|
||||
if isinstance(self._formats[key], str):
|
||||
self._formats[key] = load_format_class(self._formats[key])
|
||||
return self._formats[key]
|
||||
|
||||
|
||||
registry = Registry()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Tablib - Command-line Interface table export support.
|
||||
|
||||
Generates a representation for CLI from the dataset.
|
||||
Wrapper for tabulate library.
|
||||
"""
|
||||
from tabulate import tabulate as Tabulate
|
||||
|
||||
|
||||
class CLIFormat:
|
||||
""" Class responsible to export to CLI Format """
|
||||
title = 'cli'
|
||||
DEFAULT_FMT = 'plain'
|
||||
|
||||
@classmethod
|
||||
def export_set(cls, dataset, **kwargs):
|
||||
"""Returns CLI representation of a Dataset."""
|
||||
if dataset.headers:
|
||||
kwargs.setdefault('headers', dataset.headers)
|
||||
kwargs.setdefault('tablefmt', cls.DEFAULT_FMT)
|
||||
return Tabulate(dataset, **kwargs)
|
||||
+40
-41
@@ -1,59 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - *SV Support.
|
||||
"""
|
||||
|
||||
from tablib.compat import csv, StringIO, unicode
|
||||
import csv
|
||||
from io import StringIO
|
||||
|
||||
|
||||
title = 'csv'
|
||||
extensions = ('csv',)
|
||||
class CSVFormat:
|
||||
title = 'csv'
|
||||
extensions = ('csv',)
|
||||
|
||||
DEFAULT_DELIMITER = ','
|
||||
|
||||
DEFAULT_DELIMITER = unicode(',')
|
||||
@classmethod
|
||||
def export_stream_set(cls, dataset, **kwargs):
|
||||
"""Returns CSV representation of Dataset as file-like."""
|
||||
stream = StringIO()
|
||||
|
||||
kwargs.setdefault('delimiter', cls.DEFAULT_DELIMITER)
|
||||
|
||||
def export_stream_set(dataset, **kwargs):
|
||||
"""Returns CSV representation of Dataset as file-like."""
|
||||
stream = StringIO()
|
||||
_csv = csv.writer(stream, **kwargs)
|
||||
|
||||
kwargs.setdefault('delimiter', DEFAULT_DELIMITER)
|
||||
for row in dataset._package(dicts=False):
|
||||
_csv.writerow(row)
|
||||
|
||||
_csv = csv.writer(stream, **kwargs)
|
||||
stream.seek(0)
|
||||
return stream
|
||||
|
||||
for row in dataset._package(dicts=False):
|
||||
_csv.writerow(row)
|
||||
@classmethod
|
||||
def export_set(cls, dataset, **kwargs):
|
||||
"""Returns CSV representation of Dataset."""
|
||||
stream = cls.export_stream_set(dataset, **kwargs)
|
||||
return stream.getvalue()
|
||||
|
||||
stream.seek(0)
|
||||
return stream
|
||||
@classmethod
|
||||
def import_set(cls, dset, in_stream, headers=True, **kwargs):
|
||||
"""Returns dataset from CSV stream."""
|
||||
|
||||
dset.wipe()
|
||||
|
||||
def export_set(dataset, **kwargs):
|
||||
"""Returns CSV representation of Dataset."""
|
||||
stream = export_stream_set(dataset, **kwargs)
|
||||
return stream.getvalue()
|
||||
kwargs.setdefault('delimiter', cls.DEFAULT_DELIMITER)
|
||||
|
||||
rows = csv.reader(in_stream, **kwargs)
|
||||
for i, row in enumerate(rows):
|
||||
|
||||
def import_set(dset, in_stream, headers=True, **kwargs):
|
||||
"""Returns dataset from CSV stream."""
|
||||
if (i == 0) and (headers):
|
||||
dset.headers = row
|
||||
elif row:
|
||||
dset.append(row)
|
||||
|
||||
dset.wipe()
|
||||
|
||||
kwargs.setdefault('delimiter', DEFAULT_DELIMITER)
|
||||
|
||||
rows = csv.reader(StringIO(in_stream), **kwargs)
|
||||
for i, row in enumerate(rows):
|
||||
|
||||
if (i == 0) and (headers):
|
||||
dset.headers = row
|
||||
elif row:
|
||||
dset.append(row)
|
||||
|
||||
|
||||
def detect(stream, delimiter=DEFAULT_DELIMITER):
|
||||
"""Returns True if given stream is valid CSV."""
|
||||
try:
|
||||
csv.Sniffer().sniff(stream, delimiters=delimiter)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@classmethod
|
||||
def detect(cls, stream, delimiter=None):
|
||||
"""Returns True if given stream is valid CSV."""
|
||||
try:
|
||||
csv.Sniffer().sniff(stream.read(1024), delimiters=delimiter or cls.DEFAULT_DELIMITER)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
+50
-71
@@ -1,87 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - DBF Support.
|
||||
"""
|
||||
import tempfile
|
||||
import struct
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from tablib.compat import StringIO
|
||||
from tablib.compat import dbfpy
|
||||
from tablib.compat import is_py3
|
||||
|
||||
if is_py3:
|
||||
from tablib.packages.dbfpy3 import dbf
|
||||
from tablib.packages.dbfpy3 import dbfnew
|
||||
from tablib.packages.dbfpy3 import record as dbfrecord
|
||||
import io
|
||||
else:
|
||||
from tablib.packages.dbfpy import dbf
|
||||
from tablib.packages.dbfpy import dbfnew
|
||||
from tablib.packages.dbfpy import record as dbfrecord
|
||||
from tablib.packages.dbfpy import dbf, dbfnew
|
||||
from tablib.packages.dbfpy import record as dbfrecord
|
||||
|
||||
|
||||
title = 'dbf'
|
||||
extensions = ('csv',)
|
||||
class DBFFormat:
|
||||
title = 'dbf'
|
||||
extensions = ('csv',)
|
||||
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns DBF representation of a Dataset"""
|
||||
new_dbf = dbfnew.dbf_new()
|
||||
temp_file, temp_uri = tempfile.mkstemp()
|
||||
@classmethod
|
||||
def export_set(cls, dataset):
|
||||
"""Returns DBF representation of a Dataset"""
|
||||
new_dbf = dbfnew.dbf_new()
|
||||
temp_file, temp_uri = tempfile.mkstemp()
|
||||
|
||||
# create the appropriate fields based on the contents of the first row
|
||||
first_row = dataset[0]
|
||||
for fieldname, field_value in zip(dataset.headers, first_row):
|
||||
if type(field_value) in [int, float]:
|
||||
new_dbf.add_field(fieldname, 'N', 10, 8)
|
||||
else:
|
||||
new_dbf.add_field(fieldname, 'C', 80)
|
||||
# create the appropriate fields based on the contents of the first row
|
||||
first_row = dataset[0]
|
||||
for fieldname, field_value in zip(dataset.headers, first_row):
|
||||
if type(field_value) in [int, float]:
|
||||
new_dbf.add_field(fieldname, 'N', 10, 8)
|
||||
else:
|
||||
new_dbf.add_field(fieldname, 'C', 80)
|
||||
|
||||
new_dbf.write(temp_uri)
|
||||
new_dbf.write(temp_uri)
|
||||
|
||||
dbf_file = dbf.Dbf(temp_uri, readOnly=0)
|
||||
for row in dataset:
|
||||
record = dbfrecord.DbfRecord(dbf_file)
|
||||
for fieldname, field_value in zip(dataset.headers, row):
|
||||
record[fieldname] = field_value
|
||||
record.store()
|
||||
dbf_file = dbf.Dbf(temp_uri, readOnly=0)
|
||||
for row in dataset:
|
||||
record = dbfrecord.DbfRecord(dbf_file)
|
||||
for fieldname, field_value in zip(dataset.headers, row):
|
||||
record[fieldname] = field_value
|
||||
record.store()
|
||||
|
||||
dbf_file.close()
|
||||
dbf_stream = open(temp_uri, 'rb')
|
||||
if is_py3:
|
||||
dbf_file.close()
|
||||
dbf_stream = open(temp_uri, 'rb')
|
||||
stream = io.BytesIO(dbf_stream.read())
|
||||
else:
|
||||
stream = StringIO(dbf_stream.read())
|
||||
dbf_stream.close()
|
||||
os.close(temp_file)
|
||||
os.remove(temp_uri)
|
||||
return stream.getvalue()
|
||||
dbf_stream.close()
|
||||
os.close(temp_file)
|
||||
os.remove(temp_uri)
|
||||
return stream.getvalue()
|
||||
|
||||
def import_set(dset, in_stream, headers=True):
|
||||
"""Returns a dataset from a DBF stream."""
|
||||
@classmethod
|
||||
def import_set(cls, dset, in_stream, headers=True):
|
||||
"""Returns a dataset from a DBF stream."""
|
||||
|
||||
dset.wipe()
|
||||
if is_py3:
|
||||
_dbf = dbf.Dbf(io.BytesIO(in_stream))
|
||||
else:
|
||||
_dbf = dbf.Dbf(StringIO(in_stream))
|
||||
dset.headers = _dbf.fieldNames
|
||||
for record in range(_dbf.recordCount):
|
||||
row = [_dbf[record][f] for f in _dbf.fieldNames]
|
||||
dset.append(row)
|
||||
dset.wipe()
|
||||
_dbf = dbf.Dbf(in_stream)
|
||||
dset.headers = _dbf.fieldNames
|
||||
for record in range(_dbf.recordCount):
|
||||
row = [_dbf[record][f] for f in _dbf.fieldNames]
|
||||
dset.append(row)
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if the given stream is valid DBF"""
|
||||
#_dbf = dbf.Table(StringIO(stream))
|
||||
try:
|
||||
if is_py3:
|
||||
if type(stream) is not bytes:
|
||||
stream = bytes(stream, 'utf-8')
|
||||
_dbf = dbf.Dbf(io.BytesIO(stream), readOnly=True)
|
||||
else:
|
||||
_dbf = dbf.Dbf(StringIO(stream), readOnly=True)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@classmethod
|
||||
def detect(cls, stream):
|
||||
"""Returns True if the given stream is valid DBF"""
|
||||
try:
|
||||
_dbf = dbf.Dbf(stream, readOnly=True)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
+29
-31
@@ -1,43 +1,41 @@
|
||||
""" Tablib - DataFrame Support.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
try:
|
||||
from pandas import DataFrame
|
||||
except ImportError:
|
||||
DataFrame = None
|
||||
|
||||
import tablib
|
||||
|
||||
from tablib.compat import unicode
|
||||
class DataFrameFormat:
|
||||
title = 'df'
|
||||
extensions = ('df',)
|
||||
|
||||
title = 'df'
|
||||
extensions = ('df', )
|
||||
@classmethod
|
||||
def detect(cls, stream):
|
||||
"""Returns True if given stream is a DataFrame."""
|
||||
if DataFrame is None:
|
||||
return False
|
||||
elif isinstance(stream, DataFrame):
|
||||
return True
|
||||
try:
|
||||
DataFrame(stream.read())
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is a DataFrame."""
|
||||
if DataFrame is None:
|
||||
return False
|
||||
try:
|
||||
DataFrame(stream)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
@classmethod
|
||||
def export_set(cls, dset, index=None):
|
||||
"""Returns DataFrame representation of DataBook."""
|
||||
if DataFrame is None:
|
||||
raise NotImplementedError(
|
||||
'DataFrame Format requires `pandas` to be installed.'
|
||||
' Try `pip install tablib[pandas]`.')
|
||||
dataframe = DataFrame(dset.dict, columns=dset.headers)
|
||||
return dataframe
|
||||
|
||||
|
||||
def export_set(dset, index=None):
|
||||
"""Returns DataFrame representation of DataBook."""
|
||||
if DataFrame is None:
|
||||
raise NotImplementedError(
|
||||
'DataFrame Format requires `pandas` to be installed.'
|
||||
' Try `pip install tablib[pandas]`.')
|
||||
dataframe = DataFrame(dset.dict, columns=dset.headers)
|
||||
return dataframe
|
||||
|
||||
|
||||
def import_set(dset, in_stream):
|
||||
"""Returns dataset from DataFrame."""
|
||||
dset.wipe()
|
||||
dset.dict = in_stream.to_dict(orient='records')
|
||||
@classmethod
|
||||
def import_set(cls, dset, in_stream):
|
||||
"""Returns dataset from DataFrame."""
|
||||
dset.wipe()
|
||||
dset.dict = in_stream.to_dict(orient='records')
|
||||
|
||||
+38
-41
@@ -1,65 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - HTML export support.
|
||||
"""
|
||||
|
||||
import codecs
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
from MarkupPy import markup
|
||||
import tablib
|
||||
from tablib.compat import unicode
|
||||
|
||||
BOOK_ENDINGS = 'h3'
|
||||
|
||||
title = 'html'
|
||||
extensions = ('html', )
|
||||
|
||||
|
||||
def export_set(dataset):
|
||||
"""HTML representation of a Dataset."""
|
||||
class HTMLFormat:
|
||||
BOOK_ENDINGS = 'h3'
|
||||
|
||||
stream = BytesIO()
|
||||
title = 'html'
|
||||
extensions = ('html', )
|
||||
|
||||
page = markup.page()
|
||||
page.table.open()
|
||||
@classmethod
|
||||
def export_set(cls, dataset):
|
||||
"""HTML representation of a Dataset."""
|
||||
|
||||
if dataset.headers is not None:
|
||||
new_header = [item if item is not None else '' for item in dataset.headers]
|
||||
stream = BytesIO()
|
||||
|
||||
page.thead.open()
|
||||
headers = markup.oneliner.th(new_header)
|
||||
page.tr(headers)
|
||||
page.thead.close()
|
||||
page = markup.page()
|
||||
page.table.open()
|
||||
|
||||
for row in dataset:
|
||||
new_row = [item if item is not None else '' for item in row]
|
||||
if dataset.headers is not None:
|
||||
new_header = [item if item is not None else '' for item in dataset.headers]
|
||||
|
||||
html_row = markup.oneliner.td(new_row)
|
||||
page.tr(html_row)
|
||||
page.thead.open()
|
||||
headers = markup.oneliner.th(new_header)
|
||||
page.tr(headers)
|
||||
page.thead.close()
|
||||
|
||||
page.table.close()
|
||||
for row in dataset:
|
||||
new_row = [item if item is not None else '' for item in row]
|
||||
|
||||
# Allow unicode characters in output
|
||||
wrapper = codecs.getwriter("utf8")(stream)
|
||||
wrapper.writelines(unicode(page))
|
||||
html_row = markup.oneliner.td(new_row)
|
||||
page.tr(html_row)
|
||||
|
||||
return stream.getvalue().decode('utf-8')
|
||||
page.table.close()
|
||||
|
||||
# Allow unicode characters in output
|
||||
wrapper = codecs.getwriter("utf8")(stream)
|
||||
wrapper.writelines(str(page))
|
||||
|
||||
def export_book(databook):
|
||||
"""HTML representation of a Databook."""
|
||||
return stream.getvalue().decode('utf-8')
|
||||
|
||||
stream = BytesIO()
|
||||
@classmethod
|
||||
def export_book(cls, databook):
|
||||
"""HTML representation of a Databook."""
|
||||
|
||||
# Allow unicode characters in output
|
||||
wrapper = codecs.getwriter("utf8")(stream)
|
||||
stream = BytesIO()
|
||||
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
title = (dset.title if dset.title else 'Set %s' % (i))
|
||||
wrapper.write('<%s>%s</%s>\n' % (BOOK_ENDINGS, title, BOOK_ENDINGS))
|
||||
wrapper.write(dset.html)
|
||||
wrapper.write('\n')
|
||||
# Allow unicode characters in output
|
||||
wrapper = codecs.getwriter("utf8")(stream)
|
||||
|
||||
return stream.getvalue().decode('utf-8')
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
title = (dset.title if dset.title else 'Set %s' % (i))
|
||||
wrapper.write('<{}>{}</{}>\n'.format(cls.BOOK_ENDINGS, title, cls.BOOK_ENDINGS))
|
||||
wrapper.write(dset.html)
|
||||
wrapper.write('\n')
|
||||
|
||||
return stream.getvalue().decode('utf-8')
|
||||
|
||||
+27
-26
@@ -1,39 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Tablib - Jira table export support.
|
||||
|
||||
Generates a Jira table from the dataset.
|
||||
"""
|
||||
from tablib.compat import unicode
|
||||
|
||||
title = 'jira'
|
||||
|
||||
|
||||
def export_set(dataset):
|
||||
"""Formats the dataset according to the Jira table syntax:
|
||||
class JIRAFormat:
|
||||
title = 'jira'
|
||||
|
||||
||heading 1||heading 2||heading 3||
|
||||
|col A1|col A2|col A3|
|
||||
|col B1|col B2|col B3|
|
||||
@classmethod
|
||||
def export_set(cls, dataset):
|
||||
"""Formats the dataset according to the Jira table syntax:
|
||||
|
||||
:param dataset: dataset to serialize
|
||||
:type dataset: tablib.core.Dataset
|
||||
"""
|
||||
||heading 1||heading 2||heading 3||
|
||||
|col A1|col A2|col A3|
|
||||
|col B1|col B2|col B3|
|
||||
|
||||
header = _get_header(dataset.headers) if dataset.headers else ''
|
||||
body = _get_body(dataset)
|
||||
return '%s\n%s' % (header, body) if header else body
|
||||
:param dataset: dataset to serialize
|
||||
:type dataset: tablib.core.Dataset
|
||||
"""
|
||||
|
||||
header = cls._get_header(dataset.headers) if dataset.headers else ''
|
||||
body = cls._get_body(dataset)
|
||||
return '{}\n{}'.format(header, body) if header else body
|
||||
|
||||
def _get_body(dataset):
|
||||
return '\n'.join([_serialize_row(row) for row in dataset])
|
||||
@classmethod
|
||||
def _get_body(cls, dataset):
|
||||
return '\n'.join([cls._serialize_row(row) for row in dataset])
|
||||
|
||||
@classmethod
|
||||
def _get_header(cls, headers):
|
||||
return cls._serialize_row(headers, delimiter='||')
|
||||
|
||||
def _get_header(headers):
|
||||
return _serialize_row(headers, delimiter='||')
|
||||
|
||||
|
||||
def _serialize_row(row, delimiter='|'):
|
||||
return '%s%s%s' % (delimiter,
|
||||
delimiter.join([unicode(item) if item else ' ' for item in row]),
|
||||
delimiter)
|
||||
@classmethod
|
||||
def _serialize_row(cls, row, delimiter='|'):
|
||||
return '{}{}{}'.format(
|
||||
delimiter,
|
||||
delimiter.join([str(item) if item else ' ' for item in row]),
|
||||
delimiter
|
||||
)
|
||||
|
||||
+33
-34
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - JSON Support
|
||||
"""
|
||||
import decimal
|
||||
@@ -9,10 +7,6 @@ from uuid import UUID
|
||||
import tablib
|
||||
|
||||
|
||||
title = 'json'
|
||||
extensions = ('json', 'jsn')
|
||||
|
||||
|
||||
def serialize_objects_handler(obj):
|
||||
if isinstance(obj, (decimal.Decimal, UUID)):
|
||||
return str(obj)
|
||||
@@ -22,38 +16,43 @@ def serialize_objects_handler(obj):
|
||||
return obj
|
||||
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns JSON representation of Dataset."""
|
||||
return json.dumps(dataset.dict, default=serialize_objects_handler)
|
||||
class JSONFormat:
|
||||
title = 'json'
|
||||
extensions = ('json', 'jsn')
|
||||
|
||||
@classmethod
|
||||
def export_set(cls, dataset):
|
||||
"""Returns JSON representation of Dataset."""
|
||||
return json.dumps(dataset.dict, default=serialize_objects_handler)
|
||||
|
||||
def export_book(databook):
|
||||
"""Returns JSON representation of Databook."""
|
||||
return json.dumps(databook._package(), default=serialize_objects_handler)
|
||||
@classmethod
|
||||
def export_book(cls, databook):
|
||||
"""Returns JSON representation of Databook."""
|
||||
return json.dumps(databook._package(), default=serialize_objects_handler)
|
||||
|
||||
@classmethod
|
||||
def import_set(cls, dset, in_stream):
|
||||
"""Returns dataset from JSON stream."""
|
||||
|
||||
def import_set(dset, in_stream):
|
||||
"""Returns dataset from JSON stream."""
|
||||
dset.wipe()
|
||||
dset.dict = json.load(in_stream)
|
||||
|
||||
dset.wipe()
|
||||
dset.dict = json.loads(in_stream)
|
||||
@classmethod
|
||||
def import_book(cls, dbook, in_stream):
|
||||
"""Returns databook from JSON stream."""
|
||||
|
||||
dbook.wipe()
|
||||
for sheet in json.load(in_stream):
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet['title']
|
||||
data.dict = sheet['data']
|
||||
dbook.add_sheet(data)
|
||||
|
||||
def import_book(dbook, in_stream):
|
||||
"""Returns databook from JSON stream."""
|
||||
|
||||
dbook.wipe()
|
||||
for sheet in json.loads(in_stream):
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet['title']
|
||||
data.dict = sheet['data']
|
||||
dbook.add_sheet(data)
|
||||
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is valid JSON."""
|
||||
try:
|
||||
json.loads(stream)
|
||||
return True
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
@classmethod
|
||||
def detect(cls, stream):
|
||||
"""Returns True if given stream is valid JSON."""
|
||||
try:
|
||||
json.load(stream)
|
||||
return True
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Tablib - LaTeX table export support.
|
||||
|
||||
Generates a LaTeX booktabs-style table from the dataset.
|
||||
"""
|
||||
import re
|
||||
|
||||
from tablib.compat import unicode
|
||||
|
||||
title = 'latex'
|
||||
extensions = ('tex',)
|
||||
class LATEXFormat:
|
||||
title = 'latex'
|
||||
extensions = ('tex',)
|
||||
|
||||
TABLE_TEMPLATE = """\
|
||||
TABLE_TEMPLATE = """\
|
||||
%% Note: add \\usepackage{booktabs} to your preamble
|
||||
%%
|
||||
\\begin{table}[!htbp]
|
||||
@@ -27,108 +25,108 @@ TABLE_TEMPLATE = """\
|
||||
\\end{table}
|
||||
"""
|
||||
|
||||
TEX_RESERVED_SYMBOLS_MAP = dict([
|
||||
('\\', '\\textbackslash{}'),
|
||||
('{', '\\{'),
|
||||
('}', '\\}'),
|
||||
('$', '\\$'),
|
||||
('&', '\\&'),
|
||||
('#', '\\#'),
|
||||
('^', '\\textasciicircum{}'),
|
||||
('_', '\\_'),
|
||||
('~', '\\textasciitilde{}'),
|
||||
('%', '\\%'),
|
||||
])
|
||||
TEX_RESERVED_SYMBOLS_MAP = dict([
|
||||
('\\', '\\textbackslash{}'),
|
||||
('{', '\\{'),
|
||||
('}', '\\}'),
|
||||
('$', '\\$'),
|
||||
('&', '\\&'),
|
||||
('#', '\\#'),
|
||||
('^', '\\textasciicircum{}'),
|
||||
('_', '\\_'),
|
||||
('~', '\\textasciitilde{}'),
|
||||
('%', '\\%'),
|
||||
])
|
||||
|
||||
TEX_RESERVED_SYMBOLS_RE = re.compile(
|
||||
'(%s)' % '|'.join(map(re.escape, TEX_RESERVED_SYMBOLS_MAP.keys())))
|
||||
TEX_RESERVED_SYMBOLS_RE = re.compile(
|
||||
'(%s)' % '|'.join(map(re.escape, TEX_RESERVED_SYMBOLS_MAP.keys())))
|
||||
|
||||
@classmethod
|
||||
def export_set(cls, dataset):
|
||||
"""Returns LaTeX representation of dataset
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns LaTeX representation of dataset
|
||||
:param dataset: dataset to serialize
|
||||
:type dataset: tablib.core.Dataset
|
||||
"""
|
||||
|
||||
:param dataset: dataset to serialize
|
||||
:type dataset: tablib.core.Dataset
|
||||
"""
|
||||
caption = '\\caption{%s}' % dataset.title if dataset.title else '%'
|
||||
colspec = cls._colspec(dataset.width)
|
||||
header = cls._serialize_row(dataset.headers) if dataset.headers else ''
|
||||
midrule = cls._midrule(dataset.width)
|
||||
body = '\n'.join([cls._serialize_row(row) for row in dataset])
|
||||
return cls.TABLE_TEMPLATE % dict(CAPTION=caption, COLSPEC=colspec,
|
||||
HEADER=header, MIDRULE=midrule, BODY=body)
|
||||
|
||||
caption = '\\caption{%s}' % dataset.title if dataset.title else '%'
|
||||
colspec = _colspec(dataset.width)
|
||||
header = _serialize_row(dataset.headers) if dataset.headers else ''
|
||||
midrule = _midrule(dataset.width)
|
||||
body = '\n'.join([_serialize_row(row) for row in dataset])
|
||||
return TABLE_TEMPLATE % dict(CAPTION=caption, COLSPEC=colspec,
|
||||
HEADER=header, MIDRULE=midrule, BODY=body)
|
||||
@classmethod
|
||||
def _colspec(cls, dataset_width):
|
||||
"""Generates the column specification for the LaTeX `tabular` environment
|
||||
based on the dataset width.
|
||||
|
||||
The first column is justified to the left, all further columns are aligned
|
||||
to the right.
|
||||
|
||||
def _colspec(dataset_width):
|
||||
"""Generates the column specification for the LaTeX `tabular` environment
|
||||
based on the dataset width.
|
||||
.. note:: This is only a heuristic and most probably has to be fine-tuned
|
||||
post export. Column alignment should depend on the data type, e.g., textual
|
||||
content should usually be aligned to the left while numeric content almost
|
||||
always should be aligned to the right.
|
||||
|
||||
The first column is justified to the left, all further columns are aligned
|
||||
to the right.
|
||||
:param dataset_width: width of the dataset
|
||||
"""
|
||||
|
||||
.. note:: This is only a heuristic and most probably has to be fine-tuned
|
||||
post export. Column alignment should depend on the data type, e.g., textual
|
||||
content should usually be aligned to the left while numeric content almost
|
||||
always should be aligned to the right.
|
||||
spec = 'l'
|
||||
for _ in range(1, dataset_width):
|
||||
spec += 'r'
|
||||
return spec
|
||||
|
||||
:param dataset_width: width of the dataset
|
||||
"""
|
||||
@classmethod
|
||||
def _midrule(cls, dataset_width):
|
||||
"""Generates the table `midrule`, which may be composed of several
|
||||
`cmidrules`.
|
||||
|
||||
spec = 'l'
|
||||
for _ in range(1, dataset_width):
|
||||
spec += 'r'
|
||||
return spec
|
||||
:param dataset_width: width of the dataset to serialize
|
||||
"""
|
||||
|
||||
if not dataset_width or dataset_width == 1:
|
||||
return '\\midrule'
|
||||
return ' '.join([cls._cmidrule(colindex, dataset_width) for colindex in
|
||||
range(1, dataset_width + 1)])
|
||||
|
||||
def _midrule(dataset_width):
|
||||
"""Generates the table `midrule`, which may be composed of several
|
||||
`cmidrules`.
|
||||
@classmethod
|
||||
def _cmidrule(cls, colindex, dataset_width):
|
||||
"""Generates the `cmidrule` for a single column with appropriate trimming
|
||||
based on the column position.
|
||||
|
||||
:param dataset_width: width of the dataset to serialize
|
||||
"""
|
||||
:param colindex: Column index
|
||||
:param dataset_width: width of the dataset
|
||||
"""
|
||||
|
||||
if not dataset_width or dataset_width == 1:
|
||||
return '\\midrule'
|
||||
return ' '.join([_cmidrule(colindex, dataset_width) for colindex in
|
||||
range(1, dataset_width + 1)])
|
||||
rule = '\\cmidrule(%s){%d-%d}'
|
||||
if colindex == 1:
|
||||
# Rule of first column is trimmed on the right
|
||||
return rule % ('r', colindex, colindex)
|
||||
if colindex == dataset_width:
|
||||
# Rule of last column is trimmed on the left
|
||||
return rule % ('l', colindex, colindex)
|
||||
# Inner columns are trimmed on the left and right
|
||||
return rule % ('lr', colindex, colindex)
|
||||
|
||||
@classmethod
|
||||
def _serialize_row(cls, row):
|
||||
"""Returns string representation of a single row.
|
||||
|
||||
def _cmidrule(colindex, dataset_width):
|
||||
"""Generates the `cmidrule` for a single column with appropriate trimming
|
||||
based on the column position.
|
||||
:param row: single dataset row
|
||||
"""
|
||||
|
||||
:param colindex: Column index
|
||||
:param dataset_width: width of the dataset
|
||||
"""
|
||||
new_row = [cls._escape_tex_reserved_symbols(str(item)) if item else ''
|
||||
for item in row]
|
||||
return 6 * ' ' + ' & '.join(new_row) + ' \\\\'
|
||||
|
||||
rule = '\\cmidrule(%s){%d-%d}'
|
||||
if colindex == 1:
|
||||
# Rule of first column is trimmed on the right
|
||||
return rule % ('r', colindex, colindex)
|
||||
if colindex == dataset_width:
|
||||
# Rule of last column is trimmed on the left
|
||||
return rule % ('l', colindex, colindex)
|
||||
# Inner columns are trimmed on the left and right
|
||||
return rule % ('lr', colindex, colindex)
|
||||
@classmethod
|
||||
def _escape_tex_reserved_symbols(cls, input):
|
||||
"""Escapes all TeX reserved symbols ('_', '~', etc.) in a string.
|
||||
|
||||
|
||||
def _serialize_row(row):
|
||||
"""Returns string representation of a single row.
|
||||
|
||||
:param row: single dataset row
|
||||
"""
|
||||
|
||||
new_row = [_escape_tex_reserved_symbols(unicode(item)) if item else '' for
|
||||
item in row]
|
||||
return 6 * ' ' + ' & '.join(new_row) + ' \\\\'
|
||||
|
||||
|
||||
def _escape_tex_reserved_symbols(input):
|
||||
"""Escapes all TeX reserved symbols ('_', '~', etc.) in a string.
|
||||
|
||||
:param input: String to escape
|
||||
"""
|
||||
def replace(match):
|
||||
return TEX_RESERVED_SYMBOLS_MAP[match.group()]
|
||||
return TEX_RESERVED_SYMBOLS_RE.sub(replace, input)
|
||||
:param input: String to escape
|
||||
"""
|
||||
def replace(match):
|
||||
return cls.TEX_RESERVED_SYMBOLS_MAP[match.group()]
|
||||
return cls.TEX_RESERVED_SYMBOLS_RE.sub(replace, input)
|
||||
|
||||
+77
-76
@@ -1,104 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - ODF Support.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
from odf import opendocument, style, table, text
|
||||
from tablib.compat import unicode
|
||||
|
||||
title = 'ods'
|
||||
extensions = ('ods',)
|
||||
from odf import opendocument, style, table, text
|
||||
|
||||
bold = style.Style(name="bold", family="paragraph")
|
||||
bold.addElement(style.TextProperties(fontweight="bold", fontweightasian="bold", fontweightcomplex="bold"))
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns ODF representation of Dataset."""
|
||||
|
||||
wb = opendocument.OpenDocumentSpreadsheet()
|
||||
wb.automaticstyles.addElement(bold)
|
||||
class ODSFormat:
|
||||
title = 'ods'
|
||||
extensions = ('ods',)
|
||||
|
||||
ws = table.Table(name=dataset.title if dataset.title else 'Tablib Dataset')
|
||||
wb.spreadsheet.addElement(ws)
|
||||
dset_sheet(dataset, ws)
|
||||
@classmethod
|
||||
def export_set(cls, dataset):
|
||||
"""Returns ODF representation of Dataset."""
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
wb = opendocument.OpenDocumentSpreadsheet()
|
||||
wb.automaticstyles.addElement(bold)
|
||||
|
||||
|
||||
def export_book(databook):
|
||||
"""Returns ODF representation of DataBook."""
|
||||
|
||||
wb = opendocument.OpenDocumentSpreadsheet()
|
||||
wb.automaticstyles.addElement(bold)
|
||||
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
ws = table.Table(name=dset.title if dset.title else 'Sheet%s' % (i))
|
||||
ws = table.Table(name=dataset.title if dataset.title else 'Tablib Dataset')
|
||||
wb.spreadsheet.addElement(ws)
|
||||
dset_sheet(dset, ws)
|
||||
cls.dset_sheet(dataset, ws)
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
@classmethod
|
||||
def export_book(cls, databook):
|
||||
"""Returns ODF representation of DataBook."""
|
||||
|
||||
def dset_sheet(dataset, ws):
|
||||
"""Completes given worksheet from given Dataset."""
|
||||
_package = dataset._package(dicts=False)
|
||||
wb = opendocument.OpenDocumentSpreadsheet()
|
||||
wb.automaticstyles.addElement(bold)
|
||||
|
||||
for i, sep in enumerate(dataset._separators):
|
||||
_offset = i
|
||||
_package.insert((sep[0] + _offset), (sep[1],))
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
ws = table.Table(name=dset.title if dset.title else 'Sheet%s' % (i))
|
||||
wb.spreadsheet.addElement(ws)
|
||||
cls.dset_sheet(dset, ws)
|
||||
|
||||
for i, row in enumerate(_package):
|
||||
row_number = i + 1
|
||||
odf_row = table.TableRow(stylename=bold, defaultcellstylename='bold')
|
||||
for j, col in enumerate(row):
|
||||
try:
|
||||
col = unicode(col, errors='ignore')
|
||||
except TypeError:
|
||||
## col is already unicode
|
||||
pass
|
||||
ws.addElement(table.TableColumn())
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
# bold headers
|
||||
if (row_number == 1) and dataset.headers:
|
||||
odf_row.setAttribute('stylename', bold)
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
p = text.P()
|
||||
p.addElement(text.Span(text=col, stylename=bold))
|
||||
cell.addElement(p)
|
||||
odf_row.addElement(cell)
|
||||
@classmethod
|
||||
def dset_sheet(cls, dataset, ws):
|
||||
"""Completes given worksheet from given Dataset."""
|
||||
_package = dataset._package(dicts=False)
|
||||
|
||||
# wrap the rest
|
||||
else:
|
||||
for i, sep in enumerate(dataset._separators):
|
||||
_offset = i
|
||||
_package.insert((sep[0] + _offset), (sep[1],))
|
||||
|
||||
for i, row in enumerate(_package):
|
||||
row_number = i + 1
|
||||
odf_row = table.TableRow(stylename=bold, defaultcellstylename='bold')
|
||||
for j, col in enumerate(row):
|
||||
try:
|
||||
if '\n' in col:
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
cell.addElement(text.P(text=col))
|
||||
odf_row.addElement(cell)
|
||||
else:
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
cell.addElement(text.P(text=col))
|
||||
odf_row.addElement(cell)
|
||||
col = str(col, errors='ignore')
|
||||
except TypeError:
|
||||
# col is already str
|
||||
pass
|
||||
ws.addElement(table.TableColumn())
|
||||
|
||||
# bold headers
|
||||
if (row_number == 1) and dataset.headers:
|
||||
odf_row.setAttribute('stylename', bold)
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
cell.addElement(text.P(text=col))
|
||||
p = text.P()
|
||||
p.addElement(text.Span(text=col, stylename=bold))
|
||||
cell.addElement(p)
|
||||
odf_row.addElement(cell)
|
||||
|
||||
# wrap the rest
|
||||
else:
|
||||
try:
|
||||
if '\n' in col:
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
cell.addElement(text.P(text=col))
|
||||
odf_row.addElement(cell)
|
||||
else:
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
cell.addElement(text.P(text=col))
|
||||
odf_row.addElement(cell)
|
||||
except TypeError:
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
cell.addElement(text.P(text=col))
|
||||
odf_row.addElement(cell)
|
||||
|
||||
def detect(stream):
|
||||
if isinstance(stream, bytes):
|
||||
# load expects a file-like object.
|
||||
stream = BytesIO(stream)
|
||||
try:
|
||||
opendocument.load(stream)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@classmethod
|
||||
def detect(cls, stream):
|
||||
if isinstance(stream, bytes):
|
||||
# load expects a file-like object.
|
||||
stream = BytesIO(stream)
|
||||
try:
|
||||
opendocument.load(stream)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
+221
-229
@@ -1,273 +1,265 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - reStructuredText Support
|
||||
"""
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from itertools import zip_longest
|
||||
from statistics import median
|
||||
from textwrap import TextWrapper
|
||||
|
||||
from tablib.compat import (
|
||||
median,
|
||||
unicode,
|
||||
izip_longest,
|
||||
)
|
||||
|
||||
|
||||
title = 'rst'
|
||||
extensions = ('rst',)
|
||||
|
||||
|
||||
MAX_TABLE_WIDTH = 80 # Roughly. It may be wider to avoid breaking words.
|
||||
|
||||
|
||||
JUSTIFY_LEFT = 'left'
|
||||
JUSTIFY_CENTER = 'center'
|
||||
JUSTIFY_RIGHT = 'right'
|
||||
JUSTIFY_VALUES = (JUSTIFY_LEFT, JUSTIFY_CENTER, JUSTIFY_RIGHT)
|
||||
|
||||
|
||||
def to_unicode(value):
|
||||
def to_str(value):
|
||||
if isinstance(value, bytes):
|
||||
return value.decode('utf-8')
|
||||
return unicode(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
def _max_word_len(text):
|
||||
"""
|
||||
Return the length of the longest word in `text`.
|
||||
|
||||
|
||||
>>> _max_word_len('Python Module for Tabular Datasets')
|
||||
8
|
||||
|
||||
"""
|
||||
return max((len(word) for word in text.split()))
|
||||
return max(len(word) for word in text.split()) if text else 0
|
||||
|
||||
|
||||
def _get_column_string_lengths(dataset):
|
||||
"""
|
||||
Returns a list of string lengths of each column, and a list of
|
||||
maximum word lengths.
|
||||
"""
|
||||
if dataset.headers:
|
||||
column_lengths = [[len(h)] for h in dataset.headers]
|
||||
word_lens = [_max_word_len(h) for h in dataset.headers]
|
||||
else:
|
||||
column_lengths = [[] for _ in range(dataset.width)]
|
||||
word_lens = [0 for _ in range(dataset.width)]
|
||||
for row in dataset.dict:
|
||||
values = iter(row.values() if hasattr(row, 'values') else row)
|
||||
for i, val in enumerate(values):
|
||||
text = to_unicode(val)
|
||||
column_lengths[i].append(len(text))
|
||||
word_lens[i] = max(word_lens[i], _max_word_len(text))
|
||||
return column_lengths, word_lens
|
||||
class ReSTFormat:
|
||||
title = 'rst'
|
||||
extensions = ('rst',)
|
||||
|
||||
MAX_TABLE_WIDTH = 80 # Roughly. It may be wider to avoid breaking words.
|
||||
|
||||
def _row_to_lines(values, widths, wrapper, sep='|', justify=JUSTIFY_LEFT):
|
||||
"""
|
||||
Returns a table row of wrapped values as a list of lines
|
||||
"""
|
||||
if justify not in JUSTIFY_VALUES:
|
||||
raise ValueError('Value of "justify" must be one of "{}"'.format(
|
||||
'", "'.join(JUSTIFY_VALUES)
|
||||
))
|
||||
if justify == JUSTIFY_LEFT:
|
||||
just = lambda text, width: text.ljust(width)
|
||||
elif justify == JUSTIFY_CENTER:
|
||||
just = lambda text, width: text.center(width)
|
||||
else:
|
||||
just = lambda text, width: text.rjust(width)
|
||||
lpad = sep + ' ' if sep else ''
|
||||
rpad = ' ' + sep if sep else ''
|
||||
pad = ' ' + sep + ' '
|
||||
cells = []
|
||||
for value, width in zip(values, widths):
|
||||
wrapper.width = width
|
||||
text = to_unicode(value)
|
||||
cell = wrapper.wrap(text)
|
||||
cells.append(cell)
|
||||
lines = izip_longest(*cells, fillvalue='')
|
||||
lines = (
|
||||
(just(cell_line, widths[i]) for i, cell_line in enumerate(line))
|
||||
for line in lines
|
||||
)
|
||||
lines = [''.join((lpad, pad.join(line), rpad)) for line in lines]
|
||||
return lines
|
||||
@classmethod
|
||||
def _get_column_string_lengths(cls, dataset):
|
||||
"""
|
||||
Returns a list of string lengths of each column, and a list of
|
||||
maximum word lengths.
|
||||
"""
|
||||
if dataset.headers:
|
||||
column_lengths = [[len(h)] for h in dataset.headers]
|
||||
word_lens = [_max_word_len(h) for h in dataset.headers]
|
||||
else:
|
||||
column_lengths = [[] for _ in range(dataset.width)]
|
||||
word_lens = [0 for _ in range(dataset.width)]
|
||||
for row in dataset.dict:
|
||||
values = iter(row.values() if hasattr(row, 'values') else row)
|
||||
for i, val in enumerate(values):
|
||||
text = to_str(val)
|
||||
column_lengths[i].append(len(text))
|
||||
word_lens[i] = max(word_lens[i], _max_word_len(text))
|
||||
return column_lengths, word_lens
|
||||
|
||||
@classmethod
|
||||
def _row_to_lines(cls, values, widths, wrapper, sep='|', justify=JUSTIFY_LEFT):
|
||||
"""
|
||||
Returns a table row of wrapped values as a list of lines
|
||||
"""
|
||||
if justify not in JUSTIFY_VALUES:
|
||||
raise ValueError('Value of "justify" must be one of "{}"'.format(
|
||||
'", "'.join(JUSTIFY_VALUES)
|
||||
))
|
||||
if justify == JUSTIFY_LEFT:
|
||||
just = lambda text, width: text.ljust(width)
|
||||
elif justify == JUSTIFY_CENTER:
|
||||
just = lambda text, width: text.center(width)
|
||||
else:
|
||||
just = lambda text, width: text.rjust(width)
|
||||
lpad = sep + ' ' if sep else ''
|
||||
rpad = ' ' + sep if sep else ''
|
||||
pad = ' ' + sep + ' '
|
||||
cells = []
|
||||
for value, width in zip(values, widths):
|
||||
wrapper.width = width
|
||||
text = to_str(value)
|
||||
cell = wrapper.wrap(text)
|
||||
cells.append(cell)
|
||||
lines = zip_longest(*cells, fillvalue='')
|
||||
lines = (
|
||||
(just(cell_line, widths[i]) for i, cell_line in enumerate(line))
|
||||
for line in lines
|
||||
)
|
||||
lines = [''.join((lpad, pad.join(line), rpad)) for line in lines]
|
||||
return lines
|
||||
|
||||
def _get_column_widths(dataset, max_table_width=MAX_TABLE_WIDTH, pad_len=3):
|
||||
"""
|
||||
Returns a list of column widths proportional to the median length
|
||||
of the text in their cells.
|
||||
"""
|
||||
str_lens, word_lens = _get_column_string_lengths(dataset)
|
||||
median_lens = [int(median(lens)) for lens in str_lens]
|
||||
total = sum(median_lens)
|
||||
if total > max_table_width - (pad_len * len(median_lens)):
|
||||
column_widths = (max_table_width * l // total for l in median_lens)
|
||||
else:
|
||||
column_widths = (l for l in median_lens)
|
||||
# Allow for separator and padding:
|
||||
column_widths = (w - pad_len if w > pad_len else w for w in column_widths)
|
||||
# Rather widen table than break words:
|
||||
column_widths = [max(w, l) for w, l in zip(column_widths, word_lens)]
|
||||
return column_widths
|
||||
@classmethod
|
||||
def _get_column_widths(cls, dataset, max_table_width=MAX_TABLE_WIDTH, pad_len=3):
|
||||
"""
|
||||
Returns a list of column widths proportional to the median length
|
||||
of the text in their cells.
|
||||
"""
|
||||
str_lens, word_lens = cls._get_column_string_lengths(dataset)
|
||||
median_lens = [int(median(lens)) for lens in str_lens]
|
||||
total = sum(median_lens)
|
||||
if total > max_table_width - (pad_len * len(median_lens)):
|
||||
column_widths = (max_table_width * l // total for l in median_lens)
|
||||
else:
|
||||
column_widths = (l for l in median_lens)
|
||||
# Allow for separator and padding:
|
||||
column_widths = (w - pad_len if w > pad_len else w for w in column_widths)
|
||||
# Rather widen table than break words:
|
||||
column_widths = [max(w, l) for w, l in zip(column_widths, word_lens)]
|
||||
return column_widths
|
||||
|
||||
@classmethod
|
||||
def export_set_as_simple_table(cls, dataset, column_widths=None):
|
||||
"""
|
||||
Returns reStructuredText grid table representation of dataset.
|
||||
"""
|
||||
lines = []
|
||||
wrapper = TextWrapper()
|
||||
if column_widths is None:
|
||||
column_widths = cls._get_column_widths(dataset, pad_len=2)
|
||||
border = ' '.join(['=' * w for w in column_widths])
|
||||
|
||||
def export_set_as_simple_table(dataset, column_widths=None):
|
||||
"""
|
||||
Returns reStructuredText grid table representation of dataset.
|
||||
"""
|
||||
lines = []
|
||||
wrapper = TextWrapper()
|
||||
if column_widths is None:
|
||||
column_widths = _get_column_widths(dataset, pad_len=2)
|
||||
border = ' '.join(['=' * w for w in column_widths])
|
||||
|
||||
lines.append(border)
|
||||
if dataset.headers:
|
||||
lines.extend(_row_to_lines(
|
||||
dataset.headers,
|
||||
column_widths,
|
||||
wrapper,
|
||||
sep='',
|
||||
justify=JUSTIFY_CENTER,
|
||||
))
|
||||
lines.append(border)
|
||||
for row in dataset.dict:
|
||||
values = iter(row.values() if hasattr(row, 'values') else row)
|
||||
lines.extend(_row_to_lines(values, column_widths, wrapper, ''))
|
||||
lines.append(border)
|
||||
return '\n'.join(lines)
|
||||
if dataset.headers:
|
||||
lines.extend(cls._row_to_lines(
|
||||
dataset.headers,
|
||||
column_widths,
|
||||
wrapper,
|
||||
sep='',
|
||||
justify=JUSTIFY_CENTER,
|
||||
))
|
||||
lines.append(border)
|
||||
for row in dataset.dict:
|
||||
values = iter(row.values() if hasattr(row, 'values') else row)
|
||||
lines.extend(cls._row_to_lines(values, column_widths, wrapper, ''))
|
||||
lines.append(border)
|
||||
return '\n'.join(lines)
|
||||
|
||||
@classmethod
|
||||
def export_set_as_grid_table(cls, dataset, column_widths=None):
|
||||
"""
|
||||
Returns reStructuredText grid table representation of dataset.
|
||||
|
||||
|
||||
def export_set_as_grid_table(dataset, column_widths=None):
|
||||
"""
|
||||
Returns reStructuredText grid table representation of dataset.
|
||||
>>> from tablib import Dataset
|
||||
>>> from tablib.formats import registry
|
||||
>>> bits = ((0, 0), (1, 0), (0, 1), (1, 1))
|
||||
>>> data = Dataset()
|
||||
>>> data.headers = ['A', 'B', 'A and B']
|
||||
>>> for a, b in bits:
|
||||
... data.append([bool(a), bool(b), bool(a * b)])
|
||||
>>> rst = registry.get_format('rst')
|
||||
>>> print(rst.export_set(data, force_grid=True))
|
||||
+-------+-------+-------+
|
||||
| A | B | A and |
|
||||
| | | B |
|
||||
+=======+=======+=======+
|
||||
| False | False | False |
|
||||
+-------+-------+-------+
|
||||
| True | False | False |
|
||||
+-------+-------+-------+
|
||||
| False | True | False |
|
||||
+-------+-------+-------+
|
||||
| True | True | True |
|
||||
+-------+-------+-------+
|
||||
|
||||
"""
|
||||
lines = []
|
||||
wrapper = TextWrapper()
|
||||
if column_widths is None:
|
||||
column_widths = cls._get_column_widths(dataset)
|
||||
header_sep = '+=' + '=+='.join(['=' * w for w in column_widths]) + '=+'
|
||||
row_sep = '+-' + '-+-'.join(['-' * w for w in column_widths]) + '-+'
|
||||
|
||||
>>> from tablib import Dataset
|
||||
>>> from tablib.formats import rst
|
||||
>>> bits = ((0, 0), (1, 0), (0, 1), (1, 1))
|
||||
>>> data = Dataset()
|
||||
>>> data.headers = ['A', 'B', 'A and B']
|
||||
>>> for a, b in bits:
|
||||
... data.append([bool(a), bool(b), bool(a * b)])
|
||||
>>> print(rst.export_set(data, force_grid=True))
|
||||
+-------+-------+-------+
|
||||
| A | B | A and |
|
||||
| | | B |
|
||||
+=======+=======+=======+
|
||||
| False | False | False |
|
||||
+-------+-------+-------+
|
||||
| True | False | False |
|
||||
+-------+-------+-------+
|
||||
| False | True | False |
|
||||
+-------+-------+-------+
|
||||
| True | True | True |
|
||||
+-------+-------+-------+
|
||||
|
||||
"""
|
||||
lines = []
|
||||
wrapper = TextWrapper()
|
||||
if column_widths is None:
|
||||
column_widths = _get_column_widths(dataset)
|
||||
header_sep = '+=' + '=+='.join(['=' * w for w in column_widths]) + '=+'
|
||||
row_sep = '+-' + '-+-'.join(['-' * w for w in column_widths]) + '-+'
|
||||
|
||||
lines.append(row_sep)
|
||||
if dataset.headers:
|
||||
lines.extend(_row_to_lines(
|
||||
dataset.headers,
|
||||
column_widths,
|
||||
wrapper,
|
||||
justify=JUSTIFY_CENTER,
|
||||
))
|
||||
lines.append(header_sep)
|
||||
for row in dataset.dict:
|
||||
values = iter(row.values() if hasattr(row, 'values') else row)
|
||||
lines.extend(_row_to_lines(values, column_widths, wrapper))
|
||||
lines.append(row_sep)
|
||||
return '\n'.join(lines)
|
||||
|
||||
if dataset.headers:
|
||||
lines.extend(cls._row_to_lines(
|
||||
dataset.headers,
|
||||
column_widths,
|
||||
wrapper,
|
||||
justify=JUSTIFY_CENTER,
|
||||
))
|
||||
lines.append(header_sep)
|
||||
for row in dataset.dict:
|
||||
values = iter(row.values() if hasattr(row, 'values') else row)
|
||||
lines.extend(cls._row_to_lines(values, column_widths, wrapper))
|
||||
lines.append(row_sep)
|
||||
return '\n'.join(lines)
|
||||
|
||||
@classmethod
|
||||
def _use_simple_table(cls, head0, col0, width0):
|
||||
"""
|
||||
Use a simple table if the text in the first column is never wrapped
|
||||
|
||||
|
||||
def _use_simple_table(head0, col0, width0):
|
||||
"""
|
||||
Use a simple table if the text in the first column is never wrapped
|
||||
>>> from tablib.formats import registry
|
||||
>>> rst = registry.get_format('rst')
|
||||
>>> rst._use_simple_table('menu', ['egg', 'bacon'], 10)
|
||||
True
|
||||
>>> rst._use_simple_table(None, ['lobster thermidor', 'spam'], 10)
|
||||
False
|
||||
|
||||
"""
|
||||
if head0 is not None:
|
||||
head0 = to_str(head0)
|
||||
if len(head0) > width0:
|
||||
return False
|
||||
for cell in col0:
|
||||
cell = to_str(cell)
|
||||
if len(cell) > width0:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def export_set(cls, dataset, **kwargs):
|
||||
"""
|
||||
Returns reStructuredText table representation of dataset.
|
||||
|
||||
Returns a simple table if the text in the first column is never
|
||||
wrapped, otherwise returns a grid table.
|
||||
|
||||
|
||||
>>> _use_simple_table('menu', ['egg', 'bacon'], 10)
|
||||
True
|
||||
>>> _use_simple_table(None, ['lobster thermidor', 'spam'], 10)
|
||||
False
|
||||
>>> from tablib import Dataset
|
||||
>>> bits = ((0, 0), (1, 0), (0, 1), (1, 1))
|
||||
>>> data = Dataset()
|
||||
>>> data.headers = ['A', 'B', 'A and B']
|
||||
>>> for a, b in bits:
|
||||
... data.append([bool(a), bool(b), bool(a * b)])
|
||||
>>> table = data.rst
|
||||
>>> table.split('\\n') == [
|
||||
... '===== ===== =====',
|
||||
... ' A B A and',
|
||||
... ' B ',
|
||||
... '===== ===== =====',
|
||||
... 'False False False',
|
||||
... 'True False False',
|
||||
... 'False True False',
|
||||
... 'True True True ',
|
||||
... '===== ===== =====',
|
||||
... ]
|
||||
True
|
||||
|
||||
"""
|
||||
if head0 is not None:
|
||||
head0 = to_unicode(head0)
|
||||
if len(head0) > width0:
|
||||
return False
|
||||
for cell in col0:
|
||||
cell = to_unicode(cell)
|
||||
if len(cell) > width0:
|
||||
return False
|
||||
return True
|
||||
"""
|
||||
if not dataset.dict:
|
||||
return ''
|
||||
force_grid = kwargs.get('force_grid', False)
|
||||
max_table_width = kwargs.get('max_table_width', cls.MAX_TABLE_WIDTH)
|
||||
column_widths = cls._get_column_widths(dataset, max_table_width)
|
||||
|
||||
use_simple_table = cls._use_simple_table(
|
||||
dataset.headers[0] if dataset.headers else None,
|
||||
dataset.get_col(0),
|
||||
column_widths[0],
|
||||
)
|
||||
if use_simple_table and not force_grid:
|
||||
return cls.export_set_as_simple_table(dataset, column_widths)
|
||||
else:
|
||||
return cls.export_set_as_grid_table(dataset, column_widths)
|
||||
|
||||
def export_set(dataset, **kwargs):
|
||||
"""
|
||||
Returns reStructuredText table representation of dataset.
|
||||
@classmethod
|
||||
def export_book(cls, databook):
|
||||
"""
|
||||
reStructuredText representation of a Databook.
|
||||
|
||||
Returns a simple table if the text in the first column is never
|
||||
wrapped, otherwise returns a grid table.
|
||||
|
||||
|
||||
>>> from tablib import Dataset
|
||||
>>> bits = ((0, 0), (1, 0), (0, 1), (1, 1))
|
||||
>>> data = Dataset()
|
||||
>>> data.headers = ['A', 'B', 'A and B']
|
||||
>>> for a, b in bits:
|
||||
... data.append([bool(a), bool(b), bool(a * b)])
|
||||
>>> table = data.rst
|
||||
>>> table.split('\\n') == [
|
||||
... '===== ===== =====',
|
||||
... ' A B A and',
|
||||
... ' B ',
|
||||
... '===== ===== =====',
|
||||
... 'False False False',
|
||||
... 'True False False',
|
||||
... 'False True False',
|
||||
... 'True True True ',
|
||||
... '===== ===== =====',
|
||||
... ]
|
||||
True
|
||||
|
||||
"""
|
||||
if not dataset.dict:
|
||||
return ''
|
||||
force_grid = kwargs.get('force_grid', False)
|
||||
max_table_width = kwargs.get('max_table_width', MAX_TABLE_WIDTH)
|
||||
column_widths = _get_column_widths(dataset, max_table_width)
|
||||
|
||||
use_simple_table = _use_simple_table(
|
||||
dataset.headers[0] if dataset.headers else None,
|
||||
dataset.get_col(0),
|
||||
column_widths[0],
|
||||
)
|
||||
if use_simple_table and not force_grid:
|
||||
return export_set_as_simple_table(dataset, column_widths)
|
||||
else:
|
||||
return export_set_as_grid_table(dataset, column_widths)
|
||||
|
||||
|
||||
def export_book(databook):
|
||||
"""
|
||||
reStructuredText representation of a Databook.
|
||||
|
||||
Tables are separated by a blank line. All tables use the grid
|
||||
format.
|
||||
"""
|
||||
return '\n\n'.join(export_set(dataset, force_grid=True)
|
||||
for dataset in databook._datasets)
|
||||
Tables are separated by a blank line. All tables use the grid
|
||||
format.
|
||||
"""
|
||||
return '\n\n'.join(cls.export_set(dataset, force_grid=True)
|
||||
for dataset in databook._datasets)
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - TSV (Tab Separated Values) Support.
|
||||
"""
|
||||
|
||||
from tablib.compat import unicode
|
||||
from tablib.formats._csv import (
|
||||
export_set as export_set_wrapper,
|
||||
import_set as import_set_wrapper,
|
||||
detect as detect_wrapper,
|
||||
)
|
||||
|
||||
title = 'tsv'
|
||||
extensions = ('tsv',)
|
||||
|
||||
DELIMITER = unicode('\t')
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns TSV representation of Dataset."""
|
||||
return export_set_wrapper(dataset, delimiter=DELIMITER)
|
||||
from ._csv import CSVFormat
|
||||
|
||||
|
||||
def import_set(dset, in_stream, headers=True):
|
||||
"""Returns dataset from TSV stream."""
|
||||
return import_set_wrapper(dset, in_stream, headers=headers, delimiter=DELIMITER)
|
||||
class TSVFormat(CSVFormat):
|
||||
title = 'tsv'
|
||||
extensions = ('tsv',)
|
||||
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is valid TSV."""
|
||||
return detect_wrapper(stream, delimiter=DELIMITER)
|
||||
DEFAULT_DELIMITER = '\t'
|
||||
|
||||
+98
-97
@@ -1,137 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - XLS Support.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
from tablib.compat import xrange
|
||||
import tablib
|
||||
import xlrd
|
||||
import xlwt
|
||||
from xlrd.biffh import XLRDError
|
||||
|
||||
title = 'xls'
|
||||
extensions = ('xls',)
|
||||
|
||||
# special styles
|
||||
wrap = xlwt.easyxf("alignment: wrap on")
|
||||
bold = xlwt.easyxf("font: bold on")
|
||||
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is a readable excel file."""
|
||||
try:
|
||||
xlrd.open_workbook(file_contents=stream)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
xlrd.open_workbook(file_contents=stream.read())
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
xlrd.open_workbook(filename=stream)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
class XLSFormat:
|
||||
title = 'xls'
|
||||
extensions = ('xls',)
|
||||
|
||||
@classmethod
|
||||
def detect(cls, stream):
|
||||
"""Returns True if given stream is a readable excel file."""
|
||||
try:
|
||||
xlrd.open_workbook(file_contents=stream)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
xlrd.open_workbook(file_contents=stream.read())
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
xlrd.open_workbook(filename=stream)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns XLS representation of Dataset."""
|
||||
@classmethod
|
||||
def export_set(cls, dataset):
|
||||
"""Returns XLS representation of Dataset."""
|
||||
|
||||
wb = xlwt.Workbook(encoding='utf8')
|
||||
ws = wb.add_sheet(dataset.title if dataset.title else 'Tablib Dataset')
|
||||
wb = xlwt.Workbook(encoding='utf8')
|
||||
ws = wb.add_sheet(dataset.title if dataset.title else 'Tablib Dataset')
|
||||
|
||||
dset_sheet(dataset, ws)
|
||||
cls.dset_sheet(dataset, ws)
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
@classmethod
|
||||
def export_book(cls, databook):
|
||||
"""Returns XLS representation of DataBook."""
|
||||
|
||||
def export_book(databook):
|
||||
"""Returns XLS representation of DataBook."""
|
||||
wb = xlwt.Workbook(encoding='utf8')
|
||||
|
||||
wb = xlwt.Workbook(encoding='utf8')
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
ws = wb.add_sheet(dset.title if dset.title else 'Sheet%s' % (i))
|
||||
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
ws = wb.add_sheet(dset.title if dset.title else 'Sheet%s' % (i))
|
||||
cls.dset_sheet(dset, ws)
|
||||
|
||||
dset_sheet(dset, ws)
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
@classmethod
|
||||
def import_set(cls, dset, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dset.wipe()
|
||||
|
||||
def import_set(dset, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
xls_book = xlrd.open_workbook(file_contents=in_stream.read())
|
||||
sheet = xls_book.sheet_by_index(0)
|
||||
|
||||
dset.wipe()
|
||||
dset.title = sheet.name
|
||||
|
||||
xls_book = xlrd.open_workbook(file_contents=in_stream)
|
||||
sheet = xls_book.sheet_by_index(0)
|
||||
|
||||
dset.title = sheet.name
|
||||
|
||||
for i in xrange(sheet.nrows):
|
||||
if (i == 0) and (headers):
|
||||
dset.headers = sheet.row_values(0)
|
||||
else:
|
||||
dset.append(sheet.row_values(i))
|
||||
|
||||
def import_book(dbook, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dbook.wipe()
|
||||
|
||||
xls_book = xlrd.open_workbook(file_contents=in_stream)
|
||||
|
||||
for sheet in xls_book.sheets():
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet.name
|
||||
|
||||
for i in xrange(sheet.nrows):
|
||||
if (i == 0) and (headers):
|
||||
data.headers = sheet.row_values(0)
|
||||
for i in range(sheet.nrows):
|
||||
if i == 0 and headers:
|
||||
dset.headers = sheet.row_values(0)
|
||||
else:
|
||||
data.append(sheet.row_values(i))
|
||||
dset.append([
|
||||
val if typ != xlrd.XL_CELL_ERROR else xlrd.error_text_from_code[val]
|
||||
for val, typ in zip(sheet.row_values(i), sheet.row_types(i))
|
||||
])
|
||||
|
||||
dbook.add_sheet(data)
|
||||
@classmethod
|
||||
def import_book(cls, dbook, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dbook.wipe()
|
||||
|
||||
def dset_sheet(dataset, ws):
|
||||
"""Completes given worksheet from given Dataset."""
|
||||
_package = dataset._package(dicts=False)
|
||||
xls_book = xlrd.open_workbook(file_contents=in_stream)
|
||||
|
||||
for i, sep in enumerate(dataset._separators):
|
||||
_offset = i
|
||||
_package.insert((sep[0] + _offset), (sep[1],))
|
||||
for sheet in xls_book.sheets():
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet.name
|
||||
|
||||
for i, row in enumerate(_package):
|
||||
for j, col in enumerate(row):
|
||||
for i in range(sheet.nrows):
|
||||
if i == 0 and headers:
|
||||
data.headers = sheet.row_values(0)
|
||||
else:
|
||||
data.append(sheet.row_values(i))
|
||||
|
||||
# bold headers
|
||||
if (i == 0) and dataset.headers:
|
||||
ws.write(i, j, col, bold)
|
||||
dbook.add_sheet(data)
|
||||
|
||||
# frozen header row
|
||||
ws.panes_frozen = True
|
||||
ws.horz_split_pos = 1
|
||||
@classmethod
|
||||
def dset_sheet(cls, dataset, ws):
|
||||
"""Completes given worksheet from given Dataset."""
|
||||
_package = dataset._package(dicts=False)
|
||||
|
||||
# bold separators
|
||||
elif len(row) < dataset.width:
|
||||
ws.write(i, j, col, bold)
|
||||
for i, sep in enumerate(dataset._separators):
|
||||
_offset = i
|
||||
_package.insert((sep[0] + _offset), (sep[1],))
|
||||
|
||||
# wrap the rest
|
||||
else:
|
||||
try:
|
||||
if '\n' in col:
|
||||
ws.write(i, j, col, wrap)
|
||||
else:
|
||||
for i, row in enumerate(_package):
|
||||
for j, col in enumerate(row):
|
||||
|
||||
# bold headers
|
||||
if (i == 0) and dataset.headers:
|
||||
ws.write(i, j, col, bold)
|
||||
|
||||
# frozen header row
|
||||
ws.panes_frozen = True
|
||||
ws.horz_split_pos = 1
|
||||
|
||||
# bold separators
|
||||
elif len(row) < dataset.width:
|
||||
ws.write(i, j, col, bold)
|
||||
|
||||
# wrap the rest
|
||||
else:
|
||||
try:
|
||||
if '\n' in col:
|
||||
ws.write(i, j, col, wrap)
|
||||
else:
|
||||
ws.write(i, j, col)
|
||||
except TypeError:
|
||||
ws.write(i, j, col)
|
||||
except TypeError:
|
||||
ws.write(i, j, col)
|
||||
|
||||
+97
-104
@@ -1,9 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - XLSX Support.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
import openpyxl
|
||||
@@ -13,132 +10,128 @@ Workbook = openpyxl.workbook.Workbook
|
||||
ExcelWriter = openpyxl.writer.excel.ExcelWriter
|
||||
get_column_letter = openpyxl.utils.get_column_letter
|
||||
|
||||
from tablib.compat import unicode
|
||||
|
||||
class XLSXFormat:
|
||||
title = 'xlsx'
|
||||
extensions = ('xlsx',)
|
||||
|
||||
title = 'xlsx'
|
||||
extensions = ('xlsx',)
|
||||
@classmethod
|
||||
def detect(cls, stream):
|
||||
"""Returns True if given stream is a readable excel file."""
|
||||
try:
|
||||
openpyxl.reader.excel.load_workbook(stream, read_only=True)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def export_set(cls, dataset, freeze_panes=True):
|
||||
"""Returns XLSX representation of Dataset."""
|
||||
wb = Workbook()
|
||||
ws = wb.worksheets[0]
|
||||
ws.title = dataset.title if dataset.title else 'Tablib Dataset'
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is a readable excel file."""
|
||||
if isinstance(stream, bytes):
|
||||
# load_workbook expects a file-like object.
|
||||
stream = BytesIO(stream)
|
||||
try:
|
||||
openpyxl.reader.excel.load_workbook(stream, read_only=True)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
cls.dset_sheet(dataset, ws, freeze_panes=freeze_panes)
|
||||
|
||||
def export_set(dataset, freeze_panes=True):
|
||||
"""Returns XLSX representation of Dataset."""
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.worksheets[0]
|
||||
ws.title = dataset.title if dataset.title else 'Tablib Dataset'
|
||||
@classmethod
|
||||
def export_book(cls, databook, freeze_panes=True):
|
||||
"""Returns XLSX representation of DataBook."""
|
||||
|
||||
dset_sheet(dataset, ws, freeze_panes=freeze_panes)
|
||||
wb = Workbook()
|
||||
for sheet in wb.worksheets:
|
||||
wb.remove(sheet)
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
ws = wb.create_sheet()
|
||||
ws.title = dset.title if dset.title else 'Sheet%s' % (i)
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
cls.dset_sheet(dset, ws, freeze_panes=freeze_panes)
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
def export_book(databook, freeze_panes=True):
|
||||
"""Returns XLSX representation of DataBook."""
|
||||
@classmethod
|
||||
def import_set(cls, dset, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
wb = Workbook()
|
||||
for sheet in wb.worksheets:
|
||||
wb.remove(sheet)
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
ws = wb.create_sheet()
|
||||
ws.title = dset.title if dset.title else 'Sheet%s' % (i)
|
||||
dset.wipe()
|
||||
|
||||
dset_sheet(dset, ws, freeze_panes=freeze_panes)
|
||||
xls_book = openpyxl.reader.excel.load_workbook(in_stream, read_only=True)
|
||||
sheet = xls_book.active
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
def import_set(dset, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dset.wipe()
|
||||
|
||||
xls_book = openpyxl.reader.excel.load_workbook(BytesIO(in_stream), read_only=True)
|
||||
sheet = xls_book.active
|
||||
|
||||
dset.title = sheet.title
|
||||
|
||||
for i, row in enumerate(sheet.rows):
|
||||
row_vals = [c.value for c in row]
|
||||
if (i == 0) and (headers):
|
||||
dset.headers = row_vals
|
||||
else:
|
||||
dset.append(row_vals)
|
||||
|
||||
|
||||
def import_book(dbook, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dbook.wipe()
|
||||
|
||||
xls_book = openpyxl.reader.excel.load_workbook(BytesIO(in_stream), read_only=True)
|
||||
|
||||
for sheet in xls_book.worksheets:
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet.title
|
||||
dset.title = sheet.title
|
||||
|
||||
for i, row in enumerate(sheet.rows):
|
||||
row_vals = [c.value for c in row]
|
||||
if (i == 0) and (headers):
|
||||
data.headers = row_vals
|
||||
dset.headers = row_vals
|
||||
else:
|
||||
data.append(row_vals)
|
||||
dset.append(row_vals)
|
||||
|
||||
dbook.add_sheet(data)
|
||||
@classmethod
|
||||
def import_book(cls, dbook, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dbook.wipe()
|
||||
|
||||
def dset_sheet(dataset, ws, freeze_panes=True):
|
||||
"""Completes given worksheet from given Dataset."""
|
||||
_package = dataset._package(dicts=False)
|
||||
xls_book = openpyxl.reader.excel.load_workbook(in_stream, read_only=True)
|
||||
|
||||
for i, sep in enumerate(dataset._separators):
|
||||
_offset = i
|
||||
_package.insert((sep[0] + _offset), (sep[1],))
|
||||
for sheet in xls_book.worksheets:
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet.title
|
||||
|
||||
bold = openpyxl.styles.Font(bold=True)
|
||||
wrap_text = openpyxl.styles.Alignment(wrap_text=True)
|
||||
for i, row in enumerate(sheet.rows):
|
||||
row_vals = [c.value for c in row]
|
||||
if (i == 0) and (headers):
|
||||
data.headers = row_vals
|
||||
else:
|
||||
data.append(row_vals)
|
||||
|
||||
for i, row in enumerate(_package):
|
||||
row_number = i + 1
|
||||
for j, col in enumerate(row):
|
||||
col_idx = get_column_letter(j + 1)
|
||||
cell = ws['%s%s' % (col_idx, row_number)]
|
||||
dbook.add_sheet(data)
|
||||
|
||||
# bold headers
|
||||
if (row_number == 1) and dataset.headers:
|
||||
cell.font = bold
|
||||
if freeze_panes:
|
||||
# Export Freeze only after first Line
|
||||
ws.freeze_panes = 'A2'
|
||||
@classmethod
|
||||
def dset_sheet(cls, dataset, ws, freeze_panes=True):
|
||||
"""Completes given worksheet from given Dataset."""
|
||||
_package = dataset._package(dicts=False)
|
||||
|
||||
# bold separators
|
||||
elif len(row) < dataset.width:
|
||||
cell.font = bold
|
||||
for i, sep in enumerate(dataset._separators):
|
||||
_offset = i
|
||||
_package.insert((sep[0] + _offset), (sep[1],))
|
||||
|
||||
bold = openpyxl.styles.Font(bold=True)
|
||||
wrap_text = openpyxl.styles.Alignment(wrap_text=True)
|
||||
|
||||
for i, row in enumerate(_package):
|
||||
row_number = i + 1
|
||||
for j, col in enumerate(row):
|
||||
col_idx = get_column_letter(j + 1)
|
||||
cell = ws['{}{}'.format(col_idx, row_number)]
|
||||
|
||||
# bold headers
|
||||
if (row_number == 1) and dataset.headers:
|
||||
cell.font = bold
|
||||
if freeze_panes:
|
||||
# Export Freeze only after first Line
|
||||
ws.freeze_panes = 'A2'
|
||||
|
||||
# bold separators
|
||||
elif len(row) < dataset.width:
|
||||
cell.font = bold
|
||||
|
||||
# wrap the rest
|
||||
else:
|
||||
try:
|
||||
str_col_value = str(col)
|
||||
except TypeError:
|
||||
str_col_value = ''
|
||||
if '\n' in str_col_value:
|
||||
cell.alignment = wrap_text
|
||||
|
||||
# wrap the rest
|
||||
else:
|
||||
try:
|
||||
str_col_value = unicode(col)
|
||||
except TypeError:
|
||||
str_col_value = ''
|
||||
if '\n' in str_col_value:
|
||||
cell.alignment = wrap_text
|
||||
|
||||
try:
|
||||
cell.value = col
|
||||
except (ValueError, TypeError):
|
||||
cell.value = unicode(col)
|
||||
cell.value = col
|
||||
except (ValueError, TypeError):
|
||||
cell.value = str(col)
|
||||
|
||||
+36
-36
@@ -1,53 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - YAML Support.
|
||||
"""
|
||||
|
||||
import tablib
|
||||
import yaml
|
||||
|
||||
title = 'yaml'
|
||||
extensions = ('yaml', 'yml')
|
||||
|
||||
class YAMLFormat:
|
||||
title = 'yaml'
|
||||
extensions = ('yaml', 'yml')
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns YAML representation of Dataset."""
|
||||
@classmethod
|
||||
def export_set(cls, dataset):
|
||||
"""Returns YAML representation of Dataset."""
|
||||
|
||||
return yaml.safe_dump(dataset._package(ordered=False))
|
||||
return yaml.safe_dump(dataset._package(ordered=False), default_flow_style=None)
|
||||
|
||||
@classmethod
|
||||
def export_book(cls, databook):
|
||||
"""Returns YAML representation of Databook."""
|
||||
return yaml.safe_dump(databook._package(ordered=False), default_flow_style=None)
|
||||
|
||||
def export_book(databook):
|
||||
"""Returns YAML representation of Databook."""
|
||||
return yaml.safe_dump(databook._package(ordered=False))
|
||||
@classmethod
|
||||
def import_set(cls, dset, in_stream):
|
||||
"""Returns dataset from YAML stream."""
|
||||
|
||||
dset.wipe()
|
||||
dset.dict = yaml.safe_load(in_stream)
|
||||
|
||||
def import_set(dset, in_stream):
|
||||
"""Returns dataset from YAML stream."""
|
||||
@classmethod
|
||||
def import_book(cls, dbook, in_stream):
|
||||
"""Returns databook from YAML stream."""
|
||||
|
||||
dset.wipe()
|
||||
dset.dict = yaml.safe_load(in_stream)
|
||||
dbook.wipe()
|
||||
|
||||
for sheet in yaml.safe_load(in_stream):
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet['title']
|
||||
data.dict = sheet['data']
|
||||
dbook.add_sheet(data)
|
||||
|
||||
def import_book(dbook, in_stream):
|
||||
"""Returns databook from YAML stream."""
|
||||
|
||||
dbook.wipe()
|
||||
|
||||
for sheet in yaml.safe_load(in_stream):
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet['title']
|
||||
data.dict = sheet['data']
|
||||
dbook.add_sheet(data)
|
||||
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is valid YAML."""
|
||||
try:
|
||||
_yaml = yaml.safe_load(stream)
|
||||
if isinstance(_yaml, (list, tuple, dict)):
|
||||
return True
|
||||
else:
|
||||
@classmethod
|
||||
def detect(cls, stream):
|
||||
"""Returns True if given stream is valid YAML."""
|
||||
try:
|
||||
_yaml = yaml.safe_load(stream)
|
||||
if isinstance(_yaml, (list, tuple, dict)):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except (yaml.parser.ParserError, yaml.reader.ReaderError,
|
||||
yaml.scanner.ScannerError):
|
||||
return False
|
||||
except (yaml.parser.ParserError, yaml.reader.ReaderError,
|
||||
yaml.scanner.ScannerError):
|
||||
return False
|
||||
|
||||
@@ -62,12 +62,11 @@ __author__ = "Jeff Kunce <kuncej@mail.conservation.state.mo.us>"
|
||||
|
||||
__all__ = ["Dbf"]
|
||||
|
||||
from . import header
|
||||
from . import record
|
||||
from utils import INVALID_VALUE
|
||||
from . import header, record
|
||||
from .utils import INVALID_VALUE
|
||||
|
||||
|
||||
class Dbf(object):
|
||||
class Dbf:
|
||||
"""DBF accessor.
|
||||
|
||||
FIXME:
|
||||
@@ -114,16 +113,16 @@ class Dbf(object):
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
"""
|
||||
if isinstance(f, basestring):
|
||||
if isinstance(f, str):
|
||||
# a filename
|
||||
self.name = f
|
||||
if new:
|
||||
# new table (table file must be
|
||||
# created or opened and truncated)
|
||||
self.stream = file(f, "w+b")
|
||||
self.stream = open(f, "w+b")
|
||||
else:
|
||||
# tabe file must exist
|
||||
self.stream = file(f, ("r+b", "rb")[bool(readOnly)])
|
||||
# table file must exist
|
||||
self.stream = open(f, ("r+b", "rb")[bool(readOnly)])
|
||||
else:
|
||||
# a stream
|
||||
self.name = getattr(f, "name", "")
|
||||
@@ -177,7 +176,7 @@ class Dbf(object):
|
||||
Return value is numeric object maning valid index.
|
||||
|
||||
"""
|
||||
if not isinstance(index, (int, long)):
|
||||
if not isinstance(index, int):
|
||||
raise TypeError("Index must be a numeric object")
|
||||
if index < 0:
|
||||
# index from the right side
|
||||
@@ -187,7 +186,7 @@ class Dbf(object):
|
||||
raise IndexError("Record index out of range")
|
||||
return index
|
||||
|
||||
# iterface methods
|
||||
# interface methods
|
||||
|
||||
def close(self):
|
||||
self.flush()
|
||||
@@ -204,7 +203,8 @@ class Dbf(object):
|
||||
def indexOfFieldName(self, name):
|
||||
"""Index of field named ``name``."""
|
||||
# FIXME: move this to header class
|
||||
return self.header.fields.index(name)
|
||||
names = [f.name for f in self.header.fields]
|
||||
return names.index(name.upper())
|
||||
|
||||
def newRecord(self):
|
||||
"""Return new record, which belong to this table."""
|
||||
@@ -294,4 +294,4 @@ if __name__ == '__main__':
|
||||
demo_create(_name)
|
||||
demo_read(_name)
|
||||
|
||||
# vim: set et sw=4 sts=4 :
|
||||
# vim: set et sw=4 sts=4 :
|
||||
|
||||
@@ -6,7 +6,7 @@ Note: this is a legacy interface. New code should use Dbf class
|
||||
|
||||
TODO:
|
||||
- handle Memo fields.
|
||||
- check length of the fields accoring to the
|
||||
- check length of the fields according to the
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
|
||||
"""
|
||||
@@ -24,20 +24,20 @@ __date__ = "$Date: 2006/07/04 08:18:18 $"[7:-2]
|
||||
|
||||
__all__ = ["dbf_new"]
|
||||
|
||||
from dbf import *
|
||||
from fields import *
|
||||
from header import *
|
||||
from record import *
|
||||
from .dbf import *
|
||||
from .fields import *
|
||||
from .header import *
|
||||
from .record import *
|
||||
|
||||
|
||||
class _FieldDefinition(object):
|
||||
class _FieldDefinition:
|
||||
"""Field definition.
|
||||
|
||||
This is a simple structure, which contains ``name``, ``type``,
|
||||
``len``, ``dec`` and ``cls`` fields.
|
||||
|
||||
Objects also implement get/setitem magic functions, so fields
|
||||
could be accessed via sequence iterface, where 'name' has
|
||||
could be accessed via sequence interface, where 'name' has
|
||||
index 0, 'type' index 1, 'len' index 2, 'dec' index 3 and
|
||||
'cls' could be located at index 4.
|
||||
|
||||
@@ -87,7 +87,7 @@ class _FieldDefinition(object):
|
||||
dbfh.addField(_dbff)
|
||||
|
||||
|
||||
class dbf_new(object):
|
||||
class dbf_new:
|
||||
"""New .DBF creation helper.
|
||||
|
||||
Example Usage:
|
||||
@@ -140,17 +140,11 @@ class dbf_new(object):
|
||||
_dbfh.setCurrentDate()
|
||||
for _fldDef in self.fields:
|
||||
_fldDef.appendToHeader(_dbfh)
|
||||
_dbfStream = file(filename, "wb")
|
||||
|
||||
_dbfStream = open(filename, "wb")
|
||||
_dbfh.write(_dbfStream)
|
||||
_dbfStream.close()
|
||||
|
||||
def write_stream(self, stream):
|
||||
_dbfh = DbfHeader()
|
||||
_dbfh.setCurrentDate()
|
||||
for _fldDef in self.fields:
|
||||
_fldDef.appendToHeader(_dbfh)
|
||||
_dbfh.write(stream)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# create a new DBF-File
|
||||
@@ -182,8 +176,8 @@ if __name__ == '__main__':
|
||||
for i1 in range(len(dbft)):
|
||||
rec = dbft[i1]
|
||||
for fldName in dbft.fieldNames:
|
||||
print('%s:\t %s' % (fldName, rec[fldName]))
|
||||
print('{}:\t {}'.format(fldName, rec[fldName]))
|
||||
print()
|
||||
dbft.close()
|
||||
|
||||
# vim: set et sts=4 sw=4 :
|
||||
# vim: set et sts=4 sw=4 :
|
||||
|
||||
@@ -28,36 +28,38 @@ TODO:
|
||||
__version__ = "$Revision: 1.14 $"[11:-2]
|
||||
__date__ = "$Date: 2009/05/26 05:16:51 $"[7:-2]
|
||||
|
||||
__all__ = ["lookupFor",] # field classes added at the end of the module
|
||||
__all__ = ["lookupFor"] # field classes added at the end of the module
|
||||
|
||||
import datetime
|
||||
import struct
|
||||
import sys
|
||||
from functools import total_ordering
|
||||
|
||||
from . import utils
|
||||
|
||||
## abstract definitions
|
||||
# abstract definitions
|
||||
|
||||
class DbfFieldDef(object):
|
||||
|
||||
@total_ordering
|
||||
class DbfFieldDef:
|
||||
"""Abstract field definition.
|
||||
|
||||
Child classes must override ``type`` class attribute to provide datatype
|
||||
infromation of the field definition. For more info about types visit
|
||||
information of the field definition. For more info about types visit
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
|
||||
Also child classes must override ``defaultValue`` field to provide
|
||||
default value for the field value.
|
||||
|
||||
If child class has fixed length ``length`` class attribute must be
|
||||
overriden and set to the valid value. None value means, that field
|
||||
overridden and set to the valid value. None value means, that field
|
||||
isn't of fixed length.
|
||||
|
||||
Note: ``name`` field must not be changed after instantiation.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "length", "decimalCount",
|
||||
"start", "end", "ignoreErrors")
|
||||
__slots__ = ("name", "decimalCount", "start", "end", "ignoreErrors")
|
||||
|
||||
# length of the field, None in case of variable-length field,
|
||||
# or a number if this field is a fixed-length field
|
||||
@@ -65,21 +67,20 @@ class DbfFieldDef(object):
|
||||
|
||||
# field type. for more information about fields types visit
|
||||
# `http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
# must be overriden in child classes
|
||||
# must be overridden in child classes
|
||||
typeCode = None
|
||||
|
||||
# default value for the field. this field must be
|
||||
# overriden in child classes
|
||||
# overridden in child classes
|
||||
defaultValue = None
|
||||
|
||||
def __init__(self, name, length=None, decimalCount=None,
|
||||
start=None, stop=None, ignoreErrors=False,
|
||||
):
|
||||
start=None, stop=None, ignoreErrors=False):
|
||||
"""Initialize instance."""
|
||||
assert self.typeCode is not None, "Type code must be overriden"
|
||||
assert self.defaultValue is not None, "Default value must be overriden"
|
||||
## fix arguments
|
||||
if len(name) >10:
|
||||
assert self.typeCode is not None, "Type code must be overridden"
|
||||
assert self.defaultValue is not None, "Default value must be overridden"
|
||||
# fix arguments
|
||||
if len(name) > 10:
|
||||
raise ValueError("Field name \"%s\" is too long" % name)
|
||||
name = str(name).upper()
|
||||
if self.__class__.length is None:
|
||||
@@ -87,13 +88,12 @@ class DbfFieldDef(object):
|
||||
raise ValueError("[%s] Length isn't specified" % name)
|
||||
length = int(length)
|
||||
if length <= 0:
|
||||
raise ValueError("[%s] Length must be a positive integer"
|
||||
% name)
|
||||
raise ValueError("[%s] Length must be a positive integer" % name)
|
||||
else:
|
||||
length = self.length
|
||||
if decimalCount is None:
|
||||
decimalCount = 0
|
||||
## set fields
|
||||
# set fields
|
||||
self.name = name
|
||||
# FIXME: validate length according to the specification at
|
||||
# http://www.clicketyclick.dk/databases/xbase/format/data_types.html
|
||||
@@ -103,8 +103,14 @@ class DbfFieldDef(object):
|
||||
self.start = start
|
||||
self.end = stop
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.name, str(other).upper())
|
||||
def __eq__(self, other):
|
||||
return repr(self) == repr(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return repr(self) != repr(other)
|
||||
|
||||
def __lt__(self, other):
|
||||
return repr(self) < repr(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
@@ -123,9 +129,9 @@ class DbfFieldDef(object):
|
||||
|
||||
"""
|
||||
assert len(string) == 32
|
||||
_length = ord(string[16])
|
||||
return cls(utils.unzfill(string)[:11], _length, ord(string[17]),
|
||||
start, start + _length, ignoreErrors=ignoreErrors)
|
||||
_length = string[16]
|
||||
return cls(utils.unzfill(string)[:11].decode('utf-8'), _length,
|
||||
string[17], start, start + _length, ignoreErrors=ignoreErrors)
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
def toString(self):
|
||||
@@ -136,15 +142,11 @@ class DbfFieldDef(object):
|
||||
definition of this field.
|
||||
|
||||
"""
|
||||
if sys.version_info < (2, 4):
|
||||
# earlier versions did not support padding character
|
||||
_name = self.name[:11] + "\0" * (11 - len(self.name))
|
||||
else:
|
||||
_name = self.name.ljust(11, '\0')
|
||||
_name = self.name.ljust(11, '\0')
|
||||
return (
|
||||
_name +
|
||||
self.typeCode +
|
||||
#data address
|
||||
# data address
|
||||
chr(0) * 4 +
|
||||
chr(self.length) +
|
||||
chr(self.decimalCount) +
|
||||
@@ -171,7 +173,7 @@ class DbfFieldDef(object):
|
||||
"""Return decoded field value from the record string."""
|
||||
try:
|
||||
return self.decodeValue(self.rawFromRecord(record))
|
||||
except:
|
||||
except Exception:
|
||||
if self.ignoreErrors:
|
||||
return utils.INVALID_VALUE
|
||||
else:
|
||||
@@ -190,17 +192,18 @@ class DbfFieldDef(object):
|
||||
def encodeValue(self, value):
|
||||
"""Return str object containing encoded field value.
|
||||
|
||||
This is an abstract method and it must be overriden in child classes.
|
||||
This is an abstract method and it must be overridden in child classes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
## real classes
|
||||
# real classes
|
||||
|
||||
|
||||
class DbfCharacterFieldDef(DbfFieldDef):
|
||||
"""Definition of the character field."""
|
||||
|
||||
typeCode = "C"
|
||||
defaultValue = ""
|
||||
defaultValue = b''
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return string object.
|
||||
@@ -208,7 +211,7 @@ class DbfCharacterFieldDef(DbfFieldDef):
|
||||
Return value is a ``value`` argument with stripped right spaces.
|
||||
|
||||
"""
|
||||
return value.rstrip(" ")
|
||||
return value.rstrip(b' ').decode('utf-8')
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return raw data string encoded from a ``value``."""
|
||||
@@ -235,8 +238,8 @@ class DbfNumericFieldDef(DbfFieldDef):
|
||||
Return value is a int (long) or float instance.
|
||||
|
||||
"""
|
||||
value = value.strip(" \0")
|
||||
if "." in value:
|
||||
value = value.strip(b' \0')
|
||||
if b'.' in value:
|
||||
# a float (has decimal separator)
|
||||
return float(value)
|
||||
elif value:
|
||||
@@ -257,11 +260,13 @@ class DbfNumericFieldDef(DbfFieldDef):
|
||||
% (self.name, _rv, self.length))
|
||||
return _rv
|
||||
|
||||
|
||||
class DbfFloatFieldDef(DbfNumericFieldDef):
|
||||
"""Definition of the float field - same as numeric."""
|
||||
|
||||
typeCode = "F"
|
||||
|
||||
|
||||
class DbfIntegerFieldDef(DbfFieldDef):
|
||||
"""Definition of the integer field."""
|
||||
|
||||
@@ -277,6 +282,7 @@ class DbfIntegerFieldDef(DbfFieldDef):
|
||||
"""Return string containing encoded ``value``."""
|
||||
return struct.pack("<i", int(value))
|
||||
|
||||
|
||||
class DbfCurrencyFieldDef(DbfFieldDef):
|
||||
"""Definition of the currency field."""
|
||||
|
||||
@@ -292,6 +298,7 @@ class DbfCurrencyFieldDef(DbfFieldDef):
|
||||
"""Return string containing encoded ``value``."""
|
||||
return struct.pack("<q", round(value * 10000))
|
||||
|
||||
|
||||
class DbfLogicalFieldDef(DbfFieldDef):
|
||||
"""Definition of the logical field."""
|
||||
|
||||
@@ -308,7 +315,7 @@ class DbfLogicalFieldDef(DbfFieldDef):
|
||||
return False
|
||||
if value in "YyTt":
|
||||
return True
|
||||
raise ValueError("[%s] Invalid logical value %r" % (self.name, value))
|
||||
raise ValueError("[{}] Invalid logical value {!r}".format(self.name, value))
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a character from the "TF?" set.
|
||||
@@ -328,7 +335,7 @@ class DbfLogicalFieldDef(DbfFieldDef):
|
||||
class DbfMemoFieldDef(DbfFieldDef):
|
||||
"""Definition of the memo field.
|
||||
|
||||
Note: memos aren't currenly completely supported.
|
||||
Note: memos aren't currently completely supported.
|
||||
|
||||
"""
|
||||
|
||||
@@ -338,7 +345,7 @@ class DbfMemoFieldDef(DbfFieldDef):
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return int .dbt block number decoded from the string object."""
|
||||
#return int(value)
|
||||
# return int(value)
|
||||
raise NotImplementedError
|
||||
|
||||
def encodeValue(self, value):
|
||||
@@ -347,7 +354,7 @@ class DbfMemoFieldDef(DbfFieldDef):
|
||||
Note: this is an internal method.
|
||||
|
||||
"""
|
||||
#return str(value)[:self.length].ljust(self.length)
|
||||
# return str(value)[:self.length].ljust(self.length)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -423,6 +430,7 @@ class DbfDateTimeFieldDef(DbfFieldDef):
|
||||
|
||||
_fieldsRegistry = {}
|
||||
|
||||
|
||||
def registerField(fieldCls):
|
||||
"""Register field definition class.
|
||||
|
||||
@@ -452,13 +460,14 @@ def lookupFor(typeCode):
|
||||
"""
|
||||
# XXX: use typeCode.upper()? in case of any decign don't
|
||||
# forget to look to the same comment in ``registerField``
|
||||
return _fieldsRegistry[typeCode]
|
||||
return _fieldsRegistry[chr(typeCode)]
|
||||
|
||||
## register generic types
|
||||
# register generic types
|
||||
|
||||
for (_name, _val) in globals().items():
|
||||
|
||||
for (_name, _val) in list(globals().items()):
|
||||
if isinstance(_val, type) and issubclass(_val, DbfFieldDef) \
|
||||
and (_name != "DbfFieldDef"):
|
||||
and (_name != "DbfFieldDef"):
|
||||
__all__.append(_name)
|
||||
registerField(_val)
|
||||
del _name, _val
|
||||
|
||||
@@ -19,20 +19,16 @@ __date__ = "$Date: 2010/09/16 05:06:39 $"[7:-2]
|
||||
|
||||
__all__ = ["DbfHeader"]
|
||||
|
||||
try:
|
||||
import cStringIO
|
||||
except ImportError:
|
||||
# when we're in python3, we cStringIO has been replaced by io.StringIO
|
||||
import io as cStringIO
|
||||
import datetime
|
||||
import io
|
||||
import struct
|
||||
import time
|
||||
import sys
|
||||
|
||||
from . import fields
|
||||
from . import utils
|
||||
from .utils import getDate
|
||||
|
||||
|
||||
class DbfHeader(object):
|
||||
class DbfHeader:
|
||||
"""Dbf header definition.
|
||||
|
||||
For more information about dbf header format visit
|
||||
@@ -54,13 +50,12 @@ class DbfHeader(object):
|
||||
"""
|
||||
|
||||
__slots__ = ("signature", "fields", "lastUpdate", "recordLength",
|
||||
"recordCount", "headerLength", "changed", "_ignore_errors")
|
||||
"recordCount", "headerLength", "changed", "_ignore_errors")
|
||||
|
||||
## instance construction and initialization methods
|
||||
# instance construction and initialization methods
|
||||
|
||||
def __init__(self, fields=None, headerLength=0, recordLength=0,
|
||||
recordCount=0, signature=0x03, lastUpdate=None, ignoreErrors=False,
|
||||
):
|
||||
recordCount=0, signature=0x03, lastUpdate=None, ignoreErrors=False):
|
||||
"""Initialize instance.
|
||||
|
||||
Arguments:
|
||||
@@ -90,7 +85,7 @@ class DbfHeader(object):
|
||||
self.fields = []
|
||||
else:
|
||||
self.fields = list(fields)
|
||||
self.lastUpdate = utils.getDate(lastUpdate)
|
||||
self.lastUpdate = getDate(lastUpdate)
|
||||
self.recordLength = recordLength
|
||||
self.headerLength = headerLength
|
||||
self.recordCount = recordCount
|
||||
@@ -102,35 +97,34 @@ class DbfHeader(object):
|
||||
# @classmethod
|
||||
def fromString(cls, string):
|
||||
"""Return header instance from the string object."""
|
||||
return cls.fromStream(cStringIO.StringIO(str(string)))
|
||||
return cls.fromStream(io.StringIO(str(string)))
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
# @classmethod
|
||||
def fromStream(cls, stream):
|
||||
"""Return header object from the stream."""
|
||||
stream.seek(0)
|
||||
_data = stream.read(32)
|
||||
first_32 = stream.read(32)
|
||||
if type(first_32) != bytes:
|
||||
_data = bytes(first_32, sys.getfilesystemencoding())
|
||||
_data = first_32
|
||||
(_cnt, _hdrLen, _recLen) = struct.unpack("<I2H", _data[4:12])
|
||||
#reserved = _data[12:32]
|
||||
_year = ord(_data[1])
|
||||
# reserved = _data[12:32]
|
||||
_year = _data[1]
|
||||
if _year < 80:
|
||||
# dBase II started at 1980. It is quite unlikely
|
||||
# that actual last update date is before that year.
|
||||
_year += 2000
|
||||
else:
|
||||
_year += 1900
|
||||
## create header object
|
||||
_obj = cls(None, _hdrLen, _recLen, _cnt, ord(_data[0]),
|
||||
(_year, ord(_data[2]), ord(_data[3])))
|
||||
## append field definitions
|
||||
# create header object
|
||||
_obj = cls(None, _hdrLen, _recLen, _cnt, _data[0],
|
||||
(_year, _data[2], _data[3]))
|
||||
# append field definitions
|
||||
# position 0 is for the deletion flag
|
||||
_pos = 1
|
||||
_data = stream.read(1)
|
||||
|
||||
# The field definitions are ended either by \x0D OR a newline
|
||||
# character, so we need to handle both when reading from a stream.
|
||||
# When writing, dbfpy appears to write newlines instead of \x0D.
|
||||
while _data[0] not in ["\x0D", "\n"]:
|
||||
while _data != b'\r':
|
||||
_data += stream.read(31)
|
||||
_fld = fields.lookupFor(_data[11]).fromString(_data, _pos)
|
||||
_obj._addField(_fld)
|
||||
@@ -139,7 +133,7 @@ class DbfHeader(object):
|
||||
return _obj
|
||||
fromStream = classmethod(fromStream)
|
||||
|
||||
## properties
|
||||
# properties
|
||||
|
||||
year = property(lambda self: self.lastUpdate.year)
|
||||
month = property(lambda self: self.lastUpdate.month)
|
||||
@@ -160,7 +154,7 @@ class DbfHeader(object):
|
||||
|
||||
""")
|
||||
|
||||
## object representation
|
||||
# object representation
|
||||
|
||||
def __repr__(self):
|
||||
_rv = """\
|
||||
@@ -177,7 +171,7 @@ Version (signature): 0x%02x
|
||||
)
|
||||
return _rv
|
||||
|
||||
## internal methods
|
||||
# internal methods
|
||||
|
||||
def _addField(self, *defs):
|
||||
"""Internal variant of the `addField` method.
|
||||
@@ -201,8 +195,7 @@ Version (signature): 0x%02x
|
||||
else:
|
||||
(_name, _type, _len, _dec) = (tuple(_def) + (None,) * 4)[:4]
|
||||
_cls = fields.lookupFor(_type)
|
||||
_obj = _cls(_name, _len, _dec,
|
||||
ignoreErrors=self._ignore_errors)
|
||||
_obj = _cls(_name, _len, _dec, ignoreErrors=self._ignore_errors)
|
||||
_recordLength += _obj.length
|
||||
_defs.append(_obj)
|
||||
# and now extend field definitions and
|
||||
@@ -210,7 +203,7 @@ Version (signature): 0x%02x
|
||||
self.fields += _defs
|
||||
return _recordLength
|
||||
|
||||
## interface methods
|
||||
# interface methods
|
||||
|
||||
def addField(self, *defs):
|
||||
"""Add field definition to the header.
|
||||
@@ -240,8 +233,9 @@ Version (signature): 0x%02x
|
||||
"""Encode and write header to the stream."""
|
||||
stream.seek(0)
|
||||
stream.write(self.toString())
|
||||
stream.write("".join([_fld.toString() for _fld in self.fields]))
|
||||
stream.write(chr(0x0D)) # cr at end of all hdr data
|
||||
fields = [_fld.toString() for _fld in self.fields]
|
||||
stream.write(''.join(fields).encode(sys.getfilesystemencoding()))
|
||||
stream.write(b'\x0D') # cr at end of all header data
|
||||
self.changed = False
|
||||
|
||||
def toString(self):
|
||||
@@ -253,7 +247,8 @@ Version (signature): 0x%02x
|
||||
self.day,
|
||||
self.recordCount,
|
||||
self.headerLength,
|
||||
self.recordLength) + "\0" * 20
|
||||
self.recordLength) + (b'\x00' * 20)
|
||||
# TODO: figure out if bytes(utf-8) is correct here.
|
||||
|
||||
def setCurrentDate(self):
|
||||
"""Update ``self.lastUpdate`` field with current date value."""
|
||||
@@ -261,7 +256,7 @@ Version (signature): 0x%02x
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Return a field definition by numeric index or name string"""
|
||||
if isinstance(item, basestring):
|
||||
if isinstance(item, str):
|
||||
_name = item.upper()
|
||||
for _field in self.fields:
|
||||
if _field.name == _name:
|
||||
|
||||
@@ -16,19 +16,20 @@ __date__ = "$Date: 2007/02/11 09:05:49 $"[7:-2]
|
||||
|
||||
__all__ = ["DbfRecord"]
|
||||
|
||||
from itertools import izip
|
||||
import sys
|
||||
|
||||
import utils
|
||||
from . import utils
|
||||
|
||||
class DbfRecord(object):
|
||||
|
||||
class DbfRecord:
|
||||
"""DBF record.
|
||||
|
||||
Instances of this class shouldn't be created manualy,
|
||||
Instances of this class shouldn't be created manually,
|
||||
use `dbf.Dbf.newRecord` instead.
|
||||
|
||||
Class implements mapping/sequence interface, so
|
||||
fields could be accessed via their names or indexes
|
||||
(names is a preffered way to access fields).
|
||||
(names is a preferred way to access fields).
|
||||
|
||||
Hint:
|
||||
Use `store` method to save modified record.
|
||||
@@ -52,10 +53,10 @@ class DbfRecord(object):
|
||||
|
||||
__slots__ = "dbf", "index", "deleted", "fieldData"
|
||||
|
||||
## creation and initialization
|
||||
# creation and initialization
|
||||
|
||||
def __init__(self, dbf, index=None, deleted=False, data=None):
|
||||
"""Instance initialiation.
|
||||
"""Instance initialization.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
@@ -82,7 +83,7 @@ class DbfRecord(object):
|
||||
|
||||
# XXX: validate self.index before calculating position?
|
||||
position = property(lambda self: self.dbf.header.headerLength + \
|
||||
self.index * self.dbf.header.recordLength)
|
||||
self.index * self.dbf.header.recordLength)
|
||||
|
||||
def rawFromStream(cls, dbf, index):
|
||||
"""Return raw record contents read from the stream.
|
||||
@@ -137,10 +138,10 @@ class DbfRecord(object):
|
||||
|
||||
"""
|
||||
return cls(dbf, index, string[0]=="*",
|
||||
[_fd.decodeFromRecord(string) for _fd in dbf.header.fields])
|
||||
[_fd.decodeFromRecord(string) for _fd in dbf.header.fields])
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
## object representation
|
||||
# object representation
|
||||
|
||||
def __repr__(self):
|
||||
_template = "%%%ds: %%s (%%s)" % max([len(_fld)
|
||||
@@ -155,14 +156,14 @@ class DbfRecord(object):
|
||||
_rv.append(_template % (_fld, _val, type(_val)))
|
||||
return "\n".join(_rv)
|
||||
|
||||
## protected methods
|
||||
# protected methods
|
||||
|
||||
def _write(self):
|
||||
"""Write data to the dbf stream.
|
||||
|
||||
Note:
|
||||
This isn't a public method, it's better to
|
||||
use 'store' instead publically.
|
||||
use 'store' instead publicly.
|
||||
Be design ``_write`` method should be called
|
||||
only from the `Dbf` instance.
|
||||
|
||||
@@ -170,15 +171,16 @@ class DbfRecord(object):
|
||||
"""
|
||||
self._validateIndex(False)
|
||||
self.dbf.stream.seek(self.position)
|
||||
self.dbf.stream.write(self.toString())
|
||||
self.dbf.stream.write(bytes(self.toString(),
|
||||
sys.getfilesystemencoding()))
|
||||
# FIXME: may be move this write somewhere else?
|
||||
# why we should check this condition for each record?
|
||||
if self.index == len(self.dbf):
|
||||
# this is the last record,
|
||||
# we should write SUB (ASCII 26)
|
||||
self.dbf.stream.write("\x1A")
|
||||
self.dbf.stream.write(b"\x1A")
|
||||
|
||||
## utility methods
|
||||
# utility methods
|
||||
|
||||
def _validateIndex(self, allowUndefined=True, checkRange=False):
|
||||
"""Valid ``self.index`` value.
|
||||
@@ -194,9 +196,9 @@ class DbfRecord(object):
|
||||
raise ValueError("Index can't be negative (%s)" % self.index)
|
||||
elif checkRange and self.index <= self.dbf.header.recordCount:
|
||||
raise ValueError("There are only %d records in the DBF" %
|
||||
self.dbf.header.recordCount)
|
||||
self.dbf.header.recordCount)
|
||||
|
||||
## interface methods
|
||||
# interface methods
|
||||
|
||||
def store(self):
|
||||
"""Store current record in the DBF.
|
||||
@@ -218,9 +220,12 @@ class DbfRecord(object):
|
||||
|
||||
def toString(self):
|
||||
"""Return string packed record values."""
|
||||
# for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData):
|
||||
#
|
||||
|
||||
return "".join([" *"[self.deleted]] + [
|
||||
_def.encodeValue(_dat)
|
||||
for (_def, _dat) in izip(self.dbf.header.fields, self.fieldData)
|
||||
_def.encodeValue(_dat)
|
||||
for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData)
|
||||
])
|
||||
|
||||
def asList(self):
|
||||
@@ -241,11 +246,11 @@ class DbfRecord(object):
|
||||
real values stored in this object.
|
||||
|
||||
"""
|
||||
return dict([_i for _i in izip(self.dbf.fieldNames, self.fieldData)])
|
||||
return dict([_i for _i in zip(self.dbf.fieldNames, self.fieldData)])
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Return value by field name or field index."""
|
||||
if isinstance(key, (long, int)):
|
||||
if isinstance(key, int):
|
||||
# integer index of the field
|
||||
return self.fieldData[key]
|
||||
# assuming string field name
|
||||
@@ -253,7 +258,7 @@ class DbfRecord(object):
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set field value by integer index of the field or string name."""
|
||||
if isinstance(key, (int, long)):
|
||||
if isinstance(key, int):
|
||||
# integer index of the field
|
||||
return self.fieldData[key]
|
||||
# assuming string field name
|
||||
|
||||
@@ -20,12 +20,12 @@ import time
|
||||
def unzfill(str):
|
||||
"""Return a string without ASCII NULs.
|
||||
|
||||
This function searchers for the first NUL (ASCII 0) occurance
|
||||
This function searchers for the first NUL (ASCII 0) occurrence
|
||||
and truncates string till that position.
|
||||
|
||||
"""
|
||||
try:
|
||||
return str[:str.index('\0')]
|
||||
return str[:str.index(b'\0')]
|
||||
except ValueError:
|
||||
return str
|
||||
|
||||
@@ -48,7 +48,7 @@ def getDate(date=None):
|
||||
sequence:
|
||||
assuming (year, month, day, ...) sequence;
|
||||
|
||||
Additionaly, if ``date`` has callable ``ticks`` attribute,
|
||||
Additionally, if ``date`` has callable ``ticks`` attribute,
|
||||
it will be used and result of the called would be treated
|
||||
as a timestamp value.
|
||||
|
||||
@@ -60,10 +60,10 @@ def getDate(date=None):
|
||||
return date
|
||||
if isinstance(date, datetime.datetime):
|
||||
return date.date()
|
||||
if isinstance(date, (int, long, float)):
|
||||
if isinstance(date, (int, float)):
|
||||
# date is a timestamp
|
||||
return datetime.date.fromtimestamp(date)
|
||||
if isinstance(date, basestring):
|
||||
if isinstance(date, str):
|
||||
date = date.replace(" ", "0")
|
||||
if len(date) == 6:
|
||||
# yymmdd
|
||||
@@ -95,7 +95,7 @@ def getDateTime(value=None):
|
||||
sequence:
|
||||
assuming (year, month, day, ...) sequence;
|
||||
|
||||
Additionaly, if ``value`` has callable ``ticks`` attribute,
|
||||
Additionally, if ``value`` has callable ``ticks`` attribute,
|
||||
it will be used and result of the called would be treated
|
||||
as a timestamp value.
|
||||
|
||||
@@ -107,10 +107,10 @@ def getDateTime(value=None):
|
||||
return value
|
||||
if isinstance(value, datetime.date):
|
||||
return datetime.datetime.fromordinal(value.toordinal())
|
||||
if isinstance(value, (int, long, float)):
|
||||
if isinstance(value, (int, float)):
|
||||
# value is a timestamp
|
||||
return datetime.datetime.fromtimestamp(value)
|
||||
if isinstance(value, basestring):
|
||||
if isinstance(value, str):
|
||||
raise NotImplementedError("Strings aren't currently implemented")
|
||||
if hasattr(value, "__getitem__"):
|
||||
# a sequence (assuming date/time tuple)
|
||||
@@ -125,7 +125,7 @@ class classproperty(property):
|
||||
return self.fget(cls)
|
||||
|
||||
|
||||
class _InvalidValue(object):
|
||||
class _InvalidValue:
|
||||
|
||||
"""Value returned from DBF records when field validation fails
|
||||
|
||||
@@ -145,7 +145,7 @@ class _InvalidValue(object):
|
||||
def __ne__(self, other):
|
||||
return not (other is self)
|
||||
|
||||
def __nonzero__(self):
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __int__(self):
|
||||
@@ -158,12 +158,10 @@ class _InvalidValue(object):
|
||||
def __str__(self):
|
||||
return ""
|
||||
|
||||
def __unicode__(self):
|
||||
return u""
|
||||
|
||||
def __repr__(self):
|
||||
return "<INVALID>"
|
||||
|
||||
|
||||
# invalid value is a constant singleton
|
||||
INVALID_VALUE = _InvalidValue()
|
||||
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
#! /usr/bin/env python
|
||||
"""DBF accessing helpers.
|
||||
|
||||
FIXME: more documentation needed
|
||||
|
||||
Examples:
|
||||
|
||||
Create new table, setup structure, add records:
|
||||
|
||||
dbf = Dbf(filename, new=True)
|
||||
dbf.addField(
|
||||
("NAME", "C", 15),
|
||||
("SURNAME", "C", 25),
|
||||
("INITIALS", "C", 10),
|
||||
("BIRTHDATE", "D"),
|
||||
)
|
||||
for (n, s, i, b) in (
|
||||
("John", "Miller", "YC", (1980, 10, 11)),
|
||||
("Andy", "Larkin", "", (1980, 4, 11)),
|
||||
):
|
||||
rec = dbf.newRecord()
|
||||
rec["NAME"] = n
|
||||
rec["SURNAME"] = s
|
||||
rec["INITIALS"] = i
|
||||
rec["BIRTHDATE"] = b
|
||||
rec.store()
|
||||
dbf.close()
|
||||
|
||||
Open existed dbf, read some data:
|
||||
|
||||
dbf = Dbf(filename, True)
|
||||
for rec in dbf:
|
||||
for fldName in dbf.fieldNames:
|
||||
print('%s:\t %s (%s)' % (fldName, rec[fldName],
|
||||
type(rec[fldName])))
|
||||
dbf.close()
|
||||
|
||||
"""
|
||||
"""History (most recent first):
|
||||
11-feb-2007 [als] export INVALID_VALUE;
|
||||
Dbf: added .ignoreErrors, .INVALID_VALUE
|
||||
04-jul-2006 [als] added export declaration
|
||||
20-dec-2005 [yc] removed fromStream and newDbf methods:
|
||||
use argument of __init__ call must be used instead;
|
||||
added class fields pointing to the header and
|
||||
record classes.
|
||||
17-dec-2005 [yc] split to several modules; reimplemented
|
||||
13-dec-2005 [yc] adapted to the changes of the `strutil` module.
|
||||
13-sep-2002 [als] support FoxPro Timestamp datatype
|
||||
15-nov-1999 [jjk] documentation updates, add demo
|
||||
24-aug-1998 [jjk] add some encodeValue methods (not tested), other tweaks
|
||||
08-jun-1998 [jjk] fix problems, add more features
|
||||
20-feb-1998 [jjk] fix problems, add more features
|
||||
19-feb-1998 [jjk] add create/write capabilities
|
||||
18-feb-1998 [jjk] from dbfload.py
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.7 $"[11:-2]
|
||||
__date__ = "$Date: 2007/02/11 09:23:13 $"[7:-2]
|
||||
__author__ = "Jeff Kunce <kuncej@mail.conservation.state.mo.us>"
|
||||
|
||||
__all__ = ["Dbf"]
|
||||
|
||||
from . import header
|
||||
from . import record
|
||||
from .utils import INVALID_VALUE
|
||||
|
||||
|
||||
class Dbf(object):
|
||||
"""DBF accessor.
|
||||
|
||||
FIXME:
|
||||
docs and examples needed (dont' forget to tell
|
||||
about problems adding new fields on the fly)
|
||||
|
||||
Implementation notes:
|
||||
``_new`` field is used to indicate whether this is
|
||||
a new data table. `addField` could be used only for
|
||||
the new tables! If at least one record was appended
|
||||
to the table it's structure couldn't be changed.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "header", "stream",
|
||||
"_changed", "_new", "_ignore_errors")
|
||||
|
||||
HeaderClass = header.DbfHeader
|
||||
RecordClass = record.DbfRecord
|
||||
INVALID_VALUE = INVALID_VALUE
|
||||
|
||||
# initialization and creation helpers
|
||||
|
||||
def __init__(self, f, readOnly=False, new=False, ignoreErrors=False):
|
||||
"""Initialize instance.
|
||||
|
||||
Arguments:
|
||||
f:
|
||||
Filename or file-like object.
|
||||
new:
|
||||
True if new data table must be created. Assume
|
||||
data table exists if this argument is False.
|
||||
readOnly:
|
||||
if ``f`` argument is a string file will
|
||||
be opend in read-only mode; in other cases
|
||||
this argument is ignored. This argument is ignored
|
||||
even if ``new`` argument is True.
|
||||
headerObj:
|
||||
`header.DbfHeader` instance or None. If this argument
|
||||
is None, new empty header will be used with the
|
||||
all fields set by default.
|
||||
ignoreErrors:
|
||||
if set, failing field value conversion will return
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
"""
|
||||
if isinstance(f, str):
|
||||
# a filename
|
||||
self.name = f
|
||||
if new:
|
||||
# new table (table file must be
|
||||
# created or opened and truncated)
|
||||
self.stream = open(f, "w+b")
|
||||
else:
|
||||
# tabe file must exist
|
||||
self.stream = open(f, ("r+b", "rb")[bool(readOnly)])
|
||||
else:
|
||||
# a stream
|
||||
self.name = getattr(f, "name", "")
|
||||
self.stream = f
|
||||
if new:
|
||||
# if this is a new table, header will be empty
|
||||
self.header = self.HeaderClass()
|
||||
else:
|
||||
# or instantiated using stream
|
||||
self.header = self.HeaderClass.fromStream(self.stream)
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self._new = bool(new)
|
||||
self._changed = False
|
||||
|
||||
# properties
|
||||
|
||||
closed = property(lambda self: self.stream.closed)
|
||||
recordCount = property(lambda self: self.header.recordCount)
|
||||
fieldNames = property(
|
||||
lambda self: [_fld.name for _fld in self.header.fields])
|
||||
fieldDefs = property(lambda self: self.header.fields)
|
||||
changed = property(lambda self: self._changed or self.header.changed)
|
||||
|
||||
def ignoreErrors(self, value):
|
||||
"""Update `ignoreErrors` flag on the header object and self"""
|
||||
self.header.ignoreErrors = self._ignore_errors = bool(value)
|
||||
|
||||
ignoreErrors = property(
|
||||
lambda self: self._ignore_errors,
|
||||
ignoreErrors,
|
||||
doc="""Error processing mode for DBF field value conversion
|
||||
|
||||
if set, failing field value conversion will return
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
""")
|
||||
|
||||
# protected methods
|
||||
|
||||
def _fixIndex(self, index):
|
||||
"""Return fixed index.
|
||||
|
||||
This method fails if index isn't a numeric object
|
||||
(long or int). Or index isn't in a valid range
|
||||
(less or equal to the number of records in the db).
|
||||
|
||||
If ``index`` is a negative number, it will be
|
||||
treated as a negative indexes for list objects.
|
||||
|
||||
Return:
|
||||
Return value is numeric object maning valid index.
|
||||
|
||||
"""
|
||||
if not isinstance(index, int):
|
||||
raise TypeError("Index must be a numeric object")
|
||||
if index < 0:
|
||||
# index from the right side
|
||||
# fix it to the left-side index
|
||||
index += len(self) + 1
|
||||
if index >= len(self):
|
||||
raise IndexError("Record index out of range")
|
||||
return index
|
||||
|
||||
# iterface methods
|
||||
|
||||
def close(self):
|
||||
self.flush()
|
||||
self.stream.close()
|
||||
|
||||
def flush(self):
|
||||
"""Flush data to the associated stream."""
|
||||
if self.changed:
|
||||
self.header.setCurrentDate()
|
||||
self.header.write(self.stream)
|
||||
self.stream.flush()
|
||||
self._changed = False
|
||||
|
||||
def indexOfFieldName(self, name):
|
||||
"""Index of field named ``name``."""
|
||||
# FIXME: move this to header class
|
||||
names = [f.name for f in self.header.fields]
|
||||
return names.index(name.upper())
|
||||
|
||||
def newRecord(self):
|
||||
"""Return new record, which belong to this table."""
|
||||
return self.RecordClass(self)
|
||||
|
||||
def append(self, record):
|
||||
"""Append ``record`` to the database."""
|
||||
record.index = self.header.recordCount
|
||||
record._write()
|
||||
self.header.recordCount += 1
|
||||
self._changed = True
|
||||
self._new = False
|
||||
|
||||
def addField(self, *defs):
|
||||
"""Add field definitions.
|
||||
|
||||
For more information see `header.DbfHeader.addField`.
|
||||
|
||||
"""
|
||||
if self._new:
|
||||
self.header.addField(*defs)
|
||||
else:
|
||||
raise TypeError("At least one record was added, "
|
||||
"structure can't be changed")
|
||||
|
||||
# 'magic' methods (representation and sequence interface)
|
||||
|
||||
def __repr__(self):
|
||||
return "Dbf stream '%s'\n" % self.stream + repr(self.header)
|
||||
|
||||
def __len__(self):
|
||||
"""Return number of records."""
|
||||
return self.recordCount
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Return `DbfRecord` instance."""
|
||||
return self.RecordClass.fromStream(self, self._fixIndex(index))
|
||||
|
||||
def __setitem__(self, index, record):
|
||||
"""Write `DbfRecord` instance to the stream."""
|
||||
record.index = self._fixIndex(index)
|
||||
record._write()
|
||||
self._changed = True
|
||||
self._new = False
|
||||
|
||||
# def __del__(self):
|
||||
# """Flush stream upon deletion of the object."""
|
||||
# self.flush()
|
||||
|
||||
|
||||
def demo_read(filename):
|
||||
_dbf = Dbf(filename, True)
|
||||
for _rec in _dbf:
|
||||
print()
|
||||
print(repr(_rec))
|
||||
_dbf.close()
|
||||
|
||||
|
||||
def demo_create(filename):
|
||||
_dbf = Dbf(filename, new=True)
|
||||
_dbf.addField(
|
||||
("NAME", "C", 15),
|
||||
("SURNAME", "C", 25),
|
||||
("INITIALS", "C", 10),
|
||||
("BIRTHDATE", "D"),
|
||||
)
|
||||
for (_n, _s, _i, _b) in (
|
||||
("John", "Miller", "YC", (1981, 1, 2)),
|
||||
("Andy", "Larkin", "AL", (1982, 3, 4)),
|
||||
("Bill", "Clinth", "", (1983, 5, 6)),
|
||||
("Bobb", "McNail", "", (1984, 7, 8)),
|
||||
):
|
||||
_rec = _dbf.newRecord()
|
||||
_rec["NAME"] = _n
|
||||
_rec["SURNAME"] = _s
|
||||
_rec["INITIALS"] = _i
|
||||
_rec["BIRTHDATE"] = _b
|
||||
_rec.store()
|
||||
print(repr(_dbf))
|
||||
_dbf.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
_name = len(sys.argv) > 1 and sys.argv[1] or "county.dbf"
|
||||
demo_create(_name)
|
||||
demo_read(_name)
|
||||
|
||||
# vim: set et sw=4 sts=4 :
|
||||
@@ -1,183 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
""".DBF creation helpers.
|
||||
|
||||
Note: this is a legacy interface. New code should use Dbf class
|
||||
for table creation (see examples in dbf.py)
|
||||
|
||||
TODO:
|
||||
- handle Memo fields.
|
||||
- check length of the fields accoring to the
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
|
||||
"""
|
||||
"""History (most recent first)
|
||||
04-jul-2006 [als] added export declaration;
|
||||
updated for dbfpy 2.0
|
||||
15-dec-2005 [yc] define dbf_new.__slots__
|
||||
14-dec-2005 [yc] added vim modeline; retab'd; added doc-strings;
|
||||
dbf_new now is a new class (inherited from object)
|
||||
??-jun-2000 [--] added by Hans Fiby
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.4 $"[11:-2]
|
||||
__date__ = "$Date: 2006/07/04 08:18:18 $"[7:-2]
|
||||
|
||||
__all__ = ["dbf_new"]
|
||||
|
||||
from .dbf import *
|
||||
from .fields import *
|
||||
from .header import *
|
||||
from .record import *
|
||||
|
||||
|
||||
class _FieldDefinition(object):
|
||||
"""Field definition.
|
||||
|
||||
This is a simple structure, which contains ``name``, ``type``,
|
||||
``len``, ``dec`` and ``cls`` fields.
|
||||
|
||||
Objects also implement get/setitem magic functions, so fields
|
||||
could be accessed via sequence iterface, where 'name' has
|
||||
index 0, 'type' index 1, 'len' index 2, 'dec' index 3 and
|
||||
'cls' could be located at index 4.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = "name", "type", "len", "dec", "cls"
|
||||
|
||||
# WARNING: be attentive - dictionaries are mutable!
|
||||
FLD_TYPES = {
|
||||
# type: (cls, len)
|
||||
"C": (DbfCharacterFieldDef, None),
|
||||
"N": (DbfNumericFieldDef, None),
|
||||
"L": (DbfLogicalFieldDef, 1),
|
||||
# FIXME: support memos
|
||||
# "M": (DbfMemoFieldDef),
|
||||
"D": (DbfDateFieldDef, 8),
|
||||
# FIXME: I'm not sure length should be 14 characters!
|
||||
# but temporary I use it, cuz date is 8 characters
|
||||
# and time 6 (hhmmss)
|
||||
"T": (DbfDateTimeFieldDef, 14),
|
||||
}
|
||||
|
||||
def __init__(self, name, type, len=None, dec=0):
|
||||
_cls, _len = self.FLD_TYPES[type]
|
||||
if _len is None:
|
||||
if len is None:
|
||||
raise ValueError("Field length must be defined")
|
||||
_len = len
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.len = _len
|
||||
self.dec = dec
|
||||
self.cls = _cls
|
||||
|
||||
def getDbfField(self):
|
||||
"Return `DbfFieldDef` instance from the current definition."
|
||||
return self.cls(self.name, self.len, self.dec)
|
||||
|
||||
def appendToHeader(self, dbfh):
|
||||
"""Create a `DbfFieldDef` instance and append it to the dbf header.
|
||||
|
||||
Arguments:
|
||||
dbfh: `DbfHeader` instance.
|
||||
|
||||
"""
|
||||
_dbff = self.getDbfField()
|
||||
dbfh.addField(_dbff)
|
||||
|
||||
|
||||
class dbf_new(object):
|
||||
"""New .DBF creation helper.
|
||||
|
||||
Example Usage:
|
||||
|
||||
dbfn = dbf_new()
|
||||
dbfn.add_field("name",'C',80)
|
||||
dbfn.add_field("price",'N',10,2)
|
||||
dbfn.add_field("date",'D',8)
|
||||
dbfn.write("tst.dbf")
|
||||
|
||||
Note:
|
||||
This module cannot handle Memo-fields,
|
||||
they are special.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("fields",)
|
||||
|
||||
FieldDefinitionClass = _FieldDefinition
|
||||
|
||||
def __init__(self):
|
||||
self.fields = []
|
||||
|
||||
def add_field(self, name, typ, len, dec=0):
|
||||
"""Add field definition.
|
||||
|
||||
Arguments:
|
||||
name:
|
||||
field name (str object). field name must not
|
||||
contain ASCII NULs and it's length shouldn't
|
||||
exceed 10 characters.
|
||||
typ:
|
||||
type of the field. this must be a single character
|
||||
from the "CNLMDT" set meaning character, numeric,
|
||||
logical, memo, date and date/time respectively.
|
||||
len:
|
||||
length of the field. this argument is used only for
|
||||
the character and numeric fields. all other fields
|
||||
have fixed length.
|
||||
FIXME: use None as a default for this argument?
|
||||
dec:
|
||||
decimal precision. used only for the numric fields.
|
||||
|
||||
"""
|
||||
self.fields.append(self.FieldDefinitionClass(name, typ, len, dec))
|
||||
|
||||
def write(self, filename):
|
||||
"""Create empty .DBF file using current structure."""
|
||||
_dbfh = DbfHeader()
|
||||
_dbfh.setCurrentDate()
|
||||
for _fldDef in self.fields:
|
||||
_fldDef.appendToHeader(_dbfh)
|
||||
|
||||
_dbfStream = open(filename, "wb")
|
||||
_dbfh.write(_dbfStream)
|
||||
_dbfStream.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# create a new DBF-File
|
||||
dbfn = dbf_new()
|
||||
dbfn.add_field("name", 'C', 80)
|
||||
dbfn.add_field("price", 'N', 10, 2)
|
||||
dbfn.add_field("date", 'D', 8)
|
||||
dbfn.write("tst.dbf")
|
||||
# test new dbf
|
||||
print("*** created tst.dbf: ***")
|
||||
dbft = Dbf('tst.dbf', readOnly=0)
|
||||
print(repr(dbft))
|
||||
# add a record
|
||||
rec = DbfRecord(dbft)
|
||||
rec['name'] = 'something'
|
||||
rec['price'] = 10.5
|
||||
rec['date'] = (2000, 1, 12)
|
||||
rec.store()
|
||||
# add another record
|
||||
rec = DbfRecord(dbft)
|
||||
rec['name'] = 'foo and bar'
|
||||
rec['price'] = 12234
|
||||
rec['date'] = (1992, 7, 15)
|
||||
rec.store()
|
||||
|
||||
# show the records
|
||||
print("*** inserted 2 records into tst.dbf: ***")
|
||||
print(repr(dbft))
|
||||
for i1 in range(len(dbft)):
|
||||
rec = dbft[i1]
|
||||
for fldName in dbft.fieldNames:
|
||||
print('%s:\t %s' % (fldName, rec[fldName]))
|
||||
print()
|
||||
dbft.close()
|
||||
|
||||
# vim: set et sts=4 sw=4 :
|
||||
@@ -1,466 +0,0 @@
|
||||
"""DBF fields definitions.
|
||||
|
||||
TODO:
|
||||
- make memos work
|
||||
"""
|
||||
"""History (most recent first):
|
||||
26-may-2009 [als] DbfNumericFieldDef.decodeValue: strip zero bytes
|
||||
05-feb-2009 [als] DbfDateFieldDef.encodeValue: empty arg produces empty date
|
||||
16-sep-2008 [als] DbfNumericFieldDef decoding looks for decimal point
|
||||
in the value to select float or integer return type
|
||||
13-mar-2008 [als] check field name length in constructor
|
||||
11-feb-2007 [als] handle value conversion errors
|
||||
10-feb-2007 [als] DbfFieldDef: added .rawFromRecord()
|
||||
01-dec-2006 [als] Timestamp columns use None for empty values
|
||||
31-oct-2006 [als] support field types 'F' (float), 'I' (integer)
|
||||
and 'Y' (currency);
|
||||
automate export and registration of field classes
|
||||
04-jul-2006 [als] added export declaration
|
||||
10-mar-2006 [als] decode empty values for Date and Logical fields;
|
||||
show field name in errors
|
||||
10-mar-2006 [als] fix Numeric value decoding: according to spec,
|
||||
value always is string representation of the number;
|
||||
ensure that encoded Numeric value fits into the field
|
||||
20-dec-2005 [yc] use field names in upper case
|
||||
15-dec-2005 [yc] field definitions moved from `dbf`.
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.14 $"[11:-2]
|
||||
__date__ = "$Date: 2009/05/26 05:16:51 $"[7:-2]
|
||||
|
||||
__all__ = ["lookupFor",] # field classes added at the end of the module
|
||||
|
||||
import datetime
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from . import utils
|
||||
|
||||
## abstract definitions
|
||||
|
||||
class DbfFieldDef(object):
|
||||
"""Abstract field definition.
|
||||
|
||||
Child classes must override ``type`` class attribute to provide datatype
|
||||
infromation of the field definition. For more info about types visit
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
|
||||
Also child classes must override ``defaultValue`` field to provide
|
||||
default value for the field value.
|
||||
|
||||
If child class has fixed length ``length`` class attribute must be
|
||||
overriden and set to the valid value. None value means, that field
|
||||
isn't of fixed length.
|
||||
|
||||
Note: ``name`` field must not be changed after instantiation.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "decimalCount",
|
||||
"start", "end", "ignoreErrors")
|
||||
|
||||
# length of the field, None in case of variable-length field,
|
||||
# or a number if this field is a fixed-length field
|
||||
length = None
|
||||
|
||||
# field type. for more information about fields types visit
|
||||
# `http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
# must be overriden in child classes
|
||||
typeCode = None
|
||||
|
||||
# default value for the field. this field must be
|
||||
# overriden in child classes
|
||||
defaultValue = None
|
||||
|
||||
def __init__(self, name, length=None, decimalCount=None,
|
||||
start=None, stop=None, ignoreErrors=False,
|
||||
):
|
||||
"""Initialize instance."""
|
||||
assert self.typeCode is not None, "Type code must be overriden"
|
||||
assert self.defaultValue is not None, "Default value must be overriden"
|
||||
## fix arguments
|
||||
if len(name) >10:
|
||||
raise ValueError("Field name \"%s\" is too long" % name)
|
||||
name = str(name).upper()
|
||||
if self.__class__.length is None:
|
||||
if length is None:
|
||||
raise ValueError("[%s] Length isn't specified" % name)
|
||||
length = int(length)
|
||||
if length <= 0:
|
||||
raise ValueError("[%s] Length must be a positive integer"
|
||||
% name)
|
||||
else:
|
||||
length = self.length
|
||||
if decimalCount is None:
|
||||
decimalCount = 0
|
||||
## set fields
|
||||
self.name = name
|
||||
# FIXME: validate length according to the specification at
|
||||
# http://www.clicketyclick.dk/databases/xbase/format/data_types.html
|
||||
self.length = length
|
||||
self.decimalCount = decimalCount
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self.start = start
|
||||
self.end = stop
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.name, str(other).upper())
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def fromString(cls, string, start, ignoreErrors=False):
|
||||
"""Decode dbf field definition from the string data.
|
||||
|
||||
Arguments:
|
||||
string:
|
||||
a string, dbf definition is decoded from. length of
|
||||
the string must be 32 bytes.
|
||||
start:
|
||||
position in the database file.
|
||||
ignoreErrors:
|
||||
initial error processing mode for the new field (boolean)
|
||||
|
||||
"""
|
||||
assert len(string) == 32
|
||||
_length = string[16]
|
||||
return cls(utils.unzfill(string)[:11].decode('utf-8'), _length,
|
||||
string[17], start, start + _length, ignoreErrors=ignoreErrors)
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
def toString(self):
|
||||
"""Return encoded field definition.
|
||||
|
||||
Return:
|
||||
Return value is a string object containing encoded
|
||||
definition of this field.
|
||||
|
||||
"""
|
||||
if sys.version_info < (2, 4):
|
||||
# earlier versions did not support padding character
|
||||
_name = self.name[:11] + "\0" * (11 - len(self.name))
|
||||
else:
|
||||
_name = self.name.ljust(11, '\0')
|
||||
return (
|
||||
_name +
|
||||
self.typeCode +
|
||||
#data address
|
||||
chr(0) * 4 +
|
||||
chr(self.length) +
|
||||
chr(self.decimalCount) +
|
||||
chr(0) * 14
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "%-10s %1s %3d %3d" % self.fieldInfo()
|
||||
|
||||
def fieldInfo(self):
|
||||
"""Return field information.
|
||||
|
||||
Return:
|
||||
Return value is a (name, type, length, decimals) tuple.
|
||||
|
||||
"""
|
||||
return (self.name, self.typeCode, self.length, self.decimalCount)
|
||||
|
||||
def rawFromRecord(self, record):
|
||||
"""Return a "raw" field value from the record string."""
|
||||
return record[self.start:self.end]
|
||||
|
||||
def decodeFromRecord(self, record):
|
||||
"""Return decoded field value from the record string."""
|
||||
try:
|
||||
return self.decodeValue(self.rawFromRecord(record))
|
||||
except:
|
||||
if self.ignoreErrors:
|
||||
return utils.INVALID_VALUE
|
||||
else:
|
||||
raise
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return decoded value from string value.
|
||||
|
||||
This method shouldn't be used publicly. It's called from the
|
||||
`decodeFromRecord` method.
|
||||
|
||||
This is an abstract method and it must be overridden in child classes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return str object containing encoded field value.
|
||||
|
||||
This is an abstract method and it must be overriden in child classes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
## real classes
|
||||
|
||||
class DbfCharacterFieldDef(DbfFieldDef):
|
||||
"""Definition of the character field."""
|
||||
|
||||
typeCode = "C"
|
||||
defaultValue = b''
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return string object.
|
||||
|
||||
Return value is a ``value`` argument with stripped right spaces.
|
||||
|
||||
"""
|
||||
return value.rstrip(b' ').decode('utf-8')
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return raw data string encoded from a ``value``."""
|
||||
return str(value)[:self.length].ljust(self.length)
|
||||
|
||||
|
||||
class DbfNumericFieldDef(DbfFieldDef):
|
||||
"""Definition of the numeric field."""
|
||||
|
||||
typeCode = "N"
|
||||
# XXX: now I'm not sure it was a good idea to make a class field
|
||||
# `defaultValue` instead of a generic method as it was implemented
|
||||
# previously -- it's ok with all types except number, cuz
|
||||
# if self.decimalCount is 0, we should return 0 and 0.0 otherwise.
|
||||
defaultValue = 0
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return a number decoded from ``value``.
|
||||
|
||||
If decimals is zero, value will be decoded as an integer;
|
||||
or as a float otherwise.
|
||||
|
||||
Return:
|
||||
Return value is a int (long) or float instance.
|
||||
|
||||
"""
|
||||
value = value.strip(b' \0')
|
||||
if b'.' in value:
|
||||
# a float (has decimal separator)
|
||||
return float(value)
|
||||
elif value:
|
||||
# must be an integer
|
||||
return int(value)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return string containing encoded ``value``."""
|
||||
_rv = ("%*.*f" % (self.length, self.decimalCount, value))
|
||||
if len(_rv) > self.length:
|
||||
_ppos = _rv.find(".")
|
||||
if 0 <= _ppos <= self.length:
|
||||
_rv = _rv[:self.length]
|
||||
else:
|
||||
raise ValueError("[%s] Numeric overflow: %s (field width: %i)"
|
||||
% (self.name, _rv, self.length))
|
||||
return _rv
|
||||
|
||||
class DbfFloatFieldDef(DbfNumericFieldDef):
|
||||
"""Definition of the float field - same as numeric."""
|
||||
|
||||
typeCode = "F"
|
||||
|
||||
class DbfIntegerFieldDef(DbfFieldDef):
|
||||
"""Definition of the integer field."""
|
||||
|
||||
typeCode = "I"
|
||||
length = 4
|
||||
defaultValue = 0
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return an integer number decoded from ``value``."""
|
||||
return struct.unpack("<i", value)[0]
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return string containing encoded ``value``."""
|
||||
return struct.pack("<i", int(value))
|
||||
|
||||
class DbfCurrencyFieldDef(DbfFieldDef):
|
||||
"""Definition of the currency field."""
|
||||
|
||||
typeCode = "Y"
|
||||
length = 8
|
||||
defaultValue = 0.0
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return float number decoded from ``value``."""
|
||||
return struct.unpack("<q", value)[0] / 10000.
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return string containing encoded ``value``."""
|
||||
return struct.pack("<q", round(value * 10000))
|
||||
|
||||
class DbfLogicalFieldDef(DbfFieldDef):
|
||||
"""Definition of the logical field."""
|
||||
|
||||
typeCode = "L"
|
||||
defaultValue = -1
|
||||
length = 1
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return True, False or -1 decoded from ``value``."""
|
||||
# Note: value always is 1-char string
|
||||
if value == "?":
|
||||
return -1
|
||||
if value in "NnFf ":
|
||||
return False
|
||||
if value in "YyTt":
|
||||
return True
|
||||
raise ValueError("[%s] Invalid logical value %r" % (self.name, value))
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a character from the "TF?" set.
|
||||
|
||||
Return:
|
||||
Return value is "T" if ``value`` is True
|
||||
"?" if value is -1 or False otherwise.
|
||||
|
||||
"""
|
||||
if value is True:
|
||||
return "T"
|
||||
if value == -1:
|
||||
return "?"
|
||||
return "F"
|
||||
|
||||
|
||||
class DbfMemoFieldDef(DbfFieldDef):
|
||||
"""Definition of the memo field.
|
||||
|
||||
Note: memos aren't currenly completely supported.
|
||||
|
||||
"""
|
||||
|
||||
typeCode = "M"
|
||||
defaultValue = " " * 10
|
||||
length = 10
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return int .dbt block number decoded from the string object."""
|
||||
#return int(value)
|
||||
raise NotImplementedError
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return raw data string encoded from a ``value``.
|
||||
|
||||
Note: this is an internal method.
|
||||
|
||||
"""
|
||||
#return str(value)[:self.length].ljust(self.length)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DbfDateFieldDef(DbfFieldDef):
|
||||
"""Definition of the date field."""
|
||||
|
||||
typeCode = "D"
|
||||
defaultValue = utils.classproperty(lambda cls: datetime.date.today())
|
||||
# "yyyymmdd" gives us 8 characters
|
||||
length = 8
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return a ``datetime.date`` instance decoded from ``value``."""
|
||||
if value.strip():
|
||||
return utils.getDate(value)
|
||||
else:
|
||||
return None
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a string-encoded value.
|
||||
|
||||
``value`` argument should be a value suitable for the
|
||||
`utils.getDate` call.
|
||||
|
||||
Return:
|
||||
Return value is a string in format "yyyymmdd".
|
||||
|
||||
"""
|
||||
if value:
|
||||
return utils.getDate(value).strftime("%Y%m%d")
|
||||
else:
|
||||
return " " * self.length
|
||||
|
||||
|
||||
class DbfDateTimeFieldDef(DbfFieldDef):
|
||||
"""Definition of the timestamp field."""
|
||||
|
||||
# a difference between JDN (Julian Day Number)
|
||||
# and GDN (Gregorian Day Number). note, that GDN < JDN
|
||||
JDN_GDN_DIFF = 1721425
|
||||
typeCode = "T"
|
||||
defaultValue = utils.classproperty(lambda cls: datetime.datetime.now())
|
||||
# two 32-bits integers representing JDN and amount of
|
||||
# milliseconds respectively gives us 8 bytes.
|
||||
# note, that values must be encoded in LE byteorder.
|
||||
length = 8
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return a `datetime.datetime` instance."""
|
||||
assert len(value) == self.length
|
||||
# LE byteorder
|
||||
_jdn, _msecs = struct.unpack("<2I", value)
|
||||
if _jdn >= 1:
|
||||
_rv = datetime.datetime.fromordinal(_jdn - self.JDN_GDN_DIFF)
|
||||
_rv += datetime.timedelta(0, _msecs / 1000.0)
|
||||
else:
|
||||
# empty date
|
||||
_rv = None
|
||||
return _rv
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a string-encoded ``value``."""
|
||||
if value:
|
||||
value = utils.getDateTime(value)
|
||||
# LE byteorder
|
||||
_rv = struct.pack("<2I", value.toordinal() + self.JDN_GDN_DIFF,
|
||||
(value.hour * 3600 + value.minute * 60 + value.second) * 1000)
|
||||
else:
|
||||
_rv = "\0" * self.length
|
||||
assert len(_rv) == self.length
|
||||
return _rv
|
||||
|
||||
|
||||
_fieldsRegistry = {}
|
||||
|
||||
def registerField(fieldCls):
|
||||
"""Register field definition class.
|
||||
|
||||
``fieldCls`` should be subclass of the `DbfFieldDef`.
|
||||
|
||||
Use `lookupFor` to retrieve field definition class
|
||||
by the type code.
|
||||
|
||||
"""
|
||||
assert fieldCls.typeCode is not None, "Type code isn't defined"
|
||||
# XXX: use fieldCls.typeCode.upper()? in case of any decign
|
||||
# don't forget to look to the same comment in ``lookupFor`` method
|
||||
_fieldsRegistry[fieldCls.typeCode] = fieldCls
|
||||
|
||||
|
||||
def lookupFor(typeCode):
|
||||
"""Return field definition class for the given type code.
|
||||
|
||||
``typeCode`` must be a single character. That type should be
|
||||
previously registered.
|
||||
|
||||
Use `registerField` to register new field class.
|
||||
|
||||
Return:
|
||||
Return value is a subclass of the `DbfFieldDef`.
|
||||
|
||||
"""
|
||||
# XXX: use typeCode.upper()? in case of any decign don't
|
||||
# forget to look to the same comment in ``registerField``
|
||||
return _fieldsRegistry[chr(typeCode)]
|
||||
|
||||
## register generic types
|
||||
|
||||
for (_name, _val) in list(globals().items()):
|
||||
if isinstance(_val, type) and issubclass(_val, DbfFieldDef) \
|
||||
and (_name != "DbfFieldDef"):
|
||||
__all__.append(_name)
|
||||
registerField(_val)
|
||||
del _name, _val
|
||||
|
||||
# vim: et sts=4 sw=4 :
|
||||
@@ -1,273 +0,0 @@
|
||||
"""DBF header definition.
|
||||
|
||||
TODO:
|
||||
- handle encoding of the character fields
|
||||
(encoding information stored in the DBF header)
|
||||
|
||||
"""
|
||||
"""History (most recent first):
|
||||
16-sep-2010 [als] fromStream: fix century of the last update field
|
||||
11-feb-2007 [als] added .ignoreErrors
|
||||
10-feb-2007 [als] added __getitem__: return field definitions
|
||||
by field name or field number (zero-based)
|
||||
04-jul-2006 [als] added export declaration
|
||||
15-dec-2005 [yc] created
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.6 $"[11:-2]
|
||||
__date__ = "$Date: 2010/09/16 05:06:39 $"[7:-2]
|
||||
|
||||
__all__ = ["DbfHeader"]
|
||||
|
||||
import io
|
||||
import datetime
|
||||
import struct
|
||||
import time
|
||||
import sys
|
||||
|
||||
from . import fields
|
||||
from .utils import getDate
|
||||
|
||||
|
||||
class DbfHeader(object):
|
||||
"""Dbf header definition.
|
||||
|
||||
For more information about dbf header format visit
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_STRUCT`
|
||||
|
||||
Examples:
|
||||
Create an empty dbf header and add some field definitions:
|
||||
dbfh = DbfHeader()
|
||||
dbfh.addField(("name", "C", 10))
|
||||
dbfh.addField(("date", "D"))
|
||||
dbfh.addField(DbfNumericFieldDef("price", 5, 2))
|
||||
Create a dbf header with field definitions:
|
||||
dbfh = DbfHeader([
|
||||
("name", "C", 10),
|
||||
("date", "D"),
|
||||
DbfNumericFieldDef("price", 5, 2),
|
||||
])
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("signature", "fields", "lastUpdate", "recordLength",
|
||||
"recordCount", "headerLength", "changed", "_ignore_errors")
|
||||
|
||||
## instance construction and initialization methods
|
||||
|
||||
def __init__(self, fields=None, headerLength=0, recordLength=0,
|
||||
recordCount=0, signature=0x03, lastUpdate=None, ignoreErrors=False,
|
||||
):
|
||||
"""Initialize instance.
|
||||
|
||||
Arguments:
|
||||
fields:
|
||||
a list of field definitions;
|
||||
recordLength:
|
||||
size of the records;
|
||||
headerLength:
|
||||
size of the header;
|
||||
recordCount:
|
||||
number of records stored in DBF;
|
||||
signature:
|
||||
version number (aka signature). using 0x03 as a default meaning
|
||||
"File without DBT". for more information about this field visit
|
||||
``http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_NOTE_1_TARGET``
|
||||
lastUpdate:
|
||||
date of the DBF's update. this could be a string ('yymmdd' or
|
||||
'yyyymmdd'), timestamp (int or float), datetime/date value,
|
||||
a sequence (assuming (yyyy, mm, dd, ...)) or an object having
|
||||
callable ``ticks`` field.
|
||||
ignoreErrors:
|
||||
error processing mode for DBF fields (boolean)
|
||||
|
||||
"""
|
||||
self.signature = signature
|
||||
if fields is None:
|
||||
self.fields = []
|
||||
else:
|
||||
self.fields = list(fields)
|
||||
self.lastUpdate = getDate(lastUpdate)
|
||||
self.recordLength = recordLength
|
||||
self.headerLength = headerLength
|
||||
self.recordCount = recordCount
|
||||
self.ignoreErrors = ignoreErrors
|
||||
# XXX: I'm not sure this is safe to
|
||||
# initialize `self.changed` in this way
|
||||
self.changed = bool(self.fields)
|
||||
|
||||
# @classmethod
|
||||
def fromString(cls, string):
|
||||
"""Return header instance from the string object."""
|
||||
return cls.fromStream(io.StringIO(str(string)))
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
# @classmethod
|
||||
def fromStream(cls, stream):
|
||||
"""Return header object from the stream."""
|
||||
stream.seek(0)
|
||||
first_32 = stream.read(32)
|
||||
if type(first_32) != bytes:
|
||||
_data = bytes(first_32, sys.getfilesystemencoding())
|
||||
_data = first_32
|
||||
(_cnt, _hdrLen, _recLen) = struct.unpack("<I2H", _data[4:12])
|
||||
#reserved = _data[12:32]
|
||||
_year = _data[1]
|
||||
if _year < 80:
|
||||
# dBase II started at 1980. It is quite unlikely
|
||||
# that actual last update date is before that year.
|
||||
_year += 2000
|
||||
else:
|
||||
_year += 1900
|
||||
## create header object
|
||||
_obj = cls(None, _hdrLen, _recLen, _cnt, _data[0],
|
||||
(_year, _data[2], _data[3]))
|
||||
## append field definitions
|
||||
# position 0 is for the deletion flag
|
||||
_pos = 1
|
||||
_data = stream.read(1)
|
||||
while _data != b'\r':
|
||||
_data += stream.read(31)
|
||||
_fld = fields.lookupFor(_data[11]).fromString(_data, _pos)
|
||||
_obj._addField(_fld)
|
||||
_pos = _fld.end
|
||||
_data = stream.read(1)
|
||||
return _obj
|
||||
fromStream = classmethod(fromStream)
|
||||
|
||||
## properties
|
||||
|
||||
year = property(lambda self: self.lastUpdate.year)
|
||||
month = property(lambda self: self.lastUpdate.month)
|
||||
day = property(lambda self: self.lastUpdate.day)
|
||||
|
||||
def ignoreErrors(self, value):
|
||||
"""Update `ignoreErrors` flag on self and all fields"""
|
||||
self._ignore_errors = value = bool(value)
|
||||
for _field in self.fields:
|
||||
_field.ignoreErrors = value
|
||||
ignoreErrors = property(
|
||||
lambda self: self._ignore_errors,
|
||||
ignoreErrors,
|
||||
doc="""Error processing mode for DBF field value conversion
|
||||
|
||||
if set, failing field value conversion will return
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
""")
|
||||
|
||||
## object representation
|
||||
|
||||
def __repr__(self):
|
||||
_rv = """\
|
||||
Version (signature): 0x%02x
|
||||
Last update: %s
|
||||
Header length: %d
|
||||
Record length: %d
|
||||
Record count: %d
|
||||
FieldName Type Len Dec
|
||||
""" % (self.signature, self.lastUpdate, self.headerLength,
|
||||
self.recordLength, self.recordCount)
|
||||
_rv += "\n".join(
|
||||
["%10s %4s %3s %3s" % _fld.fieldInfo() for _fld in self.fields]
|
||||
)
|
||||
return _rv
|
||||
|
||||
## internal methods
|
||||
|
||||
def _addField(self, *defs):
|
||||
"""Internal variant of the `addField` method.
|
||||
|
||||
This method doesn't set `self.changed` field to True.
|
||||
|
||||
Return value is a length of the appended records.
|
||||
Note: this method doesn't modify ``recordLength`` and
|
||||
``headerLength`` fields. Use `addField` instead of this
|
||||
method if you don't exactly know what you're doing.
|
||||
|
||||
"""
|
||||
# insure we have dbf.DbfFieldDef instances first (instantiation
|
||||
# from the tuple could raise an error, in such a case I don't
|
||||
# wanna add any of the definitions -- all will be ignored)
|
||||
_defs = []
|
||||
_recordLength = 0
|
||||
for _def in defs:
|
||||
if isinstance(_def, fields.DbfFieldDef):
|
||||
_obj = _def
|
||||
else:
|
||||
(_name, _type, _len, _dec) = (tuple(_def) + (None,) * 4)[:4]
|
||||
_cls = fields.lookupFor(_type)
|
||||
_obj = _cls(_name, _len, _dec,
|
||||
ignoreErrors=self._ignore_errors)
|
||||
_recordLength += _obj.length
|
||||
_defs.append(_obj)
|
||||
# and now extend field definitions and
|
||||
# update record length
|
||||
self.fields += _defs
|
||||
return _recordLength
|
||||
|
||||
## interface methods
|
||||
|
||||
def addField(self, *defs):
|
||||
"""Add field definition to the header.
|
||||
|
||||
Examples:
|
||||
dbfh.addField(
|
||||
("name", "C", 20),
|
||||
dbf.DbfCharacterFieldDef("surname", 20),
|
||||
dbf.DbfDateFieldDef("birthdate"),
|
||||
("member", "L"),
|
||||
)
|
||||
dbfh.addField(("price", "N", 5, 2))
|
||||
dbfh.addField(dbf.DbfNumericFieldDef("origprice", 5, 2))
|
||||
|
||||
"""
|
||||
_oldLen = self.recordLength
|
||||
self.recordLength += self._addField(*defs)
|
||||
if not _oldLen:
|
||||
self.recordLength += 1
|
||||
# XXX: may be just use:
|
||||
# self.recordeLength += self._addField(*defs) + bool(not _oldLen)
|
||||
# recalculate headerLength
|
||||
self.headerLength = 32 + (32 * len(self.fields)) + 1
|
||||
self.changed = True
|
||||
|
||||
def write(self, stream):
|
||||
"""Encode and write header to the stream."""
|
||||
stream.seek(0)
|
||||
stream.write(self.toString())
|
||||
fields = [_fld.toString() for _fld in self.fields]
|
||||
stream.write(''.join(fields).encode(sys.getfilesystemencoding()))
|
||||
stream.write(b'\x0D') # cr at end of all header data
|
||||
self.changed = False
|
||||
|
||||
def toString(self):
|
||||
"""Returned 32 chars length string with encoded header."""
|
||||
return struct.pack("<4BI2H",
|
||||
self.signature,
|
||||
self.year - 1900,
|
||||
self.month,
|
||||
self.day,
|
||||
self.recordCount,
|
||||
self.headerLength,
|
||||
self.recordLength) + (b'\x00' * 20)
|
||||
#TODO: figure out if bytes(utf-8) is correct here.
|
||||
|
||||
def setCurrentDate(self):
|
||||
"""Update ``self.lastUpdate`` field with current date value."""
|
||||
self.lastUpdate = datetime.date.today()
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Return a field definition by numeric index or name string"""
|
||||
if isinstance(item, str):
|
||||
_name = item.upper()
|
||||
for _field in self.fields:
|
||||
if _field.name == _name:
|
||||
return _field
|
||||
else:
|
||||
raise KeyError(item)
|
||||
else:
|
||||
# item must be field index
|
||||
return self.fields[item]
|
||||
|
||||
# vim: et sts=4 sw=4 :
|
||||
@@ -1,266 +0,0 @@
|
||||
"""DBF record definition.
|
||||
|
||||
"""
|
||||
"""History (most recent first):
|
||||
11-feb-2007 [als] __repr__: added special case for invalid field values
|
||||
10-feb-2007 [als] added .rawFromStream()
|
||||
30-oct-2006 [als] fix record length in .fromStream()
|
||||
04-jul-2006 [als] added export declaration
|
||||
20-dec-2005 [yc] DbfRecord.write() -> DbfRecord._write();
|
||||
added delete() method.
|
||||
16-dec-2005 [yc] record definition moved from `dbf`.
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.7 $"[11:-2]
|
||||
__date__ = "$Date: 2007/02/11 09:05:49 $"[7:-2]
|
||||
|
||||
__all__ = ["DbfRecord"]
|
||||
|
||||
import sys
|
||||
|
||||
from . import utils
|
||||
|
||||
class DbfRecord(object):
|
||||
"""DBF record.
|
||||
|
||||
Instances of this class shouldn't be created manualy,
|
||||
use `dbf.Dbf.newRecord` instead.
|
||||
|
||||
Class implements mapping/sequence interface, so
|
||||
fields could be accessed via their names or indexes
|
||||
(names is a preffered way to access fields).
|
||||
|
||||
Hint:
|
||||
Use `store` method to save modified record.
|
||||
|
||||
Examples:
|
||||
Add new record to the database:
|
||||
db = Dbf(filename)
|
||||
rec = db.newRecord()
|
||||
rec["FIELD1"] = value1
|
||||
rec["FIELD2"] = value2
|
||||
rec.store()
|
||||
Or the same, but modify existed
|
||||
(second in this case) record:
|
||||
db = Dbf(filename)
|
||||
rec = db[2]
|
||||
rec["FIELD1"] = value1
|
||||
rec["FIELD2"] = value2
|
||||
rec.store()
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = "dbf", "index", "deleted", "fieldData"
|
||||
|
||||
## creation and initialization
|
||||
|
||||
def __init__(self, dbf, index=None, deleted=False, data=None):
|
||||
"""Instance initialiation.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance this record belonogs to.
|
||||
index:
|
||||
An integer record index or None. If this value is
|
||||
None, record will be appended to the DBF.
|
||||
deleted:
|
||||
Boolean flag indicating whether this record
|
||||
is a deleted record.
|
||||
data:
|
||||
A sequence or None. This is a data of the fields.
|
||||
If this argument is None, default values will be used.
|
||||
|
||||
"""
|
||||
self.dbf = dbf
|
||||
# XXX: I'm not sure ``index`` is necessary
|
||||
self.index = index
|
||||
self.deleted = deleted
|
||||
if data is None:
|
||||
self.fieldData = [_fd.defaultValue for _fd in dbf.header.fields]
|
||||
else:
|
||||
self.fieldData = list(data)
|
||||
|
||||
# XXX: validate self.index before calculating position?
|
||||
position = property(lambda self: self.dbf.header.headerLength + \
|
||||
self.index * self.dbf.header.recordLength)
|
||||
|
||||
def rawFromStream(cls, dbf, index):
|
||||
"""Return raw record contents read from the stream.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance containing the record.
|
||||
index:
|
||||
Index of the record in the records' container.
|
||||
This argument can't be None in this call.
|
||||
|
||||
Return value is a string containing record data in DBF format.
|
||||
|
||||
"""
|
||||
# XXX: may be write smth assuming, that current stream
|
||||
# position is the required one? it could save some
|
||||
# time required to calculate where to seek in the file
|
||||
dbf.stream.seek(dbf.header.headerLength +
|
||||
index * dbf.header.recordLength)
|
||||
return dbf.stream.read(dbf.header.recordLength)
|
||||
rawFromStream = classmethod(rawFromStream)
|
||||
|
||||
def fromStream(cls, dbf, index):
|
||||
"""Return a record read from the stream.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance new record should belong to.
|
||||
index:
|
||||
Index of the record in the records' container.
|
||||
This argument can't be None in this call.
|
||||
|
||||
Return value is an instance of the current class.
|
||||
|
||||
"""
|
||||
return cls.fromString(dbf, cls.rawFromStream(dbf, index), index)
|
||||
fromStream = classmethod(fromStream)
|
||||
|
||||
def fromString(cls, dbf, string, index=None):
|
||||
"""Return record read from the string object.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance new record should belong to.
|
||||
string:
|
||||
A string new record should be created from.
|
||||
index:
|
||||
Index of the record in the container. If this
|
||||
argument is None, record will be appended.
|
||||
|
||||
Return value is an instance of the current class.
|
||||
|
||||
"""
|
||||
return cls(dbf, index, string[0]=="*",
|
||||
[_fd.decodeFromRecord(string) for _fd in dbf.header.fields])
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
## object representation
|
||||
|
||||
def __repr__(self):
|
||||
_template = "%%%ds: %%s (%%s)" % max([len(_fld)
|
||||
for _fld in self.dbf.fieldNames])
|
||||
_rv = []
|
||||
for _fld in self.dbf.fieldNames:
|
||||
_val = self[_fld]
|
||||
if _val is utils.INVALID_VALUE:
|
||||
_rv.append(_template %
|
||||
(_fld, "None", "value cannot be decoded"))
|
||||
else:
|
||||
_rv.append(_template % (_fld, _val, type(_val)))
|
||||
return "\n".join(_rv)
|
||||
|
||||
## protected methods
|
||||
|
||||
def _write(self):
|
||||
"""Write data to the dbf stream.
|
||||
|
||||
Note:
|
||||
This isn't a public method, it's better to
|
||||
use 'store' instead publically.
|
||||
Be design ``_write`` method should be called
|
||||
only from the `Dbf` instance.
|
||||
|
||||
|
||||
"""
|
||||
self._validateIndex(False)
|
||||
self.dbf.stream.seek(self.position)
|
||||
self.dbf.stream.write(bytes(self.toString(),
|
||||
sys.getfilesystemencoding()))
|
||||
# FIXME: may be move this write somewhere else?
|
||||
# why we should check this condition for each record?
|
||||
if self.index == len(self.dbf):
|
||||
# this is the last record,
|
||||
# we should write SUB (ASCII 26)
|
||||
self.dbf.stream.write(b"\x1A")
|
||||
|
||||
## utility methods
|
||||
|
||||
def _validateIndex(self, allowUndefined=True, checkRange=False):
|
||||
"""Valid ``self.index`` value.
|
||||
|
||||
If ``allowUndefined`` argument is True functions does nothing
|
||||
in case of ``self.index`` pointing to None object.
|
||||
|
||||
"""
|
||||
if self.index is None:
|
||||
if not allowUndefined:
|
||||
raise ValueError("Index is undefined")
|
||||
elif self.index < 0:
|
||||
raise ValueError("Index can't be negative (%s)" % self.index)
|
||||
elif checkRange and self.index <= self.dbf.header.recordCount:
|
||||
raise ValueError("There are only %d records in the DBF" %
|
||||
self.dbf.header.recordCount)
|
||||
|
||||
## interface methods
|
||||
|
||||
def store(self):
|
||||
"""Store current record in the DBF.
|
||||
|
||||
If ``self.index`` is None, this record will be appended to the
|
||||
records of the DBF this records belongs to; or replaced otherwise.
|
||||
|
||||
"""
|
||||
self._validateIndex()
|
||||
if self.index is None:
|
||||
self.index = len(self.dbf)
|
||||
self.dbf.append(self)
|
||||
else:
|
||||
self.dbf[self.index] = self
|
||||
|
||||
def delete(self):
|
||||
"""Mark method as deleted."""
|
||||
self.deleted = True
|
||||
|
||||
def toString(self):
|
||||
"""Return string packed record values."""
|
||||
# for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData):
|
||||
#
|
||||
|
||||
return "".join([" *"[self.deleted]] + [
|
||||
_def.encodeValue(_dat)
|
||||
for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData)
|
||||
])
|
||||
|
||||
def asList(self):
|
||||
"""Return a flat list of fields.
|
||||
|
||||
Note:
|
||||
Change of the list's values won't change
|
||||
real values stored in this object.
|
||||
|
||||
"""
|
||||
return self.fieldData[:]
|
||||
|
||||
def asDict(self):
|
||||
"""Return a dictionary of fields.
|
||||
|
||||
Note:
|
||||
Change of the dicts's values won't change
|
||||
real values stored in this object.
|
||||
|
||||
"""
|
||||
return dict([_i for _i in zip(self.dbf.fieldNames, self.fieldData)])
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Return value by field name or field index."""
|
||||
if isinstance(key, int):
|
||||
# integer index of the field
|
||||
return self.fieldData[key]
|
||||
# assuming string field name
|
||||
return self.fieldData[self.dbf.indexOfFieldName(key)]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set field value by integer index of the field or string name."""
|
||||
if isinstance(key, int):
|
||||
# integer index of the field
|
||||
return self.fieldData[key]
|
||||
# assuming string field name
|
||||
self.fieldData[self.dbf.indexOfFieldName(key)] = value
|
||||
|
||||
# vim: et sts=4 sw=4 :
|
||||
@@ -1,170 +0,0 @@
|
||||
"""String utilities.
|
||||
|
||||
TODO:
|
||||
- allow strings in getDateTime routine;
|
||||
"""
|
||||
"""History (most recent first):
|
||||
11-feb-2007 [als] added INVALID_VALUE
|
||||
10-feb-2007 [als] allow date strings padded with spaces instead of zeroes
|
||||
20-dec-2005 [yc] handle long objects in getDate/getDateTime
|
||||
16-dec-2005 [yc] created from ``strutil`` module.
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.4 $"[11:-2]
|
||||
__date__ = "$Date: 2007/02/11 08:57:17 $"[7:-2]
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
|
||||
def unzfill(str):
|
||||
"""Return a string without ASCII NULs.
|
||||
|
||||
This function searchers for the first NUL (ASCII 0) occurance
|
||||
and truncates string till that position.
|
||||
|
||||
"""
|
||||
try:
|
||||
return str[:str.index(b'\0')]
|
||||
except ValueError:
|
||||
return str
|
||||
|
||||
|
||||
def getDate(date=None):
|
||||
"""Return `datetime.date` instance.
|
||||
|
||||
Type of the ``date`` argument could be one of the following:
|
||||
None:
|
||||
use current date value;
|
||||
datetime.date:
|
||||
this value will be returned;
|
||||
datetime.datetime:
|
||||
the result of the date.date() will be returned;
|
||||
string:
|
||||
assuming "%Y%m%d" or "%y%m%dd" format;
|
||||
number:
|
||||
assuming it's a timestamp (returned for example
|
||||
by the time.time() call;
|
||||
sequence:
|
||||
assuming (year, month, day, ...) sequence;
|
||||
|
||||
Additionaly, if ``date`` has callable ``ticks`` attribute,
|
||||
it will be used and result of the called would be treated
|
||||
as a timestamp value.
|
||||
|
||||
"""
|
||||
if date is None:
|
||||
# use current value
|
||||
return datetime.date.today()
|
||||
if isinstance(date, datetime.date):
|
||||
return date
|
||||
if isinstance(date, datetime.datetime):
|
||||
return date.date()
|
||||
if isinstance(date, (int, float)):
|
||||
# date is a timestamp
|
||||
return datetime.date.fromtimestamp(date)
|
||||
if isinstance(date, str):
|
||||
date = date.replace(" ", "0")
|
||||
if len(date) == 6:
|
||||
# yymmdd
|
||||
return datetime.date(*time.strptime(date, "%y%m%d")[:3])
|
||||
# yyyymmdd
|
||||
return datetime.date(*time.strptime(date, "%Y%m%d")[:3])
|
||||
if hasattr(date, "__getitem__"):
|
||||
# a sequence (assuming date/time tuple)
|
||||
return datetime.date(*date[:3])
|
||||
return datetime.date.fromtimestamp(date.ticks())
|
||||
|
||||
|
||||
def getDateTime(value=None):
|
||||
"""Return `datetime.datetime` instance.
|
||||
|
||||
Type of the ``value`` argument could be one of the following:
|
||||
None:
|
||||
use current date value;
|
||||
datetime.date:
|
||||
result will be converted to the `datetime.datetime` instance
|
||||
using midnight;
|
||||
datetime.datetime:
|
||||
``value`` will be returned as is;
|
||||
string:
|
||||
*** CURRENTLY NOT SUPPORTED ***;
|
||||
number:
|
||||
assuming it's a timestamp (returned for example
|
||||
by the time.time() call;
|
||||
sequence:
|
||||
assuming (year, month, day, ...) sequence;
|
||||
|
||||
Additionaly, if ``value`` has callable ``ticks`` attribute,
|
||||
it will be used and result of the called would be treated
|
||||
as a timestamp value.
|
||||
|
||||
"""
|
||||
if value is None:
|
||||
# use current value
|
||||
return datetime.datetime.today()
|
||||
if isinstance(value, datetime.datetime):
|
||||
return value
|
||||
if isinstance(value, datetime.date):
|
||||
return datetime.datetime.fromordinal(value.toordinal())
|
||||
if isinstance(value, (int, float)):
|
||||
# value is a timestamp
|
||||
return datetime.datetime.fromtimestamp(value)
|
||||
if isinstance(value, str):
|
||||
raise NotImplementedError("Strings aren't currently implemented")
|
||||
if hasattr(value, "__getitem__"):
|
||||
# a sequence (assuming date/time tuple)
|
||||
return datetime.datetime(*tuple(value)[:6])
|
||||
return datetime.datetime.fromtimestamp(value.ticks())
|
||||
|
||||
|
||||
class classproperty(property):
|
||||
"""Works in the same way as a ``property``, but for the classes."""
|
||||
|
||||
def __get__(self, obj, cls):
|
||||
return self.fget(cls)
|
||||
|
||||
|
||||
class _InvalidValue(object):
|
||||
|
||||
"""Value returned from DBF records when field validation fails
|
||||
|
||||
The value is not equal to anything except for itself
|
||||
and equal to all empty values: None, 0, empty string etc.
|
||||
In other words, invalid value is equal to None and not equal
|
||||
to None at the same time.
|
||||
|
||||
This value yields zero upon explicit conversion to a number type,
|
||||
empty string for string types, and False for boolean.
|
||||
|
||||
"""
|
||||
|
||||
def __eq__(self, other):
|
||||
return not other
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (other is self)
|
||||
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __int__(self):
|
||||
return 0
|
||||
__long__ = __int__
|
||||
|
||||
def __float__(self):
|
||||
return 0.0
|
||||
|
||||
def __str__(self):
|
||||
return ""
|
||||
|
||||
def __unicode__(self):
|
||||
return ""
|
||||
|
||||
def __repr__(self):
|
||||
return "<INVALID>"
|
||||
|
||||
# invalid value is a constant singleton
|
||||
INVALID_VALUE = _InvalidValue()
|
||||
|
||||
# vim: set et sts=4 sw=4 :
|
||||
@@ -1,24 +0,0 @@
|
||||
from __future__ import division
|
||||
|
||||
|
||||
def median(data):
|
||||
"""
|
||||
Return the median (middle value) of numeric data, using the common
|
||||
"mean of middle two" method. If data is empty, ValueError is raised.
|
||||
|
||||
Mimics the behaviour of Python3's statistics.median
|
||||
|
||||
>>> median([1, 3, 5])
|
||||
3
|
||||
>>> median([1, 3, 5, 7])
|
||||
4.0
|
||||
|
||||
"""
|
||||
data = sorted(data)
|
||||
n = len(data)
|
||||
if not n:
|
||||
raise ValueError("No median for empty data")
|
||||
i = n // 2
|
||||
if n % 2:
|
||||
return data[i]
|
||||
return (data[i - 1] + data[i]) / 2
|
||||
@@ -0,0 +1,13 @@
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
|
||||
def normalize_input(stream):
|
||||
"""
|
||||
Accept either a str/bytes stream or a file-like object and always return a
|
||||
file-like object.
|
||||
"""
|
||||
if isinstance(stream, str):
|
||||
return StringIO(stream)
|
||||
elif isinstance(stream, bytes):
|
||||
return BytesIO(stream)
|
||||
return stream
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,10 +1,10 @@
|
||||
pytest
|
||||
pytest-cov
|
||||
backports.csv; python_version < '3.0'
|
||||
MarkupPy
|
||||
odfpy
|
||||
openpyxl>=2.4.0
|
||||
pandas
|
||||
pyyaml
|
||||
tabulate
|
||||
xlrd
|
||||
xlwt
|
||||
|
||||
+293
-66
@@ -1,20 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for Tablib."""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import doctest
|
||||
import json
|
||||
import sys
|
||||
import pickle
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
from io import BytesIO, StringIO
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from MarkupPy import markup
|
||||
import tablib
|
||||
from tablib.compat import unicode, is_py3
|
||||
from MarkupPy import markup
|
||||
from tablib.core import Row, detect_format
|
||||
from tablib.formats import _csv as csv_module
|
||||
from tablib.exceptions import UnsupportedFormat
|
||||
from tablib.formats import registry
|
||||
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
@@ -50,6 +51,16 @@ class TablibTestCase(BaseTestCase):
|
||||
continue
|
||||
dataset.export(format_)
|
||||
|
||||
def test_unknown_format(self):
|
||||
with self.assertRaises(UnsupportedFormat):
|
||||
data.export('??')
|
||||
# A known format but uninstalled
|
||||
del registry._formats['ods']
|
||||
msg = (r"The 'ods' format is not available. You may want to install the "
|
||||
"odfpy package \\(or `pip install tablib\\[ods\\]`\\).")
|
||||
with self.assertRaisesRegex(UnsupportedFormat, msg):
|
||||
data.export('ods')
|
||||
|
||||
def test_empty_append(self):
|
||||
"""Verify append() correctly adds tuple with no headers."""
|
||||
new_row = (1, 2, 3)
|
||||
@@ -158,7 +169,8 @@ class TablibTestCase(BaseTestCase):
|
||||
def test_add_callable_column(self):
|
||||
"""Verify adding column with values specified as callable."""
|
||||
|
||||
new_col = lambda x: x[0]
|
||||
def new_col(x):
|
||||
return x[0]
|
||||
|
||||
self.founders.append_col(new_col, header='first_again')
|
||||
|
||||
@@ -206,7 +218,7 @@ class TablibTestCase(BaseTestCase):
|
||||
self.assertEqual(self.founders[2:], [self.tom])
|
||||
|
||||
def test_row_slicing(self):
|
||||
"""Verify Row's __getslice__ method. Issue #184."""
|
||||
"""Verify Row slicing. Issue #184."""
|
||||
|
||||
john = Row(self.john)
|
||||
|
||||
@@ -251,10 +263,7 @@ class TablibTestCase(BaseTestCase):
|
||||
def test_unicode_append(self):
|
||||
"""Passes in a single unicode character and exports."""
|
||||
|
||||
if is_py3:
|
||||
new_row = ('å', 'é')
|
||||
else:
|
||||
exec ("new_row = (u'å', u'é')")
|
||||
new_row = ('å', 'é')
|
||||
|
||||
data.append(new_row)
|
||||
self._test_export_data_in_all_formats(data)
|
||||
@@ -286,6 +295,27 @@ class TablibTestCase(BaseTestCase):
|
||||
unsupported = ['csv', 'tsv', 'jira', 'latex', 'df']
|
||||
self._test_export_data_in_all_formats(book, exclude=unsupported)
|
||||
|
||||
def test_book_unsupported_loading(self):
|
||||
with self.assertRaises(UnsupportedFormat):
|
||||
tablib.Databook().load('Any stream', 'csv')
|
||||
|
||||
def test_book_unsupported_export(self):
|
||||
book = tablib.Databook().load('[{"title": "first", "data": [{"first_name": "John"}]}]', 'json')
|
||||
with self.assertRaises(UnsupportedFormat):
|
||||
book.export('csv')
|
||||
|
||||
def test_book_import_from_file(self):
|
||||
xlsx_source = Path(__file__).parent / 'files' / 'founders.xlsx'
|
||||
with open(str(xlsx_source), mode='rb') as fh:
|
||||
book = tablib.Databook().load(fh, 'xlsx')
|
||||
self.assertEqual(eval(book.json)[0]['title'], 'Feuille1')
|
||||
|
||||
def test_dataset_import_from_file(self):
|
||||
xlsx_source = Path(__file__).parent / 'files' / 'founders.xlsx'
|
||||
with open(str(xlsx_source), mode='rb') as fh:
|
||||
dset = tablib.Dataset().load(fh, 'xlsx')
|
||||
self.assertEqual(eval(dset.json)[0]['last_name'], 'Adams')
|
||||
|
||||
def test_auto_format_detect(self):
|
||||
"""Test auto format detection."""
|
||||
# html, jira, latex, rst are export only.
|
||||
@@ -314,7 +344,9 @@ class TablibTestCase(BaseTestCase):
|
||||
_tsv = '1\t2\t3\n4\t5\t6\n7\t8\t9\n'
|
||||
self.assertEqual(tablib.detect_format(_tsv), 'tsv')
|
||||
|
||||
_bunk = '¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
|
||||
_bunk = StringIO(
|
||||
'¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
|
||||
)
|
||||
self.assertEqual(tablib.detect_format(_bunk), None)
|
||||
|
||||
def test_transpose(self):
|
||||
@@ -458,10 +490,15 @@ class TablibTestCase(BaseTestCase):
|
||||
# add another entry to test right field width for
|
||||
# integer
|
||||
self.founders.append(('Old', 'Man', 100500))
|
||||
self.assertEqual('first_name|last_name |gpa ', unicode(self.founders).split('\n')[0])
|
||||
self.assertEqual('first_name|last_name |gpa ', str(self.founders).split('\n')[0])
|
||||
|
||||
def test_pickle_unpickle_dataset(self):
|
||||
before_pickle = self.founders.export('json')
|
||||
founders = pickle.loads(pickle.dumps(self.founders))
|
||||
self.assertEqual(founders.export('json'), before_pickle)
|
||||
|
||||
def test_databook_add_sheet_accepts_only_dataset_instances(self):
|
||||
class NotDataset(object):
|
||||
class NotDataset:
|
||||
def append(self, item):
|
||||
pass
|
||||
|
||||
@@ -493,16 +530,86 @@ class TablibTestCase(BaseTestCase):
|
||||
self.founders.append(('First\nSecond', 'Name', 42))
|
||||
self.founders.export('xlsx')
|
||||
|
||||
def test_rst_force_grid(self):
|
||||
data.append(self.john)
|
||||
data.append(self.george)
|
||||
data.headers = self.headers
|
||||
def test_row_repr(self):
|
||||
"""Row repr."""
|
||||
# Arrange
|
||||
john = Row(self.john)
|
||||
|
||||
simple = tablib.formats._rst.export_set(data)
|
||||
grid = tablib.formats._rst.export_set(data, force_grid=True)
|
||||
self.assertNotEqual(simple, grid)
|
||||
self.assertNotIn('+', simple)
|
||||
self.assertIn('+', grid)
|
||||
# Act
|
||||
output = str(john)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(output, "['John', 'Adams', 90]")
|
||||
|
||||
def test_row_pickle_unpickle(self):
|
||||
"""Row __setstate__ and __getstate__."""
|
||||
# Arrange
|
||||
before_pickle = Row(self.john)
|
||||
|
||||
# Act
|
||||
output = pickle.loads(pickle.dumps(before_pickle))
|
||||
|
||||
# Assert
|
||||
self.assertEqual(output[0], before_pickle[0])
|
||||
self.assertEqual(output[1], before_pickle[1])
|
||||
self.assertEqual(output[2], before_pickle[2])
|
||||
|
||||
def test_row_lpush(self):
|
||||
"""Row lpush."""
|
||||
# Arrange
|
||||
john = Row(self.john)
|
||||
george = Row(self.george)
|
||||
|
||||
# Act
|
||||
john.lpush(george)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(john[-1], george)
|
||||
|
||||
def test_row_append(self):
|
||||
"""Row append."""
|
||||
# Arrange
|
||||
john = Row(self.john)
|
||||
george = Row(self.george)
|
||||
|
||||
# Act
|
||||
john.append(george)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(john[0], george)
|
||||
|
||||
def test_row_contains(self):
|
||||
"""Row __contains__."""
|
||||
# Arrange
|
||||
john = Row(self.john)
|
||||
|
||||
# Act / Assert
|
||||
self.assertIn("John", john)
|
||||
|
||||
def test_row_no_tag(self):
|
||||
"""Row has_tag."""
|
||||
# Arrange
|
||||
john = Row(self.john)
|
||||
|
||||
# Act / Assert
|
||||
self.assertFalse(john.has_tag("not found"))
|
||||
self.assertFalse(john.has_tag(None))
|
||||
|
||||
def test_row_has_tag(self):
|
||||
"""Row has_tag."""
|
||||
# Arrange
|
||||
john = Row(self.john, tags=["tag1"])
|
||||
|
||||
# Act / Assert
|
||||
self.assertTrue(john.has_tag("tag1"))
|
||||
|
||||
def test_row_has_tags(self):
|
||||
"""Row has_tag."""
|
||||
# Arrange
|
||||
john = Row(self.john, tags=["tag1", "tag2"])
|
||||
|
||||
# Act / Assert
|
||||
self.assertTrue(john.has_tag(["tag2", "tag1"]))
|
||||
|
||||
|
||||
class HTMLTests(BaseTestCase):
|
||||
@@ -539,27 +646,80 @@ class HTMLTests(BaseTestCase):
|
||||
html.table.close()
|
||||
html = str(html)
|
||||
|
||||
headers = ['foo', None, 'bar'];
|
||||
headers = ['foo', None, 'bar']
|
||||
d = tablib.Dataset(['foo', None, 'bar'], headers=headers)
|
||||
|
||||
self.assertEqual(html, d.html)
|
||||
|
||||
|
||||
class RSTTests(BaseTestCase):
|
||||
def test_rst_force_grid(self):
|
||||
data = tablib.Dataset()
|
||||
data.append(self.john)
|
||||
data.append(self.george)
|
||||
data.headers = self.headers
|
||||
|
||||
fmt = registry.get_format('rst')
|
||||
simple = fmt.export_set(data)
|
||||
grid = fmt.export_set(data, force_grid=True)
|
||||
self.assertNotEqual(simple, grid)
|
||||
self.assertNotIn('+', simple)
|
||||
self.assertIn('+', grid)
|
||||
|
||||
def test_empty_string(self):
|
||||
data = tablib.Dataset()
|
||||
data.headers = self.headers
|
||||
data.append(self.john)
|
||||
data.append(('Wendy', '', 43))
|
||||
self.assertEqual(
|
||||
data.export('rst'),
|
||||
'========== ========= ===\n'
|
||||
'first_name last_name gpa\n'
|
||||
'========== ========= ===\n'
|
||||
'John Adams 90 \n'
|
||||
'Wendy 43 \n'
|
||||
'========== ========= ==='
|
||||
)
|
||||
|
||||
def test_rst_export_set(self):
|
||||
# Arrange
|
||||
data = tablib.Dataset()
|
||||
data.append(self.john)
|
||||
data.headers = self.headers
|
||||
fmt = registry.get_format("rst")
|
||||
|
||||
# Act
|
||||
out1 = fmt.export_set(data)
|
||||
out2 = fmt.export_set_as_simple_table(data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(out1, out2)
|
||||
self.assertEqual(
|
||||
out1,
|
||||
"========== ========= ===\n"
|
||||
"first_name last_name gpa\n"
|
||||
"========== ========= ===\n"
|
||||
"John Adams 90 \n"
|
||||
"========== ========= ===",
|
||||
)
|
||||
|
||||
|
||||
class CSVTests(BaseTestCase):
|
||||
def test_csv_format_detect(self):
|
||||
"""Test CSV format detection."""
|
||||
|
||||
_csv = (
|
||||
_csv = StringIO(
|
||||
'1,2,3\n'
|
||||
'4,5,6\n'
|
||||
'7,8,9\n'
|
||||
)
|
||||
_bunk = (
|
||||
_bunk = StringIO(
|
||||
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
|
||||
)
|
||||
|
||||
self.assertTrue(tablib.formats.csv.detect(_csv))
|
||||
self.assertFalse(tablib.formats.csv.detect(_bunk))
|
||||
fmt = registry.get_format('csv')
|
||||
self.assertTrue(fmt.detect(_csv))
|
||||
self.assertFalse(fmt.detect(_bunk))
|
||||
|
||||
def test_csv_import_set(self):
|
||||
"""Generate and import CSV set serialization."""
|
||||
@@ -625,6 +785,16 @@ class CSVTests(BaseTestCase):
|
||||
|
||||
self.assertEqual(_csv, data.csv)
|
||||
|
||||
def test_csv_import_set_commas_embedded(self):
|
||||
"""Comma-separated CSV can include commas inside quoted string."""
|
||||
csv_text = (
|
||||
'id,name,description,count\r\n'
|
||||
'12,Smith,"Red, rounded",4\r\n'
|
||||
)
|
||||
data.csv = csv_text
|
||||
self.assertEqual(data[0][2], 'Red, rounded')
|
||||
self.assertEqual(data.csv, csv_text)
|
||||
|
||||
def test_csv_import_set_with_unicode_str(self):
|
||||
"""Import CSV set with non-ascii characters in unicode literal"""
|
||||
csv_text = (
|
||||
@@ -651,6 +821,12 @@ class CSVTests(BaseTestCase):
|
||||
|
||||
self.assertEqual(csv, self.founders.csv)
|
||||
|
||||
def test_csv_export_options(self):
|
||||
"""Exporting support csv.writer() parameters."""
|
||||
data.append(('1. a', '2. b', '3. c'))
|
||||
result = data.export('csv', delimiter=' ', quotechar='|')
|
||||
self.assertEqual(result, '|1. a| |2. b| |3. c|\r\n')
|
||||
|
||||
def test_csv_stream_export(self):
|
||||
"""Verify exporting dataset object as CSV from file object."""
|
||||
|
||||
@@ -666,7 +842,8 @@ class CSVTests(BaseTestCase):
|
||||
csv += str(col) + ','
|
||||
csv = csv.strip(',') + '\r\n'
|
||||
|
||||
csv_stream = csv_module.export_stream_set(self.founders)
|
||||
frm = registry.get_format('csv')
|
||||
csv_stream = frm.export_stream_set(self.founders)
|
||||
self.assertEqual(csv, csv_stream.getvalue())
|
||||
|
||||
def test_unicode_csv(self):
|
||||
@@ -674,10 +851,7 @@ class CSVTests(BaseTestCase):
|
||||
|
||||
data = tablib.Dataset()
|
||||
|
||||
if sys.version_info[0] > 2:
|
||||
data.append(['\xfc', '\xfd'])
|
||||
else:
|
||||
exec ("data.append([u'\xfc', u'\xfd'])")
|
||||
data.append(['\xfc', '\xfd'])
|
||||
|
||||
data.csv
|
||||
|
||||
@@ -688,7 +862,7 @@ class CSVTests(BaseTestCase):
|
||||
data.csv = self.founders.csv
|
||||
|
||||
headers = data.headers
|
||||
self.assertTrue(isinstance(headers[0], unicode))
|
||||
self.assertTrue(isinstance(headers[0], str))
|
||||
|
||||
orig_first_name = self.founders[self.headers[0]]
|
||||
csv_first_name = data[headers[0]]
|
||||
@@ -701,7 +875,7 @@ class CSVTests(BaseTestCase):
|
||||
data.csv = self.founders.csv
|
||||
|
||||
target_header = data.headers[0]
|
||||
self.assertTrue(isinstance(target_header, unicode))
|
||||
self.assertTrue(isinstance(target_header, str))
|
||||
|
||||
del data[target_header]
|
||||
|
||||
@@ -757,17 +931,18 @@ class TSVTests(BaseTestCase):
|
||||
def test_tsv_format_detect(self):
|
||||
"""Test TSV format detection."""
|
||||
|
||||
_tsv = (
|
||||
_tsv = StringIO(
|
||||
'1\t2\t3\n'
|
||||
'4\t5\t6\n'
|
||||
'7\t8\t9\n'
|
||||
)
|
||||
_bunk = (
|
||||
_bunk = StringIO(
|
||||
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
|
||||
)
|
||||
|
||||
self.assertTrue(tablib.formats.tsv.detect(_tsv))
|
||||
self.assertFalse(tablib.formats.tsv.detect(_bunk))
|
||||
fmt = registry.get_format('tsv')
|
||||
self.assertTrue(fmt.detect(_tsv))
|
||||
self.assertFalse(fmt.detect(_bunk))
|
||||
|
||||
def test_tsv_export(self):
|
||||
"""Verify exporting dataset object as TSV."""
|
||||
@@ -787,17 +962,34 @@ class TSVTests(BaseTestCase):
|
||||
self.assertEqual(tsv, self.founders.tsv)
|
||||
|
||||
|
||||
class XLSTests(BaseTestCase):
|
||||
def test_xls_format_detect(self):
|
||||
"""Test the XLS format detection."""
|
||||
in_stream = self.founders.xls
|
||||
self.assertEqual(detect_format(in_stream), 'xls')
|
||||
|
||||
def test_xls_import_with_errors(self):
|
||||
"""Errors from imported files are kept as errors."""
|
||||
xls_source = Path(__file__).parent / 'files' / 'errors.xls'
|
||||
with xls_source.open('rb') as fh:
|
||||
data = tablib.Dataset().load(fh.read())
|
||||
self.assertEqual(
|
||||
data.dict[0],
|
||||
OrderedDict([
|
||||
('div by 0', '#DIV/0!'),
|
||||
('name unknown', '#NAME?'),
|
||||
('not available (formula)', '#N/A'),
|
||||
('not available (static)', '#N/A')
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
class XLSXTests(BaseTestCase):
|
||||
def test_xlsx_format_detect(self):
|
||||
"""Test the XLSX format detection."""
|
||||
in_stream = self.founders.xlsx
|
||||
self.assertEqual(detect_format(in_stream), 'xlsx')
|
||||
|
||||
def test_xls_format_detect(self):
|
||||
"""Test the XLS format detection."""
|
||||
in_stream = self.founders.xls
|
||||
self.assertEqual(detect_format(in_stream), 'xls')
|
||||
|
||||
def test_xlsx_import_set(self):
|
||||
date_time = datetime.datetime(2019, 10, 4, 12, 30, 8)
|
||||
data.append(('string', '004', 42, 21.55, date_time))
|
||||
@@ -823,13 +1015,14 @@ class JSONTests(BaseTestCase):
|
||||
def test_json_format_detect(self):
|
||||
"""Test JSON format detection."""
|
||||
|
||||
_json = '[{"last_name": "Adams","age": 90,"first_name": "John"}]'
|
||||
_bunk = (
|
||||
_json = StringIO('[{"last_name": "Adams","age": 90,"first_name": "John"}]')
|
||||
_bunk = StringIO(
|
||||
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
|
||||
)
|
||||
|
||||
self.assertTrue(tablib.formats.json.detect(_json))
|
||||
self.assertFalse(tablib.formats.json.detect(_bunk))
|
||||
fmt = registry.get_format('json')
|
||||
self.assertTrue(fmt.detect(_json))
|
||||
self.assertFalse(fmt.detect(_bunk))
|
||||
|
||||
def test_json_import_book(self):
|
||||
"""Generate and import JSON book serialization."""
|
||||
@@ -883,12 +1076,14 @@ class YAMLTests(BaseTestCase):
|
||||
_yaml = '- {age: 90, first_name: John, last_name: Adams}'
|
||||
_tsv = 'foo\tbar'
|
||||
_bunk = (
|
||||
'¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
|
||||
'¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†'
|
||||
'ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
|
||||
)
|
||||
|
||||
self.assertTrue(tablib.formats.yaml.detect(_yaml))
|
||||
self.assertFalse(tablib.formats.yaml.detect(_bunk))
|
||||
self.assertFalse(tablib.formats.yaml.detect(_tsv))
|
||||
fmt = registry.get_format('yaml')
|
||||
self.assertTrue(fmt.detect(_yaml))
|
||||
self.assertFalse(fmt.detect(_bunk))
|
||||
self.assertFalse(fmt.detect(_tsv))
|
||||
|
||||
def test_yaml_import_book(self):
|
||||
"""Generate and import YAML book serialization."""
|
||||
@@ -918,6 +1113,17 @@ class YAMLTests(BaseTestCase):
|
||||
|
||||
self.assertEqual(_yaml, data.yaml)
|
||||
|
||||
def test_yaml_export(self):
|
||||
"""YAML export"""
|
||||
|
||||
expected = """\
|
||||
- {first_name: John, gpa: 90, last_name: Adams}
|
||||
- {first_name: George, gpa: 67, last_name: Washington}
|
||||
- {first_name: Thomas, gpa: 50, last_name: Jefferson}
|
||||
"""
|
||||
output = self.founders.yaml
|
||||
self.assertEqual(output, expected)
|
||||
|
||||
|
||||
class LatexTests(BaseTestCase):
|
||||
def test_latex_export(self):
|
||||
@@ -994,7 +1200,7 @@ class DBFTests(BaseTestCase):
|
||||
for reg_char, data_char in zip(_dbf, data.dbf):
|
||||
so_far += chr(data_char)
|
||||
if reg_char != data_char and index not in [1, 2, 3]:
|
||||
raise AssertionError('Failing at char %s: %s vs %s %s' % (
|
||||
raise AssertionError('Failing at char {}: {} vs {} {}'.format(
|
||||
index, reg_char, data_char, so_far))
|
||||
index += 1
|
||||
|
||||
@@ -1024,11 +1230,9 @@ class DBFTests(BaseTestCase):
|
||||
_regression_dbf += b' 50.0000000'
|
||||
_regression_dbf += b'\x1a'
|
||||
|
||||
if is_py3:
|
||||
# If in python3, decode regression string to binary.
|
||||
# _regression_dbf = bytes(_regression_dbf, 'utf-8')
|
||||
# _regression_dbf = _regression_dbf.replace(b'\n', b'\r')
|
||||
pass
|
||||
# If in python3, decode regression string to binary.
|
||||
# _regression_dbf = bytes(_regression_dbf, 'utf-8')
|
||||
# _regression_dbf = _regression_dbf.replace(b'\n', b'\r')
|
||||
|
||||
try:
|
||||
self.assertEqual(_regression_dbf, data.dbf)
|
||||
@@ -1039,7 +1243,7 @@ class DBFTests(BaseTestCase):
|
||||
# found_so_far += chr(data_char)
|
||||
if reg_char != data_char and index not in [1, 2, 3]:
|
||||
raise AssertionError(
|
||||
'Failing at char %s: %s vs %s (found %s)' % (
|
||||
'Failing at char {}: {} vs {} (found {})'.format(
|
||||
index, reg_char, data_char, found_so_far))
|
||||
index += 1
|
||||
|
||||
@@ -1063,6 +1267,7 @@ class DBFTests(BaseTestCase):
|
||||
_dbf += b' Jefferson' + (b' ' * 70)
|
||||
_dbf += b' 50.0000000'
|
||||
_dbf += b'\x1a'
|
||||
_dbf = BytesIO(_dbf)
|
||||
|
||||
_yaml = '- {age: 90, first_name: John, last_name: Adams}'
|
||||
_tsv = 'foo\tbar'
|
||||
@@ -1072,12 +1277,13 @@ class DBFTests(BaseTestCase):
|
||||
_bunk = (
|
||||
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
|
||||
)
|
||||
self.assertTrue(tablib.formats.dbf.detect(_dbf))
|
||||
self.assertFalse(tablib.formats.dbf.detect(_yaml))
|
||||
self.assertFalse(tablib.formats.dbf.detect(_tsv))
|
||||
self.assertFalse(tablib.formats.dbf.detect(_csv))
|
||||
self.assertFalse(tablib.formats.dbf.detect(_json))
|
||||
self.assertFalse(tablib.formats.dbf.detect(_bunk))
|
||||
fmt = registry.get_format('dbf')
|
||||
self.assertTrue(fmt.detect(_dbf))
|
||||
self.assertFalse(fmt.detect(_yaml))
|
||||
self.assertFalse(fmt.detect(_tsv))
|
||||
self.assertFalse(fmt.detect(_csv))
|
||||
self.assertFalse(fmt.detect(_json))
|
||||
self.assertFalse(fmt.detect(_bunk))
|
||||
|
||||
|
||||
class JiraTests(BaseTestCase):
|
||||
@@ -1101,5 +1307,26 @@ class JiraTests(BaseTestCase):
|
||||
class DocTests(unittest.TestCase):
|
||||
|
||||
def test_rst_formatter_doctests(self):
|
||||
import tablib.formats._rst
|
||||
results = doctest.testmod(tablib.formats._rst)
|
||||
self.assertEqual(results.failed, 0)
|
||||
|
||||
|
||||
class CliTests(BaseTestCase):
|
||||
def test_cli_export_github(self):
|
||||
self.assertEqual(
|
||||
'|---|---|---|\n| a | b | c |',
|
||||
tablib.Dataset(['a', 'b', 'c']).export('cli', tablefmt='github')
|
||||
)
|
||||
|
||||
def test_cli_export_simple(self):
|
||||
self.assertEqual(
|
||||
'- - -\na b c\n- - -',
|
||||
tablib.Dataset(['a', 'b', 'c']).export('cli', tablefmt='simple')
|
||||
)
|
||||
|
||||
def test_cli_export_grid(self):
|
||||
self.assertEqual(
|
||||
'+---+---+---+\n| a | b | c |\n+---+---+---+',
|
||||
tablib.Dataset(['a', 'b', 'c']).export('cli', tablefmt='grid')
|
||||
)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python
|
||||
"""Tests for tablib.packages.dbfpy."""
|
||||
|
||||
import unittest
|
||||
|
||||
from tablib.packages.dbfpy import fields
|
||||
|
||||
|
||||
class DbfFieldDefTestCompareCase(unittest.TestCase):
|
||||
"""dbfpy.fields.DbfFieldDef comparison test cases, via child classes."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.length = 10
|
||||
self.a = fields.DbfCharacterFieldDef("abc", self.length)
|
||||
self.z = fields.DbfCharacterFieldDef("xyz", self.length)
|
||||
self.a2 = fields.DbfCharacterFieldDef("abc", self.length)
|
||||
|
||||
def test_compare__eq__(self):
|
||||
# Act / Assert
|
||||
self.assertEqual(self.a, self.a2)
|
||||
|
||||
def test_compare__ne__(self):
|
||||
# Act / Assert
|
||||
self.assertNotEqual(self.a, self.z)
|
||||
|
||||
def test_compare__lt__(self):
|
||||
# Act / Assert
|
||||
self.assertLess(self.a, self.z)
|
||||
|
||||
def test_compare__le__(self):
|
||||
# Act / Assert
|
||||
self.assertLessEqual(self.a, self.a2)
|
||||
self.assertLessEqual(self.a, self.z)
|
||||
|
||||
def test_compare__gt__(self):
|
||||
# Act / Assert
|
||||
self.assertGreater(self.z, self.a)
|
||||
|
||||
def test_compare__ge__(self):
|
||||
# Act / Assert
|
||||
self.assertGreaterEqual(self.a2, self.a)
|
||||
self.assertGreaterEqual(self.z, self.a)
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python
|
||||
"""Tests for tablib.packages.dbfpy."""
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
from tablib.packages.dbfpy import utils
|
||||
|
||||
|
||||
class UtilsUnzfillTestCase(unittest.TestCase):
|
||||
"""dbfpy.utils.unzfill test cases."""
|
||||
|
||||
def test_unzfill_with_nul(self):
|
||||
# Arrange
|
||||
text = b"abc\0xyz"
|
||||
|
||||
# Act
|
||||
output = utils.unzfill(text)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(output, b"abc")
|
||||
|
||||
def test_unzfill_without_nul(self):
|
||||
# Arrange
|
||||
text = b"abcxyz"
|
||||
|
||||
# Act
|
||||
output = utils.unzfill(text)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(output, b"abcxyz")
|
||||
|
||||
|
||||
class UtilsGetDateTestCase(unittest.TestCase):
|
||||
"""dbfpy.utils.getDate test cases."""
|
||||
|
||||
def test_getDate_none(self):
|
||||
# Arrange
|
||||
value = None
|
||||
|
||||
# Act
|
||||
output = utils.getDate(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.date)
|
||||
|
||||
def test_getDate_datetime_date(self):
|
||||
# Arrange
|
||||
value = datetime.date(2019, 10, 19)
|
||||
|
||||
# Act
|
||||
output = utils.getDate(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.date)
|
||||
self.assertEqual(output, value)
|
||||
|
||||
def test_getDate_datetime_datetime(self):
|
||||
# Arrange
|
||||
value = datetime.datetime(2019, 10, 19, 12, 00, 00)
|
||||
|
||||
# Act
|
||||
output = utils.getDate(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.date)
|
||||
self.assertEqual(output, value)
|
||||
|
||||
def test_getDate_datetime_timestamp(self):
|
||||
# Arrange
|
||||
value = 1571515306
|
||||
|
||||
# Act
|
||||
output = utils.getDate(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.date)
|
||||
self.assertEqual(output, datetime.date(2019, 10, 19))
|
||||
|
||||
def test_getDate_datetime_string_yyyy_mm_dd(self):
|
||||
# Arrange
|
||||
value = "20191019"
|
||||
|
||||
# Act
|
||||
output = utils.getDate(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.date)
|
||||
self.assertEqual(output, datetime.date(2019, 10, 19))
|
||||
|
||||
def test_getDate_datetime_string_yymmdd(self):
|
||||
# Arrange
|
||||
value = "191019"
|
||||
|
||||
# Act
|
||||
output = utils.getDate(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.date)
|
||||
self.assertEqual(output, datetime.date(2019, 10, 19))
|
||||
|
||||
|
||||
class UtilsGetDateTimeTestCase(unittest.TestCase):
|
||||
"""dbfpy.utils.getDateTime test cases."""
|
||||
|
||||
def test_getDateTime_none(self):
|
||||
# Arrange
|
||||
value = None
|
||||
|
||||
# Act
|
||||
output = utils.getDateTime(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.datetime)
|
||||
|
||||
def test_getDateTime_datetime_datetime(self):
|
||||
# Arrange
|
||||
value = datetime.datetime(2019, 10, 19, 12, 00, 00)
|
||||
|
||||
# Act
|
||||
output = utils.getDateTime(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.date)
|
||||
self.assertEqual(output, value)
|
||||
|
||||
def test_getDateTime_datetime_date(self):
|
||||
# Arrange
|
||||
value = datetime.date(2019, 10, 19)
|
||||
|
||||
# Act
|
||||
output = utils.getDateTime(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.date)
|
||||
self.assertEqual(output, datetime.datetime(2019, 10, 19, 00, 00))
|
||||
|
||||
def test_getDateTime_datetime_timestamp(self):
|
||||
# Arrange
|
||||
value = 1571515306
|
||||
|
||||
# Act
|
||||
output = utils.getDateTime(value)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(output, datetime.datetime)
|
||||
|
||||
def test_getDateTime_datetime_string(self):
|
||||
# Arrange
|
||||
value = "20191019"
|
||||
|
||||
# Act / Assert
|
||||
with self.assertRaises(NotImplementedError):
|
||||
output = utils.getDateTime(value)
|
||||
|
||||
|
||||
class InvalidValueTestCase(unittest.TestCase):
|
||||
"""dbfpy.utils._InvalidValue test cases."""
|
||||
|
||||
def test_sanity(self):
|
||||
# Arrange
|
||||
INVALID_VALUE = utils.INVALID_VALUE
|
||||
|
||||
# Act / Assert
|
||||
self.assertEqual(INVALID_VALUE, INVALID_VALUE)
|
||||
self.assertNotEqual(INVALID_VALUE, 123)
|
||||
self.assertEqual(int(INVALID_VALUE), 0)
|
||||
self.assertEqual(float(INVALID_VALUE), 0.0)
|
||||
self.assertEqual(str(INVALID_VALUE), "")
|
||||
self.assertEqual(repr(INVALID_VALUE), "<INVALID>")
|
||||
@@ -2,16 +2,11 @@
|
||||
usedevelop = true
|
||||
minversion = 2.4
|
||||
envlist =
|
||||
py{27,35,36,37,38}-tests,
|
||||
py37-{docs,lint}
|
||||
docs
|
||||
lint
|
||||
py{35,36,37,38}
|
||||
|
||||
[testenv]
|
||||
basepython =
|
||||
py27: python2.7
|
||||
py35: python3.5
|
||||
py36: python3.6
|
||||
py37: python3.7
|
||||
py38: python3.8
|
||||
deps =
|
||||
tests: -rtests/requirements.txt
|
||||
docs: sphinx
|
||||
@@ -20,19 +15,18 @@ commands =
|
||||
tests: pytest {posargs:tests}
|
||||
docs: sphinx-build -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html
|
||||
|
||||
[testenv:py37-lint]
|
||||
basepython = python3.7
|
||||
[testenv:lint]
|
||||
deps =
|
||||
flake8
|
||||
# flake8-black
|
||||
# flake8-isort
|
||||
pre-commit
|
||||
twine
|
||||
check-manifest
|
||||
commands =
|
||||
# flake8 src/tablib tests/
|
||||
check-manifest -v
|
||||
pre-commit run --all-files
|
||||
python setup.py sdist
|
||||
twine check dist/*
|
||||
skip_install = true
|
||||
|
||||
[flake8]
|
||||
exclude =
|
||||
|
||||
Reference in New Issue
Block a user