Merge branch 'wip/github'
This commit is contained in:
commit
5ec55c71dc
|
@ -0,0 +1,4 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: chfw
|
||||
patreon: chfw
|
|
@ -0,0 +1,8 @@
|
|||
With your PR, here is a check list:
|
||||
|
||||
- [ ] Has test cases written?
|
||||
- [ ] Has all code lines tested?
|
||||
- [ ] Has `make format` been run?
|
||||
- [ ] Please update CHANGELOG.yml(not CHANGELOG.rst)
|
||||
- [ ] Has fair amount of documentation if your change is complex
|
||||
- [ ] Agree on NEW BSD License for your contribution
|
|
@ -0,0 +1,20 @@
|
|||
name: lint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: lint code
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: lint
|
||||
run: |
|
||||
pip --use-deprecated=legacy-resolver install flake8
|
||||
pip --use-deprecated=legacy-resolver install -r tests/requirements.txt
|
||||
flake8 --exclude=.moban.d,docs,setup.py --builtins=unicode,xrange,long .
|
||||
python setup.py checkdocs
|
|
@ -0,0 +1,28 @@
|
|||
on: [push]
|
||||
|
||||
jobs:
|
||||
run_moban:
|
||||
runs-on: ubuntu-latest
|
||||
name: synchronize templates via moban
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
token: ${{ secrets.PAT }}
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
- name: check changes
|
||||
run: |
|
||||
pip install moban gitfs2 pypifs moban-jinja2-github moban-ansible
|
||||
moban
|
||||
git status
|
||||
git diff --exit-code
|
||||
- name: Auto-commit
|
||||
if: failure()
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: >-
|
||||
This is an auto-commit, updating project meta data,
|
||||
such as changelog.rst, contributors.rst
|
|
@ -0,0 +1,26 @@
|
|||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
|
@ -0,0 +1,35 @@
|
|||
name: run_tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
os: [macOs-latest, ubuntu-latest, windows-latest]
|
||||
exclude:
|
||||
- os: macOs-latest
|
||||
python-version: 3.6
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: run tests
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: install
|
||||
run: |
|
||||
pip --use-deprecated=legacy-resolver install -r requirements.txt
|
||||
pip --use-deprecated=legacy-resolver install -r tests/requirements.txt
|
||||
- name: test
|
||||
run: |
|
||||
pip freeze
|
||||
nosetests --verbosity=3 --with-coverage --cover-package pyexcel_xls --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_xls
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
|
@ -1,3 +1,546 @@
|
|||
*.pyc
|
||||
*~
|
||||
# moban hashes
|
||||
.moban.hashes
|
||||
|
||||
# Extra rules from https://github.com/github/gitignore/
|
||||
# Python rules
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# VirtualEnv rules
|
||||
# Virtualenv
|
||||
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
|
||||
.Python
|
||||
[Bb]in
|
||||
[Ii]nclude
|
||||
[Ll]ib
|
||||
[Ll]ib64
|
||||
[Ll]ocal
|
||||
[Ss]cripts
|
||||
pyvenv.cfg
|
||||
.venv
|
||||
pip-selfcheck.json
|
||||
|
||||
# Linux rules
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
# Windows rules
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# macOS rules
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Emacs rules
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
|
||||
# server auth directory
|
||||
/server/
|
||||
|
||||
# projectiles files
|
||||
.projectile
|
||||
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
|
||||
# network security
|
||||
/network-security.data
|
||||
|
||||
|
||||
# Vim rules
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
# JetBrains rules
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
# SublimeText rules
|
||||
# Cache files for Sublime Text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# Workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# Project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using Sublime Text
|
||||
# *.sublime-project
|
||||
|
||||
# SFTP configuration file
|
||||
sftp-config.json
|
||||
sftp-config-alt*.json
|
||||
|
||||
# Package control specific files
|
||||
Package Control.last-run
|
||||
Package Control.ca-list
|
||||
Package Control.ca-bundle
|
||||
Package Control.system-ca-bundle
|
||||
Package Control.cache/
|
||||
Package Control.ca-certs/
|
||||
Package Control.merged-ca-bundle
|
||||
Package Control.user-ca-bundle
|
||||
oscrypto-ca-bundle.crt
|
||||
bh_unicode_properties.cache
|
||||
|
||||
# Sublime-github package stores a github token in this file
|
||||
# https://packagecontrol.io/packages/sublime-github
|
||||
GitHub.sublime-settings
|
||||
|
||||
# KDevelop4 rules
|
||||
*.kdev4
|
||||
.kdev4/
|
||||
|
||||
# Kate rules
|
||||
# Swap Files #
|
||||
.*.kate-swp
|
||||
.swp.*
|
||||
|
||||
# TextMate rules
|
||||
*.tmproj
|
||||
*.tmproject
|
||||
tmtags
|
||||
|
||||
# VisualStudioCode rules
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Xcode rules
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
## Gcc Patch
|
||||
/*.gcno
|
||||
|
||||
# Eclipse rules
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# PyDev specific (Python IDE for Eclipse)
|
||||
*.pydevproject
|
||||
|
||||
# CDT-specific (C/C++ Development Tooling)
|
||||
.cproject
|
||||
|
||||
# CDT- autotools
|
||||
.autotools
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
|
||||
# PDT-specific (PHP Development Tools)
|
||||
.buildpath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
# STS (Spring Tool Suite)
|
||||
.springBeans
|
||||
|
||||
# Code Recommenders
|
||||
.recommenders/
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
.apt_generated_test/
|
||||
|
||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||
.cache-main
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
# Uncomment this line if you wish to ignore the project description file.
|
||||
# Typically, this file would be tracked if it contains build/dependency configurations:
|
||||
#.project
|
||||
|
||||
# TortoiseGit rules
|
||||
# Project-level settings
|
||||
/.tgitconfig
|
||||
|
||||
# Tags rules
|
||||
# Ignore tags created by etags, ctags, gtags (GNU global) and cscope
|
||||
TAGS
|
||||
.TAGS
|
||||
!TAGS/
|
||||
tags
|
||||
.tags
|
||||
!tags/
|
||||
gtags.files
|
||||
GTAGS
|
||||
GRTAGS
|
||||
GPATH
|
||||
GSYMS
|
||||
cscope.files
|
||||
cscope.out
|
||||
cscope.in.out
|
||||
cscope.po.out
|
||||
|
||||
|
||||
# remove moban hash dictionary
|
||||
.moban.hashes
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
[settings]
|
||||
line_length=79
|
||||
known_first_party=pyexcel,xlrd,xlwt
|
||||
known_third_party=mock,nose
|
||||
indent=' '
|
||||
multi_line_output=3
|
||||
length_sort=1
|
||||
default_section=FIRSTPARTY
|
||||
no_lines_before=LOCALFOLDER
|
||||
sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER
|
|
@ -1,13 +0,0 @@
|
|||
{%extends 'README.rst.jj2' %}
|
||||
|
||||
{%block description%}
|
||||
**pyexcel-{{file_type}}** is a tiny wrapper library to read, manipulate and write data in {{file_type}} format and it can read xlsx and xlsm fromat. You are likely to use it with `pyexcel <https://github.com/pyexcel/pyexcel>`_.
|
||||
{%endblock%}
|
||||
|
||||
{%block extras %}
|
||||
Known Issues
|
||||
=============
|
||||
|
||||
* If a zero was typed in a DATE formatted field in xls, you will get "01/01/1900".
|
||||
* If a zero was typed in a TIME formatted field in xls, you will get "00:00:00".
|
||||
{%endblock%}
|
|
@ -0,0 +1,50 @@
|
|||
{%extends 'README.rst.jj2' %}
|
||||
|
||||
{% block documentation_link %}
|
||||
{% endblock %}
|
||||
|
||||
{%block description%}
|
||||
**pyexcel-{{file_type}}** is a tiny wrapper library to read, manipulate and
|
||||
write data in {{file_type}} format and it can read xlsx and xlsm fromat.
|
||||
You are likely to use it with `pyexcel <https://github.com/pyexcel/pyexcel>`_.
|
||||
|
||||
Oct 2021 - Update:
|
||||
===================
|
||||
|
||||
1. v0.7.0 removed the pin on xlrd < 2. If you have xlrd >= 2, this
|
||||
library will NOT read 'xlsx' format and you need to install pyexcel-xlsx. Othwise,
|
||||
this library can use xlrd < 2 to read xlsx format for you. So 'xlsx' support
|
||||
in this library will vary depending on the installed version of xlrd.
|
||||
|
||||
2. v0.7.0 can write datetime.timedelta. but when the value is read out,
|
||||
you will get datetime.datetime. so you as the developer decides what to do with it.
|
||||
|
||||
Past news
|
||||
===========
|
||||
|
||||
`detect_merged_cells` allows you to spread the same value among
|
||||
all merged cells. But be aware that this may slow down its reading
|
||||
performance.
|
||||
|
||||
`skip_hidden_row_and_column` allows you to skip hidden rows
|
||||
and columns and is defaulted to **True**. It may slow down its reading
|
||||
performance. And it is only valid for 'xls' files. For 'xlsx' files,
|
||||
please use pyexcel-xlsx.
|
||||
|
||||
Warning
|
||||
================================================================================
|
||||
|
||||
**xls file cannot contain more than 65,000 rows**. You are risking the reputation
|
||||
of yourself/your company/
|
||||
`your country <https://www.bbc.co.uk/news/technology-54423988>`_ if you keep
|
||||
using xls and are not aware of its row limit.
|
||||
|
||||
{%endblock%}
|
||||
|
||||
{%block extras %}
|
||||
Known Issues
|
||||
=============
|
||||
|
||||
* If a zero was typed in a DATE formatted field in xls, you will get "01/01/1900".
|
||||
* If a zero was typed in a TIME formatted field in xls, you will get "00:00:00".
|
||||
{%endblock%}
|
|
@ -0,0 +1,16 @@
|
|||
{% extends 'setup.py.jj2' %}
|
||||
|
||||
{%block platform_block%}
|
||||
{%endblock%}
|
||||
|
||||
{%block additional_keywords %}
|
||||
'xls',
|
||||
'xlsx',
|
||||
'xlsm'
|
||||
{%endblock%}
|
||||
|
||||
{% block morefiles %}"CONTRIBUTORS.rst",{% endblock %}
|
||||
|
||||
{%block additional_classifiers%}
|
||||
'Programming Language :: Python :: Implementation :: PyPy'
|
||||
{%endblock%}
|
|
@ -1,20 +0,0 @@
|
|||
{% extends 'setup.py.jj2' %}
|
||||
|
||||
{%block platform_block%}
|
||||
{%endblock%}
|
||||
|
||||
{%block additional_keywords %}
|
||||
'xls',
|
||||
'xlsx',
|
||||
'xlsm'
|
||||
{%endblock%}
|
||||
|
||||
{%block additional_classifiers%}
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: Implementation :: PyPy'
|
||||
{%endblock%}
|
|
@ -0,0 +1,4 @@
|
|||
{% extends 'tests/base.py.jj2' %}
|
||||
|
||||
{%block ods_types%}
|
||||
{%endblock%}
|
|
@ -1,4 +1,8 @@
|
|||
{% extends 'tests/requirements.txt.jj2' %}
|
||||
{%block extras %}
|
||||
pyexcel
|
||||
xlrd==1.1.0
|
||||
moban
|
||||
black;python_version>="3.6"
|
||||
isort;python_version>="3.6"
|
||||
{%endblock%}
|
|
@ -1,28 +0,0 @@
|
|||
{% extends "travis.yml.jj2" %}
|
||||
{%block test_other_python_versions%} - 2.6
|
||||
- 3.3
|
||||
- 3.4
|
||||
- 3.5
|
||||
- pypy
|
||||
{%endblock%}
|
||||
|
||||
{%block extra_matrix%}
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
dist: trusty
|
||||
sudo: required
|
||||
virtualenv:
|
||||
system_site_packages: true
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- python-xlwt
|
||||
- python-xlrd
|
||||
- python-coverage
|
||||
- python-nose
|
||||
- python-mock
|
||||
{%endblock%}
|
||||
|
||||
{%block custom_install %} - if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then pip install ordereddict; fi
|
||||
{%endblock%}
|
19
.moban.yml
19
.moban.yml
|
@ -1,17 +1,10 @@
|
|||
overrides: "git://github.com/pyexcel/pyexcel-mobans!/mobanfile.yaml"
|
||||
configuration:
|
||||
configuration_dir: "commons/config"
|
||||
template_dir:
|
||||
- "commons/templates"
|
||||
- ".moban.d"
|
||||
configuration: pyexcel_xls.yaml
|
||||
configuration: pyexcel-xls.yml
|
||||
targets:
|
||||
- README.rst: README.rst
|
||||
- setup.py: setup.py
|
||||
- README.rst: custom_README.rst.jj2
|
||||
- setup.py: custom_setup.py.jj2
|
||||
- "docs/source/conf.py": "docs/source/conf.py.jj2"
|
||||
- .travis.yml: travis.yml
|
||||
- requirements.txt: requirements.txt
|
||||
- LICENSE: LICENSE.jj2
|
||||
- .gitignore: gitignore.jj2
|
||||
- MANIFEST.in: MANIFEST.in.jj2
|
||||
- "tests/requirements.txt": "tests/requirements.txt"
|
||||
- test.sh: test.sh.jj2
|
||||
- test.bat: test.sh.jj2
|
||||
- "tests/requirements.txt": "tests/custom_requirements.txt.jj2"
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# .readthedocs.yml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/source/conf.py
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF
|
||||
formats:
|
||||
- pdf
|
||||
|
||||
# Optionally set the version of Python and requirements required to build your docs
|
||||
python:
|
||||
version: 3.7
|
41
.travis.yml
41
.travis.yml
|
@ -1,41 +0,0 @@
|
|||
sudo: false
|
||||
language: python
|
||||
notifications:
|
||||
email: false
|
||||
python:
|
||||
- pypy
|
||||
- 2.6
|
||||
- 2.7
|
||||
- pypy3
|
||||
- 3.3
|
||||
- 3.4
|
||||
- 3.5
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
dist: trusty
|
||||
sudo: required
|
||||
virtualenv:
|
||||
system_site_packages: true
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- python-xlwt
|
||||
- python-xlrd
|
||||
- python-coverage
|
||||
- python-nose
|
||||
- python-mock
|
||||
before_install:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then pip install ordereddict; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then pip install flake8==2.6.2; fi
|
||||
- if [[ -f min_requirements.txt && "$MINREQ" -eq 1 ]]; then
|
||||
mv min_requirements.txt requirements.txt ;
|
||||
fi
|
||||
- pip install --upgrade setuptools "pip==7.1"
|
||||
- test ! -f rnd_requirements.txt || pip install --no-deps -r rnd_requirements.txt
|
||||
- test ! -f rnd_requirements.txt || pip install -r rnd_requirements.txt ;
|
||||
- pip install -r tests/requirements.txt
|
||||
script:
|
||||
- make test
|
||||
after_success:
|
||||
codecov
|
203
CHANGELOG.rst
203
CHANGELOG.rst
|
@ -1,11 +1,191 @@
|
|||
Change log
|
||||
================================================================================
|
||||
|
||||
0.7.0 - 07.10.2021
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Removed**
|
||||
|
||||
#. `#46 <https://github.com/pyexcel/pyexcel-xls/issues/46>`_: remove the hard
|
||||
pin on xlrd version < 2.0
|
||||
|
||||
**Added**
|
||||
|
||||
#. `#47 <https://github.com/pyexcel/pyexcel-xls/issues/47>`_: limit support to
|
||||
persist datetime.timedelta. see more details in doc
|
||||
|
||||
0.6.2 - 12.12.2020
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. lock down xlrd version less than version 2.0, because 2.0+ does not support
|
||||
xlsx read
|
||||
|
||||
0.6.1 - 21.10.2020
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. Restrict this library to get installed on python 3.6+, because pyexcel-io
|
||||
0.6.0+ supports only python 3.6+.
|
||||
|
||||
0.6.0 - 8.10.2020
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. New style xlsx plugins, promoted by pyexcel-io v0.6.2.
|
||||
|
||||
0.5.9 - 29.08.2020
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Added**
|
||||
|
||||
#. `#35 <https://github.com/pyexcel/pyexcel-xls/issues/35>`_, include tests
|
||||
|
||||
0.5.8 - 22.08.2018
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Added**
|
||||
|
||||
#. `pyexcel#151 <https://github.com/pyexcel/pyexcel/issues/151>`_, read cell
|
||||
error as #N/A.
|
||||
|
||||
0.5.7 - 15.03.2018
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Added**
|
||||
|
||||
#. `pyexcel#54 <https://github.com/pyexcel/pyexcel/issues/54>`_, Book.datemode
|
||||
attribute of that workbook should be passed always.
|
||||
|
||||
0.5.6 - 15.03.2018
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Added**
|
||||
|
||||
#. `pyexcel#120 <https://github.com/pyexcel/pyexcel/issues/120>`_, xlwt cannot
|
||||
save a book without any sheet. So, let's raise an exception in this case in
|
||||
order to warn the developers.
|
||||
|
||||
0.5.5 - 8.11.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Added**
|
||||
|
||||
#. `#25 <https://github.com/pyexcel/pyexcel-xls/issues/25>`_, detect merged cell
|
||||
in .xls
|
||||
|
||||
0.5.4 - 2.11.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Added**
|
||||
|
||||
#. `#24 <https://github.com/pyexcel/pyexcel-xls/issues/24>`_, xlsx format cannot
|
||||
use skip_hidden_row_and_column. please use pyexcel-xlsx instead.
|
||||
|
||||
0.5.3 - 2.11.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Added**
|
||||
|
||||
#. `#21 <https://github.com/pyexcel/pyexcel-xls/issues/21>`_, skip hidden rows
|
||||
and columns under 'skip_hidden_row_and_column' flag.
|
||||
|
||||
0.5.2 - 23.10.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**updated**
|
||||
|
||||
#. pyexcel `pyexcel#105 <https://github.com/pyexcel/pyexcel/issues/105>`_,
|
||||
remove gease from setup_requires, introduced by 0.5.1.
|
||||
#. remove python2.6 test support
|
||||
#. update its dependecy on pyexcel-io to 0.5.3
|
||||
|
||||
0.5.1 - 20.10.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**added**
|
||||
|
||||
#. `pyexcel#103 <https://github.com/pyexcel/pyexcel/issues/103>`_, include
|
||||
LICENSE file in MANIFEST.in, meaning LICENSE file will appear in the released
|
||||
tar ball.
|
||||
|
||||
0.5.0 - 30.08.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. `#20 <https://github.com/pyexcel/pyexcel-xls/issues/20>`_, is handled in
|
||||
pyexcel-io
|
||||
#. put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO.
|
||||
Hence, there will be performance boost in handling files in memory.
|
||||
|
||||
0.4.1 - 25.08.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. `#20 <https://github.com/pyexcel/pyexcel-xls/issues/20>`_, handle unseekable
|
||||
stream given by http response.
|
||||
|
||||
0.4.0 - 19.06.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. `pyexcel-xlsx#15 <https://github.com/pyexcel/pyexcel-xlsx/issues/15>`_, close
|
||||
file handle
|
||||
#. pyexcel-io plugin interface now updated to use `lml
|
||||
<https://github.com/chfw/lml>`_.
|
||||
|
||||
0.3.3 - 30/05/2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. `#18 <https://github.com/pyexcel/pyexcel-xls/issues/18>`_, pass on
|
||||
encoding_override and others to xlrd.
|
||||
|
||||
0.3.2 - 18.05.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. `#16 <https://github.com/pyexcel/pyexcel-xls/issues/16>`_, allow mmap to be
|
||||
passed as file content
|
||||
|
||||
0.3.1 - 16.01.2017
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. `#14 <https://github.com/pyexcel/pyexcel-xls/issues/14>`_, Python 3.6 -
|
||||
cannot use LOCALE flag with a str pattern
|
||||
#. fix its dependency on pyexcel-io 0.3.0
|
||||
|
||||
0.3.0 - 22.12.2016
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. `#13 <https://github.com/pyexcel/pyexcel-xls/issues/13>`_, alert on empyty
|
||||
file content
|
||||
#. Support pyexcel-io v0.3.0
|
||||
|
||||
0.2.3 - 20.09.2016
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
**Updated**
|
||||
|
||||
#. `#10 <https://github.com/pyexcel/pyexcel-xls/issues/10>`_, To support
|
||||
generator as member of the incoming two dimensional data
|
||||
|
||||
0.2.2 - 31.08.2016
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Added
|
||||
********************************************************************************
|
||||
**Added**
|
||||
|
||||
#. support pagination. two pairs: start_row, row_limit and start_column,
|
||||
column_limit help you deal with large files.
|
||||
|
@ -13,29 +193,24 @@ Added
|
|||
0.2.1 - 13.07.2016
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Added
|
||||
********************************************************************************
|
||||
**Added**
|
||||
|
||||
#. `#9 <https://github.com/pyexcel/pyexcel-xls/issues/9>`_, `skip_hidden_sheets`
|
||||
is added. By default, hidden sheets are skipped when reading all sheets.
|
||||
Reading sheet by name or by index are not affected.
|
||||
|
||||
|
||||
0.2.0 - 01.06.2016
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Added
|
||||
********************************************************************************
|
||||
**Added**
|
||||
|
||||
#. By default, `float` will be converted to `int` where fits. `auto_detect_int`,
|
||||
a flag to switch off the autoatic conversion from `float` to `int`.
|
||||
#. 'library=pyexcel-xls' was added so as to inform pyexcel to use it instead of
|
||||
other libraries, in the situation where there are more than one plugin for
|
||||
a file type, e.g. xlsm
|
||||
other libraries, in the situation where there are more than one plugin for a
|
||||
file type, e.g. xlsm
|
||||
|
||||
|
||||
Updated
|
||||
********************************************************************************
|
||||
**Updated**
|
||||
|
||||
#. support the auto-import feature of pyexcel-io 0.2.0
|
||||
#. xlwt is now used for python 2 implementation while xlwt-future is used for
|
||||
|
@ -44,10 +219,8 @@ Updated
|
|||
0.1.0 - 17.01.2016
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Added
|
||||
********************************************************************************
|
||||
**Added**
|
||||
|
||||
#. Passing "streaming=True" to get_data, you will get the two dimensional array
|
||||
as a generator
|
||||
#. Passing "data=your_generator" to save_data is acceptable too.
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
|
||||
3 contributors
|
||||
================================================================================
|
||||
|
||||
In alphabetical order:
|
||||
|
||||
* `John Vandenberg <https://github.com/jayvdb>`_
|
||||
* `Peter Carnesciali <https://github.com/pcarn>`_
|
||||
* `vinraspa <https://github.com/vinraspa>`_
|
4
LICENSE
4
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2015-2016 by Onni Software Ltd. and its contributors
|
||||
Copyright (c) 2015-2021 by Onni Software Ltd. and its contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms of the software as well
|
||||
|
@ -27,4 +27,4 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
DAMAGE.
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
include README.rst
|
||||
include LICENSE
|
||||
include CHANGELOG.rst
|
||||
include CONTRIBUTORS.rst
|
||||
recursive-include tests *
|
||||
recursive-include docs *
|
||||
|
|
163
README.rst
163
README.rst
|
@ -2,23 +2,105 @@
|
|||
pyexcel-xls - Let you focus on data, instead of xls format
|
||||
================================================================================
|
||||
|
||||
.. image:: https://api.travis-ci.org/pyexcel/pyexcel-xls.png
|
||||
:target: http://travis-ci.org/pyexcel/pyexcel-xls
|
||||
.. image:: https://raw.githubusercontent.com/pyexcel/pyexcel.github.io/master/images/patreon.png
|
||||
:target: https://www.patreon.com/chfw
|
||||
|
||||
.. image:: https://codecov.io/github/pyexcel/pyexcel-xls/coverage.png
|
||||
:target: https://codecov.io/github/pyexcel/pyexcel-xls
|
||||
.. image:: https://raw.githubusercontent.com/pyexcel/pyexcel-mobans/master/images/awesome-badge.svg
|
||||
:target: https://awesome-python.com/#specific-formats-processing
|
||||
|
||||
.. image:: https://github.com/pyexcel/pyexcel-xls/workflows/run_tests/badge.svg
|
||||
:target: http://github.com/pyexcel/pyexcel-xls/actions
|
||||
|
||||
.. image:: https://codecov.io/gh/pyexcel/pyexcel-xls/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/pyexcel/pyexcel-xls
|
||||
|
||||
.. image:: https://badge.fury.io/py/pyexcel-xls.svg
|
||||
:target: https://pypi.org/project/pyexcel-xls
|
||||
|
||||
.. image:: https://anaconda.org/conda-forge/pyexcel-xls/badges/version.svg
|
||||
:target: https://anaconda.org/conda-forge/pyexcel-xls
|
||||
|
||||
.. image:: https://pepy.tech/badge/pyexcel-xls/month
|
||||
:target: https://pepy.tech/project/pyexcel-xls
|
||||
|
||||
.. image:: https://anaconda.org/conda-forge/pyexcel-xls/badges/downloads.svg
|
||||
:target: https://anaconda.org/conda-forge/pyexcel-xls
|
||||
|
||||
.. image:: https://img.shields.io/gitter/room/gitterHQ/gitter.svg
|
||||
:target: https://gitter.im/pyexcel/Lobby
|
||||
|
||||
.. image:: https://img.shields.io/static/v1?label=continuous%20templating&message=%E6%A8%A1%E7%89%88%E6%9B%B4%E6%96%B0&color=blue&style=flat-square
|
||||
:target: https://moban.readthedocs.io/en/latest/#at-scale-continous-templating-for-open-source-projects
|
||||
|
||||
.. image:: https://img.shields.io/static/v1?label=coding%20style&message=black&color=black&style=flat-square
|
||||
:target: https://github.com/psf/black
|
||||
|
||||
**pyexcel-xls** is a tiny wrapper library to read, manipulate and
|
||||
write data in xls format and it can read xlsx and xlsm fromat.
|
||||
You are likely to use it with `pyexcel <https://github.com/pyexcel/pyexcel>`_.
|
||||
|
||||
Oct 2021 - Update:
|
||||
===================
|
||||
|
||||
1. v0.7.0 removed the pin on xlrd < 2. If you have xlrd >= 2, this
|
||||
library will NOT read 'xlsx' format and you need to install pyexcel-xlsx. Othwise,
|
||||
this library can use xlrd < 2 to read xlsx format for you. So 'xlsx' support
|
||||
in this library will vary depending on the installed version of xlrd.
|
||||
|
||||
2. v0.7.0 can write datetime.timedelta. but when the value is read out,
|
||||
you will get datetime.datetime. so you as the developer decides what to do with it.
|
||||
|
||||
Past news
|
||||
===========
|
||||
|
||||
`detect_merged_cells` allows you to spread the same value among
|
||||
all merged cells. But be aware that this may slow down its reading
|
||||
performance.
|
||||
|
||||
`skip_hidden_row_and_column` allows you to skip hidden rows
|
||||
and columns and is defaulted to **True**. It may slow down its reading
|
||||
performance. And it is only valid for 'xls' files. For 'xlsx' files,
|
||||
please use pyexcel-xlsx.
|
||||
|
||||
Warning
|
||||
================================================================================
|
||||
|
||||
**xls file cannot contain more than 65,000 rows**. You are risking the reputation
|
||||
of yourself/your company/
|
||||
`your country <https://www.bbc.co.uk/news/technology-54423988>`_ if you keep
|
||||
using xls and are not aware of its row limit.
|
||||
|
||||
|
||||
Support the project
|
||||
================================================================================
|
||||
|
||||
If your company has embedded pyexcel and its components into a revenue generating
|
||||
product, please support me on github, `patreon <https://www.patreon.com/bePatron?u=5537627>`_
|
||||
or `bounty source <https://salt.bountysource.com/teams/chfw-pyexcel>`_ to maintain
|
||||
the project and develop it further.
|
||||
|
||||
If you are an individual, you are welcome to support me too and for however long
|
||||
you feel like. As my backer, you will receive
|
||||
`early access to pyexcel related contents <https://www.patreon.com/pyexcel/posts>`_.
|
||||
|
||||
And your issues will get prioritized if you would like to become my patreon as `pyexcel pro user`.
|
||||
|
||||
With your financial support, I will be able to invest
|
||||
a little bit more time in coding, documentation and writing interesting posts.
|
||||
|
||||
**pyexcel-xls** is a tiny wrapper library to read, manipulate and write data in xls format and it can read xlsx and xlsm fromat. You are likely to use it with `pyexcel <https://github.com/pyexcel/pyexcel>`_.
|
||||
|
||||
Known constraints
|
||||
==================
|
||||
|
||||
Fonts, colors and charts are not supported.
|
||||
|
||||
Nor to read password protected xls, xlsx and ods files.
|
||||
|
||||
Installation
|
||||
================================================================================
|
||||
|
||||
You can install it via pip:
|
||||
|
||||
You can install pyexcel-xls via pip:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
@ -29,7 +111,7 @@ or clone it and install it:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone http://github.com/pyexcel/pyexcel-xls.git
|
||||
$ git clone https://github.com/pyexcel/pyexcel-xls.git
|
||||
$ cd pyexcel-xls
|
||||
$ python setup.py install
|
||||
|
||||
|
@ -39,9 +121,6 @@ Usage
|
|||
As a standalone library
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Write to an xls file
|
||||
********************************************************************************
|
||||
|
||||
.. testcode::
|
||||
:hide:
|
||||
|
||||
|
@ -58,6 +137,11 @@ Write to an xls file
|
|||
... from collections import OrderedDict
|
||||
|
||||
|
||||
Write to an xls file
|
||||
********************************************************************************
|
||||
|
||||
|
||||
|
||||
Here's the sample code to write a dictionary to an xls file:
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -68,6 +152,7 @@ Here's the sample code to write a dictionary to an xls file:
|
|||
>>> data.update({"Sheet 2": [["row 1", "row 2", "row 3"]]})
|
||||
>>> save_data("your_file.xls", data)
|
||||
|
||||
|
||||
Read from an xls file
|
||||
********************************************************************************
|
||||
|
||||
|
@ -101,6 +186,7 @@ Here's the sample code to write a dictionary to an xls file:
|
|||
|
||||
|
||||
|
||||
|
||||
Read from an xls from memory
|
||||
********************************************************************************
|
||||
|
||||
|
@ -119,6 +205,8 @@ Continue from previous example:
|
|||
Pagination feature
|
||||
********************************************************************************
|
||||
|
||||
|
||||
|
||||
Let's assume the following file is a huge xls file:
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -175,16 +263,6 @@ No longer, explicit import is needed since pyexcel version 0.2.2. Instead,
|
|||
this library is auto-loaded. So if you want to read data in xls format,
|
||||
installing it is enough.
|
||||
|
||||
Any version under pyexcel 0.2.2, you have to keep doing the following:
|
||||
|
||||
Import it in your file to enable this plugin:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pyexcel.ext import xls
|
||||
|
||||
Please note only pyexcel version 0.0.4+ support this.
|
||||
|
||||
|
||||
Reading from an xls file
|
||||
********************************************************************************
|
||||
|
@ -194,7 +272,6 @@ Here is the sample code:
|
|||
.. code-block:: python
|
||||
|
||||
>>> import pyexcel as pe
|
||||
>>> # from pyexcel.ext import xls
|
||||
>>> sheet = pe.get_book(file_name="your_file.xls")
|
||||
>>> sheet
|
||||
Sheet 1:
|
||||
|
@ -265,6 +342,7 @@ You need to pass a StringIO instance to Writer:
|
|||
>>> # In reality, you might give it to your http response
|
||||
>>> # object for downloading
|
||||
|
||||
|
||||
License
|
||||
================================================================================
|
||||
|
||||
|
@ -280,7 +358,7 @@ Development steps for code changes
|
|||
|
||||
Upgrade your setup tools and pip. They are needed for development and testing only:
|
||||
|
||||
#. pip install --upgrade setuptools "pip==7.1"
|
||||
#. pip install --upgrade setuptools pip
|
||||
|
||||
Then install relevant development requirements:
|
||||
|
||||
|
@ -288,28 +366,16 @@ Then install relevant development requirements:
|
|||
#. pip install -r requirements.txt
|
||||
#. pip install -r tests/requirements.txt
|
||||
|
||||
Once you have finished your changes, please provide test case(s), relevant documentation
|
||||
and update changelog.yml
|
||||
|
||||
In order to update test environment, and documentation, additional setps are
|
||||
required:
|
||||
.. note::
|
||||
|
||||
#. pip install moban
|
||||
#. git clone https://github.com/pyexcel/pyexcel-commons.git
|
||||
#. make your changes in `.moban.d` directory, then issue command `moban`
|
||||
As to rnd_requirements.txt, usually, it is created when a dependent
|
||||
library is not released. Once the dependecy is installed
|
||||
(will be released), the future
|
||||
version of the dependency in the requirements.txt will be valid.
|
||||
|
||||
What is rnd_requirements.txt
|
||||
-------------------------------
|
||||
|
||||
Usually, it is created when a dependent library is not released. Once the dependecy is installed(will be released), the future version of the dependency in the requirements.txt will be valid.
|
||||
|
||||
What is pyexcel-commons
|
||||
---------------------------------
|
||||
|
||||
Many information that are shared across pyexcel projects, such as: this developer guide, license info, etc. are stored in `pyexcel-commons` project.
|
||||
|
||||
What is .moban.d
|
||||
---------------------------------
|
||||
|
||||
`.moban.d` stores the specific meta data for the library.
|
||||
|
||||
How to test your contribution
|
||||
------------------------------
|
||||
|
@ -318,12 +384,23 @@ Although `nose` and `doctest` are both used in code testing, it is adviable that
|
|||
|
||||
On Linux/Unix systems, please launch your tests like this::
|
||||
|
||||
$ make test
|
||||
$ make
|
||||
|
||||
On Windows systems, please issue this command::
|
||||
On Windows, please issue this command::
|
||||
|
||||
> test.bat
|
||||
|
||||
|
||||
Before you commit
|
||||
------------------------------
|
||||
|
||||
Please run::
|
||||
|
||||
$ make format
|
||||
|
||||
so as to beautify your code otherwise your build may fail your unit test.
|
||||
|
||||
|
||||
Known Issues
|
||||
=============
|
||||
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
name: pyexcel-xls
|
||||
organisation: pyexcel
|
||||
releases:
|
||||
- changes:
|
||||
- action: Removed
|
||||
details:
|
||||
- "`#46`: remove the hard pin on xlrd version < 2.0"
|
||||
- action: Added
|
||||
details:
|
||||
- "`#47`: limit support to persist datetime.timedelta. see more details in doc"
|
||||
date: 07.10.2021
|
||||
version: 0.7.0
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- "lock down xlrd version less than version 2.0, because 2.0+ does not support xlsx read"
|
||||
date: 12.12.2020
|
||||
version: 0.6.2
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- "Restrict this library to get installed on python 3.6+, because pyexcel-io 0.6.0+ supports only python 3.6+."
|
||||
date: 21.10.2020
|
||||
version: 0.6.1
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- 'New style xlsx plugins, promoted by pyexcel-io v0.6.2.'
|
||||
date: 8.10.2020
|
||||
version: 0.6.0
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- "`#35`, include tests"
|
||||
date: 29.08.2020
|
||||
version: 0.5.9
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- "`pyexcel#151`, read cell error as #N/A."
|
||||
date: 22.08.2018
|
||||
version: 0.5.8
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- "`pyexcel#54`, Book.datemode attribute of that workbook should be passed always."
|
||||
date: 15.03.2018
|
||||
version: 0.5.7
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- "`pyexcel#120`, xlwt cannot save a book without any sheet. So, let's raise an exception in this case in order to warn the developers."
|
||||
date: 15.03.2018
|
||||
version: 0.5.6
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- '`#25`, detect merged cell in .xls'
|
||||
date: 8.11.2017
|
||||
version: 0.5.5
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- '`#24`, xlsx format cannot use skip_hidden_row_and_column. please use pyexcel-xlsx
|
||||
instead.'
|
||||
date: 2.11.2017
|
||||
version: 0.5.4
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- '`#21`, skip hidden rows and columns under ''skip_hidden_row_and_column'' flag.'
|
||||
date: 2.11.2017
|
||||
version: 0.5.3
|
||||
- changes:
|
||||
- action: updated
|
||||
details:
|
||||
- pyexcel `pyexcel#105`, remove gease from setup_requires, introduced by 0.5.1.
|
||||
- remove python2.6 test support
|
||||
- update its dependecy on pyexcel-io to 0.5.3
|
||||
date: 23.10.2017
|
||||
version: 0.5.2
|
||||
- changes:
|
||||
- action: added
|
||||
details:
|
||||
- '`pyexcel#103`, include LICENSE file in MANIFEST.in, meaning LICENSE file will
|
||||
appear in the released tar ball.'
|
||||
date: 20.10.2017
|
||||
version: 0.5.1
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- '`#20`, is handled in pyexcel-io'
|
||||
- put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO. Hence,
|
||||
there will be performance boost in handling files in memory.
|
||||
date: 30.08.2017
|
||||
version: 0.5.0
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- '`#20`, handle unseekable stream given by http response.'
|
||||
date: 25.08.2017
|
||||
version: 0.4.1
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- '`pyexcel-xlsx#15`, close file handle'
|
||||
- pyexcel-io plugin interface now updated to use `lml <https://github.com/chfw/lml>`_.
|
||||
date: 19.06.2017
|
||||
version: 0.4.0
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- '`#18`, pass on encoding_override and others to xlrd.'
|
||||
date: 30/05/2017
|
||||
version: 0.3.3
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- '`#16`, allow mmap to be passed as file content'
|
||||
date: 18.05.2017
|
||||
version: 0.3.2
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- '`#14`, Python 3.6 - cannot use LOCALE flag with a str pattern'
|
||||
- fix its dependency on pyexcel-io 0.3.0
|
||||
date: 16.01.2017
|
||||
version: 0.3.1
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- '`#13`, alert on empyty file content'
|
||||
- Support pyexcel-io v0.3.0
|
||||
date: 22.12.2016
|
||||
version: 0.3.0
|
||||
- changes:
|
||||
- action: Updated
|
||||
details:
|
||||
- '`#10`, To support generator as member of the incoming two dimensional data'
|
||||
date: 20.09.2016
|
||||
version: 0.2.3
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- 'support pagination. two pairs: start_row, row_limit and start_column, column_limit
|
||||
help you deal with large files.'
|
||||
date: 31.08.2016
|
||||
version: 0.2.2
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- '`#9`, `skip_hidden_sheets` is added. By default, hidden sheets are skipped
|
||||
when reading all sheets. Reading sheet by name or by index are not affected.'
|
||||
date: 13.07.2016
|
||||
version: 0.2.1
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- By default, `float` will be converted to `int` where fits. `auto_detect_int`, a
|
||||
flag to switch off the autoatic conversion from `float` to `int`.
|
||||
- '''library=pyexcel-xls'' was added so as to inform pyexcel to use it instead
|
||||
of other libraries, in the situation where there are more than one plugin for a
|
||||
file type, e.g. xlsm'
|
||||
- action: Updated
|
||||
details:
|
||||
- support the auto-import feature of pyexcel-io 0.2.0
|
||||
- xlwt is now used for python 2 implementation while xlwt-future is used for python
|
||||
3
|
||||
date: 01.06.2016
|
||||
version: 0.2.0
|
||||
- changes:
|
||||
- action: Added
|
||||
details:
|
||||
- Passing "streaming=True" to get_data, you will get the two dimensional array as
|
||||
a generator
|
||||
- Passing "data=your_generator" to save_data is acceptable too.
|
||||
date: 17.01.2016
|
||||
version: 0.1.0
|
|
@ -1,39 +1,87 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
DESCRIPTION = (
|
||||
'A wrapper library to read, manipulate and write data in xls format. It' +
|
||||
' reads xlsx and xlsm format' +
|
||||
''
|
||||
)
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
intersphinx_mapping = {
|
||||
'pyexcel': ('http://pyexcel.readthedocs.org/en/latest/', None)
|
||||
}
|
||||
spelling_word_list_filename = 'spelling_wordlist.txt'
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# 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
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'pyexcel-xls'
|
||||
copyright = '2015-2021 Onni Software Ltd.'
|
||||
author = 'C.W.'
|
||||
# The short X.Y version
|
||||
version = '0.7.0'
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '0.7.0'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode',]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
project = u'pyexcel-xls'
|
||||
copyright = u'2015-2016 Onni Software Ltd.'
|
||||
version = '0.2.1'
|
||||
release = '0.2.2'
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = []
|
||||
pygments_style = 'sphinx'
|
||||
html_theme = 'default'
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
htmlhelp_basename = 'pyexcel-xlsdoc'
|
||||
latex_elements = {}
|
||||
latex_documents = [
|
||||
('index', 'pyexcel-xls.tex', u'pyexcel-xls Documentation',
|
||||
'Onni Software Ltd.', 'manual'),
|
||||
]
|
||||
man_pages = [
|
||||
('index', 'pyexcel-xls', u'pyexcel-xls Documentation',
|
||||
[u'Onni Software Ltd.'], 1)
|
||||
]
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
# -- Options for intersphinx extension ---------------------------------------
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'https://docs.python.org/3/': None}
|
||||
# TODO: html_theme not configurable upstream
|
||||
html_theme = 'default'
|
||||
|
||||
# TODO: DESCRIPTION not configurable upstream
|
||||
texinfo_documents = [
|
||||
('index', 'pyexcel-xls', u'pyexcel-xls Documentation',
|
||||
'Onni Software Ltd.', 'pyexcel-xls', 'One line description of project.',
|
||||
('index', 'pyexcel-xls',
|
||||
'pyexcel-xls Documentation',
|
||||
'Onni Software Ltd.', 'pyexcel-xls',
|
||||
DESCRIPTION,
|
||||
'Miscellaneous'),
|
||||
]
|
||||
intersphinx_mapping.update({
|
||||
'pyexcel': ('http://pyexcel.readthedocs.io/en/latest/', None),
|
||||
})
|
||||
master_doc = "index"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
isort $(find pyexcel_xls -name "*.py"|xargs echo) $(find tests -name "*.py"|xargs echo)
|
||||
black -l 79 pyexcel_xls
|
||||
black -l 79 tests
|
|
@ -0,0 +1,2 @@
|
|||
pip install flake8
|
||||
flake8 --exclude=.moban.d,docs,setup.py --builtins=unicode,xrange,long . && python setup.py checkdocs
|
|
@ -1,12 +1,18 @@
|
|||
overrides: "pyexcel.yaml"
|
||||
name: "pyexcel-xls"
|
||||
nick_name: xls
|
||||
version: 0.2.2
|
||||
release: 0.2.1
|
||||
version: 0.7.0
|
||||
current_version: 0.7.0
|
||||
release: 0.7.0
|
||||
file_type: xls
|
||||
is_on_conda: true
|
||||
dependencies:
|
||||
- pyexcel-io>=0.2.2
|
||||
- pyexcel-io>=0.6.2
|
||||
- xlrd
|
||||
- xlwt;python_version<"3"
|
||||
- xlwt-future;python_version>="3"
|
||||
- xlwt
|
||||
test_dependencies:
|
||||
- pyexcel
|
||||
description: A wrapper library to read, manipulate and write data in xls format. It reads xlsx and xlsm format
|
||||
moban_command: false
|
||||
python_requires: ">=3.6"
|
||||
min_python_version: "3.6"
|
|
@ -4,28 +4,71 @@
|
|||
|
||||
The lower level xls/xlsx/xlsm file format handler using xlrd/xlwt
|
||||
|
||||
:copyright: (c) 2015-2016 by Onni Software Ltd
|
||||
:copyright: (c) 2016-2021 by Onni Software Ltd
|
||||
:license: New BSD License
|
||||
"""
|
||||
import xlrd
|
||||
|
||||
# flake8: noqa
|
||||
from pyexcel_io.io import get_data as read_data
|
||||
from pyexcel_io.io import isstream
|
||||
from pyexcel_io.io import save_data as write_data
|
||||
|
||||
# this line has to be place above all else
|
||||
# because of dynamic import
|
||||
_FILE_TYPE = 'xls'
|
||||
__pyexcel_io_plugins__ = [_FILE_TYPE]
|
||||
from pyexcel_io.plugins import IOPluginInfoChainV2
|
||||
|
||||
__FILE_TYPE__ = "xls"
|
||||
|
||||
|
||||
from pyexcel_io.io import get_data as read_data, isstream, store_data as write_data
|
||||
def xlrd_version_2_or_greater():
|
||||
xlrd_version = getattr(xlrd, "__version__")
|
||||
|
||||
if xlrd_version:
|
||||
major = int(xlrd_version.split(".")[0])
|
||||
if major >= 2:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
XLRD_VERSION_2_OR_ABOVE = xlrd_version_2_or_greater()
|
||||
supported_file_formats = [__FILE_TYPE__, "xlsx", "xlsm"]
|
||||
if XLRD_VERSION_2_OR_ABOVE:
|
||||
supported_file_formats.remove("xlsx")
|
||||
|
||||
|
||||
IOPluginInfoChainV2(__name__).add_a_reader(
|
||||
relative_plugin_class_path="xlsr.XLSInFile",
|
||||
locations=["file"],
|
||||
file_types=supported_file_formats,
|
||||
stream_type="binary",
|
||||
).add_a_reader(
|
||||
relative_plugin_class_path="xlsr.XLSInMemory",
|
||||
locations=["memory"],
|
||||
file_types=supported_file_formats,
|
||||
stream_type="binary",
|
||||
).add_a_reader(
|
||||
relative_plugin_class_path="xlsr.XLSInContent",
|
||||
locations=["content"],
|
||||
file_types=supported_file_formats,
|
||||
stream_type="binary",
|
||||
).add_a_writer(
|
||||
relative_plugin_class_path="xlsw.XLSWriter",
|
||||
locations=["file", "memory"],
|
||||
file_types=[__FILE_TYPE__],
|
||||
stream_type="binary",
|
||||
)
|
||||
|
||||
|
||||
def get_data(afile, file_type=None, **keywords):
|
||||
"""standalone module function for reading module supported file type"""
|
||||
if isstream(afile) and file_type is None:
|
||||
file_type = _FILE_TYPE
|
||||
file_type = __FILE_TYPE__
|
||||
return read_data(afile, file_type=file_type, **keywords)
|
||||
|
||||
|
||||
def save_data(afile, data, file_type=None, **keywords):
|
||||
"""standalone module function for writing module supported file type"""
|
||||
if isstream(afile) and file_type is None:
|
||||
file_type = _FILE_TYPE
|
||||
file_type = __FILE_TYPE__
|
||||
write_data(afile, data, file_type=file_type, **keywords)
|
||||
|
|
|
@ -1,277 +0,0 @@
|
|||
"""
|
||||
pyexcel_xls
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The lower level xls/xlsm file format handler using xlrd/xlwt
|
||||
|
||||
:copyright: (c) 2015-2016 by Onni Software Ltd
|
||||
:license: New BSD License
|
||||
"""
|
||||
import sys
|
||||
import math
|
||||
import datetime
|
||||
import xlrd
|
||||
from xlwt import Workbook, XFStyle
|
||||
|
||||
from pyexcel_io.book import BookReader, BookWriter
|
||||
from pyexcel_io.sheet import SheetReader, SheetWriter
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
if PY2 and sys.version_info[1] < 7:
|
||||
from ordereddict import OrderedDict
|
||||
else:
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
DEFAULT_DATE_FORMAT = "DD/MM/YY"
|
||||
DEFAULT_TIME_FORMAT = "HH:MM:SS"
|
||||
DEFAULT_DATETIME_FORMAT = "%s %s" % (DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT)
|
||||
|
||||
|
||||
def is_integer_ok_for_xl_float(value):
|
||||
"""check if a float value had zero value in digits"""
|
||||
return value == math.floor(value)
|
||||
|
||||
|
||||
def xldate_to_python_date(value):
|
||||
"""
|
||||
convert xl date to python date
|
||||
"""
|
||||
date_tuple = xlrd.xldate_as_tuple(value, 0)
|
||||
ret = None
|
||||
if date_tuple == (0, 0, 0, 0, 0, 0):
|
||||
ret = datetime.datetime(1900, 1, 1, 0, 0, 0)
|
||||
elif date_tuple[0:3] == (0, 0, 0):
|
||||
ret = datetime.time(date_tuple[3],
|
||||
date_tuple[4],
|
||||
date_tuple[5])
|
||||
elif date_tuple[3:6] == (0, 0, 0):
|
||||
ret = datetime.date(date_tuple[0],
|
||||
date_tuple[1],
|
||||
date_tuple[2])
|
||||
else:
|
||||
ret = datetime.datetime(
|
||||
date_tuple[0],
|
||||
date_tuple[1],
|
||||
date_tuple[2],
|
||||
date_tuple[3],
|
||||
date_tuple[4],
|
||||
date_tuple[5]
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
class XLSheet(SheetReader):
|
||||
"""
|
||||
xls, xlsx, xlsm sheet reader
|
||||
|
||||
Currently only support first sheet in the file
|
||||
"""
|
||||
def __init__(self, sheet, auto_detect_int=True, **keywords):
|
||||
SheetReader.__init__(self, sheet, **keywords)
|
||||
self.auto_detect_int = auto_detect_int
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.native_sheet.name
|
||||
|
||||
def number_of_rows(self):
|
||||
"""
|
||||
Number of rows in the xls sheet
|
||||
"""
|
||||
return self.native_sheet.nrows
|
||||
|
||||
def number_of_columns(self):
|
||||
"""
|
||||
Number of columns in the xls sheet
|
||||
"""
|
||||
return self.native_sheet.ncols
|
||||
|
||||
def _cell_value(self, row, column):
|
||||
"""
|
||||
Random access to the xls cells
|
||||
"""
|
||||
cell_type = self.native_sheet.cell_type(row, column)
|
||||
value = self.native_sheet.cell_value(row, column)
|
||||
if cell_type == xlrd.XL_CELL_DATE:
|
||||
value = xldate_to_python_date(value)
|
||||
elif cell_type == xlrd.XL_CELL_NUMBER and self.auto_detect_int:
|
||||
if is_integer_ok_for_xl_float(value):
|
||||
value = int(value)
|
||||
return value
|
||||
|
||||
|
||||
class XLSBook(BookReader):
|
||||
"""
|
||||
XLSBook reader
|
||||
|
||||
It reads xls, xlsm, xlsx work book
|
||||
"""
|
||||
def __init__(self):
|
||||
BookReader.__init__(self)
|
||||
self.file_content = None
|
||||
|
||||
def open(self, file_name, **keywords):
|
||||
BookReader.open(self, file_name, **keywords)
|
||||
self._get_params()
|
||||
|
||||
def open_stream(self, file_stream, **keywords):
|
||||
BookReader.open_stream(self, file_stream, **keywords)
|
||||
self._get_params()
|
||||
|
||||
def open_content(self, file_content, **keywords):
|
||||
self.keywords = keywords
|
||||
self.file_content = file_content
|
||||
self._get_params()
|
||||
|
||||
def close(self):
|
||||
if self.native_book:
|
||||
self.native_book.release_resources()
|
||||
|
||||
def read_sheet_by_index(self, sheet_index):
|
||||
self.native_book = self._get_book(on_demand=True)
|
||||
sheet = self.native_book.sheet_by_index(sheet_index)
|
||||
return self.read_sheet(sheet)
|
||||
|
||||
def read_sheet_by_name(self, sheet_name):
|
||||
self.native_book = self._get_book(on_demand=True)
|
||||
try:
|
||||
sheet = self.native_book.sheet_by_name(sheet_name)
|
||||
except xlrd.XLRDError:
|
||||
raise ValueError("%s cannot be found" % sheet_name)
|
||||
return self.read_sheet(sheet)
|
||||
|
||||
def read_all(self):
|
||||
result = OrderedDict()
|
||||
self.native_book = self._get_book()
|
||||
for sheet in self.native_book.sheets():
|
||||
if self.skip_hidden_sheets and sheet.visibility != 0:
|
||||
continue
|
||||
data_dict = self.read_sheet(sheet)
|
||||
result.update(data_dict)
|
||||
return result
|
||||
|
||||
def read_sheet(self, native_sheet):
|
||||
sheet = XLSheet(native_sheet, **self.keywords)
|
||||
return {sheet.name: sheet.to_array()}
|
||||
|
||||
def _get_book(self, on_demand=False):
|
||||
if self.file_name:
|
||||
xls_book = xlrd.open_workbook(self.file_name, on_demand=on_demand)
|
||||
elif self.file_stream:
|
||||
xls_book = xlrd.open_workbook(
|
||||
None,
|
||||
file_contents=self.file_stream.getvalue(),
|
||||
on_demand=on_demand
|
||||
)
|
||||
elif self.file_content:
|
||||
xls_book = xlrd.open_workbook(
|
||||
None,
|
||||
file_contents=self.file_content,
|
||||
on_demand=on_demand
|
||||
)
|
||||
return xls_book
|
||||
|
||||
def _get_params(self):
|
||||
self.skip_hidden_sheets = self.keywords.get('skip_hidden_sheets', True)
|
||||
|
||||
|
||||
class XLSheetWriter(SheetWriter):
|
||||
"""
|
||||
xls sheet writer
|
||||
"""
|
||||
def set_sheet_name(self, name):
|
||||
"""Create a sheet
|
||||
"""
|
||||
self.native_sheet = self.native_book.add_sheet(name)
|
||||
self.current_row = 0
|
||||
|
||||
def write_row(self, array):
|
||||
"""
|
||||
write a row into the file
|
||||
"""
|
||||
for i in range(len(array)):
|
||||
value = array[i]
|
||||
style = None
|
||||
tmp_array = []
|
||||
if isinstance(value, datetime.datetime):
|
||||
tmp_array = [
|
||||
value.year, value.month, value.day,
|
||||
value.hour, value.minute, value.second
|
||||
]
|
||||
value = xlrd.xldate.xldate_from_datetime_tuple(tmp_array, 0)
|
||||
style = XFStyle()
|
||||
style.num_format_str = DEFAULT_DATETIME_FORMAT
|
||||
elif isinstance(value, datetime.date):
|
||||
tmp_array = [value.year, value.month, value.day]
|
||||
value = xlrd.xldate.xldate_from_date_tuple(tmp_array, 0)
|
||||
style = XFStyle()
|
||||
style.num_format_str = DEFAULT_DATE_FORMAT
|
||||
elif isinstance(value, datetime.time):
|
||||
tmp_array = [value.hour, value.minute, value.second]
|
||||
value = xlrd.xldate.xldate_from_time_tuple(tmp_array)
|
||||
style = XFStyle()
|
||||
style.num_format_str = DEFAULT_TIME_FORMAT
|
||||
if style:
|
||||
self.native_sheet.write(self.current_row, i, value, style)
|
||||
else:
|
||||
self.native_sheet.write(self.current_row, i, value)
|
||||
self.current_row += 1
|
||||
|
||||
|
||||
class XLSWriter(BookWriter):
|
||||
"""
|
||||
xls writer
|
||||
"""
|
||||
def __init__(self):
|
||||
BookWriter.__init__(self)
|
||||
self.work_book = None
|
||||
|
||||
def open(self, file_name,
|
||||
encoding='ascii', style_compression=2, **keywords):
|
||||
BookWriter.open(self, file_name, **keywords)
|
||||
self.work_book = Workbook(style_compression=style_compression,
|
||||
encoding=encoding)
|
||||
|
||||
def create_sheet(self, name):
|
||||
return XLSheetWriter(self.work_book, None, name)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
This call actually save the file
|
||||
"""
|
||||
self.work_book.save(self.file_alike_object)
|
||||
|
||||
|
||||
_xls_reader_registry = {
|
||||
"file_type": "xls",
|
||||
"reader": XLSBook,
|
||||
"writer": XLSWriter,
|
||||
"stream_type": "binary",
|
||||
"mime_type": "application/vnd.ms-excel",
|
||||
"library": "pyexcel-xls"
|
||||
}
|
||||
|
||||
_XLSM_MIME = (
|
||||
"application/" +
|
||||
"vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
|
||||
_xlsm_registry = {
|
||||
"file_type": "xlsm",
|
||||
"reader": XLSBook,
|
||||
"stream_type": "binary",
|
||||
"mime_type": _XLSM_MIME,
|
||||
"library": "pyexcel-xls"
|
||||
}
|
||||
|
||||
_xlsx_registry = {
|
||||
"file_type": "xlsx",
|
||||
"reader": XLSBook,
|
||||
"stream_type": "binary",
|
||||
"mime_type": "application/vnd.ms-excel.sheet.macroenabled.12",
|
||||
"library": "pyexcel-xls"
|
||||
}
|
||||
|
||||
exports = (_xls_reader_registry,
|
||||
_xlsm_registry,
|
||||
_xlsx_registry)
|
|
@ -0,0 +1,217 @@
|
|||
"""
|
||||
pyexcel_xlsr
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The lower level xls/xlsm file format handler using xlrd
|
||||
|
||||
:copyright: (c) 2016-2021 by Onni Software Ltd
|
||||
:license: New BSD License
|
||||
"""
|
||||
import datetime
|
||||
|
||||
import xlrd
|
||||
from pyexcel_io.service import has_no_digits_in_float
|
||||
from pyexcel_io.plugin_api import ISheet, IReader
|
||||
|
||||
XLS_KEYWORDS = [
|
||||
"filename",
|
||||
"logfile",
|
||||
"verbosity",
|
||||
"use_mmap",
|
||||
"file_contents",
|
||||
"encoding_override",
|
||||
"formatting_info",
|
||||
"on_demand",
|
||||
"ragged_rows",
|
||||
]
|
||||
DEFAULT_ERROR_VALUE = "#N/A"
|
||||
|
||||
|
||||
class MergedCell(object):
|
||||
def __init__(self, row_low, row_high, column_low, column_high):
|
||||
self.__rl = row_low
|
||||
self.__rh = row_high
|
||||
self.__cl = column_low
|
||||
self.__ch = column_high
|
||||
self.value = None
|
||||
|
||||
def register_cells(self, registry):
|
||||
for rowx in range(self.__rl, self.__rh):
|
||||
for colx in range(self.__cl, self.__ch):
|
||||
key = "%s-%s" % (rowx, colx)
|
||||
registry[key] = self
|
||||
|
||||
|
||||
class XLSheet(ISheet):
|
||||
"""
|
||||
xls, xlsx, xlsm sheet reader
|
||||
|
||||
Currently only support first sheet in the file
|
||||
"""
|
||||
|
||||
def __init__(self, sheet, auto_detect_int=True, date_mode=0, **keywords):
|
||||
self.__auto_detect_int = auto_detect_int
|
||||
self.__hidden_cols = []
|
||||
self.__hidden_rows = []
|
||||
self.__merged_cells = {}
|
||||
self._book_date_mode = date_mode
|
||||
self.xls_sheet = sheet
|
||||
self._keywords = keywords
|
||||
if keywords.get("detect_merged_cells") is True:
|
||||
for merged_cell_ranges in sheet.merged_cells:
|
||||
merged_cells = MergedCell(*merged_cell_ranges)
|
||||
merged_cells.register_cells(self.__merged_cells)
|
||||
if keywords.get("skip_hidden_row_and_column") is True:
|
||||
for col_index, info in self.xls_sheet.colinfo_map.items():
|
||||
if info.hidden == 1:
|
||||
self.__hidden_cols.append(col_index)
|
||||
for row_index, info in self.xls_sheet.rowinfo_map.items():
|
||||
if info.hidden == 1:
|
||||
self.__hidden_rows.append(row_index)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.xls_sheet.name
|
||||
|
||||
def row_iterator(self):
|
||||
number_of_rows = self.xls_sheet.nrows - len(self.__hidden_rows)
|
||||
return range(number_of_rows)
|
||||
|
||||
def column_iterator(self, row):
|
||||
number_of_columns = self.xls_sheet.ncols - len(self.__hidden_cols)
|
||||
for column in range(number_of_columns):
|
||||
yield self.cell_value(row, column)
|
||||
|
||||
def cell_value(self, row, column):
|
||||
"""
|
||||
Random access to the xls cells
|
||||
"""
|
||||
if self._keywords.get("skip_hidden_row_and_column") is True:
|
||||
row, column = self._offset_hidden_indices(row, column)
|
||||
cell_type = self.xls_sheet.cell_type(row, column)
|
||||
value = self.xls_sheet.cell_value(row, column)
|
||||
|
||||
if cell_type == xlrd.XL_CELL_DATE:
|
||||
value = xldate_to_python_date(value, self._book_date_mode)
|
||||
elif cell_type == xlrd.XL_CELL_NUMBER and self.__auto_detect_int:
|
||||
if has_no_digits_in_float(value):
|
||||
value = int(value)
|
||||
elif cell_type == xlrd.XL_CELL_ERROR:
|
||||
value = DEFAULT_ERROR_VALUE
|
||||
|
||||
if self.__merged_cells:
|
||||
merged_cell = self.__merged_cells.get("%s-%s" % (row, column))
|
||||
if merged_cell:
|
||||
if merged_cell.value:
|
||||
value = merged_cell.value
|
||||
else:
|
||||
merged_cell.value = value
|
||||
return value
|
||||
|
||||
def _offset_hidden_indices(self, row, column):
|
||||
row = calculate_offsets(row, self.__hidden_rows)
|
||||
column = calculate_offsets(column, self.__hidden_cols)
|
||||
return row, column
|
||||
|
||||
|
||||
def calculate_offsets(incoming_index, hidden_indices):
|
||||
offset = 0
|
||||
for index in hidden_indices:
|
||||
if index <= (incoming_index + offset):
|
||||
offset += 1
|
||||
return incoming_index + offset
|
||||
|
||||
|
||||
class XLSReader(IReader):
|
||||
"""
|
||||
XLSBook reader
|
||||
|
||||
It reads xls, xlsm, xlsx work book
|
||||
"""
|
||||
|
||||
def __init__(self, file_type, **keywords):
|
||||
self.__skip_hidden_sheets = keywords.get("skip_hidden_sheets", True)
|
||||
self.__skip_hidden_row_column = keywords.get(
|
||||
"skip_hidden_row_and_column", True
|
||||
)
|
||||
self.__detect_merged_cells = keywords.get("detect_merged_cells", False)
|
||||
self._keywords = keywords
|
||||
xlrd_params = self._extract_xlrd_params()
|
||||
if self.__skip_hidden_row_column and file_type == "xls":
|
||||
xlrd_params["formatting_info"] = True
|
||||
if self.__detect_merged_cells:
|
||||
xlrd_params["formatting_info"] = True
|
||||
|
||||
self.content_array = []
|
||||
self.xls_book = self.get_xls_book(**xlrd_params)
|
||||
for sheet in self.xls_book.sheets():
|
||||
if self.__skip_hidden_sheets and sheet.visibility != 0:
|
||||
continue
|
||||
self.content_array.append(sheet)
|
||||
|
||||
def read_sheet(self, index):
|
||||
native_sheet = self.content_array[index]
|
||||
sheet = XLSheet(
|
||||
native_sheet, date_mode=self.xls_book.datemode, **self._keywords
|
||||
)
|
||||
return sheet
|
||||
|
||||
def close(self):
|
||||
if self.xls_book:
|
||||
self.xls_book.release_resources()
|
||||
self.xls_book = None
|
||||
|
||||
def get_xls_book(self, **xlrd_params):
|
||||
xls_book = xlrd.open_workbook(**xlrd_params)
|
||||
return xls_book
|
||||
|
||||
def _extract_xlrd_params(self):
|
||||
params = {}
|
||||
if self._keywords is not None:
|
||||
for key in list(self._keywords.keys()):
|
||||
if key in XLS_KEYWORDS:
|
||||
params[key] = self._keywords.pop(key)
|
||||
return params
|
||||
|
||||
|
||||
class XLSInFile(XLSReader):
|
||||
def __init__(self, file_name, file_type, **keywords):
|
||||
super().__init__(file_type, filename=file_name, **keywords)
|
||||
|
||||
|
||||
class XLSInContent(XLSReader):
|
||||
def __init__(self, file_content, file_type, **keywords):
|
||||
super().__init__(file_type, file_contents=file_content, **keywords)
|
||||
|
||||
|
||||
class XLSInMemory(XLSReader):
|
||||
def __init__(self, file_stream, file_type, **keywords):
|
||||
file_stream.seek(0)
|
||||
super().__init__(
|
||||
file_type, file_contents=file_stream.read(), **keywords
|
||||
)
|
||||
|
||||
|
||||
def xldate_to_python_date(value, date_mode):
|
||||
"""
|
||||
convert xl date to python date
|
||||
"""
|
||||
date_tuple = xlrd.xldate_as_tuple(value, date_mode)
|
||||
|
||||
ret = None
|
||||
if date_tuple == (0, 0, 0, 0, 0, 0):
|
||||
ret = datetime.datetime(1900, 1, 1, 0, 0, 0)
|
||||
elif date_tuple[0:3] == (0, 0, 0):
|
||||
ret = datetime.time(date_tuple[3], date_tuple[4], date_tuple[5])
|
||||
elif date_tuple[3:6] == (0, 0, 0):
|
||||
ret = datetime.date(date_tuple[0], date_tuple[1], date_tuple[2])
|
||||
else:
|
||||
ret = datetime.datetime(
|
||||
date_tuple[0],
|
||||
date_tuple[1],
|
||||
date_tuple[2],
|
||||
date_tuple[3],
|
||||
date_tuple[4],
|
||||
date_tuple[5],
|
||||
)
|
||||
return ret
|
|
@ -0,0 +1,111 @@
|
|||
"""
|
||||
pyexcel_xlsw
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The lower level xls file format handler using xlwt
|
||||
|
||||
:copyright: (c) 2016-2021 by Onni Software Ltd
|
||||
:license: New BSD License
|
||||
"""
|
||||
import datetime
|
||||
|
||||
import xlrd
|
||||
from xlwt import XFStyle, Workbook
|
||||
from pyexcel_io import constants
|
||||
from pyexcel_io.plugin_api import IWriter, ISheetWriter
|
||||
|
||||
DEFAULT_DATE_FORMAT = "DD/MM/YY"
|
||||
DEFAULT_TIME_FORMAT = "HH:MM:SS"
|
||||
DEFAULT_LONGTIME_FORMAT = "[HH]:MM:SS"
|
||||
DEFAULT_DATETIME_FORMAT = "%s %s" % (DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT)
|
||||
EMPTY_SHEET_NOT_ALLOWED = "xlwt does not support a book without any sheets"
|
||||
|
||||
|
||||
class XLSheetWriter(ISheetWriter):
|
||||
"""
|
||||
xls sheet writer
|
||||
"""
|
||||
|
||||
def __init__(self, xls_book, xls_sheet, sheet_name):
|
||||
if sheet_name is None:
|
||||
sheet_name = constants.DEFAULT_SHEET_NAME
|
||||
self._xls_book = xls_book
|
||||
self._xls_sheet = xls_sheet
|
||||
self._xls_sheet = self._xls_book.add_sheet(sheet_name)
|
||||
self.current_row = 0
|
||||
|
||||
def write_row(self, array):
|
||||
"""
|
||||
write a row into the file
|
||||
"""
|
||||
for i, value in enumerate(array):
|
||||
style = None
|
||||
tmp_array = []
|
||||
if isinstance(value, datetime.datetime):
|
||||
tmp_array = [
|
||||
value.year,
|
||||
value.month,
|
||||
value.day,
|
||||
value.hour,
|
||||
value.minute,
|
||||
value.second,
|
||||
]
|
||||
value = xlrd.xldate.xldate_from_datetime_tuple(tmp_array, 0)
|
||||
style = XFStyle()
|
||||
style.num_format_str = DEFAULT_DATETIME_FORMAT
|
||||
elif isinstance(value, datetime.timedelta):
|
||||
value = value.days + value.seconds / 86_400
|
||||
style = XFStyle()
|
||||
style.num_format_str = DEFAULT_LONGTIME_FORMAT
|
||||
elif isinstance(value, datetime.date):
|
||||
tmp_array = [value.year, value.month, value.day]
|
||||
value = xlrd.xldate.xldate_from_date_tuple(tmp_array, 0)
|
||||
style = XFStyle()
|
||||
style.num_format_str = DEFAULT_DATE_FORMAT
|
||||
elif isinstance(value, datetime.time):
|
||||
tmp_array = [value.hour, value.minute, value.second]
|
||||
value = xlrd.xldate.xldate_from_time_tuple(tmp_array)
|
||||
style = XFStyle()
|
||||
style.num_format_str = DEFAULT_TIME_FORMAT
|
||||
if style:
|
||||
self._xls_sheet.write(self.current_row, i, value, style)
|
||||
else:
|
||||
self._xls_sheet.write(self.current_row, i, value)
|
||||
self.current_row += 1
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class XLSWriter(IWriter):
|
||||
"""
|
||||
xls writer
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_alike_object,
|
||||
_, # file_type not used
|
||||
encoding="ascii",
|
||||
style_compression=2,
|
||||
**keywords,
|
||||
):
|
||||
self.file_alike_object = file_alike_object
|
||||
self.work_book = Workbook(
|
||||
style_compression=style_compression, encoding=encoding
|
||||
)
|
||||
|
||||
def create_sheet(self, name):
|
||||
return XLSheetWriter(self.work_book, None, name)
|
||||
|
||||
def write(self, incoming_dict):
|
||||
if incoming_dict:
|
||||
IWriter.write(self, incoming_dict)
|
||||
else:
|
||||
raise NotImplementedError(EMPTY_SHEET_NOT_ALLOWED)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
This call actually save the file
|
||||
"""
|
||||
self.work_book.save(self.file_alike_object)
|
|
@ -1,4 +1,3 @@
|
|||
pyexcel-io>=0.2.2
|
||||
pyexcel-io>=0.6.2
|
||||
xlrd
|
||||
xlwt;python_version<"3"
|
||||
xlwt-future;python_version>="3"
|
||||
xlwt
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
https://github.com/pyexcel/pyexcel-io/archive/master.zip
|
||||
https://github.com/pyexcel/pyexcel-io/archive/dev.zip
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[metadata]
|
||||
description-file = README.rst
|
||||
[bdist_wheel]
|
||||
universal = 1
|
224
setup.py
224
setup.py
|
@ -1,92 +1,212 @@
|
|||
try:
|
||||
from setuptools import setup, find_packages
|
||||
except ImportError:
|
||||
from ez_setup import use_setuptools
|
||||
use_setuptools()
|
||||
from setuptools import setup, find_packages
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Template by pypi-mobans
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import codecs
|
||||
import locale
|
||||
import platform
|
||||
from shutil import rmtree
|
||||
|
||||
from setuptools import Command, setup, find_packages
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
PY26 = PY2 and sys.version_info[1] < 7
|
||||
PY33 = sys.version_info < (3, 4)
|
||||
|
||||
NAME = 'pyexcel-xls'
|
||||
AUTHOR = 'C.W.'
|
||||
VERSION = '0.2.2'
|
||||
EMAIL = 'wangc_2011 (at) hotmail.com'
|
||||
LICENSE = 'New BSD'
|
||||
PACKAGES = find_packages(exclude=['ez_setup', 'examples', 'tests'])
|
||||
# Work around mbcs bug in distutils.
|
||||
# http://bugs.python.org/issue10945
|
||||
# This work around is only if a project supports Python < 3.4
|
||||
|
||||
# Work around for locale not being set
|
||||
try:
|
||||
lc = locale.getlocale()
|
||||
pf = platform.system()
|
||||
if pf != "Windows" and lc == (None, None):
|
||||
locale.setlocale(locale.LC_ALL, "C.UTF-8")
|
||||
except (ValueError, UnicodeError, locale.Error):
|
||||
locale.setlocale(locale.LC_ALL, "en_US.UTF-8")
|
||||
|
||||
NAME = "pyexcel-xls"
|
||||
AUTHOR = "C.W."
|
||||
VERSION = "0.7.0"
|
||||
EMAIL = "info@pyexcel.org"
|
||||
LICENSE = "New BSD"
|
||||
DESCRIPTION = (
|
||||
'A wrapper library to read, manipulate and write data in xls format. It' +
|
||||
' reads xlsx and xlsm format' +
|
||||
''
|
||||
"A wrapper library to read, manipulate and write data in xls format. It" +
|
||||
"reads xlsx and xlsm format"
|
||||
)
|
||||
URL = "https://github.com/pyexcel/pyexcel-xls"
|
||||
DOWNLOAD_URL = "%s/archive/0.7.0.tar.gz" % URL
|
||||
FILES = ["README.rst","CONTRIBUTORS.rst", "CHANGELOG.rst"]
|
||||
KEYWORDS = [
|
||||
'excel',
|
||||
'python',
|
||||
'pyexcel',
|
||||
"python",
|
||||
'xls',
|
||||
'xlsx',
|
||||
'xlsm'
|
||||
]
|
||||
|
||||
INSTALL_REQUIRES = [
|
||||
'pyexcel-io>=0.2.2',
|
||||
'xlrd',
|
||||
]
|
||||
|
||||
if PY2:
|
||||
INSTALL_REQUIRES.append('xlwt')
|
||||
if not PY2:
|
||||
INSTALL_REQUIRES.append('xlwt-future')
|
||||
|
||||
EXTRAS_REQUIRE = {
|
||||
}
|
||||
|
||||
CLASSIFIERS = [
|
||||
'Topic :: Office/Business',
|
||||
'Topic :: Utilities',
|
||||
'Topic :: Software Development :: Libraries',
|
||||
'Programming Language :: Python',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Intended Audience :: Developers',
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Programming Language :: Python",
|
||||
"Intended Audience :: Developers",
|
||||
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
|
||||
|
||||
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
|
||||
'Programming Language :: Python :: Implementation :: PyPy'
|
||||
]
|
||||
|
||||
PYTHON_REQUIRES = ">=3.6"
|
||||
|
||||
INSTALL_REQUIRES = [
|
||||
"pyexcel-io>=0.6.2",
|
||||
"xlrd",
|
||||
"xlwt",
|
||||
]
|
||||
SETUP_COMMANDS = {}
|
||||
|
||||
PACKAGES = find_packages(exclude=["ez_setup", "examples", "tests", "tests.*"])
|
||||
EXTRAS_REQUIRE = {
|
||||
}
|
||||
# You do not need to read beyond this line
|
||||
PUBLISH_COMMAND = "{0} setup.py sdist bdist_wheel upload -r pypi".format(sys.executable)
|
||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
GS_COMMAND = ("gease pyexcel-xls v0.7.0 " +
|
||||
"Find 0.7.0 in changelog for more details")
|
||||
NO_GS_MESSAGE = ("Automatic github release is disabled. " +
|
||||
"Please install gease to enable it.")
|
||||
UPLOAD_FAILED_MSG = (
|
||||
'Upload failed. please run "%s" yourself.' % PUBLISH_COMMAND)
|
||||
|
||||
|
||||
class PublishCommand(Command):
|
||||
"""Support setup.py upload."""
|
||||
|
||||
description = "Build and publish the package on github and pypi"
|
||||
user_options = []
|
||||
|
||||
@staticmethod
|
||||
def status(s):
|
||||
"""Prints things in bold."""
|
||||
print("\033[1m{0}\033[0m".format(s))
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.status("Removing previous builds...")
|
||||
rmtree(os.path.join(HERE, "dist"))
|
||||
rmtree(os.path.join(HERE, "build"))
|
||||
rmtree(os.path.join(HERE, "pyexcel_xls.egg-info"))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.status("Building Source and Wheel (universal) distribution...")
|
||||
run_status = True
|
||||
if has_gease():
|
||||
run_status = os.system(GS_COMMAND) == 0
|
||||
else:
|
||||
self.status(NO_GS_MESSAGE)
|
||||
if run_status:
|
||||
if os.system(PUBLISH_COMMAND) != 0:
|
||||
self.status(UPLOAD_FAILED_MSG)
|
||||
|
||||
sys.exit()
|
||||
|
||||
|
||||
SETUP_COMMANDS.update({
|
||||
"publish": PublishCommand
|
||||
})
|
||||
|
||||
def has_gease():
|
||||
"""
|
||||
test if github release command is installed
|
||||
|
||||
visit http://github.com/moremoban/gease for more info
|
||||
"""
|
||||
try:
|
||||
import gease # noqa
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def read_files(*files):
|
||||
"""Read files into setup"""
|
||||
text = ""
|
||||
for single_file in files:
|
||||
text = text + read(single_file) + "\n"
|
||||
content = read(single_file)
|
||||
text = text + content + "\n"
|
||||
return text
|
||||
|
||||
|
||||
def read(afile):
|
||||
"""Read a file into setup"""
|
||||
with open(afile, 'r') as opened_file:
|
||||
return opened_file.read()
|
||||
the_relative_file = os.path.join(HERE, afile)
|
||||
with codecs.open(the_relative_file, "r", "utf-8") as opened_file:
|
||||
content = filter_out_test_code(opened_file)
|
||||
content = "".join(list(content))
|
||||
return content
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def filter_out_test_code(file_handle):
|
||||
found_test_code = False
|
||||
for line in file_handle.readlines():
|
||||
if line.startswith(".. testcode:"):
|
||||
found_test_code = True
|
||||
continue
|
||||
if found_test_code is True:
|
||||
if line.startswith(" "):
|
||||
continue
|
||||
else:
|
||||
empty_line = line.strip()
|
||||
if len(empty_line) == 0:
|
||||
continue
|
||||
else:
|
||||
found_test_code = False
|
||||
yield line
|
||||
else:
|
||||
for keyword in ["|version|", "|today|"]:
|
||||
if keyword in line:
|
||||
break
|
||||
else:
|
||||
yield line
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup(
|
||||
test_suite="tests",
|
||||
name=NAME,
|
||||
author=AUTHOR,
|
||||
version=VERSION,
|
||||
author_email=EMAIL,
|
||||
description=DESCRIPTION,
|
||||
install_requires=INSTALL_REQUIRES,
|
||||
url=URL,
|
||||
download_url=DOWNLOAD_URL,
|
||||
long_description=read_files(*FILES),
|
||||
license=LICENSE,
|
||||
keywords=KEYWORDS,
|
||||
python_requires=PYTHON_REQUIRES,
|
||||
extras_require=EXTRAS_REQUIRE,
|
||||
tests_require=["nose"],
|
||||
install_requires=INSTALL_REQUIRES,
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
long_description=read_files('README.rst', 'CHANGELOG.rst'),
|
||||
zip_safe=False,
|
||||
tests_require=['nose'],
|
||||
license=LICENSE,
|
||||
classifiers=CLASSIFIERS
|
||||
classifiers=CLASSIFIERS,
|
||||
cmdclass=SETUP_COMMANDS
|
||||
)
|
||||
|
|
3
test.bat
3
test.bat
|
@ -1,3 +1,2 @@
|
|||
|
||||
pip freeze
|
||||
nosetests --with-cov --cover-package pyexcel_xls --cover-package tests --with-doctest --doctest-extension=.rst tests README.rst pyexcel_xls && flake8 . --exclude=.moban.d --builtins=unicode,xrange,long
|
||||
nosetests --with-coverage --cover-package pyexcel_xls --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_xls
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# flake8: noqa
|
||||
import sys
|
||||
|
||||
|
||||
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||
from ordereddict import OrderedDict
|
||||
else:
|
||||
|
|
|
@ -1,31 +1,35 @@
|
|||
import os
|
||||
import os # noqa
|
||||
import datetime # noqa
|
||||
|
||||
import pyexcel
|
||||
|
||||
from nose.tools import eq_, raises # noqa
|
||||
|
||||
|
||||
def create_sample_file1(file):
|
||||
data = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 1.1, 1]
|
||||
data = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1]
|
||||
table = []
|
||||
table.append(data[:4])
|
||||
table.append(data[4:8])
|
||||
table.append(data[8:12])
|
||||
pyexcel.save_as(dest_file_name=file, array=table)
|
||||
pyexcel.save_as(array=table, dest_file_name=file)
|
||||
|
||||
|
||||
class PyexcelHatWriterBase:
|
||||
"""
|
||||
Abstract functional test for hat writers
|
||||
"""
|
||||
|
||||
content = {
|
||||
"X": [1, 2, 3, 4, 5],
|
||||
"Y": [6, 7, 8, 9, 10],
|
||||
"Z": [11, 12, 13, 14, 15]
|
||||
"Z": [11, 12, 13, 14, 15],
|
||||
}
|
||||
|
||||
def test_series_table(self):
|
||||
pyexcel.save_as(adict=self.content, dest_file_name=self.testfile)
|
||||
r = pyexcel.get_sheet(file_name=self.testfile, name_columns_by_row=0)
|
||||
actual = pyexcel.utils.to_dict(r)
|
||||
assert actual == self.content
|
||||
eq_(r.dict, self.content)
|
||||
|
||||
|
||||
class PyexcelWriterBase:
|
||||
|
@ -35,11 +39,12 @@ class PyexcelWriterBase:
|
|||
testfile and testfile2 have to be initialized before
|
||||
it is used for testing
|
||||
"""
|
||||
|
||||
content = [
|
||||
[1, 2, 3, 4, 5],
|
||||
[1, 2, 3, 4, 5],
|
||||
[1, 2, 3, 4, 5],
|
||||
[1, 2, 3, 4, 5]
|
||||
[1, 2, 3, 4, 5],
|
||||
]
|
||||
|
||||
def _create_a_file(self, file):
|
||||
|
@ -48,29 +53,13 @@ class PyexcelWriterBase:
|
|||
def test_write_array(self):
|
||||
self._create_a_file(self.testfile)
|
||||
r = pyexcel.get_sheet(file_name=self.testfile)
|
||||
actual = pyexcel.utils.to_array(r.rows())
|
||||
assert actual == self.content
|
||||
|
||||
def test_write_reader(self):
|
||||
"""
|
||||
Use reader as data container
|
||||
|
||||
this test case shows the file written by pyexcel
|
||||
can be read back by itself
|
||||
"""
|
||||
self._create_a_file(self.testfile)
|
||||
r = pyexcel.Reader(self.testfile)
|
||||
r.save_as(self.testfile2)
|
||||
r2 = pyexcel.Reader(self.testfile2)
|
||||
r2.format(int)
|
||||
actual = pyexcel.utils.to_array(r2.rows())
|
||||
actual = list(r.rows())
|
||||
assert actual == self.content
|
||||
|
||||
|
||||
class PyexcelMultipleSheetBase:
|
||||
|
||||
def _write_test_file(self, filename):
|
||||
pyexcel.save_book_as(dest_file_name=filename, bookdict=self.content)
|
||||
pyexcel.save_book_as(bookdict=self.content, dest_file_name=filename)
|
||||
|
||||
def _clean_up(self):
|
||||
if os.path.exists(self.testfile2):
|
||||
|
@ -87,36 +76,17 @@ class PyexcelMultipleSheetBase:
|
|||
|
||||
def test_reading_through_sheets(self):
|
||||
b = pyexcel.BookReader(self.testfile)
|
||||
data = pyexcel.utils.to_array(b["Sheet1"].rows())
|
||||
data = list(b["Sheet1"].rows())
|
||||
expected = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]
|
||||
assert data == expected
|
||||
data = pyexcel.utils.to_array(b["Sheet2"].rows())
|
||||
data = list(b["Sheet2"].rows())
|
||||
expected = [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]]
|
||||
assert data == expected
|
||||
data = pyexcel.utils.to_array(b["Sheet3"].rows())
|
||||
expected = [[u'X', u'Y', u'Z'], [1, 4, 7], [2, 5, 8], [3, 6, 9]]
|
||||
data = list(b["Sheet3"].rows())
|
||||
expected = [[u"X", u"Y", u"Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]]
|
||||
assert data == expected
|
||||
sheet3 = b["Sheet3"]
|
||||
sheet3.name_columns_by_row(0)
|
||||
data = pyexcel.utils.to_array(b["Sheet3"].rows())
|
||||
data = list(b["Sheet3"].rows())
|
||||
expected = [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
|
||||
assert data == expected
|
||||
|
||||
def test_iterate_through_sheets(self):
|
||||
b = pyexcel.BookReader(self.testfile)
|
||||
for s in b:
|
||||
data = pyexcel.utils.to_array(s)
|
||||
assert self.content[s.name] == data
|
||||
si = pyexcel.iterators.SheetIterator(b)
|
||||
for s in si:
|
||||
data = pyexcel.utils.to_array(s)
|
||||
assert self.content[s.name] == data
|
||||
|
||||
def test_random_access_operator(self):
|
||||
r = pyexcel.BookReader(self.testfile)
|
||||
value = r["Sheet1"][0, 1]
|
||||
assert value == 1
|
||||
value = r["Sheet3"][0, 1]
|
||||
assert value == 'Y'
|
||||
r["Sheet3"].name_columns_by_row(0)
|
||||
assert r["Sheet3"][0, 1] == 4
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,5 +1,12 @@
|
|||
nose
|
||||
mock;python_version<"3"
|
||||
codecov
|
||||
coverage
|
||||
flake8
|
||||
black
|
||||
isort
|
||||
collective.checkdocs
|
||||
pygments
|
||||
moban
|
||||
moban_jinja2_github
|
||||
pyexcel
|
||||
|
|
|
@ -5,43 +5,122 @@
|
|||
"""
|
||||
|
||||
import os
|
||||
import pyexcel as pe
|
||||
from pyexcel_xls import save_data
|
||||
from _compact import OrderedDict
|
||||
from nose.tools import eq_
|
||||
import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pyexcel as pe
|
||||
from _compact import OrderedDict
|
||||
from pyexcel_xls import XLRD_VERSION_2_OR_ABOVE, save_data
|
||||
from pyexcel_xls.xlsr import xldate_to_python_date
|
||||
from pyexcel_xls.xlsw import XLSWriter as Writer
|
||||
|
||||
from nose import SkipTest
|
||||
from nose.tools import eq_, raises
|
||||
|
||||
IN_TRAVIS = "TRAVIS" in os.environ
|
||||
|
||||
|
||||
class TestBugFix:
|
||||
def test_pyexcel_issue_5(self):
|
||||
"""pyexcel issue #5
|
||||
def test_pyexcel_issue_5():
|
||||
"""pyexcel issue #5
|
||||
|
||||
datetime is not properly parsed
|
||||
"""
|
||||
s = pe.load(os.path.join("tests",
|
||||
"test-fixtures",
|
||||
"test-date-format.xls"))
|
||||
assert s[0, 0] == datetime.datetime(2015, 11, 11, 11, 12, 0)
|
||||
datetime is not properly parsed
|
||||
"""
|
||||
s = pe.load(get_fixture("test-date-format.xls"))
|
||||
assert s[0, 0] == datetime.datetime(2015, 11, 11, 11, 12, 0)
|
||||
|
||||
def test_pyexcel_xls_issue_2(self):
|
||||
data = OrderedDict()
|
||||
array = []
|
||||
for i in range(4100):
|
||||
array.append([datetime.datetime.now()])
|
||||
data.update({"test": array})
|
||||
save_data("test.xls", data)
|
||||
os.unlink("test.xls")
|
||||
|
||||
def test_issue_9_hidden_sheet(self):
|
||||
test_file = os.path.join("tests", "fixtures", "hidden_sheets.xls")
|
||||
book_dict = pe.get_book_dict(file_name=test_file)
|
||||
assert "hidden" not in book_dict
|
||||
eq_(book_dict['shown'], [['A', 'B']])
|
||||
def test_pyexcel_xls_issue_2():
|
||||
data = OrderedDict()
|
||||
array = []
|
||||
for i in range(4100):
|
||||
array.append([datetime.datetime.now()])
|
||||
data.update({"test": array})
|
||||
save_data("test.xls", data)
|
||||
os.unlink("test.xls")
|
||||
|
||||
def test_issue_9_hidden_sheet_2(self):
|
||||
test_file = os.path.join("tests", "fixtures", "hidden_sheets.xls")
|
||||
book_dict = pe.get_book_dict(file_name=test_file,
|
||||
skip_hidden_sheets=False)
|
||||
assert "hidden" in book_dict
|
||||
eq_(book_dict['shown'], [['A', 'B']])
|
||||
eq_(book_dict['hidden'], [['a', 'b']])
|
||||
|
||||
def test_issue_9_hidden_sheet():
|
||||
test_file = get_fixture("hidden_sheets.xls")
|
||||
book_dict = pe.get_book_dict(file_name=test_file)
|
||||
assert "hidden" not in book_dict
|
||||
eq_(book_dict["shown"], [["A", "B"]])
|
||||
|
||||
|
||||
def test_issue_9_hidden_sheet_2():
|
||||
test_file = get_fixture("hidden_sheets.xls")
|
||||
book_dict = pe.get_book_dict(file_name=test_file, skip_hidden_sheets=False)
|
||||
assert "hidden" in book_dict
|
||||
eq_(book_dict["shown"], [["A", "B"]])
|
||||
eq_(book_dict["hidden"], [["a", "b"]])
|
||||
|
||||
|
||||
def test_issue_10_generator_as_content():
|
||||
def data_gen():
|
||||
def custom_row_renderer(row):
|
||||
for e in row:
|
||||
yield e
|
||||
|
||||
for i in range(2):
|
||||
yield custom_row_renderer([1, 2])
|
||||
|
||||
save_data("test.xls", {"sheet": data_gen()})
|
||||
|
||||
|
||||
@raises(IOError)
|
||||
def test_issue_13_empty_file_content():
|
||||
pe.get_sheet(file_content="", file_type="xls")
|
||||
|
||||
|
||||
def test_issue_16_file_stream_has_no_getvalue():
|
||||
test_file = get_fixture("hidden_sheets.xls")
|
||||
with open(test_file, "rb") as f:
|
||||
pe.get_sheet(file_stream=f, file_type="xls")
|
||||
|
||||
|
||||
@patch("xlrd.open_workbook")
|
||||
def test_issue_18_encoding_override_isnt_passed(fake_open):
|
||||
fake_open.return_value = MagicMock(sheets=MagicMock(return_value=[]))
|
||||
test_encoding = "utf-32"
|
||||
from pyexcel_xls.xlsr import XLSInFile
|
||||
|
||||
XLSInFile("fake_file.xls", "xls", encoding_override=test_encoding)
|
||||
keywords = fake_open.call_args[1]
|
||||
assert keywords["encoding_override"] == test_encoding
|
||||
|
||||
|
||||
def test_issue_20():
|
||||
if not IN_TRAVIS:
|
||||
raise SkipTest()
|
||||
pe.get_book(
|
||||
url="https://github.com/pyexcel/pyexcel-xls/raw/master/tests/fixtures/file_with_an_empty_sheet.xls" # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
def test_issue_151():
|
||||
if XLRD_VERSION_2_OR_ABOVE:
|
||||
raise SkipTest()
|
||||
s = pe.get_sheet(
|
||||
file_name=get_fixture("pyexcel_issue_151.xlsx"),
|
||||
skip_hidden_row_and_column=False,
|
||||
library="pyexcel-xls",
|
||||
)
|
||||
eq_("#N/A", s[0, 0])
|
||||
|
||||
|
||||
@raises(NotImplementedError)
|
||||
def test_empty_book_pyexcel_issue_120():
|
||||
"""
|
||||
https://github.com/pyexcel/pyexcel/issues/120
|
||||
"""
|
||||
writer = Writer("fake.xls", "xls")
|
||||
writer.write({})
|
||||
|
||||
|
||||
def test_pyexcel_issue_54():
|
||||
xlvalue = 41071.0
|
||||
date = xldate_to_python_date(xlvalue, 1)
|
||||
eq_(date, datetime.date(2016, 6, 12))
|
||||
|
||||
|
||||
def get_fixture(file_name):
|
||||
return os.path.join("tests", "fixtures", file_name)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
|
||||
from pyexcel_io import get_data, save_data
|
||||
|
||||
from nose.tools import eq_
|
||||
|
||||
|
||||
|
@ -13,49 +14,58 @@ class TestFilter:
|
|||
[3, 23, 33],
|
||||
[4, 24, 34],
|
||||
[5, 25, 35],
|
||||
[6, 26, 36]
|
||||
[6, 26, 36],
|
||||
]
|
||||
save_data(self.test_file, sample)
|
||||
self.sheet_name = "pyexcel_sheet1"
|
||||
|
||||
def test_filter_row(self):
|
||||
filtered_data = get_data(self.test_file, start_row=3,
|
||||
library="pyexcel-xls")
|
||||
filtered_data = get_data(
|
||||
self.test_file, start_row=3, library="pyexcel-xls"
|
||||
)
|
||||
expected = [[4, 24, 34], [5, 25, 35], [6, 26, 36]]
|
||||
eq_(filtered_data[self.sheet_name], expected)
|
||||
|
||||
def test_filter_row_2(self):
|
||||
filtered_data = get_data(self.test_file, start_row=3, row_limit=1,
|
||||
library="pyexcel-xls")
|
||||
filtered_data = get_data(
|
||||
self.test_file, start_row=3, row_limit=1, library="pyexcel-xls"
|
||||
)
|
||||
expected = [[4, 24, 34]]
|
||||
eq_(filtered_data[self.sheet_name], expected)
|
||||
|
||||
def test_filter_column(self):
|
||||
filtered_data = get_data(self.test_file, start_column=1,
|
||||
library="pyexcel-xls")
|
||||
expected = [[21, 31], [22, 32], [23, 33],
|
||||
[24, 34], [25, 35], [26, 36]]
|
||||
filtered_data = get_data(
|
||||
self.test_file, start_column=1, library="pyexcel-xls"
|
||||
)
|
||||
expected = [[21, 31], [22, 32], [23, 33], [24, 34], [25, 35], [26, 36]]
|
||||
eq_(filtered_data[self.sheet_name], expected)
|
||||
|
||||
def test_filter_column_2(self):
|
||||
filtered_data = get_data(self.test_file,
|
||||
start_column=1, column_limit=1,
|
||||
library="pyexcel-xls")
|
||||
filtered_data = get_data(
|
||||
self.test_file,
|
||||
start_column=1,
|
||||
column_limit=1,
|
||||
library="pyexcel-xls",
|
||||
)
|
||||
expected = [[21], [22], [23], [24], [25], [26]]
|
||||
eq_(filtered_data[self.sheet_name], expected)
|
||||
|
||||
def test_filter_both_ways(self):
|
||||
filtered_data = get_data(self.test_file,
|
||||
start_column=1, start_row=3,
|
||||
library="pyexcel-xls")
|
||||
filtered_data = get_data(
|
||||
self.test_file, start_column=1, start_row=3, library="pyexcel-xls"
|
||||
)
|
||||
expected = [[24, 34], [25, 35], [26, 36]]
|
||||
eq_(filtered_data[self.sheet_name], expected)
|
||||
|
||||
def test_filter_both_ways_2(self):
|
||||
filtered_data = get_data(self.test_file,
|
||||
start_column=1, column_limit=1,
|
||||
start_row=3, row_limit=1,
|
||||
library="pyexcel-xls")
|
||||
filtered_data = get_data(
|
||||
self.test_file,
|
||||
start_column=1,
|
||||
column_limit=1,
|
||||
start_row=3,
|
||||
row_limit=1,
|
||||
library="pyexcel-xls",
|
||||
)
|
||||
expected = [[24]]
|
||||
eq_(filtered_data[self.sheet_name], expected)
|
||||
|
||||
|
|
|
@ -1,86 +1,123 @@
|
|||
import os
|
||||
import datetime
|
||||
from unittest import TestCase
|
||||
from textwrap import dedent
|
||||
|
||||
import pyexcel as pe
|
||||
|
||||
from nose.tools import eq_
|
||||
|
||||
|
||||
class TestDateFormat:
|
||||
def test_reading_date_format(self):
|
||||
"""
|
||||
date time
|
||||
25/12/14 11:11:11
|
||||
25/12/14 12:11:11
|
||||
25/12/14 12:12:12
|
||||
01/01/15 13:13:13
|
||||
0.0 0.0
|
||||
"""
|
||||
r = pe.get_sheet(file_name=os.path.join("tests", "fixtures",
|
||||
"date_field.xls"))
|
||||
assert isinstance(r[1, 0], datetime.date) is True
|
||||
assert r[1, 0].strftime("%d/%m/%y") == "25/12/14"
|
||||
import datetime
|
||||
|
||||
r = pe.get_sheet(
|
||||
file_name=os.path.join("tests", "fixtures", "date_field.xls"),
|
||||
library="pyexcel-xls",
|
||||
)
|
||||
assert isinstance(r[1, 0], datetime.date)
|
||||
eq_(r[1, 0].strftime("%d/%m/%y"), "25/12/14")
|
||||
assert isinstance(r[1, 1], datetime.time) is True
|
||||
assert r[1, 1].strftime("%H:%M:%S") == "11:11:11"
|
||||
assert r[4, 0].strftime("%d/%m/%Y") == "01/01/1900"
|
||||
assert r[4, 1].strftime("%H:%M:%S") == "00:00:00"
|
||||
|
||||
def test_writing_date_format(self):
|
||||
import datetime
|
||||
|
||||
excel_filename = "testdateformat.xls"
|
||||
data = [[datetime.date(2014, 12, 25),
|
||||
data = [
|
||||
[
|
||||
datetime.date(2014, 12, 25),
|
||||
datetime.time(11, 11, 11),
|
||||
datetime.datetime(2014, 12, 25, 11, 11, 11)]]
|
||||
datetime.datetime(2014, 12, 25, 11, 11, 11),
|
||||
datetime.timedelta(
|
||||
days=50,
|
||||
seconds=27,
|
||||
microseconds=10,
|
||||
milliseconds=29000,
|
||||
minutes=5,
|
||||
hours=8,
|
||||
weeks=2,
|
||||
),
|
||||
]
|
||||
]
|
||||
pe.save_as(dest_file_name=excel_filename, array=data)
|
||||
r = pe.get_sheet(file_name=excel_filename)
|
||||
r = pe.get_sheet(file_name=excel_filename, library="pyexcel-xls")
|
||||
assert isinstance(r[0, 0], datetime.date) is True
|
||||
assert r[0, 0].strftime("%d/%m/%y") == "25/12/14"
|
||||
assert isinstance(r[0, 1], datetime.time) is True
|
||||
assert r[0, 1].strftime("%H:%M:%S") == "11:11:11"
|
||||
assert isinstance(r[0, 2], datetime.date) is True
|
||||
assert r[0, 2].strftime("%d/%m/%y %H:%M:%S") == "25/12/14 11:11:11"
|
||||
assert isinstance(r[0, 3], datetime.datetime)
|
||||
assert r[0, 3].strftime("%D-%H:%M:%S") == "03/04/00-08:05:56"
|
||||
os.unlink(excel_filename)
|
||||
|
||||
|
||||
class TestAutoDetectInt(TestCase):
|
||||
class TestAutoDetectInt:
|
||||
def setUp(self):
|
||||
self.content = [[1, 2, 3.1]]
|
||||
self.test_file = "test_auto_detect_init.xls"
|
||||
pe.save_as(array=self.content, dest_file_name=self.test_file)
|
||||
|
||||
def test_auto_detect_int(self):
|
||||
sheet = pe.get_sheet(file_name=self.test_file)
|
||||
expected = dedent("""
|
||||
sheet = pe.get_sheet(file_name=self.test_file, library="pyexcel-xls")
|
||||
expected = dedent(
|
||||
"""
|
||||
pyexcel_sheet1:
|
||||
+---+---+-----+
|
||||
| 1 | 2 | 3.1 |
|
||||
+---+---+-----+""").strip()
|
||||
self.assertEqual(str(sheet), expected)
|
||||
+---+---+-----+"""
|
||||
).strip()
|
||||
eq_(str(sheet), expected)
|
||||
|
||||
def test_get_book_auto_detect_int(self):
|
||||
book = pe.get_book(file_name=self.test_file)
|
||||
expected = dedent("""
|
||||
book = pe.get_book(file_name=self.test_file, library="pyexcel-xls")
|
||||
expected = dedent(
|
||||
"""
|
||||
pyexcel_sheet1:
|
||||
+---+---+-----+
|
||||
| 1 | 2 | 3.1 |
|
||||
+---+---+-----+""").strip()
|
||||
self.assertEqual(str(book), expected)
|
||||
+---+---+-----+"""
|
||||
).strip()
|
||||
eq_(str(book), expected)
|
||||
|
||||
def test_auto_detect_int_false(self):
|
||||
sheet = pe.get_sheet(file_name=self.test_file, auto_detect_int=False)
|
||||
expected = dedent("""
|
||||
sheet = pe.get_sheet(
|
||||
file_name=self.test_file,
|
||||
auto_detect_int=False,
|
||||
library="pyexcel-xls",
|
||||
)
|
||||
expected = dedent(
|
||||
"""
|
||||
pyexcel_sheet1:
|
||||
+-----+-----+-----+
|
||||
| 1.0 | 2.0 | 3.1 |
|
||||
+-----+-----+-----+""").strip()
|
||||
self.assertEqual(str(sheet), expected)
|
||||
+-----+-----+-----+"""
|
||||
).strip()
|
||||
eq_(str(sheet), expected)
|
||||
|
||||
def test_get_book_auto_detect_int_false(self):
|
||||
book = pe.get_book(file_name=self.test_file, auto_detect_int=False)
|
||||
expected = dedent("""
|
||||
book = pe.get_book(
|
||||
file_name=self.test_file,
|
||||
auto_detect_int=False,
|
||||
library="pyexcel-xls",
|
||||
)
|
||||
expected = dedent(
|
||||
"""
|
||||
pyexcel_sheet1:
|
||||
+-----+-----+-----+
|
||||
| 1.0 | 2.0 | 3.1 |
|
||||
+-----+-----+-----+""").strip()
|
||||
self.assertEqual(str(book), expected)
|
||||
+-----+-----+-----+"""
|
||||
).strip()
|
||||
eq_(str(book), expected)
|
||||
|
||||
def tearDown(self):
|
||||
os.unlink(self.test_file)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import os
|
||||
|
||||
from pyexcel_xls import get_data
|
||||
|
||||
from nose.tools import eq_
|
||||
|
||||
|
||||
def test_simple_hidden_sheets():
|
||||
data = get_data(
|
||||
os.path.join("tests", "fixtures", "hidden.xls"),
|
||||
skip_hidden_row_and_column=True,
|
||||
)
|
||||
expected = [[1, 3], [7, 9]]
|
||||
eq_(data["Sheet1"], expected)
|
||||
|
||||
|
||||
def test_complex_hidden_sheets():
|
||||
data = get_data(
|
||||
os.path.join("tests", "fixtures", "complex_hidden_sheets.xls"),
|
||||
skip_hidden_row_and_column=True,
|
||||
)
|
||||
expected = [[1, 3, 5, 7, 9], [31, 33, 35, 37, 39], [61, 63, 65, 67]]
|
||||
eq_(data["Sheet1"], expected)
|
|
@ -0,0 +1,84 @@
|
|||
import os
|
||||
|
||||
from pyexcel_xls import get_data
|
||||
from pyexcel_xls.xlsr import MergedCell
|
||||
|
||||
from nose.tools import eq_
|
||||
|
||||
|
||||
def test_merged_cells():
|
||||
data = get_data(
|
||||
get_fixture("merged-cell-sheet.xls"),
|
||||
detect_merged_cells=True,
|
||||
library="pyexcel-xls",
|
||||
)
|
||||
expected = [[1, 2, 3], [1, 5, 6], [1, 8, 9], [10, 11, 11]]
|
||||
eq_(data["Sheet1"], expected)
|
||||
|
||||
|
||||
def test_complex_merged_cells():
|
||||
data = get_data(
|
||||
get_fixture("complex-merged-cells-sheet.xls"),
|
||||
detect_merged_cells=True,
|
||||
library="pyexcel-xls",
|
||||
)
|
||||
expected = [
|
||||
[1, 1, 2, 3, 15, 16, 22, 22, 24, 24],
|
||||
[1, 1, 4, 5, 15, 17, 22, 22, 24, 24],
|
||||
[6, 7, 8, 9, 15, 18, 22, 22, 24, 24],
|
||||
[10, 11, 11, 12, 19, 19, 23, 23, 24, 24],
|
||||
[13, 11, 11, 14, 20, 20, 23, 23, 24, 24],
|
||||
[21, 21, 21, 21, 21, 21, 23, 23, 24, 24],
|
||||
[25, 25, 25, 25, 25, 25, 25, 25, 25, 25],
|
||||
[25, 25, 25, 25, 25, 25, 25, 25, 25, 25],
|
||||
]
|
||||
eq_(data["Sheet1"], expected)
|
||||
|
||||
|
||||
def test_exploration():
|
||||
data = get_data(
|
||||
get_fixture("merged-sheet-exploration.xls"),
|
||||
detect_merged_cells=True,
|
||||
library="pyexcel-xls",
|
||||
)
|
||||
expected_sheet1 = [
|
||||
[1, 1, 1, 1, 1, 1],
|
||||
[2],
|
||||
[2],
|
||||
[2],
|
||||
[2],
|
||||
[2],
|
||||
[2],
|
||||
[2],
|
||||
[2],
|
||||
[2],
|
||||
]
|
||||
eq_(data["Sheet1"], expected_sheet1)
|
||||
expected_sheet2 = [[3], [3], [3], [3, 4, 4, 4, 4, 4, 4], [3], [3], [3]]
|
||||
eq_(data["Sheet2"], expected_sheet2)
|
||||
expected_sheet3 = [
|
||||
["", "", "", "", "", 2, 2, 2],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
["", "", "", 5],
|
||||
["", "", "", 5],
|
||||
["", "", "", 5],
|
||||
["", "", "", 5],
|
||||
["", "", "", 5],
|
||||
]
|
||||
eq_(data["Sheet3"], expected_sheet3)
|
||||
|
||||
|
||||
def test_merged_cell_class():
|
||||
test_dict = {}
|
||||
merged_cell = MergedCell(1, 4, 1, 4)
|
||||
merged_cell.register_cells(test_dict)
|
||||
keys = sorted(list(test_dict.keys()))
|
||||
expected = ["1-1", "1-2", "1-3", "2-1", "2-2", "2-3", "3-1", "3-2", "3-3"]
|
||||
eq_(keys, expected)
|
||||
eq_(merged_cell, test_dict["3-1"])
|
||||
|
||||
|
||||
def get_fixture(file_name):
|
||||
return os.path.join("tests", "fixtures", file_name)
|
|
@ -1,9 +1,11 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
import pyexcel
|
||||
from nose.tools import raises
|
||||
from base import PyexcelMultipleSheetBase
|
||||
|
||||
from nose.tools import raises
|
||||
|
||||
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||
from ordereddict import OrderedDict
|
||||
else:
|
||||
|
@ -31,8 +33,7 @@ class TestAddBooks:
|
|||
3,3,3,3
|
||||
"""
|
||||
self.rows = 3
|
||||
pyexcel.save_book_as(bookdict=self.content,
|
||||
dest_file_name=file)
|
||||
pyexcel.save_book_as(bookdict=self.content, dest_file_name=file)
|
||||
|
||||
def setUp(self):
|
||||
self.testfile = "multiple1.xls"
|
||||
|
@ -45,12 +46,12 @@ class TestAddBooks:
|
|||
def test_load_a_single_sheet(self):
|
||||
b1 = pyexcel.get_book(file_name=self.testfile, sheet_name="Sheet1")
|
||||
assert len(b1.sheet_names()) == 1
|
||||
assert b1['Sheet1'].to_array() == self.content['Sheet1']
|
||||
assert b1["Sheet1"].to_array() == self.content["Sheet1"]
|
||||
|
||||
def test_load_a_single_sheet2(self):
|
||||
b1 = pyexcel.load_book(self.testfile, sheet_index=0)
|
||||
assert len(b1.sheet_names()) == 1
|
||||
assert b1['Sheet1'].to_array() == self.content['Sheet1']
|
||||
assert b1["Sheet1"].to_array() == self.content["Sheet1"]
|
||||
|
||||
@raises(IndexError)
|
||||
def test_load_a_single_sheet3(self):
|
||||
|
@ -92,10 +93,10 @@ class TestAddBooks:
|
|||
"""
|
||||
test this scenario: book3 = book1 + book2
|
||||
"""
|
||||
b1 = pyexcel.BookReader(self.testfile)
|
||||
b2 = pyexcel.BookReader(self.testfile2)
|
||||
b1 = pyexcel.get_book(file_name=self.testfile)
|
||||
b2 = pyexcel.get_book(file_name=self.testfile2)
|
||||
b3 = b1 + b2
|
||||
content = pyexcel.utils.to_dict(b3)
|
||||
content = b3.dict
|
||||
sheet_names = content.keys()
|
||||
assert len(sheet_names) == 6
|
||||
for name in sheet_names:
|
||||
|
@ -108,12 +109,12 @@ class TestAddBooks:
|
|||
|
||||
def test_add_book1_in_place(self):
|
||||
"""
|
||||
test this scenario book1 += book2
|
||||
test this scenario: book1 += book2
|
||||
"""
|
||||
b1 = pyexcel.BookReader(self.testfile)
|
||||
b2 = pyexcel.BookReader(self.testfile2)
|
||||
b1 += b2
|
||||
content = pyexcel.utils.to_dict(b1)
|
||||
content = b1.dict
|
||||
sheet_names = content.keys()
|
||||
assert len(sheet_names) == 6
|
||||
for name in sheet_names:
|
||||
|
@ -126,12 +127,12 @@ class TestAddBooks:
|
|||
|
||||
def test_add_book2(self):
|
||||
"""
|
||||
test this scenario book3 = book1 + sheet3
|
||||
test this scenario: book3 = book1 + sheet3
|
||||
"""
|
||||
b1 = pyexcel.BookReader(self.testfile)
|
||||
b2 = pyexcel.BookReader(self.testfile2)
|
||||
b3 = b1 + b2["Sheet3"]
|
||||
content = pyexcel.utils.to_dict(b3)
|
||||
content = b3.dict
|
||||
sheet_names = content.keys()
|
||||
assert len(sheet_names) == 4
|
||||
for name in sheet_names:
|
||||
|
@ -144,12 +145,12 @@ class TestAddBooks:
|
|||
|
||||
def test_add_book2_in_place(self):
|
||||
"""
|
||||
test this scenario book3 = book1 + sheet3
|
||||
test this scenario: book3 = book1 + sheet3
|
||||
"""
|
||||
b1 = pyexcel.BookReader(self.testfile)
|
||||
b2 = pyexcel.BookReader(self.testfile2)
|
||||
b1 += b2["Sheet3"]
|
||||
content = pyexcel.utils.to_dict(b1)
|
||||
content = b1.dict
|
||||
sheet_names = content.keys()
|
||||
assert len(sheet_names) == 4
|
||||
for name in sheet_names:
|
||||
|
@ -162,12 +163,12 @@ class TestAddBooks:
|
|||
|
||||
def test_add_book3(self):
|
||||
"""
|
||||
test this scenario book3 = sheet1 + sheet2
|
||||
test this scenario: book3 = sheet1 + sheet2
|
||||
"""
|
||||
b1 = pyexcel.BookReader(self.testfile)
|
||||
b2 = pyexcel.BookReader(self.testfile2)
|
||||
b3 = b1["Sheet1"] + b2["Sheet3"]
|
||||
content = pyexcel.utils.to_dict(b3)
|
||||
content = b3.dict
|
||||
sheet_names = content.keys()
|
||||
assert len(sheet_names) == 2
|
||||
assert content["Sheet3"] == self.content["Sheet3"]
|
||||
|
@ -175,12 +176,12 @@ class TestAddBooks:
|
|||
|
||||
def test_add_book4(self):
|
||||
"""
|
||||
test this scenario book3 = sheet1 + book
|
||||
test this scenario: book3 = sheet1 + book
|
||||
"""
|
||||
b1 = pyexcel.BookReader(self.testfile)
|
||||
b2 = pyexcel.BookReader(self.testfile2)
|
||||
b3 = b1["Sheet1"] + b2
|
||||
content = pyexcel.utils.to_dict(b3)
|
||||
content = b3.dict
|
||||
sheet_names = content.keys()
|
||||
assert len(sheet_names) == 4
|
||||
for name in sheet_names:
|
||||
|
@ -219,18 +220,17 @@ class TestMultiSheetReader:
|
|||
self.testfile = "file_with_an_empty_sheet.xls"
|
||||
|
||||
def test_reader_with_correct_sheets(self):
|
||||
r = pyexcel.BookReader(os.path.join("tests",
|
||||
"fixtures",
|
||||
self.testfile))
|
||||
r = pyexcel.BookReader(
|
||||
os.path.join("tests", "fixtures", self.testfile)
|
||||
)
|
||||
assert r.number_of_sheets() == 3
|
||||
|
||||
|
||||
def _produce_ordered_dict():
|
||||
data_dict = OrderedDict()
|
||||
data_dict.update({
|
||||
"Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]})
|
||||
data_dict.update({
|
||||
"Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]]})
|
||||
data_dict.update({
|
||||
"Sheet3": [[u'X', u'Y', u'Z'], [1, 4, 7], [2, 5, 8], [3, 6, 9]]})
|
||||
data_dict.update({"Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]})
|
||||
data_dict.update({"Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]]})
|
||||
data_dict.update(
|
||||
{"Sheet3": [[u"X", u"Y", u"Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]]}
|
||||
)
|
||||
return data_dict
|
|
@ -1,30 +1,32 @@
|
|||
import os
|
||||
|
||||
import pyexcel
|
||||
from base import create_sample_file1
|
||||
|
||||
from nose.tools import eq_
|
||||
|
||||
|
||||
class TestStringIO:
|
||||
|
||||
def test_xls_stringio(self):
|
||||
xlsfile = "cute.xls"
|
||||
create_sample_file1(xlsfile)
|
||||
with open(xlsfile, "rb") as f:
|
||||
testfile = "cute.xls"
|
||||
create_sample_file1(testfile)
|
||||
with open(testfile, "rb") as f:
|
||||
content = f.read()
|
||||
r = pyexcel.get_sheet(file_type="xls", file_content=content)
|
||||
result = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 1.1, 1]
|
||||
actual = pyexcel.utils.to_array(r.enumerate())
|
||||
assert result == actual
|
||||
if os.path.exists(xlsfile):
|
||||
os.unlink(xlsfile)
|
||||
r = pyexcel.get_sheet(
|
||||
file_type="xls", file_content=content, library="pyexcel-xls"
|
||||
)
|
||||
result = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1]
|
||||
actual = list(r.enumerate())
|
||||
eq_(result, actual)
|
||||
if os.path.exists(testfile):
|
||||
os.unlink(testfile)
|
||||
|
||||
def test_xls_output_stringio(self):
|
||||
data = [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6]
|
||||
]
|
||||
io = pyexcel.save_as(dest_file_type="xls",
|
||||
array=data)
|
||||
r = pyexcel.get_sheet(file_type="xls", file_content=io.getvalue())
|
||||
data = [[1, 2, 3], [4, 5, 6]]
|
||||
io = pyexcel.save_as(dest_file_type="xls", array=data)
|
||||
r = pyexcel.get_sheet(
|
||||
file_type="xls", file_content=io.getvalue(), library="pyexcel-xls"
|
||||
)
|
||||
result = [1, 2, 3, 4, 5, 6]
|
||||
actual = pyexcel.utils.to_array(r.enumerate())
|
||||
assert result == actual
|
||||
actual = list(r.enumerate())
|
||||
eq_(result, actual)
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
import os
|
||||
from pyexcel_xls.xls import XLSWriter, XLSBook
|
||||
|
||||
from base import PyexcelWriterBase, PyexcelHatWriterBase
|
||||
from pyexcel_xls import get_data
|
||||
from pyexcel_xls.xlsw import XLSWriter as Writer
|
||||
|
||||
|
||||
class TestNativeXLWriter:
|
||||
class TestNativeXLSWriter:
|
||||
def test_write_book(self):
|
||||
self.content = {
|
||||
"Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]],
|
||||
"Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]],
|
||||
"Sheet3": [[u'X', u'Y', u'Z'], [1, 4, 7], [2, 5, 8], [3, 6, 9]]
|
||||
"Sheet3": [[u"X", u"Y", u"Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]],
|
||||
}
|
||||
self.testfile = "xlwriter.xls"
|
||||
writer = XLSWriter()
|
||||
writer.open(self.testfile)
|
||||
self.testfile = "writer.xls"
|
||||
writer = Writer(self.testfile, "xls")
|
||||
writer.write(self.content)
|
||||
writer.close()
|
||||
reader = XLSBook()
|
||||
reader.open(self.testfile)
|
||||
content = reader.read_all()
|
||||
reader.close()
|
||||
content = get_data(self.testfile)
|
||||
for key in content.keys():
|
||||
content[key] = list(content[key])
|
||||
assert content == self.content
|
||||
|
@ -28,7 +26,7 @@ class TestNativeXLWriter:
|
|||
os.unlink(self.testfile)
|
||||
|
||||
|
||||
class TestXLSnCSVWriter(PyexcelWriterBase):
|
||||
class TestxlsnCSVWriter(PyexcelWriterBase):
|
||||
def setUp(self):
|
||||
self.testfile = "test.xls"
|
||||
self.testfile2 = "test.csv"
|
||||
|
@ -40,7 +38,7 @@ class TestXLSnCSVWriter(PyexcelWriterBase):
|
|||
os.unlink(self.testfile2)
|
||||
|
||||
|
||||
class TestXLSHatWriter(PyexcelHatWriterBase):
|
||||
class TestxlsHatWriter(PyexcelHatWriterBase):
|
||||
def setUp(self):
|
||||
self.testfile = "test.xls"
|
||||
|
||||
|
|
Loading…
Reference in New Issue