Compare commits

...

85 Commits

Author SHA1 Message Date
dmosberger e8f54811c7 Expose 'read_only' parameter for 'import_set' and 'import_book' (#483) 2020-12-04 10:10:02 +02:00
Nuno André e8774043ed Substitute tuples for dicts in __getstate__/__setstate__ to speed up the pickling 2020-11-29 22:11:46 +01:00
Jannis Leidel dc1729fc6f Move releases to GitHub actions. 2020-11-23 13:14:21 +01:00
Hugo van Kemenade 3dc62685f8 Reduce Travis CI testing (#479) 2020-11-23 11:01:10 +01:00
Hugo van Kemenade 22c88de90d Upload coverage from GHA (#480)
* Upload coverage from GHA

* Fix PytestConfigWarning: Unknown config option: python_paths
2020-11-14 23:51:05 +02:00
Hugo van Kemenade 615e308559 Docs: Add link to changelog/history (#478) 2020-11-12 10:30:57 +02:00
Hugo van Kemenade 8c5404591b Add support for Python 3.9, drop EOL 3.5 (#477) 2020-10-30 19:01:48 +02:00
Hugo van Kemenade 5fa4496f9d Suggest quotes when pip installing with optional dependencies (#474) 2020-08-12 16:12:57 +03:00
Ran Benita bc8438bda4 Stop using pkg_resources
tablib imports pkg_resources in order to find its own version. Importing
pkg_resources is very slow (100ms-250ms is common).

Avoid it by letting setuptools-scm generate a file with the version
instead.
2020-08-10 15:49:51 +02:00
Claude Paroz ce79e44d14 Fixes #469 - Prevented rst crash with only-space strings (#470)
Thanks nexone for the report.
2020-06-15 08:42:51 +03:00
Claude Paroz 985c3d98b0 Set the release date for 2.0.0 2020-05-16 14:04:19 +02:00
Claude Paroz 6d097c0214 Fixes #465 - Allow importing 'ragged' .xlsx files (#466) 2020-05-16 09:07:32 +03:00
dragonworks 16b5565354 Fixes #462 - Update xlsx import to read cell values instead of cell formulas
Co-authored-by: Claude Paroz <claude@2xlibre.net>
2020-03-11 09:05:43 +01:00
Claude Paroz c25fe54b6f Refs #373 - Import dates from xls files as Python datetime objects 2020-03-09 17:05:32 +01:00
Tim Gates b39aefb8d8 Fix simple typo: belonogs -> belongs (#460)
Closes #459
2020-02-21 10:26:58 +02:00
Claude Paroz a442758729 Fixes #457 - Bumped openpyxl dependency to 2.6.0 (#458) 2020-02-16 15:05:20 +02:00
Claude Paroz 21479001a7 Fixes #453 - Reversing behavior of Row.lpush/Row.rpush (#454)
Co-authored-by: chim <chenpan@xiaomai5.com>
2020-02-13 20:51:49 +02:00
Claude Paroz f7e39c1ad5 Set the 1.1.0 release date 2020-02-13 18:56:15 +01:00
Claude Paroz aaeb5c8360 Fixes #226 - Allow importing ragged CSV files (#456) 2020-02-12 21:12:53 +02:00
Hugo 7a6c623cca Document upcoming breaking change in 2.0 2020-02-12 19:04:51 +01:00
Hugo 0c31fcb3e4 Test on Python 3.8 2020-02-02 16:44:26 +01:00
Hugo fa7fdb0443 pre-commit autoupdate 2020-02-02 16:44:26 +01:00
Hugo 8e19479cea Simplify config: uses the interpreter tox is installed to 2020-02-02 16:44:26 +01:00
Claude Paroz 8f39ac5055 Optimize xlsx detection (#448)
Reading the whole file is a bit too much to detect if the file
looks like an xlsx file.
2020-01-26 22:02:52 +02:00
Hugo 8d02934c53 Fix tox config 2020-01-26 20:48:20 +01:00
Claude Paroz d0963c206f Fix the missing xls dependencies message 2020-01-14 17:58:32 +01:00
Claude Paroz 993af5b0b4 Add release date for 1.0.0 2020-01-13 19:08:47 +01:00
Hugo van Kemenade 0accb4c437 Add project_urls metadata for programmatic use 2020-01-11 15:00:51 +01:00
Claude Paroz 0821716983 Refs #401 - Fixed some flake8 errors 2020-01-11 11:57:53 +01:00
Claude Paroz 660990b6b0 Fixes #440 -Normalize stream inputs as IO streams 2020-01-11 11:30:16 +01:00
Claude Paroz 6152d995f0 Tablib docs isn't the place to debate GPL vs MIT licensing 2019-12-31 14:08:08 +01:00
Claude Paroz 0ea6d706a9 Refs #293 - Ensured Dataset can be pickled/unpickled without damages 2019-12-30 16:23:38 +01:00
Hugo van Kemenade 00d8ab0b37 Remove unnecessary MANIFEST.in (#439)
* This MANIFEST.in unnecessary with setuptools_scm

https://github.com/pypa/setuptools_scm/blob/master/README.rst#file-finders-hook-makes-most-of-manifestin-unnecessary

* No manifest to check
2019-12-11 10:51:21 +01:00
Hugo van Kemenade 06c2326dc0 Refactor error raising to remove duplication 2019-12-10 10:55:30 +01:00
Daniel Santos fa30ea858d Implement feature that allows to export tabular data suited to a… (#437) 2019-12-10 01:04:03 +02:00
Hugo van Kemenade 4de2e17984 README: Add more badges (#435) 2019-12-02 11:10:54 +02:00
Hugo van Kemenade 52b64757b7 Remove unused Pipfile (#436) 2019-12-02 11:10:41 +02:00
Joseph Herlant 5ff4a55ae6 Force default_flow_style for pyyaml safe_dump
This is to keep behavior of pre-5.1 pyyaml.
2019-11-24 20:43:12 +01:00
Claude Paroz ce7d887adc Documented csv import/export options from standard lib (#431) 2019-11-14 18:08:51 +02:00
Hugo van Kemenade 57a535f577 Fix NameError: name '_get_column_widths' is not defined (#433)
* Fix NameError: name '_get_column_widths' is not defined

* Also test ReSTFormat.export_set
2019-11-12 10:53:20 +02:00
Claude Paroz 357a5594c5 Admonitions must have a title 2019-11-11 21:25:56 +01:00
Claude Paroz f61b8d8926 Fixes #422 - Allow ability to lazy-load external modules (#430) 2019-11-11 21:46:28 +02:00
Hugo van Kemenade 22a193dafb No __cmp__ or cmp in Python 3 (#429)
* No __cmp__ or cmp in Python 3

* Add rich comparisons

* Simplify using total_ordering decorator
2019-11-11 12:06:25 +02:00
Hugo van Kemenade b539e96697 Update testing: add docs + lint jobs; use pre-commit for linting (#426)
* Move docs and lint to their own [3.8] build job for more parallelism

* No codecov for docs or lint

* Move isort into pre-commit

* Add some handy linters to pre-commit

* Add rst-backticks linter and fix the errors

* Add pyupgrade and add upgrades

* Test docs and lint on GitHub Actions

* Xenial is default
2019-11-10 21:09:18 +02:00
Claude Paroz 626a062747 Fixes #421 - Make all dependencies optional
Thanks Hugo van Kemenade for the review.
2019-11-10 18:00:31 +01:00
Claude Paroz 9d2f7d6999 Point README to the documentation 2019-11-08 17:31:25 +01:00
Claude Paroz a9d9671b7f Moved format documentation from code to docs (#420) 2019-11-06 22:37:01 +02:00
Claude Paroz f1046cd13e Refs #256 - Implement class-based formats
This allows to extend Tablib with new formats far more easily.
2019-11-02 17:44:05 +01:00
Claude Paroz d21bd10908 Revert " Implement feature new format: Cli. Generate adapter for tabulate. This close issue #340"
This reverts commit c26159d48f.
The patch was NOT ready to be merged.
2019-10-30 14:24:07 +01:00
Daniel Santos c26159d48f Implement feature new format: Cli. Generate adapter for tabulate. This close issue #340
* Implement feature new format: Cli. Generate adapter for  tabulate. This close issue #340

* Write respective tests.

* Correct name Clase Base Test

* Implement missing class method to export cli.

* Remove property headers in method export book Cli.

* Remove cli from list to test Iterable data books.
2019-10-30 14:13:39 +01:00
Daniel Santos 34fe72305e Add missing extraline. 2019-10-29 22:13:57 +01:00
Daniel Santos d94420d968 Elucidate the use of filters (and, or). 2019-10-29 22:08:31 +01:00
Daniel Santos 51a720b21c Merge pull request #416 from xdanielsb/doc-formats
Update doc, clarify the use and scope of the flag headers.
2019-10-28 16:53:02 +01:00
Daniel 20f51d0bc1 Update doc, apply requested changes in headers flag doc. 2019-10-28 16:45:26 +01:00
Daniel 87d15a1529 Update doc, clarify the use and scope of the flag headers. 2019-10-27 21:00:21 +01:00
Hugo van Kemenade 08a6759520 Fixes #202 - Keep error content when importing xls files (#415)
Fixes #202 - Keep error content when importing xls files
2019-10-22 22:49:10 +03:00
Claude Paroz 205403d377 Fixes #202 - Keep error content when importing xls files 2019-10-22 20:48:45 +02:00
Claude Paroz 9858539c87 Add known third parties to isort 2019-10-22 14:19:20 +02:00
Hugo van Kemenade 201d8d9910 Merge pull request #412 from claudep/isort
Display isort errors
2019-10-22 14:06:31 +03:00
Claude Paroz fede4a4f13 Display isort errors 2019-10-22 12:29:55 +02:00
Hugo a76933edd5 Refs #401 - Sort imports with isort 2019-10-22 11:59:19 +02:00
Hugo 3197e59b25 Add pyproject.toml as per PEP 518 2019-10-22 11:52:20 +02:00
Claude Paroz 1f000f2f2c Removed unused imports 2019-10-20 12:23:05 +02:00
Hugo van Kemenade 7879fef65a Remove Python 2 code (#410)
Remove Python 2 code
2019-10-20 12:58:52 +03:00
Hugo b8bff1190e Don't omit tests from coverage https://nedbatchelder.com/blog/201908/dont_omit_tests_from_coverage.html 2019-10-20 12:36:20 +03:00
Hugo d77aba6210 Add more tests 2019-10-20 12:32:00 +03:00
Hugo bf6e5c2e78 100% Row test coverage 2019-10-20 12:27:51 +03:00
Hugo 088b916bab __unicode__ not used in Python 3 2019-10-20 12:04:33 +03:00
Hugo e4ac50260e __getslice__ is deprecated since Python 2.0 and it is not available in Python 3
https://docs.python.org/2/reference/datamodel.html#object.__getslice__
2019-10-19 20:01:07 +03:00
Hugo 7347d07624 Upgrade Python syntax with pyupgrade --py3-plus 2019-10-19 19:25:34 +03:00
Hugo c9027b446c Drop support fo Python 2.7 2019-10-19 19:24:03 +03:00
Hugo van Kemenade 825de0193b Test Windows, macOS and Linux on GitHub Actions (#409)
Test Windows, macOS and Linux on GitHub Actions
2019-10-19 19:20:42 +03:00
Claude Paroz 78b483d39e Fixes #368 - Avoid crashing when exporting empty string in ReST 2019-10-19 18:00:47 +02:00
Claude Paroz 8f09789d40 [csv] Fixes #342 - Feed only 1k of content to csv.Sniffer
Thanks Rivo Laks for the suggestion.
2019-10-19 17:37:56 +02:00
Hugo e0e75ed43c Test Windows, macOS and Linux on GitHub Actions 2019-10-19 18:25:02 +03:00
Peyman Salehi bdc84255a8 Fix some linting errors 2019-10-19 16:57:14 +02:00
Peyman Salehi b3c7145c40 Drop python 2 support
Remove support python 2 from doc, requirements.txt and config
Replace unicode with str
Remove dbfpy folder and rename dbfpy3 to dbfpy
Remove compat file and remove python2 packages from dependency
2019-10-19 16:30:57 +02:00
Claude Paroz 44f43516a5 Refs #250 - Test that commas embedded in quoted strings can be imported 2019-10-19 15:05:46 +02:00
Hugo van Kemenade e0a40577fd Update docs (#406)
Update docs
2019-10-19 16:02:55 +03:00
Hugo 0329eb6168 Update docs 2019-10-19 15:33:33 +03:00
Hugo van Kemenade 4c3dc847b0 Add release checklist (#403)
Add release checklist
2019-10-19 15:00:14 +03:00
Hugo 89fbd54b00 Fix check-manifest 2019-10-19 14:55:31 +03:00
Hugo van Kemenade 067dc769dc Fix typos (#404)
Fix typos
2019-10-19 14:54:25 +03:00
Hugo 1726c1cf37 Add release checklist 2019-10-19 14:45:55 +03:00
Hugo debe77e432 Fix typos 2019-10-19 13:56:34 +03:00
69 changed files with 2523 additions and 3504 deletions
+46
View File
@@ -0,0 +1,46 @@
name: Docs and lint
on: [push, pull_request]
env:
FORCE_COLOR: 1
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
env:
- TOXENV: docs
- TOXENV: lint
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ matrix.os }}-${{ matrix.python-version }}-v1-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade tox
- name: Tox
run: tox
env: ${{ matrix.env }}
+56
View File
@@ -0,0 +1,56 @@
name: Release
on:
push:
branches:
- master
release:
types:
- published
jobs:
build:
if: github.repository == 'jazzband/tablib'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: release-${{ hashFiles('**/setup.py') }}
restore-keys: |
release-
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools twine wheel
- name: Build package
run: |
python setup.py --version
python setup.py sdist --format=gztar bdist_wheel
twine check dist/*
- name: Upload packages to Jazzband
if: github.event.action == 'published'
uses: pypa/gh-action-pypi-publish@master
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/tablib/upload
+53
View File
@@ -0,0 +1,53 @@
name: Test
on: [push, pull_request]
env:
FORCE_COLOR: 1
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ matrix.os }}-${{ matrix.python-version }}-v1-
- 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
run: |
tox -e py
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
+3
View File
@@ -38,3 +38,6 @@ htmlcov
# setuptools noise # setuptools noise
.eggs .eggs
*.egg-info *.egg-info
# generated by setuptools-scm
/src/tablib/_version.py
-6
View File
@@ -1,6 +0,0 @@
[settings]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
+25
View File
@@ -0,0 +1,25 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.7.3
hooks:
- id: pyupgrade
args: ["--py36-plus"]
- repo: https://github.com/PyCQA/isort
rev: 5.6.4
hooks:
- id: isort
additional_dependencies: [toml]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.7.0
hooks:
- id: python-check-blanket-noqa
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
hooks:
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
-22
View File
@@ -1,22 +0,0 @@
language: python
python:
- 2.7
- 3.6
- 3.7
- 3.8
cache: pip
dist: xenial
install: travis_retry pip install tox-travis
script: tox
after_success: bash <(curl -s https://codecov.io/bash)
deploy:
provider: pypi
user: jazzband
server: https://jazzband.co/projects/tablib/upload
distributions: sdist bdist_wheel
password:
secure: svV4fYtodwW+iTyFOm5ISEfhVwcA+6vTskD3x6peznc40TdMV9Ek8nT3Q/NB4lCbXoUw2qR4H6uhLCjesnv/VvVk/qbitCyD8ySlgwOV5n7NzJs8lC8EYaHSjGQjatTwJAokfGVYkPawkI7HXDqtDggLUQBK+Ag8HDW+XBSbQIU=
on:
tags: true
repo: jazzband/tablib
python: 3.7
+2
View File
@@ -8,6 +8,7 @@ Here is a list of passed and present much-appreciated contributors:
Benjamin Wohlwend Benjamin Wohlwend
Bruno Soares Bruno Soares
Claude Paroz Claude Paroz
Daniel Santos
Erik Youngren Erik Youngren
Hugo van Kemenade Hugo van Kemenade
Iuri de Silvio Iuri de Silvio
@@ -24,6 +25,7 @@ Here is a list of passed and present much-appreciated contributors:
Mark Walling Mark Walling
Mathias Loesch Mathias Loesch
Mike Waldner Mike Waldner
Peyman Salehi
Rabin Nankhwa Rabin Nankhwa
Tommy Anthony Tommy Anthony
Tsuyoshi Hombashi Tsuyoshi Hombashi
+68
View File
@@ -1,5 +1,73 @@
# History # History
## Unreleased
### Breaking changes
- Dropped Python 3.5 support
### Improvements
- Added Python 3.9 support
- Added read_only option to xlsx file reader (#482).
### Bugfixes
- Prevented crash in rst export with only-space strings (#469).
## 2.0.0 (2020-05-16)
### Breaking changes
- The `Row.lpush/rpush` logic was reversed. `lpush` was appending while `rpush`
and `append` were prepending. This was fixed (reversed behavior). If you
counted on the broken behavior, please update your code (#453).
### Bugfixes
- Fixed minimal openpyxl dependency version to 2.6.0 (#457).
- Dates from xls files are now read as Python datetime objects (#373).
- Allow import of "ragged" xlsx files (#465).
### Improvements
- When importing an xlsx file, Tablib will now read cell values instead of formulas (#462).
## 1.1.0 (2020-02-13)
### Deprecations
- Upcoming breaking change in Tablib 2.0.0: the `Row.lpush/rpush` logic is reversed.
`lpush` is appending while `rpush` and `append` are prepending. The broken behavior
will remain in Tablib 1.x and will be fixed (reversed) in Tablib 2.0.0 (#453). If you
count on the broken behavior, please update your code when you upgrade to Tablib 2.x.
### Improvements
- Tablib is now able to import CSV content where not all rows have the same
length. Missing columns on any line receive the empty string (#226).
## 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) ## 0.14.0 (2019-10-19)
### Deprecations ### Deprecations
-6
View File
@@ -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__
+7 -140
View File
@@ -1,8 +1,12 @@
# Tablib: format-agnostic tabular dataset library # Tablib: format-agnostic tabular dataset library
[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/)
[![Build Status](https://travis-ci.org/jazzband/tablib.svg?branch=master)](https://travis-ci.org/jazzband/tablib) [![PyPI version](https://img.shields.io/pypi/v/tablib.svg)](https://pypi.org/project/tablib/)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/tablib.svg)](https://pypi.org/project/tablib/)
[![PyPI downloads](https://img.shields.io/pypi/dm/tablib.svg)](https://pypistats.org/packages/tablib)
[![GitHub Actions status](https://github.com/jazzband/tablib/workflows/Test/badge.svg)](https://github.com/jazzband/tablib/actions)
[![codecov](https://codecov.io/gh/jazzband/tablib/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/tablib) [![codecov](https://codecov.io/gh/jazzband/tablib/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/tablib)
[![GitHub](https://img.shields.io/github/license/jazzband/tablib.svg)](LICENSE)
_____ ______ ___________ ______ _____ ______ ___________ ______
__ /_______ ____ /_ ___ /___(_)___ /_ __ /_______ ____ /_ ___ /___(_)___ /_
@@ -29,149 +33,12 @@ Output formats supported:
Note that tablib *purposefully* excludes XML support. It always will. (Note: This is a Note that tablib *purposefully* excludes XML support. It always will. (Note: This is a
joke. Pull requests are welcome.) joke. Pull requests are welcome.)
Tablib documentation is graciously hosted on https://tablib.readthedocs.io
## Overview It is also available in the ``docs`` directory of the source distribution.
`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]
```
Make sure to check out [Tablib on PyPI](https://pypi.org/project/tablib/)! Make sure to check out [Tablib on PyPI](https://pypi.org/project/tablib/)!
## Contribute ## Contribute
Please see the [contributing guide](https://github.com/jazzband/tablib/blob/master/.github/CONTRIBUTING.md). Please see the [contributing guide](https://github.com/jazzband/tablib/blob/master/.github/CONTRIBUTING.md).
+29
View File
@@ -0,0 +1,29 @@
# Release checklist
Jazzband guidelines: https://jazzband.co/about/releases
* [ ] Get master to the appropriate code release state.
[GitHub Actions](https://github.com/jazzband/tablib/actions)
should pass on master.
[![GitHub Actions status](https://github.com/jazzband/tablib/workflows/Test/badge.svg)](https://github.com/jazzband/tablib/actions)
* [ ] 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 GitHub Actions 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"
-18
View File
@@ -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"
+1
View File
@@ -8,4 +8,5 @@
<li><a href="https://pypi.org/project/tablib">Tablib @ PyPI</a></li> <li><a href="https://pypi.org/project/tablib">Tablib @ PyPI</a></li>
<li><a href="https://github.com/jazzband/tablib">Tablib @ GitHub</a></li> <li><a href="https://github.com/jazzband/tablib">Tablib @ GitHub</a></li>
<li><a href="https://github.com/jazzband/tablib/issues">Issue Tracker</a></li> <li><a href="https://github.com/jazzband/tablib/issues">Issue Tracker</a></li>
<li><a href="https://github.com/jazzband/tablib/blob/master/HISTORY.md">Changelog</a></li>
</ul> </ul>
+14 -11
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Tablib documentation build configuration file, created by # Tablib documentation build configuration file, created by
# sphinx-quickstart on Tue Oct 5 15:25:21 2010. # sphinx-quickstart on Tue Oct 5 15:25:21 2010.
@@ -10,7 +9,7 @@
# #
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
from pkg_resources import get_distribution import tablib
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
@@ -23,7 +22,11 @@ from pkg_resources import get_distribution
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # 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. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@@ -38,17 +41,17 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'Tablib' project = 'Tablib'
copyright = u'2019 Jazzband' copyright = '2019 Jazzband'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = get_distribution('tablib').version release = tablib.__version__
# The short X.Y version. # The short X.Y version.
version = '.'.join(release.split('.')[:2]) version = '.'.join(tablib.__version__.split('.')[:2])
# for example take major/minor # for example take major/minor
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
@@ -181,8 +184,8 @@ htmlhelp_basename = 'Tablibdoc'
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'Tablib.tex', u'Tablib Documentation', ('index', 'Tablib.tex', 'Tablib Documentation',
u'Jazzband', 'manual'), 'Jazzband', 'manual'),
] ]
latex_use_modindex = False latex_use_modindex = False
@@ -222,6 +225,6 @@ latex_use_parts = True
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'tablib', u'Tablib Documentation', ('index', 'tablib', 'Tablib Documentation',
[u'Jazzband'], 1) ['Jazzband'], 1)
] ]
+25 -18
View File
@@ -90,32 +90,36 @@ Tablib features a micro-framework for adding format support.
The easiest way to understand it is to use it. The easiest way to understand it is to use it.
So, let's define our own format, named *xxx*. 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. 1. Write your custom format class::
Function names are crucial.
Example **tablib/formats/_xxx.py**: ::
class MyXXXFormatClass:
title = 'xxx' title = 'xxx'
def export_set(dset): @classmethod
def export_set(cls, dset):
.... ....
# returns string representation of given dataset # returns string representation of given dataset
def export_book(dbook): @classmethod
def export_book(cls, dbook):
.... ....
# returns string representation of given databook # 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 # populates given Dataset with given datastream
def import_book(dbook, in_stream): @classmethod
def import_book(cls, dbook, in_stream):
... ...
# returns Databook instance # returns Databook instance
def detect(stream): @classmethod
def detect(cls, stream):
... ...
# returns True if given stream is parsable as xxx # 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.* If the format excludes support for an import/export mechanism (*e.g.*
:class:`csv <tablib.Dataset.csv>` excludes :class:`csv <tablib.Dataset.csv>` excludes
:class:`Databook <tablib.Databook>` support), :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. 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. from tablib.formats import registry
This alleviates IDE confusion, and allows for pretty auto-generated Sphinx_ documentation.
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: .. _testing:
@@ -156,16 +163,16 @@ the easiest way to test your changes for potential issues is to simply run the t
Continuous Integration Continuous Integration
---------------------- ----------------------
Every pull request is automatically tested and inspected upon receipt with `Travis CI`_. Every pull request is automatically tested and inspected upon receipt with `GitHub Actions`_.
If you broke the build, you will receive an email accordingly. If you broke the build, you will receive an email accordingly.
Anyone may view the build status and history at any time. Anyone may view the build status and history at any time.
https://travis-ci.org/jazzband/tablib https://github.com/jazzband/tablib/actions
Additional reports will also be included here in the future, including :pep:`8` checks and stress reports for extremely large datasets. Additional reports will also be included here in the future, including :pep:`8` checks and stress reports for extremely large datasets.
.. _`Travis CI`: https://travis-ci.org/ .. _`GitHub Actions`: https://github.com/jazzband/tablib/actions
.. _docs: .. _docs:
+247
View File
@@ -0,0 +1,247 @@
.. _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.
The ``import_set()`` and ``import_book()`` methods accept keyword
argument ``read_only``. If its value is ``True`` (the default), the
XLSX data source is read lazily. Lazy reading generally reduces time
and memory consumption, especially for large spreadsheets. However,
it relies on the XLSX data source declaring correct dimensions. Some
programs generate XLSX files with incorrect dimensions. Such files
may need to be loaded with this optimization turned off by passing
``read_only=False``.
.. note::
When reading an ``xlsx`` file containing formulas in its cells, Tablib will
read the cell values, not the cell formulas.
.. versionchanged:: 2.0.0
Reads cell values instead of formulas.
.. 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
View File
@@ -1,7 +1,7 @@
.. Tablib documentation master file, created by .. Tablib documentation master file, created by
sphinx-quickstart on Tue Oct 5 15:25:21 2010. sphinx-quickstart on Tue Oct 5 15:25:21 2010.
You can adapt this file completely to your liking, but it should at least 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 Tablib: Pythonic Tabular Datasets
================================= =================================
@@ -98,6 +98,11 @@ This part of the documentation, which is mostly prose, begins with some backgrou
tutorial tutorial
.. toctree::
:maxdepth: 2
formats
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
+20 -2
View File
@@ -19,14 +19,32 @@ Of course, the recommended way to install Tablib is with `pip <https://pip.pypa.
.. code-block:: console .. 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 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. The latest release (|version|) is available from GitHub.
* tarball_ * tarball_
+6 -27
View File
@@ -6,7 +6,7 @@ Introduction
This part of the documentation covers all the interfaces of Tablib. This part of the documentation covers all the interfaces of Tablib.
Tablib is a format-agnostic tabular dataset library, written in Python. Tablib is a format-agnostic tabular dataset library, written in Python.
It allows you to Pythonically import, export, and manipulate tabular data sets. 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. 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. 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: .. _license:
Tablib License Tablib License
-------------- --------------
Tablib is released under terms of `The MIT License`_.
Copyright 2017 Kenneth Reitz Copyright 2017 Kenneth Reitz
Permission is hereby granted, free of charge, to any person obtaining a copy 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. _`The MIT License`: https://opensource.org/licenses/mit-license.php
.. _pythonsupport: .. _pythonsupport:
Pythons Supported Pythons Supported
----------------- -----------------
At this time, the following Python versions are officially supported: Python 3.6+ is officially supported.
* CPython 2.7 Now, go :ref:`install Tablib <install>`.
* CPython 3.5
* CPython 3.6
* CPython 3.7
Now, go :ref:`Install Tablib <install>`.
+30 -10
View File
@@ -106,10 +106,18 @@ Importing Data
-------------- --------------
Creating a :class:`tablib.Dataset` object by importing a pre-existing file is simple. :: 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. 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 Exporting Data
-------------- --------------
@@ -200,7 +208,7 @@ Delete a range of rows::
Advanced Usage 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. And now for something completely different.
@@ -287,24 +295,36 @@ Let's tag some students. ::
students.headers = ['first', 'last'] students.headers = ['first', 'last']
students.rpush(['Kenneth', 'Reitz'], tags=['male', 'technical']) students.rpush(['Kenneth', 'Reitz'], tags=['male', 'technical'])
students.rpush(['Daniel', 'Dupont'], tags=['male', 'creative' ])
students.rpush(['Bessie', 'Monke'], tags=['female', '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 By default, when you pass a list of tags you get filter type or. ::
- {first: Kenneth, Last: Reitz}
>>> 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. It's that simple. The original :class:`Dataset` is untouched.
Open an Excel Workbook and read first sheet 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 = tablib.Dataset()
data.xlsx = open('my_excel_file.xlsx', 'rb').read() with open('my_excel_file.xlsx', 'rb') as fh:
print(data) data.load(fh, 'xlsx')
print(data)
Excel Workbook With Multiple Sheets 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>`. :: ... and export to Excel just like :class:`Datasets <Dataset>`. ::
with open('students.xls', 'wb') as f: 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`. The resulting ``students.xls`` file will contain a separate spreadsheet for each :class:`Dataset` object in the :class:`Databook`.
+2
View File
@@ -0,0 +1,2 @@
[tool.isort]
profile = "black"
+1 -2
View File
@@ -1,4 +1,3 @@
[pytest] [pytest]
norecursedirs = .git .* 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 = .
+20 -25
View File
@@ -1,37 +1,27 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import re
import sys
from setuptools import find_packages, setup from setuptools import find_packages, setup
install = [
'odfpy',
'openpyxl>=2.4.0',
'backports.csv;python_version<"3.0"',
'markuppy',
'xlrd',
'xlwt',
'pyyaml',
]
setup( setup(
name='tablib', name='tablib',
use_scm_version=True, use_scm_version={
'write_to': 'src/tablib/_version.py',
},
setup_requires=['setuptools_scm'], setup_requires=['setuptools_scm'],
description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)', description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)',
long_description=(open('README.md').read() + '\n\n' + long_description=(
open('HISTORY.md').read()), open('README.md').read() + '\n\n' + open('HISTORY.md').read()
),
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
author='Kenneth Reitz', author='Kenneth Reitz',
author_email='me@kennethreitz.org', author_email='me@kennethreitz.org',
maintainer='Jazzband', maintainer='Jazzband',
maintainer_email='roadies@jazzband.co', maintainer_email='roadies@jazzband.co',
url='https://tablib.readthedocs.io', url='https://tablib.readthedocs.io',
project_urls={
"Documentation": "https://tablib.readthedocs.io",
"Source": "https://github.com/jazzband/tablib",
},
packages=find_packages(where="src"), packages=find_packages(where="src"),
package_dir={"": "src"}, package_dir={"": "src"},
license='MIT', license='MIT',
@@ -41,17 +31,22 @@ setup(
'Natural Language :: English', 'Natural Language :: English',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
], ],
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', python_requires='>=3.6',
install_requires=install,
extras_require={ extras_require={
'all': ['markuppy', 'odfpy', 'openpyxl>=2.6.0', 'pandas', 'pyyaml', 'tabulate', 'xlrd', 'xlwt'],
'cli': ['tabulate'],
'html': ['markuppy'],
'ods': ['odfpy'],
'pandas': ['pandas'], 'pandas': ['pandas'],
'xls': ['xlrd', 'xlwt'],
'xlsx': ['openpyxl>=2.6.0'],
'yaml': ['pyyaml'],
}, },
) )
+16 -10
View File
@@ -1,13 +1,19 @@
""" Tablib. """ """ Tablib. """
from pkg_resources import get_distribution, DistributionNotFound
from tablib.core import (
Databook, Dataset, detect_format, import_set, import_book,
InvalidDatasetType, InvalidDimensions, UnsupportedFormat
)
try: try:
__version__ = get_distribution(__name__).version # Generated by setuptools-scm.
except DistributionNotFound: from ._version import version as __version__
# package is not installed except ImportError:
# Some broken installation.
__version__ = None __version__ = None
from tablib.core import ( # noqa: F401
Databook,
Dataset,
InvalidDatasetType,
InvalidDimensions,
UnsupportedFormat,
detect_format,
import_book,
import_set,
)
-36
View File
@@ -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
+94 -337
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
""" """
tablib.core tablib.core
~~~~~~~~~~~ ~~~~~~~~~~~
@@ -13,10 +12,15 @@ from collections import OrderedDict
from copy import copy from copy import copy
from operator import itemgetter from operator import itemgetter
from tablib import formats from tablib.exceptions import (
HeadersNeeded,
from tablib.compat import unicode InvalidDatasetIndex,
InvalidDatasetType,
InvalidDimensions,
UnsupportedFormat,
)
from tablib.formats import registry
from tablib.utils import normalize_input
__title__ = 'tablib' __title__ = 'tablib'
__author__ = 'Kenneth Reitz' __author__ = 'Kenneth Reitz'
@@ -25,7 +29,7 @@ __copyright__ = 'Copyright 2017 Kenneth Reitz. 2019 Jazzband.'
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
class Row(object): class Row:
"""Internal Row object. Mainly used for filtering.""" """Internal Row object. Mainly used for filtering."""
__slots__ = ['_row', 'tags'] __slots__ = ['_row', 'tags']
@@ -43,9 +47,6 @@ class Row(object):
def __repr__(self): def __repr__(self):
return repr(self._row) return repr(self._row)
def __getslice__(self, i, j):
return self._row[i:j]
def __getitem__(self, i): def __getitem__(self, i):
return self._row[i] return self._row[i]
@@ -56,23 +57,16 @@ class Row(object):
del self._row[i] del self._row[i]
def __getstate__(self): def __getstate__(self):
return self._row, self.tags
slots = dict()
for slot in self.__slots__:
attribute = getattr(self, slot)
slots[slot] = attribute
return slots
def __setstate__(self, state): def __setstate__(self, state):
for (k, v) in list(state.items()): setattr(self, k, v) self._row, self.tags = state
def rpush(self, value): def rpush(self, value):
self.insert(0, value) self.insert(len(self._row), value)
def lpush(self, value): def lpush(self, value):
self.insert(len(value), value) self.insert(0, value)
def append(self, value): def append(self, value):
self.rpush(value) self.rpush(value)
@@ -96,7 +90,7 @@ class Row(object):
def has_tag(self, tag): def has_tag(self, tag):
"""Returns true if current row contains tag.""" """Returns true if current row contains tag."""
if tag == None: if tag is None:
return False return False
elif isinstance(tag, str): elif isinstance(tag, str):
return (tag in self.tags) return (tag in self.tags)
@@ -104,7 +98,7 @@ class Row(object):
return bool(len(set(tag) & set(self.tags))) 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 """The :class:`Dataset` object is the heart of Tablib. It provides all core
functionality. functionality.
@@ -119,7 +113,7 @@ class Dataset(object):
Setting columns is similar. The column data length must equal the 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 = tablib.Dataset()
data.headers = ('first_name', 'last_name') data.headers = ('first_name', 'last_name')
@@ -145,14 +139,12 @@ class Dataset(object):
.. admonition:: Format Attributes Definition .. admonition:: Format Attributes Definition
If you look at the code, the various output/import formats are not If you look at the code, the various output/import formats are not
defined within the :class:`Dataset` object. To add support for a new format, see defined within the :class:`Dataset` object. To add support for a new format, see
:ref:`Adding New Formats <newformats>`. :ref:`Adding New Formats <newformats>`.
""" """
_formats = {}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._data = list(Row(arg) for arg in args) self._data = list(Row(arg) for arg in args)
self.__headers = None self.__headers = None
@@ -167,15 +159,13 @@ class Dataset(object):
self.title = kwargs.get('title') self.title = kwargs.get('title')
self._register_formats()
def __len__(self): def __len__(self):
return self.height return self.height
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, (str, unicode)): if isinstance(key, str):
if key in self.headers: 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] return [row[pos] for row in self._data]
else: else:
raise KeyError raise KeyError
@@ -191,7 +181,7 @@ class Dataset(object):
self._data[key] = Row(value) self._data[key] = Row(value)
def __delitem__(self, key): def __delitem__(self, key):
if isinstance(key, (str, unicode)): if isinstance(key, str):
if key in self.headers: if key in self.headers:
@@ -213,15 +203,15 @@ class Dataset(object):
except AttributeError: except AttributeError:
return '<dataset object>' return '<dataset object>'
def __unicode__(self): def __str__(self):
result = [] result = []
# Add unicode representation of headers. # Add str representation of headers.
if self.__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. # Add str representation of rows.
result.extend(list(map(unicode, row)) for row in self._data) result.extend(list(map(str, row)) for row in self._data)
lens = [list(map(len, row)) for row in result] lens = [list(map(len, row)) for row in result]
field_lens = list(map(max, zip(*lens))) field_lens = list(map(max, zip(*lens)))
@@ -234,30 +224,16 @@ class Dataset(object):
return '\n'.join(format_string.format(*row) for row in result) return '\n'.join(format_string.format(*row) for row in result)
def __str__(self):
return self.__unicode__()
# --------- # ---------
# Internals # Internals
# --------- # ---------
@classmethod def _get_in_format(self, fmt_key, **kwargs):
def _register_formats(cls): return registry.get_format(fmt_key).export_set(self, **kwargs)
"""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)
except AttributeError: def _set_in_format(self, fmt_key, in_stream, **kwargs):
cls._formats[fmt.title] = (None, None) 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): def _validate(self, row=None, col=None, safety=False):
"""Assures size of every row in dataset is of proper proportions.""" """Assures size of every row in dataset is of proper proportions."""
@@ -269,7 +245,7 @@ class Dataset(object):
else: else:
is_valid = (len(col) == self.height) if self.height else True is_valid = (len(col) == self.height) if self.height else True
else: 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: if is_valid:
return True return True
@@ -315,7 +291,7 @@ class Dataset(object):
def _get_headers(self): def _get_headers(self):
"""An *optional* list of strings to be used for header rows and attribute names. """An *optional* list of strings to be used for header rows and attribute names.
This must be set manually. The given list length must equal :class:`Dataset.width`. This must be set manually. The given list length must equal :attr:`Dataset.width`.
""" """
return self.__headers return self.__headers
@@ -351,7 +327,7 @@ class Dataset(object):
set, a list of Python dictionaries will be returned. If no headers have been set, a list of Python dictionaries will be returned. If no headers have been
set, a list of tuples (rows) will be returned instead. set, a list of tuples (rows) will be returned instead.
A dataset object can also be imported by setting the :class:`Dataset.dict` attribute. :: A dataset object can also be imported by setting the :attr:`Dataset.dict` attribute. ::
data = tablib.Dataset() data = tablib.Dataset()
data.dict = [{'age': 90, 'first_name': 'Kenneth', 'last_name': 'Reitz'}] data.dict = [{'age': 90, 'first_name': 'Kenneth', 'last_name': 'Reitz'}]
@@ -419,18 +395,23 @@ class Dataset(object):
def load(self, in_stream, format=None, **kwargs): def load(self, in_stream, format=None, **kwargs):
""" """
Import `in_stream` to the :class:`Dataset` object using the `format`. 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`. :param \\*\\*kwargs: (optional) custom configuration to the format `import_set`.
""" """
stream = normalize_input(in_stream)
if not format: 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(f'Format {format} cannot be imported.')
export_set, import_set = self._formats.get(format, (None, None))
if not import_set: if not import_set:
raise UnsupportedFormat('Format {0} cannot be imported.'.format(format)) raise UnsupportedFormat(f'Format {format} cannot be imported.')
import_set(self, in_stream, **kwargs) fmt.import_set(self, stream, **kwargs)
return self return self
def export(self, format, **kwargs): def export(self, format, **kwargs):
@@ -439,204 +420,11 @@ class Dataset(object):
:param \\*\\*kwargs: (optional) custom configuration to the format `export_set`. :param \\*\\*kwargs: (optional) custom configuration to the format `export_set`.
""" """
export_set, import_set = self._formats.get(format, (None, None)) fmt = registry.get_format(format)
if not export_set: if not hasattr(fmt, 'export_set'):
raise UnsupportedFormat('Format {0} cannot be exported.'.format(format)) raise UnsupportedFormat(f'Format {format} cannot be exported.')
return export_set(self, **kwargs) return fmt.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
# ---- # ----
# Rows # Rows
@@ -656,28 +444,28 @@ class Dataset(object):
def rpush(self, row, tags=list()): def rpush(self, row, tags=list()):
"""Adds a row to the end of the :class:`Dataset`. """Adds a row to the end of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.insert(self.height, row=row, tags=tags) self.insert(self.height, row=row, tags=tags)
def lpush(self, row, tags=list()): def lpush(self, row, tags=list()):
"""Adds a row to the top of the :class:`Dataset`. """Adds a row to the top of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.insert(0, row=row, tags=tags) self.insert(0, row=row, tags=tags)
def append(self, row, tags=list()): def append(self, row, tags=list()):
"""Adds a row to the :class:`Dataset`. """Adds a row to the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.rpush(row, tags) self.rpush(row, tags)
def extend(self, rows, tags=list()): def extend(self, rows, tags=list()):
"""Adds a list of rows to the :class:`Dataset` using """Adds a list of rows to the :class:`Dataset` using
:class:`Dataset.append` :method:`Dataset.append`
""" """
for row in rows: for row in rows:
@@ -719,20 +507,20 @@ class Dataset(object):
data.append_col(col=random.randint) data.append_col(col=random.randint)
If inserting a column, and :class:`Dataset.headers` is set, the If inserting a column, and :attr:`Dataset.headers` is set, the
header attribute must be set, and will be considered the header for header attribute must be set, and will be considered the header for
that row. that row.
See :ref:`dyncols` for an in-depth example. See :ref:`dyncols` for an in-depth example.
.. versionchanged:: 0.9.0 .. versionchanged:: 0.9.0
If inserting a column, and :class:`Dataset.headers` is set, the If inserting a column, and :attr:`Dataset.headers` is set, the
header attribute must be set, and will be considered the header for header attribute must be set, and will be considered the header for
that row. that row.
.. versionadded:: 0.9.0 .. versionadded:: 0.9.0
If inserting a row, you can add :ref:`tags <tags>` to the row you are inserting. If inserting a row, you can add :ref:`tags <tags>` to the row you are inserting.
This gives you the ability to :class:`filter <Dataset.filter>` your This gives you the ability to :method:`filter <Dataset.filter>` your
:class:`Dataset` later. :class:`Dataset` later.
""" """
@@ -769,14 +557,14 @@ class Dataset(object):
def rpush_col(self, col, header=None): def rpush_col(self, col, header=None):
"""Adds a column to the end of the :class:`Dataset`. """Adds a column to the end of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.insert_col(self.width, col, header=header) self.insert_col(self.width, col, header=header)
def lpush_col(self, col, header=None): def lpush_col(self, col, header=None):
"""Adds a column to the top of the :class:`Dataset`. """Adds a column to the top of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.insert_col(0, col, header=header) self.insert_col(0, col, header=header)
@@ -800,7 +588,7 @@ class Dataset(object):
def append_col(self, col, header=None): def append_col(self, col, header=None):
"""Adds a column to the :class:`Dataset`. """Adds a column to the :class:`Dataset`.
See :class:`Dataset.insert_col` for additional documentation. See :method:`Dataset.insert_col` for additional documentation.
""" """
self.rpush_col(col, header) self.rpush_col(col, header)
@@ -824,9 +612,9 @@ class Dataset(object):
each cell value. each cell value.
""" """
if isinstance(col, unicode): if isinstance(col, str):
if col in self.headers: 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: else:
raise KeyError raise KeyError
@@ -855,7 +643,7 @@ class Dataset(object):
sorted. sorted.
""" """
if isinstance(col, (str, unicode)): if isinstance(col, str):
if not self.headers: if not self.headers:
raise HeadersNeeded raise HeadersNeeded
@@ -992,13 +780,13 @@ class Dataset(object):
if cols is None: if cols is None:
cols = list(self.headers) 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)] rows = [row for row in rows if row in range(self.height)]
cols = [header for header in cols if header in self.headers] cols = [header for header in cols if header in self.headers]
_dset = Dataset() _dset = Dataset()
#filtering rows and columns # filtering rows and columns
_dset.headers = list(cols) _dset.headers = list(cols)
_dset._data = [] _dset._data = []
@@ -1017,20 +805,12 @@ class Dataset(object):
return _dset return _dset
class Databook(object): class Databook:
"""A book of :class:`Dataset` objects. """A book of :class:`Dataset` objects.
""" """
_formats = {}
def __init__(self, sets=None): def __init__(self, sets=None):
self._datasets = sets or []
if sets is None:
self._datasets = list()
else:
self._datasets = sets
self._register_formats()
def __repr__(self): def __repr__(self):
try: try:
@@ -1042,21 +822,6 @@ class Databook(object):
"""Removes all :class:`Dataset` objects from the :class:`Databook`.""" """Removes all :class:`Dataset` objects from the :class:`Databook`."""
self._datasets = [] 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): def sheets(self):
return self._datasets return self._datasets
@@ -1078,8 +843,8 @@ class Databook(object):
for dset in self._datasets: for dset in self._datasets:
collector.append(dict_pack( collector.append(dict_pack(
title = dset.title, title=dset.title,
data = dset._package(ordered=ordered) data=dset._package(ordered=ordered)
)) ))
return collector return collector
@@ -1091,18 +856,20 @@ class Databook(object):
def load(self, in_stream, format, **kwargs): def load(self, in_stream, format, **kwargs):
""" """
Import `in_stream` to the :class:`Databook` object using the `format`. 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`. :param \\*\\*kwargs: (optional) custom configuration to the format `import_book`.
""" """
stream = normalize_input(in_stream)
if not format: if not format:
format = detect_format(in_stream) format = detect_format(stream)
export_book, import_book = self._formats.get(format, (None, None)) fmt = registry.get_format(format)
if not import_book: if not hasattr(fmt, 'import_book'):
raise UnsupportedFormat('Format {0} cannot be loaded.'.format(format)) raise UnsupportedFormat(f'Format {format} cannot be loaded.')
import_book(self, in_stream, **kwargs) fmt.import_book(self, stream, **kwargs)
return self return self
def export(self, format, **kwargs): def export(self, format, **kwargs):
@@ -1111,50 +878,40 @@ class Databook(object):
:param \\*\\*kwargs: (optional) custom configuration to the format `export_book`. :param \\*\\*kwargs: (optional) custom configuration to the format `export_book`.
""" """
export_book, import_book = self._formats.get(format, (None, None)) fmt = registry.get_format(format)
if not export_book: if not hasattr(fmt, 'export_book'):
raise UnsupportedFormat('Format {0} cannot be exported.'.format(format)) raise UnsupportedFormat(f'Format {format} cannot be exported.')
return export_book(self, **kwargs) return fmt.export_book(self, **kwargs)
def detect_format(stream): def detect_format(stream):
"""Return format name of given stream.""" """Return format name of given stream (file-like object, string, or bytestring)."""
for fmt in formats.available: stream = normalize_input(stream)
fmt_title = None
for fmt in registry.formats():
try: try:
if fmt.detect(stream): if fmt.detect(stream):
return fmt.title fmt_title = fmt.title
break
except AttributeError: except AttributeError:
pass pass
finally:
if hasattr(stream, 'seek'):
stream.seek(0)
return fmt_title
def import_set(stream, format=None, **kwargs): 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): 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): registry.register_builtins()
"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"
+18
View File
@@ -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
View File
@@ -1,21 +1,135 @@
# -*- coding: utf-8 -*-
""" Tablib - formats """ 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 tablib.exceptions import UnsupportedFormat
from . import _json as json from tablib.utils import normalize_input
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
# xlsx before as xls (xlrd) can also read xlsx from ._csv import CSVFormat
available = (json, xlsx, xls, yaml, csv, dbf, tsv, html, jira, latex, ods, df, rst) 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": "xlrd and xlwt packages", "extras_name": "xls"},
"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(f"Unable to load format class '{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()
+20
View File
@@ -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)
+42 -41
View File
@@ -1,59 +1,60 @@
# -*- coding: utf-8 -*-
""" Tablib - *SV Support. """ Tablib - *SV Support.
""" """
from tablib.compat import csv, StringIO, unicode import csv
from io import StringIO
title = 'csv' class CSVFormat:
extensions = ('csv',) 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): _csv = csv.writer(stream, **kwargs)
"""Returns CSV representation of Dataset as file-like."""
stream = StringIO()
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): @classmethod
_csv.writerow(row) def export_set(cls, dataset, **kwargs):
"""Returns CSV representation of Dataset."""
stream = cls.export_stream_set(dataset, **kwargs)
return stream.getvalue()
stream.seek(0) @classmethod
return stream def import_set(cls, dset, in_stream, headers=True, **kwargs):
"""Returns dataset from CSV stream."""
dset.wipe()
def export_set(dataset, **kwargs): kwargs.setdefault('delimiter', cls.DEFAULT_DELIMITER)
"""Returns CSV representation of Dataset."""
stream = export_stream_set(dataset, **kwargs)
return stream.getvalue()
rows = csv.reader(in_stream, **kwargs)
for i, row in enumerate(rows):
def import_set(dset, in_stream, headers=True, **kwargs): if (i == 0) and (headers):
"""Returns dataset from CSV stream.""" dset.headers = row
elif row:
if i > 0 and len(row) < dset.width:
row += [''] * (dset.width - len(row))
dset.append(row)
dset.wipe() @classmethod
def detect(cls, stream, delimiter=None):
kwargs.setdefault('delimiter', DEFAULT_DELIMITER) """Returns True if given stream is valid CSV."""
try:
rows = csv.reader(StringIO(in_stream), **kwargs) csv.Sniffer().sniff(stream.read(1024), delimiters=delimiter or cls.DEFAULT_DELIMITER)
for i, row in enumerate(rows): return True
except Exception:
if (i == 0) and (headers): return False
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
+50 -71
View File
@@ -1,87 +1,66 @@
# -*- coding: utf-8 -*-
""" Tablib - DBF Support. """ Tablib - DBF Support.
""" """
import tempfile import io
import struct
import os import os
import tempfile
from tablib.compat import StringIO from tablib.packages.dbfpy import dbf, dbfnew
from tablib.compat import dbfpy from tablib.packages.dbfpy import record as dbfrecord
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
title = 'dbf' class DBFFormat:
extensions = ('csv',) title = 'dbf'
extensions = ('csv',)
DEFAULT_ENCODING = 'utf-8' DEFAULT_ENCODING = 'utf-8'
def export_set(dataset): @classmethod
"""Returns DBF representation of a Dataset""" def export_set(cls, dataset):
new_dbf = dbfnew.dbf_new() """Returns DBF representation of a Dataset"""
temp_file, temp_uri = tempfile.mkstemp() new_dbf = dbfnew.dbf_new()
temp_file, temp_uri = tempfile.mkstemp()
# create the appropriate fields based on the contents of the first row # create the appropriate fields based on the contents of the first row
first_row = dataset[0] first_row = dataset[0]
for fieldname, field_value in zip(dataset.headers, first_row): for fieldname, field_value in zip(dataset.headers, first_row):
if type(field_value) in [int, float]: if type(field_value) in [int, float]:
new_dbf.add_field(fieldname, 'N', 10, 8) new_dbf.add_field(fieldname, 'N', 10, 8)
else: else:
new_dbf.add_field(fieldname, 'C', 80) 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) dbf_file = dbf.Dbf(temp_uri, readOnly=0)
for row in dataset: for row in dataset:
record = dbfrecord.DbfRecord(dbf_file) record = dbfrecord.DbfRecord(dbf_file)
for fieldname, field_value in zip(dataset.headers, row): for fieldname, field_value in zip(dataset.headers, row):
record[fieldname] = field_value record[fieldname] = field_value
record.store() record.store()
dbf_file.close() dbf_file.close()
dbf_stream = open(temp_uri, 'rb') dbf_stream = open(temp_uri, 'rb')
if is_py3:
stream = io.BytesIO(dbf_stream.read()) stream = io.BytesIO(dbf_stream.read())
else: dbf_stream.close()
stream = StringIO(dbf_stream.read()) os.close(temp_file)
dbf_stream.close() os.remove(temp_uri)
os.close(temp_file) return stream.getvalue()
os.remove(temp_uri)
return stream.getvalue()
def import_set(dset, in_stream, headers=True): @classmethod
"""Returns a dataset from a DBF stream.""" def import_set(cls, dset, in_stream, headers=True):
"""Returns a dataset from a DBF stream."""
dset.wipe() dset.wipe()
if is_py3: _dbf = dbf.Dbf(in_stream)
_dbf = dbf.Dbf(io.BytesIO(in_stream)) dset.headers = _dbf.fieldNames
else: for record in range(_dbf.recordCount):
_dbf = dbf.Dbf(StringIO(in_stream)) row = [_dbf[record][f] for f in _dbf.fieldNames]
dset.headers = _dbf.fieldNames dset.append(row)
for record in range(_dbf.recordCount):
row = [_dbf[record][f] for f in _dbf.fieldNames]
dset.append(row)
def detect(stream): @classmethod
"""Returns True if the given stream is valid DBF""" def detect(cls, stream):
#_dbf = dbf.Table(StringIO(stream)) """Returns True if the given stream is valid DBF"""
try: try:
if is_py3: _dbf = dbf.Dbf(stream, readOnly=True)
if type(stream) is not bytes: return True
stream = bytes(stream, 'utf-8') except Exception:
_dbf = dbf.Dbf(io.BytesIO(stream), readOnly=True) return False
else:
_dbf = dbf.Dbf(StringIO(stream), readOnly=True)
return True
except Exception:
return False
+29 -31
View File
@@ -1,43 +1,41 @@
""" Tablib - DataFrame Support. """ Tablib - DataFrame Support.
""" """
import sys
from io import BytesIO
try: try:
from pandas import DataFrame from pandas import DataFrame
except ImportError: except ImportError:
DataFrame = None DataFrame = None
import tablib
from tablib.compat import unicode class DataFrameFormat:
title = 'df'
extensions = ('df',)
title = 'df' @classmethod
extensions = ('df', ) 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): @classmethod
"""Returns True if given stream is a DataFrame.""" def export_set(cls, dset, index=None):
if DataFrame is None: """Returns DataFrame representation of DataBook."""
return False if DataFrame is None:
try: raise NotImplementedError(
DataFrame(stream) 'DataFrame Format requires `pandas` to be installed.'
return True ' Try `pip install "tablib[pandas]"`.')
except ValueError: dataframe = DataFrame(dset.dict, columns=dset.headers)
return False return dataframe
@classmethod
def export_set(dset, index=None): def import_set(cls, dset, in_stream):
"""Returns DataFrame representation of DataBook.""" """Returns dataset from DataFrame."""
if DataFrame is None: dset.wipe()
raise NotImplementedError( dset.dict = in_stream.to_dict(orient='records')
'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')
+38 -41
View File
@@ -1,65 +1,62 @@
# -*- coding: utf-8 -*-
""" Tablib - HTML export support. """ Tablib - HTML export support.
""" """
import codecs import codecs
import sys
from io import BytesIO from io import BytesIO
from MarkupPy import markup from MarkupPy import markup
import tablib
from tablib.compat import unicode
BOOK_ENDINGS = 'h3'
title = 'html'
extensions = ('html', )
def export_set(dataset): class HTMLFormat:
"""HTML representation of a Dataset.""" BOOK_ENDINGS = 'h3'
stream = BytesIO() title = 'html'
extensions = ('html', )
page = markup.page() @classmethod
page.table.open() def export_set(cls, dataset):
"""HTML representation of a Dataset."""
if dataset.headers is not None: stream = BytesIO()
new_header = [item if item is not None else '' for item in dataset.headers]
page.thead.open() page = markup.page()
headers = markup.oneliner.th(new_header) page.table.open()
page.tr(headers)
page.thead.close()
for row in dataset: if dataset.headers is not None:
new_row = [item if item is not None else '' for item in row] new_header = [item if item is not None else '' for item in dataset.headers]
html_row = markup.oneliner.td(new_row) page.thead.open()
page.tr(html_row) 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 html_row = markup.oneliner.td(new_row)
wrapper = codecs.getwriter("utf8")(stream) page.tr(html_row)
wrapper.writelines(unicode(page))
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): return stream.getvalue().decode('utf-8')
"""HTML representation of a Databook."""
stream = BytesIO() @classmethod
def export_book(cls, databook):
"""HTML representation of a Databook."""
# Allow unicode characters in output stream = BytesIO()
wrapper = codecs.getwriter("utf8")(stream)
for i, dset in enumerate(databook._datasets): # Allow unicode characters in output
title = (dset.title if dset.title else 'Set %s' % (i)) wrapper = codecs.getwriter("utf8")(stream)
wrapper.write('<%s>%s</%s>\n' % (BOOK_ENDINGS, title, BOOK_ENDINGS))
wrapper.write(dset.html)
wrapper.write('\n')
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(f'<{cls.BOOK_ENDINGS}>{title}</{cls.BOOK_ENDINGS}>\n')
wrapper.write(dset.html)
wrapper.write('\n')
return stream.getvalue().decode('utf-8')
+27 -26
View File
@@ -1,39 +1,40 @@
# -*- coding: utf-8 -*-
"""Tablib - Jira table export support. """Tablib - Jira table export support.
Generates a Jira table from the dataset. Generates a Jira table from the dataset.
""" """
from tablib.compat import unicode
title = 'jira'
def export_set(dataset): class JIRAFormat:
"""Formats the dataset according to the Jira table syntax: title = 'jira'
||heading 1||heading 2||heading 3|| @classmethod
|col A1|col A2|col A3| def export_set(cls, dataset):
|col B1|col B2|col B3| """Formats the dataset according to the Jira table syntax:
:param dataset: dataset to serialize ||heading 1||heading 2||heading 3||
:type dataset: tablib.core.Dataset |col A1|col A2|col A3|
""" |col B1|col B2|col B3|
header = _get_header(dataset.headers) if dataset.headers else '' :param dataset: dataset to serialize
body = _get_body(dataset) :type dataset: tablib.core.Dataset
return '%s\n%s' % (header, body) if header else body """
header = cls._get_header(dataset.headers) if dataset.headers else ''
body = cls._get_body(dataset)
return f'{header}\n{body}' if header else body
def _get_body(dataset): @classmethod
return '\n'.join([_serialize_row(row) for row in dataset]) 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): @classmethod
return _serialize_row(headers, delimiter='||') def _serialize_row(cls, row, delimiter='|'):
return '{}{}{}'.format(
delimiter,
def _serialize_row(row, delimiter='|'): delimiter.join([str(item) if item else ' ' for item in row]),
return '%s%s%s' % (delimiter, delimiter
delimiter.join([unicode(item) if item else ' ' for item in row]), )
delimiter)
+33 -34
View File
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" Tablib - JSON Support """ Tablib - JSON Support
""" """
import decimal import decimal
@@ -9,10 +7,6 @@ from uuid import UUID
import tablib import tablib
title = 'json'
extensions = ('json', 'jsn')
def serialize_objects_handler(obj): def serialize_objects_handler(obj):
if isinstance(obj, (decimal.Decimal, UUID)): if isinstance(obj, (decimal.Decimal, UUID)):
return str(obj) return str(obj)
@@ -22,38 +16,43 @@ def serialize_objects_handler(obj):
return obj return obj
def export_set(dataset): class JSONFormat:
"""Returns JSON representation of Dataset.""" title = 'json'
return json.dumps(dataset.dict, default=serialize_objects_handler) 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): @classmethod
"""Returns JSON representation of Databook.""" def export_book(cls, databook):
return json.dumps(databook._package(), default=serialize_objects_handler) """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): dset.wipe()
"""Returns dataset from JSON stream.""" dset.dict = json.load(in_stream)
dset.wipe() @classmethod
dset.dict = json.loads(in_stream) 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): @classmethod
"""Returns databook from JSON stream.""" def detect(cls, stream):
"""Returns True if given stream is valid JSON."""
dbook.wipe() try:
for sheet in json.loads(in_stream): json.load(stream)
data = tablib.Dataset() return True
data.title = sheet['title'] except (TypeError, ValueError):
data.dict = sheet['data'] return False
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
+89 -91
View File
@@ -1,17 +1,15 @@
# -*- coding: utf-8 -*-
"""Tablib - LaTeX table export support. """Tablib - LaTeX table export support.
Generates a LaTeX booktabs-style table from the dataset. Generates a LaTeX booktabs-style table from the dataset.
""" """
import re import re
from tablib.compat import unicode
title = 'latex' class LATEXFormat:
extensions = ('tex',) title = 'latex'
extensions = ('tex',)
TABLE_TEMPLATE = """\ TABLE_TEMPLATE = """\
%% Note: add \\usepackage{booktabs} to your preamble %% Note: add \\usepackage{booktabs} to your preamble
%% %%
\\begin{table}[!htbp] \\begin{table}[!htbp]
@@ -27,108 +25,108 @@ TABLE_TEMPLATE = """\
\\end{table} \\end{table}
""" """
TEX_RESERVED_SYMBOLS_MAP = dict([ TEX_RESERVED_SYMBOLS_MAP = dict([
('\\', '\\textbackslash{}'), ('\\', '\\textbackslash{}'),
('{', '\\{'), ('{', '\\{'),
('}', '\\}'), ('}', '\\}'),
('$', '\\$'), ('$', '\\$'),
('&', '\\&'), ('&', '\\&'),
('#', '\\#'), ('#', '\\#'),
('^', '\\textasciicircum{}'), ('^', '\\textasciicircum{}'),
('_', '\\_'), ('_', '\\_'),
('~', '\\textasciitilde{}'), ('~', '\\textasciitilde{}'),
('%', '\\%'), ('%', '\\%'),
]) ])
TEX_RESERVED_SYMBOLS_RE = re.compile( TEX_RESERVED_SYMBOLS_RE = re.compile(
'(%s)' % '|'.join(map(re.escape, TEX_RESERVED_SYMBOLS_MAP.keys()))) '(%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): :param dataset: dataset to serialize
"""Returns LaTeX representation of dataset :type dataset: tablib.core.Dataset
"""
:param dataset: dataset to serialize caption = '\\caption{%s}' % dataset.title if dataset.title else '%'
:type dataset: tablib.core.Dataset 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 '%' @classmethod
colspec = _colspec(dataset.width) def _colspec(cls, dataset_width):
header = _serialize_row(dataset.headers) if dataset.headers else '' """Generates the column specification for the LaTeX `tabular` environment
midrule = _midrule(dataset.width) based on the 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)
The first column is justified to the left, all further columns are aligned
to the right.
def _colspec(dataset_width): .. note:: This is only a heuristic and most probably has to be fine-tuned
"""Generates the column specification for the LaTeX `tabular` environment post export. Column alignment should depend on the data type, e.g., textual
based on the dataset width. 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 :param dataset_width: width of the dataset
to the right. """
.. note:: This is only a heuristic and most probably has to be fine-tuned spec = 'l'
post export. Column alignment should depend on the data type, e.g., textual for _ in range(1, dataset_width):
content should usually be aligned to the left while numeric content almost spec += 'r'
always should be aligned to the right. 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' :param dataset_width: width of the dataset to serialize
for _ in range(1, dataset_width): """
spec += 'r'
return spec
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): @classmethod
"""Generates the table `midrule`, which may be composed of several def _cmidrule(cls, colindex, dataset_width):
`cmidrules`. """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: rule = '\\cmidrule(%s){%d-%d}'
return '\\midrule' if colindex == 1:
return ' '.join([_cmidrule(colindex, dataset_width) for colindex in # Rule of first column is trimmed on the right
range(1, dataset_width + 1)]) 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): :param row: single dataset row
"""Generates the `cmidrule` for a single column with appropriate trimming """
based on the column position.
:param colindex: Column index new_row = [cls._escape_tex_reserved_symbols(str(item)) if item else ''
:param dataset_width: width of the dataset for item in row]
""" return 6 * ' ' + ' & '.join(new_row) + ' \\\\'
rule = '\\cmidrule(%s){%d-%d}' @classmethod
if colindex == 1: def _escape_tex_reserved_symbols(cls, input):
# Rule of first column is trimmed on the right """Escapes all TeX reserved symbols ('_', '~', etc.) in a string.
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)
:param input: String to escape
def _serialize_row(row): """
"""Returns string representation of a single row. def replace(match):
return cls.TEX_RESERVED_SYMBOLS_MAP[match.group()]
:param row: single dataset row return cls.TEX_RESERVED_SYMBOLS_RE.sub(replace, input)
"""
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)
+77 -76
View File
@@ -1,104 +1,105 @@
# -*- coding: utf-8 -*-
""" Tablib - ODF Support. """ Tablib - ODF Support.
""" """
from io import BytesIO from io import BytesIO
from odf import opendocument, style, table, text
from tablib.compat import unicode
title = 'ods' from odf import opendocument, style, table, text
extensions = ('ods',)
bold = style.Style(name="bold", family="paragraph") bold = style.Style(name="bold", family="paragraph")
bold.addElement(style.TextProperties(fontweight="bold", fontweightasian="bold", fontweightcomplex="bold")) bold.addElement(style.TextProperties(fontweight="bold", fontweightasian="bold", fontweightcomplex="bold"))
def export_set(dataset):
"""Returns ODF representation of Dataset."""
wb = opendocument.OpenDocumentSpreadsheet() class ODSFormat:
wb.automaticstyles.addElement(bold) title = 'ods'
extensions = ('ods',)
ws = table.Table(name=dataset.title if dataset.title else 'Tablib Dataset') @classmethod
wb.spreadsheet.addElement(ws) def export_set(cls, dataset):
dset_sheet(dataset, ws) """Returns ODF representation of Dataset."""
stream = BytesIO() wb = opendocument.OpenDocumentSpreadsheet()
wb.save(stream) wb.automaticstyles.addElement(bold)
return stream.getvalue()
ws = table.Table(name=dataset.title if dataset.title else 'Tablib Dataset')
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))
wb.spreadsheet.addElement(ws) wb.spreadsheet.addElement(ws)
dset_sheet(dset, ws) cls.dset_sheet(dataset, ws)
stream = BytesIO() stream = BytesIO()
wb.save(stream) wb.save(stream)
return stream.getvalue() return stream.getvalue()
@classmethod
def export_book(cls, databook):
"""Returns ODF representation of DataBook."""
def dset_sheet(dataset, ws): wb = opendocument.OpenDocumentSpreadsheet()
"""Completes given worksheet from given Dataset.""" wb.automaticstyles.addElement(bold)
_package = dataset._package(dicts=False)
for i, sep in enumerate(dataset._separators): for i, dset in enumerate(databook._datasets):
_offset = i ws = table.Table(name=dset.title if dset.title else 'Sheet%s' % (i))
_package.insert((sep[0] + _offset), (sep[1],)) wb.spreadsheet.addElement(ws)
cls.dset_sheet(dset, ws)
for i, row in enumerate(_package): stream = BytesIO()
row_number = i + 1 wb.save(stream)
odf_row = table.TableRow(stylename=bold, defaultcellstylename='bold') return stream.getvalue()
for j, col in enumerate(row):
try:
col = unicode(col, errors='ignore')
except TypeError:
## col is already unicode
pass
ws.addElement(table.TableColumn())
# bold headers @classmethod
if (row_number == 1) and dataset.headers: def dset_sheet(cls, dataset, ws):
odf_row.setAttribute('stylename', bold) """Completes given worksheet from given Dataset."""
ws.addElement(odf_row) _package = dataset._package(dicts=False)
cell = table.TableCell()
p = text.P()
p.addElement(text.Span(text=col, stylename=bold))
cell.addElement(p)
odf_row.addElement(cell)
# wrap the rest for i, sep in enumerate(dataset._separators):
else: _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: try:
if '\n' in col: col = str(col, errors='ignore')
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: 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) ws.addElement(odf_row)
cell = table.TableCell() 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) 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): @classmethod
if isinstance(stream, bytes): def detect(cls, stream):
# load expects a file-like object. if isinstance(stream, bytes):
stream = BytesIO(stream) # load expects a file-like object.
try: stream = BytesIO(stream)
opendocument.load(stream) try:
return True opendocument.load(stream)
except Exception: return True
return False except Exception:
return False
+221 -229
View File
@@ -1,273 +1,265 @@
# -*- coding: utf-8 -*-
""" Tablib - reStructuredText Support """ 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 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_LEFT = 'left'
JUSTIFY_CENTER = 'center' JUSTIFY_CENTER = 'center'
JUSTIFY_RIGHT = 'right' JUSTIFY_RIGHT = 'right'
JUSTIFY_VALUES = (JUSTIFY_LEFT, JUSTIFY_CENTER, JUSTIFY_RIGHT) JUSTIFY_VALUES = (JUSTIFY_LEFT, JUSTIFY_CENTER, JUSTIFY_RIGHT)
def to_unicode(value): def to_str(value):
if isinstance(value, bytes): if isinstance(value, bytes):
return value.decode('utf-8') return value.decode('utf-8')
return unicode(value) return str(value)
def _max_word_len(text): def _max_word_len(text):
""" """
Return the length of the longest word in `text`. Return the length of the longest word in `text`.
>>> _max_word_len('Python Module for Tabular Datasets') >>> _max_word_len('Python Module for Tabular Datasets')
8 8
""" """
return max((len(word) for word in text.split())) return max([len(word) for word in text.split()], default=0) if text else 0
def _get_column_string_lengths(dataset): class ReSTFormat:
""" title = 'rst'
Returns a list of string lengths of each column, and a list of extensions = ('rst',)
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
MAX_TABLE_WIDTH = 80 # Roughly. It may be wider to avoid breaking words.
def _row_to_lines(values, widths, wrapper, sep='|', justify=JUSTIFY_LEFT): @classmethod
""" def _get_column_string_lengths(cls, dataset):
Returns a table row of wrapped values as a list of lines """
""" Returns a list of string lengths of each column, and a list of
if justify not in JUSTIFY_VALUES: maximum word lengths.
raise ValueError('Value of "justify" must be one of "{}"'.format( """
'", "'.join(JUSTIFY_VALUES) if dataset.headers:
)) column_lengths = [[len(h)] for h in dataset.headers]
if justify == JUSTIFY_LEFT: word_lens = [_max_word_len(h) for h in dataset.headers]
just = lambda text, width: text.ljust(width) else:
elif justify == JUSTIFY_CENTER: column_lengths = [[] for _ in range(dataset.width)]
just = lambda text, width: text.center(width) word_lens = [0 for _ in range(dataset.width)]
else: for row in dataset.dict:
just = lambda text, width: text.rjust(width) values = iter(row.values() if hasattr(row, 'values') else row)
lpad = sep + ' ' if sep else '' for i, val in enumerate(values):
rpad = ' ' + sep if sep else '' text = to_str(val)
pad = ' ' + sep + ' ' column_lengths[i].append(len(text))
cells = [] word_lens[i] = max(word_lens[i], _max_word_len(text))
for value, width in zip(values, widths): return column_lengths, word_lens
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 _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): @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. 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] str_lens, word_lens = cls._get_column_string_lengths(dataset)
total = sum(median_lens) median_lens = [int(median(lens)) for lens in str_lens]
if total > max_table_width - (pad_len * len(median_lens)): total = sum(median_lens)
column_widths = (max_table_width * l // total for l in median_lens) if total > max_table_width - (pad_len * len(median_lens)):
else: column_widths = (max_table_width * l // total for l in median_lens)
column_widths = (l for l in median_lens) else:
# Allow for separator and padding: column_widths = (l for l in median_lens)
column_widths = (w - pad_len if w > pad_len else w for w in column_widths) # Allow for separator and padding:
# Rather widen table than break words: column_widths = (w - pad_len if w > pad_len else w for w in column_widths)
column_widths = [max(w, l) for w, l in zip(column_widths, word_lens)] # Rather widen table than break words:
return column_widths 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) lines.append(border)
for row in dataset.dict: if dataset.headers:
values = iter(row.values() if hasattr(row, 'values') else row) lines.extend(cls._row_to_lines(
lines.extend(_row_to_lines(values, column_widths, wrapper, '')) dataset.headers,
lines.append(border) column_widths,
return '\n'.join(lines) 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): >>> from tablib import Dataset
""" >>> from tablib.formats import registry
Returns reStructuredText grid table representation of 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)])
>>> 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) 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): >>> from tablib.formats import registry
""" >>> rst = registry.get_format('rst')
Use a simple table if the text in the first column is never wrapped >>> 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) >>> from tablib import Dataset
True >>> bits = ((0, 0), (1, 0), (0, 1), (1, 1))
>>> _use_simple_table(None, ['lobster thermidor', 'spam'], 10) >>> data = Dataset()
False >>> 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: if not dataset.dict:
head0 = to_unicode(head0) return ''
if len(head0) > width0: force_grid = kwargs.get('force_grid', False)
return False max_table_width = kwargs.get('max_table_width', cls.MAX_TABLE_WIDTH)
for cell in col0: column_widths = cls._get_column_widths(dataset, max_table_width)
cell = to_unicode(cell)
if len(cell) > width0:
return False
return True
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): @classmethod
""" def export_book(cls, databook):
Returns reStructuredText table representation of dataset. """
reStructuredText representation of a Databook.
Returns a simple table if the text in the first column is never Tables are separated by a blank line. All tables use the grid
wrapped, otherwise returns a grid table. format.
"""
return '\n\n'.join(cls.export_set(dataset, force_grid=True)
>>> from tablib import Dataset for dataset in databook._datasets)
>>> 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)
+5 -24
View File
@@ -1,30 +1,11 @@
# -*- coding: utf-8 -*-
""" Tablib - TSV (Tab Separated Values) Support. """ Tablib - TSV (Tab Separated Values) Support.
""" """
from tablib.compat import unicode from ._csv import CSVFormat
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)
def import_set(dset, in_stream, headers=True): class TSVFormat(CSVFormat):
"""Returns dataset from TSV stream.""" title = 'tsv'
return import_set_wrapper(dset, in_stream, headers=headers, delimiter=DELIMITER) extensions = ('tsv',)
DEFAULT_DELIMITER = '\t'
def detect(stream):
"""Returns True if given stream is valid TSV."""
return detect_wrapper(stream, delimiter=DELIMITER)
+106 -96
View File
@@ -1,137 +1,147 @@
# -*- coding: utf-8 -*-
""" Tablib - XLS Support. """ Tablib - XLS Support.
""" """
import sys
from io import BytesIO from io import BytesIO
from tablib.compat import xrange
import tablib
import xlrd import xlrd
import xlwt import xlwt
from xlrd.biffh import XLRDError from xlrd.xldate import xldate_as_datetime
title = 'xls' import tablib
extensions = ('xls',)
# special styles # special styles
wrap = xlwt.easyxf("alignment: wrap on") wrap = xlwt.easyxf("alignment: wrap on")
bold = xlwt.easyxf("font: bold on") bold = xlwt.easyxf("font: bold on")
def detect(stream): class XLSFormat:
"""Returns True if given stream is a readable excel file.""" title = 'xls'
try: extensions = ('xls',)
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
@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): @classmethod
"""Returns XLS representation of Dataset.""" def export_set(cls, dataset):
"""Returns XLS representation of Dataset."""
wb = xlwt.Workbook(encoding='utf8') wb = xlwt.Workbook(encoding='utf8')
ws = wb.add_sheet(dataset.title if dataset.title else 'Tablib Dataset') ws = wb.add_sheet(dataset.title if dataset.title else 'Tablib Dataset')
dset_sheet(dataset, ws) cls.dset_sheet(dataset, ws)
stream = BytesIO() stream = BytesIO()
wb.save(stream) wb.save(stream)
return stream.getvalue() return stream.getvalue()
@classmethod
def export_book(cls, databook):
"""Returns XLS representation of DataBook."""
def export_book(databook): wb = xlwt.Workbook(encoding='utf8')
"""Returns XLS representation of DataBook."""
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): cls.dset_sheet(dset, ws)
ws = wb.add_sheet(dset.title if dset.title else 'Sheet%s' % (i))
dset_sheet(dset, ws) stream = BytesIO()
wb.save(stream)
return stream.getvalue()
stream = BytesIO() @classmethod
wb.save(stream) def import_set(cls, dset, in_stream, headers=True):
return stream.getvalue() """Returns databook from XLS stream."""
dset.wipe()
def import_set(dset, in_stream, headers=True): xls_book = xlrd.open_workbook(file_contents=in_stream.read())
"""Returns databook from XLS stream.""" sheet = xls_book.sheet_by_index(0)
dset.wipe() dset.title = sheet.name
xls_book = xlrd.open_workbook(file_contents=in_stream) def cell_value(value, type_):
sheet = xls_book.sheet_by_index(0) if type_ == xlrd.XL_CELL_ERROR:
return xlrd.error_text_from_code[value]
elif type_ == xlrd.XL_CELL_DATE:
return xldate_as_datetime(value, xls_book.datemode)
return value
dset.title = sheet.name for i in range(sheet.nrows):
if i == 0 and headers:
for i in xrange(sheet.nrows): dset.headers = sheet.row_values(0)
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)
else: else:
data.append(sheet.row_values(i)) dset.append([
cell_value(val, typ)
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): xls_book = xlrd.open_workbook(file_contents=in_stream)
"""Completes given worksheet from given Dataset."""
_package = dataset._package(dicts=False)
for i, sep in enumerate(dataset._separators): for sheet in xls_book.sheets():
_offset = i data = tablib.Dataset()
_package.insert((sep[0] + _offset), (sep[1],)) data.title = sheet.name
for i, row in enumerate(_package): for i in range(sheet.nrows):
for j, col in enumerate(row): if i == 0 and headers:
data.headers = sheet.row_values(0)
else:
data.append(sheet.row_values(i))
# bold headers dbook.add_sheet(data)
if (i == 0) and dataset.headers:
ws.write(i, j, col, bold)
# frozen header row @classmethod
ws.panes_frozen = True def dset_sheet(cls, dataset, ws):
ws.horz_split_pos = 1 """Completes given worksheet from given Dataset."""
_package = dataset._package(dicts=False)
# bold separators for i, sep in enumerate(dataset._separators):
elif len(row) < dataset.width: _offset = i
ws.write(i, j, col, bold) _package.insert((sep[0] + _offset), (sep[1],))
# wrap the rest for i, row in enumerate(_package):
else: for j, col in enumerate(row):
try:
if '\n' in col: # bold headers
ws.write(i, j, col, wrap) if (i == 0) and dataset.headers:
else: 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) ws.write(i, j, col)
except TypeError:
ws.write(i, j, col)
+108 -109
View File
@@ -1,144 +1,143 @@
# -*- coding: utf-8 -*-
""" Tablib - XLSX Support. """ Tablib - XLSX Support.
""" """
import sys
from io import BytesIO from io import BytesIO
import openpyxl from openpyxl.reader.excel import ExcelReader, load_workbook
from openpyxl.styles import Alignment, Font
from openpyxl.utils import get_column_letter
from openpyxl.workbook import Workbook
from openpyxl.writer.excel import ExcelWriter
import tablib import tablib
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',)
@classmethod
def detect(cls, stream):
"""Returns True if given stream is a readable excel file."""
try:
# No need to fully load the file, it should be enough to be able to
# read the manifest.
reader = ExcelReader(stream, read_only=False)
reader.read_manifest()
return True
except Exception:
return False
title = 'xlsx' @classmethod
extensions = ('xlsx',) 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'
cls.dset_sheet(dataset, ws, freeze_panes=freeze_panes)
def detect(stream): stream = BytesIO()
"""Returns True if given stream is a readable excel file.""" wb.save(stream)
if isinstance(stream, bytes): return stream.getvalue()
# 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
def export_set(dataset, freeze_panes=True): @classmethod
"""Returns XLSX representation of Dataset.""" def export_book(cls, databook, freeze_panes=True):
"""Returns XLSX representation of DataBook."""
wb = Workbook() wb = Workbook()
ws = wb.worksheets[0] for sheet in wb.worksheets:
ws.title = dataset.title if dataset.title else 'Tablib Dataset' 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_sheet(dataset, ws, freeze_panes=freeze_panes) cls.dset_sheet(dset, ws, freeze_panes=freeze_panes)
stream = BytesIO() stream = BytesIO()
wb.save(stream) wb.save(stream)
return stream.getvalue() return stream.getvalue()
@classmethod
def import_set(cls, dset, in_stream, headers=True, read_only=True):
"""Returns databook from XLS stream."""
def export_book(databook, freeze_panes=True): dset.wipe()
"""Returns XLSX representation of DataBook."""
wb = Workbook() xls_book = load_workbook(in_stream, read_only=read_only, data_only=True)
for sheet in wb.worksheets: sheet = xls_book.active
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_sheet(dset, ws, freeze_panes=freeze_panes) dset.title = sheet.title
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
for i, row in enumerate(sheet.rows): for i, row in enumerate(sheet.rows):
row_vals = [c.value for c in row] row_vals = [c.value for c in row]
if (i == 0) and (headers): if (i == 0) and (headers):
data.headers = row_vals dset.headers = row_vals
else: else:
data.append(row_vals) dset.append(row_vals)
dbook.add_sheet(data) @classmethod
def import_book(cls, dbook, in_stream, headers=True, read_only=True):
"""Returns databook from XLS stream."""
dbook.wipe()
def dset_sheet(dataset, ws, freeze_panes=True): xls_book = load_workbook(in_stream, read_only=read_only, data_only=True)
"""Completes given worksheet from given Dataset."""
_package = dataset._package(dicts=False)
for i, sep in enumerate(dataset._separators): for sheet in xls_book.worksheets:
_offset = i data = tablib.Dataset()
_package.insert((sep[0] + _offset), (sep[1],)) data.title = sheet.title
bold = openpyxl.styles.Font(bold=True) for i, row in enumerate(sheet.rows):
wrap_text = openpyxl.styles.Alignment(wrap_text=True) row_vals = [c.value for c in row]
if (i == 0) and (headers):
data.headers = row_vals
else:
if i > 0 and len(row_vals) < data.width:
row_vals += [''] * (data.width - len(row_vals))
data.append(row_vals)
for i, row in enumerate(_package): dbook.add_sheet(data)
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)]
# bold headers @classmethod
if (row_number == 1) and dataset.headers: def dset_sheet(cls, dataset, ws, freeze_panes=True):
cell.font = bold """Completes given worksheet from given Dataset."""
if freeze_panes: _package = dataset._package(dicts=False)
# Export Freeze only after first Line
ws.freeze_panes = 'A2'
# bold separators for i, sep in enumerate(dataset._separators):
elif len(row) < dataset.width: _offset = i
cell.font = bold _package.insert((sep[0] + _offset), (sep[1],))
bold = Font(bold=True)
wrap_text = 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[f'{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: try:
str_col_value = unicode(col) cell.value = col
except TypeError: except (ValueError, TypeError):
str_col_value = '' cell.value = str(col)
if '\n' in str_col_value:
cell.alignment = wrap_text
try:
cell.value = col
except (ValueError, TypeError):
cell.value = unicode(col)
+37 -36
View File
@@ -1,53 +1,54 @@
# -*- coding: utf-8 -*-
""" Tablib - YAML Support. """ Tablib - YAML Support.
""" """
import tablib
import yaml import yaml
title = 'yaml' import tablib
extensions = ('yaml', 'yml')
def export_set(dataset): class YAMLFormat:
"""Returns YAML representation of Dataset.""" title = 'yaml'
extensions = ('yaml', 'yml')
return yaml.safe_dump(dataset._package(ordered=False)) @classmethod
def export_set(cls, dataset):
"""Returns YAML representation of Dataset."""
return yaml.safe_dump(dataset._package(ordered=False), default_flow_style=None)
def export_book(databook): @classmethod
"""Returns YAML representation of Databook.""" def export_book(cls, databook):
return yaml.safe_dump(databook._package(ordered=False)) """Returns YAML representation of Databook."""
return yaml.safe_dump(databook._package(ordered=False), default_flow_style=None)
@classmethod
def import_set(cls, dset, in_stream):
"""Returns dataset from YAML stream."""
def import_set(dset, in_stream): dset.wipe()
"""Returns dataset from YAML stream.""" dset.dict = yaml.safe_load(in_stream)
dset.wipe() @classmethod
dset.dict = yaml.safe_load(in_stream) def import_book(cls, dbook, in_stream):
"""Returns databook from YAML stream."""
dbook.wipe()
def import_book(dbook, in_stream): for sheet in yaml.safe_load(in_stream):
"""Returns databook from YAML stream.""" data = tablib.Dataset()
data.title = sheet['title']
data.dict = sheet['data']
dbook.add_sheet(data)
dbook.wipe() @classmethod
def detect(cls, stream):
for sheet in yaml.safe_load(in_stream): """Returns True if given stream is valid YAML."""
data = tablib.Dataset() try:
data.title = sheet['title'] _yaml = yaml.safe_load(stream)
data.dict = sheet['data'] if isinstance(_yaml, (list, tuple, dict)):
dbook.add_sheet(data) return True
else:
return False
def detect(stream): except (yaml.parser.ParserError, yaml.reader.ReaderError,
"""Returns True if given stream is valid YAML.""" yaml.scanner.ScannerError):
try:
_yaml = yaml.safe_load(stream)
if isinstance(_yaml, (list, tuple, dict)):
return True
else:
return False return False
except (yaml.parser.ParserError, yaml.reader.ReaderError,
yaml.scanner.ScannerError):
return False
+12 -12
View File
@@ -62,12 +62,11 @@ __author__ = "Jeff Kunce <kuncej@mail.conservation.state.mo.us>"
__all__ = ["Dbf"] __all__ = ["Dbf"]
from . import header from . import header, record
from . import record from .utils import INVALID_VALUE
from utils import INVALID_VALUE
class Dbf(object): class Dbf:
"""DBF accessor. """DBF accessor.
FIXME: FIXME:
@@ -114,16 +113,16 @@ class Dbf(object):
``INVALID_VALUE`` instead of raising conversion error. ``INVALID_VALUE`` instead of raising conversion error.
""" """
if isinstance(f, basestring): if isinstance(f, str):
# a filename # a filename
self.name = f self.name = f
if new: if new:
# new table (table file must be # new table (table file must be
# created or opened and truncated) # created or opened and truncated)
self.stream = file(f, "w+b") self.stream = open(f, "w+b")
else: else:
# tabe file must exist # table file must exist
self.stream = file(f, ("r+b", "rb")[bool(readOnly)]) self.stream = open(f, ("r+b", "rb")[bool(readOnly)])
else: else:
# a stream # a stream
self.name = getattr(f, "name", "") self.name = getattr(f, "name", "")
@@ -177,7 +176,7 @@ class Dbf(object):
Return value is numeric object maning valid index. 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") raise TypeError("Index must be a numeric object")
if index < 0: if index < 0:
# index from the right side # index from the right side
@@ -187,7 +186,7 @@ class Dbf(object):
raise IndexError("Record index out of range") raise IndexError("Record index out of range")
return index return index
# iterface methods # interface methods
def close(self): def close(self):
self.flush() self.flush()
@@ -204,7 +203,8 @@ class Dbf(object):
def indexOfFieldName(self, name): def indexOfFieldName(self, name):
"""Index of field named ``name``.""" """Index of field named ``name``."""
# FIXME: move this to header class # 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): def newRecord(self):
"""Return new record, which belong to this table.""" """Return new record, which belong to this table."""
@@ -294,4 +294,4 @@ if __name__ == '__main__':
demo_create(_name) demo_create(_name)
demo_read(_name) demo_read(_name)
# vim: set et sw=4 sts=4 : # vim: set et sw=4 sts=4 :
+12 -18
View File
@@ -6,7 +6,7 @@ Note: this is a legacy interface. New code should use Dbf class
TODO: TODO:
- handle Memo fields. - 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` `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"] __all__ = ["dbf_new"]
from dbf import * from .dbf import *
from fields import * from .fields import *
from header import * from .header import *
from record import * from .record import *
class _FieldDefinition(object): class _FieldDefinition:
"""Field definition. """Field definition.
This is a simple structure, which contains ``name``, ``type``, This is a simple structure, which contains ``name``, ``type``,
``len``, ``dec`` and ``cls`` fields. ``len``, ``dec`` and ``cls`` fields.
Objects also implement get/setitem magic functions, so 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 index 0, 'type' index 1, 'len' index 2, 'dec' index 3 and
'cls' could be located at index 4. 'cls' could be located at index 4.
@@ -87,7 +87,7 @@ class _FieldDefinition(object):
dbfh.addField(_dbff) dbfh.addField(_dbff)
class dbf_new(object): class dbf_new:
"""New .DBF creation helper. """New .DBF creation helper.
Example Usage: Example Usage:
@@ -140,17 +140,11 @@ class dbf_new(object):
_dbfh.setCurrentDate() _dbfh.setCurrentDate()
for _fldDef in self.fields: for _fldDef in self.fields:
_fldDef.appendToHeader(_dbfh) _fldDef.appendToHeader(_dbfh)
_dbfStream = file(filename, "wb")
_dbfStream = open(filename, "wb")
_dbfh.write(_dbfStream) _dbfh.write(_dbfStream)
_dbfStream.close() _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__': if __name__ == '__main__':
# create a new DBF-File # create a new DBF-File
@@ -182,8 +176,8 @@ if __name__ == '__main__':
for i1 in range(len(dbft)): for i1 in range(len(dbft)):
rec = dbft[i1] rec = dbft[i1]
for fldName in dbft.fieldNames: for fldName in dbft.fieldNames:
print('%s:\t %s' % (fldName, rec[fldName])) print('{}:\t {}'.format(fldName, rec[fldName]))
print() print()
dbft.close() dbft.close()
# vim: set et sts=4 sw=4 : # vim: set et sts=4 sw=4 :
+53 -44
View File
@@ -28,36 +28,38 @@ TODO:
__version__ = "$Revision: 1.14 $"[11:-2] __version__ = "$Revision: 1.14 $"[11:-2]
__date__ = "$Date: 2009/05/26 05:16:51 $"[7:-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 datetime
import struct import struct
import sys import sys
from functools import total_ordering
from . import utils from . import utils
## abstract definitions # abstract definitions
class DbfFieldDef(object):
@total_ordering
class DbfFieldDef:
"""Abstract field definition. """Abstract field definition.
Child classes must override ``type`` class attribute to provide datatype 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` `http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
Also child classes must override ``defaultValue`` field to provide Also child classes must override ``defaultValue`` field to provide
default value for the field value. default value for the field value.
If child class has fixed length ``length`` class attribute must be 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. isn't of fixed length.
Note: ``name`` field must not be changed after instantiation. Note: ``name`` field must not be changed after instantiation.
""" """
__slots__ = ("name", "length", "decimalCount", __slots__ = ("name", "decimalCount", "start", "end", "ignoreErrors")
"start", "end", "ignoreErrors")
# length of the field, None in case of variable-length field, # length of the field, None in case of variable-length field,
# or a number if this field is a fixed-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 # field type. for more information about fields types visit
# `http://www.clicketyclick.dk/databases/xbase/format/data_types.html` # `http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
# must be overriden in child classes # must be overridden in child classes
typeCode = None typeCode = None
# default value for the field. this field must be # default value for the field. this field must be
# overriden in child classes # overridden in child classes
defaultValue = None defaultValue = None
def __init__(self, name, length=None, decimalCount=None, def __init__(self, name, length=None, decimalCount=None,
start=None, stop=None, ignoreErrors=False, start=None, stop=None, ignoreErrors=False):
):
"""Initialize instance.""" """Initialize instance."""
assert self.typeCode is not None, "Type code must be overriden" assert self.typeCode is not None, "Type code must be overridden"
assert self.defaultValue is not None, "Default value must be overriden" assert self.defaultValue is not None, "Default value must be overridden"
## fix arguments # fix arguments
if len(name) >10: if len(name) > 10:
raise ValueError("Field name \"%s\" is too long" % name) raise ValueError("Field name \"%s\" is too long" % name)
name = str(name).upper() name = str(name).upper()
if self.__class__.length is None: if self.__class__.length is None:
@@ -87,13 +88,12 @@ class DbfFieldDef(object):
raise ValueError("[%s] Length isn't specified" % name) raise ValueError("[%s] Length isn't specified" % name)
length = int(length) length = int(length)
if length <= 0: if length <= 0:
raise ValueError("[%s] Length must be a positive integer" raise ValueError("[%s] Length must be a positive integer" % name)
% name)
else: else:
length = self.length length = self.length
if decimalCount is None: if decimalCount is None:
decimalCount = 0 decimalCount = 0
## set fields # set fields
self.name = name self.name = name
# FIXME: validate length according to the specification at # FIXME: validate length according to the specification at
# http://www.clicketyclick.dk/databases/xbase/format/data_types.html # http://www.clicketyclick.dk/databases/xbase/format/data_types.html
@@ -103,8 +103,14 @@ class DbfFieldDef(object):
self.start = start self.start = start
self.end = stop self.end = stop
def __cmp__(self, other): def __eq__(self, other):
return cmp(self.name, str(other).upper()) 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): def __hash__(self):
return hash(self.name) return hash(self.name)
@@ -123,9 +129,9 @@ class DbfFieldDef(object):
""" """
assert len(string) == 32 assert len(string) == 32
_length = ord(string[16]) _length = string[16]
return cls(utils.unzfill(string)[:11], _length, ord(string[17]), return cls(utils.unzfill(string)[:11].decode('utf-8'), _length,
start, start + _length, ignoreErrors=ignoreErrors) string[17], start, start + _length, ignoreErrors=ignoreErrors)
fromString = classmethod(fromString) fromString = classmethod(fromString)
def toString(self): def toString(self):
@@ -136,15 +142,11 @@ class DbfFieldDef(object):
definition of this field. definition of this field.
""" """
if sys.version_info < (2, 4): _name = self.name.ljust(11, '\0')
# earlier versions did not support padding character
_name = self.name[:11] + "\0" * (11 - len(self.name))
else:
_name = self.name.ljust(11, '\0')
return ( return (
_name + _name +
self.typeCode + self.typeCode +
#data address # data address
chr(0) * 4 + chr(0) * 4 +
chr(self.length) + chr(self.length) +
chr(self.decimalCount) + chr(self.decimalCount) +
@@ -171,7 +173,7 @@ class DbfFieldDef(object):
"""Return decoded field value from the record string.""" """Return decoded field value from the record string."""
try: try:
return self.decodeValue(self.rawFromRecord(record)) return self.decodeValue(self.rawFromRecord(record))
except: except Exception:
if self.ignoreErrors: if self.ignoreErrors:
return utils.INVALID_VALUE return utils.INVALID_VALUE
else: else:
@@ -190,17 +192,18 @@ class DbfFieldDef(object):
def encodeValue(self, value): def encodeValue(self, value):
"""Return str object containing encoded field 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 raise NotImplementedError
## real classes # real classes
class DbfCharacterFieldDef(DbfFieldDef): class DbfCharacterFieldDef(DbfFieldDef):
"""Definition of the character field.""" """Definition of the character field."""
typeCode = "C" typeCode = "C"
defaultValue = "" defaultValue = b''
def decodeValue(self, value): def decodeValue(self, value):
"""Return string object. """Return string object.
@@ -208,7 +211,7 @@ class DbfCharacterFieldDef(DbfFieldDef):
Return value is a ``value`` argument with stripped right spaces. Return value is a ``value`` argument with stripped right spaces.
""" """
return value.rstrip(" ") return value.rstrip(b' ').decode('utf-8')
def encodeValue(self, value): def encodeValue(self, value):
"""Return raw data string encoded from a ``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. Return value is a int (long) or float instance.
""" """
value = value.strip(" \0") value = value.strip(b' \0')
if "." in value: if b'.' in value:
# a float (has decimal separator) # a float (has decimal separator)
return float(value) return float(value)
elif value: elif value:
@@ -257,11 +260,13 @@ class DbfNumericFieldDef(DbfFieldDef):
% (self.name, _rv, self.length)) % (self.name, _rv, self.length))
return _rv return _rv
class DbfFloatFieldDef(DbfNumericFieldDef): class DbfFloatFieldDef(DbfNumericFieldDef):
"""Definition of the float field - same as numeric.""" """Definition of the float field - same as numeric."""
typeCode = "F" typeCode = "F"
class DbfIntegerFieldDef(DbfFieldDef): class DbfIntegerFieldDef(DbfFieldDef):
"""Definition of the integer field.""" """Definition of the integer field."""
@@ -277,6 +282,7 @@ class DbfIntegerFieldDef(DbfFieldDef):
"""Return string containing encoded ``value``.""" """Return string containing encoded ``value``."""
return struct.pack("<i", int(value)) return struct.pack("<i", int(value))
class DbfCurrencyFieldDef(DbfFieldDef): class DbfCurrencyFieldDef(DbfFieldDef):
"""Definition of the currency field.""" """Definition of the currency field."""
@@ -292,6 +298,7 @@ class DbfCurrencyFieldDef(DbfFieldDef):
"""Return string containing encoded ``value``.""" """Return string containing encoded ``value``."""
return struct.pack("<q", round(value * 10000)) return struct.pack("<q", round(value * 10000))
class DbfLogicalFieldDef(DbfFieldDef): class DbfLogicalFieldDef(DbfFieldDef):
"""Definition of the logical field.""" """Definition of the logical field."""
@@ -308,7 +315,7 @@ class DbfLogicalFieldDef(DbfFieldDef):
return False return False
if value in "YyTt": if value in "YyTt":
return True return True
raise ValueError("[%s] Invalid logical value %r" % (self.name, value)) raise ValueError(f"[{self.name}] Invalid logical value {value!r}")
def encodeValue(self, value): def encodeValue(self, value):
"""Return a character from the "TF?" set. """Return a character from the "TF?" set.
@@ -328,7 +335,7 @@ class DbfLogicalFieldDef(DbfFieldDef):
class DbfMemoFieldDef(DbfFieldDef): class DbfMemoFieldDef(DbfFieldDef):
"""Definition of the memo field. """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): def decodeValue(self, value):
"""Return int .dbt block number decoded from the string object.""" """Return int .dbt block number decoded from the string object."""
#return int(value) # return int(value)
raise NotImplementedError raise NotImplementedError
def encodeValue(self, value): def encodeValue(self, value):
@@ -347,7 +354,7 @@ class DbfMemoFieldDef(DbfFieldDef):
Note: this is an internal method. Note: this is an internal method.
""" """
#return str(value)[:self.length].ljust(self.length) # return str(value)[:self.length].ljust(self.length)
raise NotImplementedError raise NotImplementedError
@@ -423,6 +430,7 @@ class DbfDateTimeFieldDef(DbfFieldDef):
_fieldsRegistry = {} _fieldsRegistry = {}
def registerField(fieldCls): def registerField(fieldCls):
"""Register field definition class. """Register field definition class.
@@ -452,13 +460,14 @@ def lookupFor(typeCode):
""" """
# XXX: use typeCode.upper()? in case of any decign don't # XXX: use typeCode.upper()? in case of any decign don't
# forget to look to the same comment in ``registerField`` # 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) \ if isinstance(_val, type) and issubclass(_val, DbfFieldDef) \
and (_name != "DbfFieldDef"): and (_name != "DbfFieldDef"):
__all__.append(_name) __all__.append(_name)
registerField(_val) registerField(_val)
del _name, _val del _name, _val
+31 -36
View File
@@ -19,20 +19,16 @@ __date__ = "$Date: 2010/09/16 05:06:39 $"[7:-2]
__all__ = ["DbfHeader"] __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 datetime
import io
import struct import struct
import time import sys
from . import fields from . import fields
from . import utils from .utils import getDate
class DbfHeader(object): class DbfHeader:
"""Dbf header definition. """Dbf header definition.
For more information about dbf header format visit For more information about dbf header format visit
@@ -54,13 +50,12 @@ class DbfHeader(object):
""" """
__slots__ = ("signature", "fields", "lastUpdate", "recordLength", __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, 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. """Initialize instance.
Arguments: Arguments:
@@ -90,7 +85,7 @@ class DbfHeader(object):
self.fields = [] self.fields = []
else: else:
self.fields = list(fields) self.fields = list(fields)
self.lastUpdate = utils.getDate(lastUpdate) self.lastUpdate = getDate(lastUpdate)
self.recordLength = recordLength self.recordLength = recordLength
self.headerLength = headerLength self.headerLength = headerLength
self.recordCount = recordCount self.recordCount = recordCount
@@ -102,35 +97,34 @@ class DbfHeader(object):
# @classmethod # @classmethod
def fromString(cls, string): def fromString(cls, string):
"""Return header instance from the string object.""" """Return header instance from the string object."""
return cls.fromStream(cStringIO.StringIO(str(string))) return cls.fromStream(io.StringIO(str(string)))
fromString = classmethod(fromString) fromString = classmethod(fromString)
# @classmethod # @classmethod
def fromStream(cls, stream): def fromStream(cls, stream):
"""Return header object from the stream.""" """Return header object from the stream."""
stream.seek(0) 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]) (_cnt, _hdrLen, _recLen) = struct.unpack("<I2H", _data[4:12])
#reserved = _data[12:32] # reserved = _data[12:32]
_year = ord(_data[1]) _year = _data[1]
if _year < 80: if _year < 80:
# dBase II started at 1980. It is quite unlikely # dBase II started at 1980. It is quite unlikely
# that actual last update date is before that year. # that actual last update date is before that year.
_year += 2000 _year += 2000
else: else:
_year += 1900 _year += 1900
## create header object # create header object
_obj = cls(None, _hdrLen, _recLen, _cnt, ord(_data[0]), _obj = cls(None, _hdrLen, _recLen, _cnt, _data[0],
(_year, ord(_data[2]), ord(_data[3]))) (_year, _data[2], _data[3]))
## append field definitions # append field definitions
# position 0 is for the deletion flag # position 0 is for the deletion flag
_pos = 1 _pos = 1
_data = stream.read(1) _data = stream.read(1)
while _data != b'\r':
# 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"]:
_data += stream.read(31) _data += stream.read(31)
_fld = fields.lookupFor(_data[11]).fromString(_data, _pos) _fld = fields.lookupFor(_data[11]).fromString(_data, _pos)
_obj._addField(_fld) _obj._addField(_fld)
@@ -139,7 +133,7 @@ class DbfHeader(object):
return _obj return _obj
fromStream = classmethod(fromStream) fromStream = classmethod(fromStream)
## properties # properties
year = property(lambda self: self.lastUpdate.year) year = property(lambda self: self.lastUpdate.year)
month = property(lambda self: self.lastUpdate.month) month = property(lambda self: self.lastUpdate.month)
@@ -160,7 +154,7 @@ class DbfHeader(object):
""") """)
## object representation # object representation
def __repr__(self): def __repr__(self):
_rv = """\ _rv = """\
@@ -177,7 +171,7 @@ Version (signature): 0x%02x
) )
return _rv return _rv
## internal methods # internal methods
def _addField(self, *defs): def _addField(self, *defs):
"""Internal variant of the `addField` method. """Internal variant of the `addField` method.
@@ -201,8 +195,7 @@ Version (signature): 0x%02x
else: else:
(_name, _type, _len, _dec) = (tuple(_def) + (None,) * 4)[:4] (_name, _type, _len, _dec) = (tuple(_def) + (None,) * 4)[:4]
_cls = fields.lookupFor(_type) _cls = fields.lookupFor(_type)
_obj = _cls(_name, _len, _dec, _obj = _cls(_name, _len, _dec, ignoreErrors=self._ignore_errors)
ignoreErrors=self._ignore_errors)
_recordLength += _obj.length _recordLength += _obj.length
_defs.append(_obj) _defs.append(_obj)
# and now extend field definitions and # and now extend field definitions and
@@ -210,7 +203,7 @@ Version (signature): 0x%02x
self.fields += _defs self.fields += _defs
return _recordLength return _recordLength
## interface methods # interface methods
def addField(self, *defs): def addField(self, *defs):
"""Add field definition to the header. """Add field definition to the header.
@@ -240,8 +233,9 @@ Version (signature): 0x%02x
"""Encode and write header to the stream.""" """Encode and write header to the stream."""
stream.seek(0) stream.seek(0)
stream.write(self.toString()) stream.write(self.toString())
stream.write("".join([_fld.toString() for _fld in self.fields])) fields = [_fld.toString() for _fld in self.fields]
stream.write(chr(0x0D)) # cr at end of all hdr data stream.write(''.join(fields).encode(sys.getfilesystemencoding()))
stream.write(b'\x0D') # cr at end of all header data
self.changed = False self.changed = False
def toString(self): def toString(self):
@@ -253,7 +247,8 @@ Version (signature): 0x%02x
self.day, self.day,
self.recordCount, self.recordCount,
self.headerLength, self.headerLength,
self.recordLength) + "\0" * 20 self.recordLength) + (b'\x00' * 20)
# TODO: figure out if bytes(utf-8) is correct here.
def setCurrentDate(self): def setCurrentDate(self):
"""Update ``self.lastUpdate`` field with current date value.""" """Update ``self.lastUpdate`` field with current date value."""
@@ -261,7 +256,7 @@ Version (signature): 0x%02x
def __getitem__(self, item): def __getitem__(self, item):
"""Return a field definition by numeric index or name string""" """Return a field definition by numeric index or name string"""
if isinstance(item, basestring): if isinstance(item, str):
_name = item.upper() _name = item.upper()
for _field in self.fields: for _field in self.fields:
if _field.name == _name: if _field.name == _name:
+28 -23
View File
@@ -16,19 +16,20 @@ __date__ = "$Date: 2007/02/11 09:05:49 $"[7:-2]
__all__ = ["DbfRecord"] __all__ = ["DbfRecord"]
from itertools import izip import sys
import utils from . import utils
class DbfRecord(object):
class DbfRecord:
"""DBF record. """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. use `dbf.Dbf.newRecord` instead.
Class implements mapping/sequence interface, so Class implements mapping/sequence interface, so
fields could be accessed via their names or indexes 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: Hint:
Use `store` method to save modified record. Use `store` method to save modified record.
@@ -52,14 +53,14 @@ class DbfRecord(object):
__slots__ = "dbf", "index", "deleted", "fieldData" __slots__ = "dbf", "index", "deleted", "fieldData"
## creation and initialization # creation and initialization
def __init__(self, dbf, index=None, deleted=False, data=None): def __init__(self, dbf, index=None, deleted=False, data=None):
"""Instance initialiation. """Instance initialization.
Arguments: Arguments:
dbf: dbf:
A `Dbf.Dbf` instance this record belonogs to. A `Dbf.Dbf` instance this record belongs to.
index: index:
An integer record index or None. If this value is An integer record index or None. If this value is
None, record will be appended to the DBF. None, record will be appended to the DBF.
@@ -82,7 +83,7 @@ class DbfRecord(object):
# XXX: validate self.index before calculating position? # XXX: validate self.index before calculating position?
position = property(lambda self: self.dbf.header.headerLength + \ 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): def rawFromStream(cls, dbf, index):
"""Return raw record contents read from the stream. """Return raw record contents read from the stream.
@@ -137,10 +138,10 @@ class DbfRecord(object):
""" """
return cls(dbf, index, string[0]=="*", 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) fromString = classmethod(fromString)
## object representation # object representation
def __repr__(self): def __repr__(self):
_template = "%%%ds: %%s (%%s)" % max([len(_fld) _template = "%%%ds: %%s (%%s)" % max([len(_fld)
@@ -155,14 +156,14 @@ class DbfRecord(object):
_rv.append(_template % (_fld, _val, type(_val))) _rv.append(_template % (_fld, _val, type(_val)))
return "\n".join(_rv) return "\n".join(_rv)
## protected methods # protected methods
def _write(self): def _write(self):
"""Write data to the dbf stream. """Write data to the dbf stream.
Note: Note:
This isn't a public method, it's better to 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 Be design ``_write`` method should be called
only from the `Dbf` instance. only from the `Dbf` instance.
@@ -170,15 +171,16 @@ class DbfRecord(object):
""" """
self._validateIndex(False) self._validateIndex(False)
self.dbf.stream.seek(self.position) 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? # FIXME: may be move this write somewhere else?
# why we should check this condition for each record? # why we should check this condition for each record?
if self.index == len(self.dbf): if self.index == len(self.dbf):
# this is the last record, # this is the last record,
# we should write SUB (ASCII 26) # 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): def _validateIndex(self, allowUndefined=True, checkRange=False):
"""Valid ``self.index`` value. """Valid ``self.index`` value.
@@ -194,9 +196,9 @@ class DbfRecord(object):
raise ValueError("Index can't be negative (%s)" % self.index) raise ValueError("Index can't be negative (%s)" % self.index)
elif checkRange and self.index <= self.dbf.header.recordCount: elif checkRange and self.index <= self.dbf.header.recordCount:
raise ValueError("There are only %d records in the DBF" % 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): def store(self):
"""Store current record in the DBF. """Store current record in the DBF.
@@ -218,9 +220,12 @@ class DbfRecord(object):
def toString(self): def toString(self):
"""Return string packed record values.""" """Return string packed record values."""
# for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData):
#
return "".join([" *"[self.deleted]] + [ return "".join([" *"[self.deleted]] + [
_def.encodeValue(_dat) _def.encodeValue(_dat)
for (_def, _dat) in izip(self.dbf.header.fields, self.fieldData) for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData)
]) ])
def asList(self): def asList(self):
@@ -241,11 +246,11 @@ class DbfRecord(object):
real values stored in this 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): def __getitem__(self, key):
"""Return value by field name or field index.""" """Return value by field name or field index."""
if isinstance(key, (long, int)): if isinstance(key, int):
# integer index of the field # integer index of the field
return self.fieldData[key] return self.fieldData[key]
# assuming string field name # assuming string field name
@@ -253,7 +258,7 @@ class DbfRecord(object):
def __setitem__(self, key, value): def __setitem__(self, key, value):
"""Set field value by integer index of the field or string name.""" """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 # integer index of the field
return self.fieldData[key] return self.fieldData[key]
# assuming string field name # assuming string field name
+11 -13
View File
@@ -20,12 +20,12 @@ import time
def unzfill(str): def unzfill(str):
"""Return a string without ASCII NULs. """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. and truncates string till that position.
""" """
try: try:
return str[:str.index('\0')] return str[:str.index(b'\0')]
except ValueError: except ValueError:
return str return str
@@ -48,7 +48,7 @@ def getDate(date=None):
sequence: sequence:
assuming (year, month, day, ...) 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 it will be used and result of the called would be treated
as a timestamp value. as a timestamp value.
@@ -60,10 +60,10 @@ def getDate(date=None):
return date return date
if isinstance(date, datetime.datetime): if isinstance(date, datetime.datetime):
return date.date() return date.date()
if isinstance(date, (int, long, float)): if isinstance(date, (int, float)):
# date is a timestamp # date is a timestamp
return datetime.date.fromtimestamp(date) return datetime.date.fromtimestamp(date)
if isinstance(date, basestring): if isinstance(date, str):
date = date.replace(" ", "0") date = date.replace(" ", "0")
if len(date) == 6: if len(date) == 6:
# yymmdd # yymmdd
@@ -95,7 +95,7 @@ def getDateTime(value=None):
sequence: sequence:
assuming (year, month, day, ...) 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 it will be used and result of the called would be treated
as a timestamp value. as a timestamp value.
@@ -107,10 +107,10 @@ def getDateTime(value=None):
return value return value
if isinstance(value, datetime.date): if isinstance(value, datetime.date):
return datetime.datetime.fromordinal(value.toordinal()) return datetime.datetime.fromordinal(value.toordinal())
if isinstance(value, (int, long, float)): if isinstance(value, (int, float)):
# value is a timestamp # value is a timestamp
return datetime.datetime.fromtimestamp(value) return datetime.datetime.fromtimestamp(value)
if isinstance(value, basestring): if isinstance(value, str):
raise NotImplementedError("Strings aren't currently implemented") raise NotImplementedError("Strings aren't currently implemented")
if hasattr(value, "__getitem__"): if hasattr(value, "__getitem__"):
# a sequence (assuming date/time tuple) # a sequence (assuming date/time tuple)
@@ -125,7 +125,7 @@ class classproperty(property):
return self.fget(cls) return self.fget(cls)
class _InvalidValue(object): class _InvalidValue:
"""Value returned from DBF records when field validation fails """Value returned from DBF records when field validation fails
@@ -145,7 +145,7 @@ class _InvalidValue(object):
def __ne__(self, other): def __ne__(self, other):
return not (other is self) return not (other is self)
def __nonzero__(self): def __bool__(self):
return False return False
def __int__(self): def __int__(self):
@@ -158,12 +158,10 @@ class _InvalidValue(object):
def __str__(self): def __str__(self):
return "" return ""
def __unicode__(self):
return u""
def __repr__(self): def __repr__(self):
return "<INVALID>" return "<INVALID>"
# invalid value is a constant singleton # invalid value is a constant singleton
INVALID_VALUE = _InvalidValue() INVALID_VALUE = _InvalidValue()
-297
View File
@@ -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 :
-183
View File
@@ -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 :
-466
View File
@@ -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 :
-273
View File
@@ -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 :
-266
View File
@@ -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 :
-170
View File
@@ -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 :
-24
View File
@@ -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
+13
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -1,10 +1,10 @@
pytest pytest
pytest-cov pytest-cov
backports.csv; python_version < '3.0'
MarkupPy MarkupPy
odfpy odfpy
openpyxl>=2.4.0 openpyxl>=2.6.0
pandas pandas
pyyaml pyyaml
tabulate
xlrd xlrd
xlwt xlwt
+329 -65
View File
@@ -1,20 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Tests for Tablib.""" """Tests for Tablib."""
from __future__ import unicode_literals
import datetime import datetime
import doctest import doctest
import json import json
import sys import pickle
import unittest import unittest
from collections import OrderedDict
from io import BytesIO, StringIO
from pathlib import Path
from uuid import uuid4 from uuid import uuid4
from MarkupPy import markup from MarkupPy import markup
import tablib import tablib
from tablib.compat import unicode, is_py3
from tablib.core import Row, detect_format 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): class BaseTestCase(unittest.TestCase):
@@ -50,6 +52,16 @@ class TablibTestCase(BaseTestCase):
continue continue
dataset.export(format_) 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): def test_empty_append(self):
"""Verify append() correctly adds tuple with no headers.""" """Verify append() correctly adds tuple with no headers."""
new_row = (1, 2, 3) new_row = (1, 2, 3)
@@ -158,7 +170,8 @@ class TablibTestCase(BaseTestCase):
def test_add_callable_column(self): def test_add_callable_column(self):
"""Verify adding column with values specified as callable.""" """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') self.founders.append_col(new_col, header='first_again')
@@ -206,7 +219,7 @@ class TablibTestCase(BaseTestCase):
self.assertEqual(self.founders[2:], [self.tom]) self.assertEqual(self.founders[2:], [self.tom])
def test_row_slicing(self): def test_row_slicing(self):
"""Verify Row's __getslice__ method. Issue #184.""" """Verify Row slicing. Issue #184."""
john = Row(self.john) john = Row(self.john)
@@ -251,10 +264,7 @@ class TablibTestCase(BaseTestCase):
def test_unicode_append(self): def test_unicode_append(self):
"""Passes in a single unicode character and exports.""" """Passes in a single unicode character and exports."""
if is_py3: new_row = ('å', 'é')
new_row = ('å', 'é')
else:
exec ("new_row = (u'å', u'é')")
data.append(new_row) data.append(new_row)
self._test_export_data_in_all_formats(data) self._test_export_data_in_all_formats(data)
@@ -286,6 +296,27 @@ class TablibTestCase(BaseTestCase):
unsupported = ['csv', 'tsv', 'jira', 'latex', 'df'] unsupported = ['csv', 'tsv', 'jira', 'latex', 'df']
self._test_export_data_in_all_formats(book, exclude=unsupported) 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): def test_auto_format_detect(self):
"""Test auto format detection.""" """Test auto format detection."""
# html, jira, latex, rst are export only. # html, jira, latex, rst are export only.
@@ -314,7 +345,9 @@ class TablibTestCase(BaseTestCase):
_tsv = '1\t2\t3\n4\t5\t6\n7\t8\t9\n' _tsv = '1\t2\t3\n4\t5\t6\n7\t8\t9\n'
self.assertEqual(tablib.detect_format(_tsv), 'tsv') self.assertEqual(tablib.detect_format(_tsv), 'tsv')
_bunk = '¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶' _bunk = StringIO(
'¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
)
self.assertEqual(tablib.detect_format(_bunk), None) self.assertEqual(tablib.detect_format(_bunk), None)
def test_transpose(self): def test_transpose(self):
@@ -458,10 +491,15 @@ class TablibTestCase(BaseTestCase):
# add another entry to test right field width for # add another entry to test right field width for
# integer # integer
self.founders.append(('Old', 'Man', 100500)) 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): def test_databook_add_sheet_accepts_only_dataset_instances(self):
class NotDataset(object): class NotDataset:
def append(self, item): def append(self, item):
pass pass
@@ -493,16 +531,74 @@ class TablibTestCase(BaseTestCase):
self.founders.append(('First\nSecond', 'Name', 42)) self.founders.append(('First\nSecond', 'Name', 42))
self.founders.export('xlsx') self.founders.export('xlsx')
def test_rst_force_grid(self): def test_row_repr(self):
data.append(self.john) """Row repr."""
data.append(self.george) # Arrange
data.headers = self.headers john = Row(self.john)
simple = tablib.formats._rst.export_set(data) # Act
grid = tablib.formats._rst.export_set(data, force_grid=True) output = str(john)
self.assertNotEqual(simple, grid)
self.assertNotIn('+', simple) # Assert
self.assertIn('+', grid) 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."""
john = Row(self.john)
john.lpush(53)
self.assertEqual(john.list, [53, 'John', 'Adams', 90])
def test_row_append(self):
"""Row append."""
john = Row(self.john)
john.append('stuff')
self.assertEqual(john.list, ['John', 'Adams', 90, 'stuff'])
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): class HTMLTests(BaseTestCase):
@@ -539,27 +635,82 @@ class HTMLTests(BaseTestCase):
html.table.close() html.table.close()
html = str(html) html = str(html)
headers = ['foo', None, 'bar']; headers = ['foo', None, 'bar']
d = tablib.Dataset(['foo', None, 'bar'], headers=headers) d = tablib.Dataset(['foo', None, 'bar'], headers=headers)
self.assertEqual(html, d.html) 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))
data.append(('Esther', ' ', 31))
self.assertEqual(
data.export('rst'),
'========== ========= ===\n'
'first_name last_name gpa\n'
'========== ========= ===\n'
'John Adams 90 \n'
'Wendy 43 \n'
'Esther 31 \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): class CSVTests(BaseTestCase):
def test_csv_format_detect(self): def test_csv_format_detect(self):
"""Test CSV format detection.""" """Test CSV format detection."""
_csv = ( _csv = StringIO(
'1,2,3\n' '1,2,3\n'
'4,5,6\n' '4,5,6\n'
'7,8,9\n' '7,8,9\n'
) )
_bunk = ( _bunk = StringIO(
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶' '¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
) )
self.assertTrue(tablib.formats.csv.detect(_csv)) fmt = registry.get_format('csv')
self.assertFalse(tablib.formats.csv.detect(_bunk)) self.assertTrue(fmt.detect(_csv))
self.assertFalse(fmt.detect(_bunk))
def test_csv_import_set(self): def test_csv_import_set(self):
"""Generate and import CSV set serialization.""" """Generate and import CSV set serialization."""
@@ -625,6 +776,16 @@ class CSVTests(BaseTestCase):
self.assertEqual(_csv, data.csv) 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): def test_csv_import_set_with_unicode_str(self):
"""Import CSV set with non-ascii characters in unicode literal""" """Import CSV set with non-ascii characters in unicode literal"""
csv_text = ( csv_text = (
@@ -634,6 +795,25 @@ class CSVTests(BaseTestCase):
data.csv = csv_text data.csv = csv_text
self.assertEqual(data.width, 7) self.assertEqual(data.width, 7)
def test_csv_import_set_ragged(self):
"""Import CSV set when not all rows have the same length."""
csv_text = (
"H1,H2,H3\n"
"A,B\n"
"C,D,E\n"
"\n"
"F\n"
)
dataset = tablib.import_set(csv_text, format="csv")
self.assertEqual(
str(dataset),
'H1|H2|H3\n'
'--|--|--\n'
'A |B | \n'
'C |D |E \n'
'F | | '
)
def test_csv_export(self): def test_csv_export(self):
"""Verify exporting dataset object as CSV.""" """Verify exporting dataset object as CSV."""
@@ -651,6 +831,12 @@ class CSVTests(BaseTestCase):
self.assertEqual(csv, self.founders.csv) 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): def test_csv_stream_export(self):
"""Verify exporting dataset object as CSV from file object.""" """Verify exporting dataset object as CSV from file object."""
@@ -666,7 +852,8 @@ class CSVTests(BaseTestCase):
csv += str(col) + ',' csv += str(col) + ','
csv = csv.strip(',') + '\r\n' 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()) self.assertEqual(csv, csv_stream.getvalue())
def test_unicode_csv(self): def test_unicode_csv(self):
@@ -674,10 +861,7 @@ class CSVTests(BaseTestCase):
data = tablib.Dataset() data = tablib.Dataset()
if sys.version_info[0] > 2: data.append(['\xfc', '\xfd'])
data.append(['\xfc', '\xfd'])
else:
exec ("data.append([u'\xfc', u'\xfd'])")
data.csv data.csv
@@ -688,7 +872,7 @@ class CSVTests(BaseTestCase):
data.csv = self.founders.csv data.csv = self.founders.csv
headers = data.headers headers = data.headers
self.assertTrue(isinstance(headers[0], unicode)) self.assertTrue(isinstance(headers[0], str))
orig_first_name = self.founders[self.headers[0]] orig_first_name = self.founders[self.headers[0]]
csv_first_name = data[headers[0]] csv_first_name = data[headers[0]]
@@ -701,7 +885,7 @@ class CSVTests(BaseTestCase):
data.csv = self.founders.csv data.csv = self.founders.csv
target_header = data.headers[0] target_header = data.headers[0]
self.assertTrue(isinstance(target_header, unicode)) self.assertTrue(isinstance(target_header, str))
del data[target_header] del data[target_header]
@@ -757,17 +941,18 @@ class TSVTests(BaseTestCase):
def test_tsv_format_detect(self): def test_tsv_format_detect(self):
"""Test TSV format detection.""" """Test TSV format detection."""
_tsv = ( _tsv = StringIO(
'1\t2\t3\n' '1\t2\t3\n'
'4\t5\t6\n' '4\t5\t6\n'
'7\t8\t9\n' '7\t8\t9\n'
) )
_bunk = ( _bunk = StringIO(
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶' '¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
) )
self.assertTrue(tablib.formats.tsv.detect(_tsv)) fmt = registry.get_format('tsv')
self.assertFalse(tablib.formats.tsv.detect(_bunk)) self.assertTrue(fmt.detect(_tsv))
self.assertFalse(fmt.detect(_bunk))
def test_tsv_export(self): def test_tsv_export(self):
"""Verify exporting dataset object as TSV.""" """Verify exporting dataset object as TSV."""
@@ -787,17 +972,40 @@ class TSVTests(BaseTestCase):
self.assertEqual(tsv, self.founders.tsv) 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_date_import(self):
xls_source = Path(__file__).parent / 'files' / 'dates.xls'
with open(str(xls_source), mode='rb') as fh:
dset = tablib.Dataset().load(fh, 'xls')
self.assertEqual(dset.dict[0]['birth_date'], datetime.datetime(2015, 4, 12, 0, 0))
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): class XLSXTests(BaseTestCase):
def test_xlsx_format_detect(self): def test_xlsx_format_detect(self):
"""Test the XLSX format detection.""" """Test the XLSX format detection."""
in_stream = self.founders.xlsx in_stream = self.founders.xlsx
self.assertEqual(detect_format(in_stream), '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): def test_xlsx_import_set(self):
date_time = datetime.datetime(2019, 10, 4, 12, 30, 8) date_time = datetime.datetime(2019, 10, 4, 12, 30, 8)
data.append(('string', '004', 42, 21.55, date_time)) data.append(('string', '004', 42, 21.55, date_time))
@@ -810,6 +1018,13 @@ class XLSXTests(BaseTestCase):
self.assertEqual(data.dict[0]['float'], 21.55) self.assertEqual(data.dict[0]['float'], 21.55)
self.assertEqual(data.dict[0]['date/time'], date_time) self.assertEqual(data.dict[0]['date/time'], date_time)
def test_xlsx_import_set_ragged(self):
"""Import XLSX file when not all rows have the same length."""
xlsx_source = Path(__file__).parent / 'files' / 'ragged.xlsx'
with open(str(xlsx_source), mode='rb') as fh:
book = tablib.Databook().load(fh, 'xlsx')
self.assertEqual(book.sheets()[0].pop(), (1.0, ''))
def test_xlsx_wrong_char(self): def test_xlsx_wrong_char(self):
"""Bad characters are not silently ignored. We let the exception bubble up.""" """Bad characters are not silently ignored. We let the exception bubble up."""
from openpyxl.utils.exceptions import IllegalCharacterError from openpyxl.utils.exceptions import IllegalCharacterError
@@ -818,18 +1033,33 @@ class XLSXTests(BaseTestCase):
data.append(('string', b'\x0cf')) data.append(('string', b'\x0cf'))
data.xlsx data.xlsx
def test_xlsx_cell_values(self):
"""Test cell values are read and not formulas"""
xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx'
with xls_source.open('rb') as fh:
data = tablib.Dataset().load(fh)
self.assertEqual(data.headers[0], 'Hello World')
def test_xlsx_bad_dimensions(self):
"""Test loading file with bad dimension. Must be done with
read_only=False."""
xls_source = Path(__file__).parent / 'files' / 'bad_dimensions.xlsx'
with xls_source.open('rb') as fh:
data = tablib.Dataset().load(fh, read_only=False)
self.assertEqual(data.height, 3)
class JSONTests(BaseTestCase): class JSONTests(BaseTestCase):
def test_json_format_detect(self): def test_json_format_detect(self):
"""Test JSON format detection.""" """Test JSON format detection."""
_json = '[{"last_name": "Adams","age": 90,"first_name": "John"}]' _json = StringIO('[{"last_name": "Adams","age": 90,"first_name": "John"}]')
_bunk = ( _bunk = StringIO(
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶' '¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
) )
self.assertTrue(tablib.formats.json.detect(_json)) fmt = registry.get_format('json')
self.assertFalse(tablib.formats.json.detect(_bunk)) self.assertTrue(fmt.detect(_json))
self.assertFalse(fmt.detect(_bunk))
def test_json_import_book(self): def test_json_import_book(self):
"""Generate and import JSON book serialization.""" """Generate and import JSON book serialization."""
@@ -883,12 +1113,14 @@ class YAMLTests(BaseTestCase):
_yaml = '- {age: 90, first_name: John, last_name: Adams}' _yaml = '- {age: 90, first_name: John, last_name: Adams}'
_tsv = 'foo\tbar' _tsv = 'foo\tbar'
_bunk = ( _bunk = (
'¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶' '¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†'
'ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
) )
self.assertTrue(tablib.formats.yaml.detect(_yaml)) fmt = registry.get_format('yaml')
self.assertFalse(tablib.formats.yaml.detect(_bunk)) self.assertTrue(fmt.detect(_yaml))
self.assertFalse(tablib.formats.yaml.detect(_tsv)) self.assertFalse(fmt.detect(_bunk))
self.assertFalse(fmt.detect(_tsv))
def test_yaml_import_book(self): def test_yaml_import_book(self):
"""Generate and import YAML book serialization.""" """Generate and import YAML book serialization."""
@@ -918,6 +1150,17 @@ class YAMLTests(BaseTestCase):
self.assertEqual(_yaml, data.yaml) 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): class LatexTests(BaseTestCase):
def test_latex_export(self): def test_latex_export(self):
@@ -994,7 +1237,7 @@ class DBFTests(BaseTestCase):
for reg_char, data_char in zip(_dbf, data.dbf): for reg_char, data_char in zip(_dbf, data.dbf):
so_far += chr(data_char) so_far += chr(data_char)
if reg_char != data_char and index not in [1, 2, 3]: 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, reg_char, data_char, so_far))
index += 1 index += 1
@@ -1024,11 +1267,9 @@ class DBFTests(BaseTestCase):
_regression_dbf += b' 50.0000000' _regression_dbf += b' 50.0000000'
_regression_dbf += b'\x1a' _regression_dbf += b'\x1a'
if is_py3: # If in python3, decode regression string to binary.
# If in python3, decode regression string to binary. # _regression_dbf = bytes(_regression_dbf, 'utf-8')
# _regression_dbf = bytes(_regression_dbf, 'utf-8') # _regression_dbf = _regression_dbf.replace(b'\n', b'\r')
# _regression_dbf = _regression_dbf.replace(b'\n', b'\r')
pass
try: try:
self.assertEqual(_regression_dbf, data.dbf) self.assertEqual(_regression_dbf, data.dbf)
@@ -1039,7 +1280,7 @@ class DBFTests(BaseTestCase):
# found_so_far += chr(data_char) # found_so_far += chr(data_char)
if reg_char != data_char and index not in [1, 2, 3]: if reg_char != data_char and index not in [1, 2, 3]:
raise AssertionError( 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, reg_char, data_char, found_so_far))
index += 1 index += 1
@@ -1063,6 +1304,7 @@ class DBFTests(BaseTestCase):
_dbf += b' Jefferson' + (b' ' * 70) _dbf += b' Jefferson' + (b' ' * 70)
_dbf += b' 50.0000000' _dbf += b' 50.0000000'
_dbf += b'\x1a' _dbf += b'\x1a'
_dbf = BytesIO(_dbf)
_yaml = '- {age: 90, first_name: John, last_name: Adams}' _yaml = '- {age: 90, first_name: John, last_name: Adams}'
_tsv = 'foo\tbar' _tsv = 'foo\tbar'
@@ -1072,12 +1314,13 @@ class DBFTests(BaseTestCase):
_bunk = ( _bunk = (
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶' '¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
) )
self.assertTrue(tablib.formats.dbf.detect(_dbf)) fmt = registry.get_format('dbf')
self.assertFalse(tablib.formats.dbf.detect(_yaml)) self.assertTrue(fmt.detect(_dbf))
self.assertFalse(tablib.formats.dbf.detect(_tsv)) self.assertFalse(fmt.detect(_yaml))
self.assertFalse(tablib.formats.dbf.detect(_csv)) self.assertFalse(fmt.detect(_tsv))
self.assertFalse(tablib.formats.dbf.detect(_json)) self.assertFalse(fmt.detect(_csv))
self.assertFalse(tablib.formats.dbf.detect(_bunk)) self.assertFalse(fmt.detect(_json))
self.assertFalse(fmt.detect(_bunk))
class JiraTests(BaseTestCase): class JiraTests(BaseTestCase):
@@ -1101,5 +1344,26 @@ class JiraTests(BaseTestCase):
class DocTests(unittest.TestCase): class DocTests(unittest.TestCase):
def test_rst_formatter_doctests(self): def test_rst_formatter_doctests(self):
import tablib.formats._rst
results = doctest.testmod(tablib.formats._rst) results = doctest.testmod(tablib.formats._rst)
self.assertEqual(results.failed, 0) 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)
+170
View File
@@ -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>")
+17 -18
View File
@@ -2,39 +2,38 @@
usedevelop = true usedevelop = true
minversion = 2.4 minversion = 2.4
envlist = envlist =
py{27,35,36,37,38}-tests, docs
py37-{docs,lint} lint
py{36,37,38,39}
[testenv] [testenv]
basepython =
py27: python2.7
py35: python3.5
py36: python3.6
py37: python3.7
py38: python3.8
deps = deps =
tests: -rtests/requirements.txt -rtests/requirements.txt
docs: sphinx
extras = pandas extras = pandas
passenv =
FORCE_COLOR
commands = commands =
tests: pytest {posargs:tests} pytest {posargs:tests}
docs: sphinx-build -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html
[testenv:py37-lint] [testenv:docs]
basepython = python3.7 deps =
sphinx
commands =
sphinx-build -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html
[testenv:lint]
deps = deps =
flake8 flake8
# flake8-black # flake8-black
# flake8-isort pre-commit
twine twine
check-manifest
commands = commands =
# flake8 src/tablib tests/ # flake8 src/tablib tests/
check-manifest -v pre-commit run --all-files
python setup.py sdist python setup.py sdist
twine check dist/* twine check dist/*
skip_install = true
[flake8] [flake8]
exclude = exclude =
.tox .tox
ignore=E501,E127,E128,E124