From e03d79f8d54e2b328d94edcf4b2252a3ea809352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Fri, 24 Aug 2018 18:19:28 -0300 Subject: [PATCH 01/13] add .gitignore Source: https://github.com/fyndata/gcp-utils-python/blob/6e54c24/.gitignore --- .gitignore | 273 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6d44c92d --- /dev/null +++ b/.gitignore @@ -0,0 +1,273 @@ +### Python template +# 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/ +*.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/ +coverage +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +# nyc test coverage +.nyc_output +# custom directories +test-reports + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +staticfiles/ + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# SQLite DBs +*.db +*.sqlite3 + +# Sphinx documentation +docs/_build/ + +# mkdocs documentation +/site + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mypy +.mypy_cache/ + + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + + +### VirtualEnv template +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json + + +### Misc editors, IDES + +# JetBrain editors +.idea + +*~ +*.swp +*.swo + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + + +### Git ### +*.orig + + +### Linux template +*~ + +# 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 template +# Windows thumbnail cache files +Thumbs.db +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 template +# 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 + + +### VisualStudioCode template +.vscode + + +### SublimeText template +# 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 + +# 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 + + +### Vim template +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] + +# Session +Session.vim + +# Temporary +.netrwhist + +# Auto-generated tag files +tags From acbf383d31bf83937da72ba705fb65169746fa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Fri, 24 Aug 2018 18:01:05 -0300 Subject: [PATCH 02/13] add .editorconfig --- .editorconfig | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..b5e63dd1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,40 @@ +# editorconfig.org + +root = true + +[*] +max_line_length = 100 +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{ini,py,rst}] +indent_style = space +indent_size = 4 + +[*.py] +multi_line_output = 3 + +[*.{css,html,js,json,scss,xml,yml,yaml}] +indent_style = space +indent_size = 2 + +# minified JavaScript files should not be modified +[**.min.js] +indent_style = ignore +insert_final_newline = ignore + +[*.md] +trim_trailing_whitespace = false + +[*.{diff,patch}] +trim_trailing_whitespace = false + +[*.sh] +indent_style = tab + +[Makefile] +indent_style = tab From e9c08e39022ae956a3786379e9ce74410809f1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Mon, 27 Aug 2018 10:15:36 -0300 Subject: [PATCH 03/13] create Python dependencies structure Create the "requirements" file structure, with one file for each "run mode" (`dev`, `test`, `release`), a base one, and another one for the `extras` packages. --- requirements.txt | 3 +++ requirements/base.txt | 8 ++++++++ requirements/dev.txt | 8 ++++++++ requirements/extras.txt | 4 ++++ requirements/release.txt | 9 +++++++++ requirements/test.txt | 8 ++++++++ 6 files changed, 40 insertions(+) create mode 100644 requirements.txt create mode 100644 requirements/base.txt create mode 100644 requirements/dev.txt create mode 100644 requirements/extras.txt create mode 100644 requirements/release.txt create mode 100644 requirements/test.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..be8f58d7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# This file exists because many tools, services and platforms expect it to be +# in the root directory of a Python project. +-r requirements/base.txt diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 00000000..1601bfc4 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,8 @@ +# requirements common to all "run modes" +# note: it is mandatory to register all dependencies of the required packages. + +# Required packages: +#none + +# Packages dependencies: +#none diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 00000000..a9df4bed --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,8 @@ +# note: it is mandatory to register all dependencies of the required packages. +-r base.txt + +# Required packages: +#none + +# Packages dependencies: +#none diff --git a/requirements/extras.txt b/requirements/extras.txt new file mode 100644 index 00000000..7cb924e2 --- /dev/null +++ b/requirements/extras.txt @@ -0,0 +1,4 @@ +# note: it is NOT mandatory to register all dependencies of the required packages. + +# Required packages: +#none diff --git a/requirements/release.txt b/requirements/release.txt new file mode 100644 index 00000000..de3a5cbb --- /dev/null +++ b/requirements/release.txt @@ -0,0 +1,9 @@ +# note: it is mandatory to register all dependencies of the required packages. +# (pro tip: keep to the minimum the number of packages declared here) +-r base.txt + +# Required packages: +#none + +# Packages dependencies: +#none diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 00000000..a9df4bed --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,8 @@ +# note: it is mandatory to register all dependencies of the required packages. +-r base.txt + +# Required packages: +#none + +# Packages dependencies: +#none From dae17bc0b62268896a708c7ac6d3d6f89df1cfca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 5 Dec 2018 12:39:59 -0300 Subject: [PATCH 04/13] requirements: add release packages Source: https://github.com/fyndata/gcp-utils-python/tree/d7d6a44 Packages: - 'setuptools': "Easily download, build, install, upgrade, and uninstall Python packages". https://setuptools.readthedocs.io https://github.com/pypa/setuptools - 'wheel': "A built-package format for Python." https://wheel.readthedocs.io https://github.com/pypa/wheel - 'twine': "Collection of utilities for publishing packages on PyPI". https://twine.readthedocs.io https://github.com/pypa/twine - 'bumpversion': "Version-bump your software with a single command!" https://github.com/peritus/bumpversion --- requirements/release.txt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/requirements/release.txt b/requirements/release.txt index de3a5cbb..49ca0a96 100644 --- a/requirements/release.txt +++ b/requirements/release.txt @@ -3,7 +3,21 @@ -r base.txt # Required packages: -#none +bumpversion==0.5.3 +setuptools==40.8.0 +twine==1.13.0 +wheel==0.33.1 # Packages dependencies: -#none +# - twine: +# - pkginfo +# - readme-renderer +# - requests +# - requests-toolbelt +# - setuptools +# - tqdm +pkginfo==1.5.0.1 +readme-renderer==24.0 +requests==2.21.0 +requests-toolbelt==0.9.1 +tqdm==4.31.1 From 996cae40cc3331a5515d4d337e1629510d38f0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 4 Apr 2019 12:49:54 -0300 Subject: [PATCH 05/13] requirements: add test packages Source: https://github.com/fyndata/gcp-utils-python/tree/d7d6a44 Packages: - 'mypy': "Optional static typing for Python" http://www.mypy-lang.org https://github.com/python/mypy - 'flake8': "tool that glues together pep8, pyflakes, mccabe, and third-party plugins to check the style and quality of some python code" https://gitlab.com/pycqa/flake8 - 'coverage': "Code coverage measurement for Python" https://coverage.readthedocs.io https://github.com/nedbat/coveragepy - 'tox': "Command line driven CI frontend and development task automation tool" https://tox.readthedocs.io https://github.com/tox-dev/tox - 'codecov': "Hosted coverage reports for Github, Bitbucket and Gitlab" https://codecov.io https://github.com/codecov/codecov-python --- requirements/test.txt | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/requirements/test.txt b/requirements/test.txt index a9df4bed..5fb90db5 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -2,7 +2,36 @@ -r base.txt # Required packages: -#none +codecov==2.0.15 +coverage==4.5.2 +flake8==3.7.6 +mypy==0.670 +tox==3.7.0 # Packages dependencies: -#none +# - codecov: +# - coverage +# - requests +# - flake8: +# - mccabe +# - pycodestyle +# - pyflakes +# - mypy: +# - mypy-extensions +# - typed-ast +# - tox: +# - filelock +# - pluggy +# - py +# - toml +# - virtualenv +filelock==3.0.10 +mccabe==0.6.1 +mypy-extensions==0.4.1 +pluggy==0.9.0 +py==1.8.0 +pycodestyle==2.5.0 +pyflakes==2.1.0 +toml==0.10.0 +typed-ast==1.3.1 +virtualenv==16.4.3 From c29d928daadc57bd0b17ba3ef0adc5ff0855c393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Mon, 27 Aug 2018 10:01:19 -0300 Subject: [PATCH 06/13] add whole setup for a Python package/library project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Directory layout for Python files of main package and test suite. - Basic project metadata. - File 'setup.py' (along with 'MANIFEST.in') for package installation,   package generation, etc. - Source package and wheel distribution generation (`make dist`). - Test suite setup for regular run or with test coverage analysis.   (`make test`, `make test-coverage`). - Test coverage report generation (`make test-coverage-report-*`). - Code style analysis (flake8) and static type check (mypy)   (`make lint`). - Tox: test for multiple Python versions. - 'bumpversion' for bumping version with a single command. - Other minor things. Sources: - '.bumpversion.cfg': https://github.com/fyndata/gcp-utils-python/blob/8b7f105/.bumpversion.cfg - 'HISTORY.rst': https://github.com/fyndata/fyndata-django-accounts/blob/aec7610/HISTORY.rst - 'LICENSE' (identical): https://github.com/fyndata/gcp-utils-python/blob/efb59cd/LICENSE https://github.com/fyndata/fyndata-django-accounts/blob/31cf034/LICENSE - 'MANIFEST.in' (combination): https://github.com/fyndata/fyndata-django-accounts/blob/aec7610/MANIFEST.in https://github.com/fyndata/gcp-utils-python/blob/3574c47/MANIFEST.in - 'Makefile': https://github.com/fyndata/gcp-utils-python/blob/99935b5/Makefile - 'setup.cfg': https://github.com/fyndata/gcp-utils-python/blob/aeeebb4/setup.cfg - 'setup.py': https://github.com/fyndata/fyndata-django-accounts/blob/aec7610/setup.py - 'tox.ini' https://github.com/fyndata/gcp-utils-python/blob/3ce8d2f/tox.ini Also some influence by "Cookiecutter PyPackage" https://github.com/audreyr/cookiecutter-pypackage --- .bumpversion.cfg | 7 ++++ HISTORY.rst | 9 +++++ LICENSE | 21 ++++++++++++ MANIFEST.in | 5 +++ Makefile | 61 ++++++++++++++++++++++++++++++++++ README.rst | 7 ++++ cl_sii/__init__.py | 8 +++++ cl_sii/py.typed | 0 docs/.gitkeep | 0 setup.cfg | 49 +++++++++++++++++++++++++++ setup.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++ tests/__init__.py | 0 tox.ini | 15 +++++++++ 13 files changed, 265 insertions(+) create mode 100644 .bumpversion.cfg create mode 100644 HISTORY.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 cl_sii/__init__.py create mode 100644 cl_sii/py.typed create mode 100644 docs/.gitkeep create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 tests/__init__.py create mode 100644 tox.ini diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 00000000..4e97be69 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,7 @@ +[bumpversion] +current_version = 0.0.1 +commit = True +tag = True + +[bumpversion:file:cl_sii/__init__.py] + diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 00000000..ed9b260d --- /dev/null +++ b/HISTORY.rst @@ -0,0 +1,9 @@ +.. :changelog: + +History +------- + +unreleased (YYYY-MM-DD) ++++++++++++++++++++++++ + +* TODO: abc diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a148b844 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019, Fyndata (Fynpal SpA) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..81f56241 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include HISTORY.rst +include LICENSE +include README.rst +recursive-include cl_sii *py +include cl_sii/py.typed diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..0bfa3b51 --- /dev/null +++ b/Makefile @@ -0,0 +1,61 @@ +SHELL = /usr/bin/env bash + +.DEFAULT_GOAL := help +.PHONY: help +.PHONY: clean clean-build clean-pyc clean-test +.PHONY: lint test test-all test-coverage test-coverage-report-console test-coverage-report-html +.PHONY: dist upload-release + +help: + @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' + +clean: clean-build clean-pyc clean-test ## remove all build, test, lint, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -rf .eggs/ + rm -rf build/ + rm -rf dist/ + find . -name '*.egg-info' -exec rm -rf {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -rf {} + + +clean-test: ## remove test, lint and coverage artifacts + rm -rf .cache/ + rm -rf .tox/ + rm -f .coverage + rm -rf htmlcov/ + rm -rf test-reports/ + rm -rf .mypy_cache/ + +lint: ## run tools for code style analysis, static type check, etc + flake8 --config=setup.cfg cl_sii tests + mypy --config-file setup.cfg cl_sii + +test: ## run tests quickly with the default Python + python setup.py test + +test-all: ## run tests on every Python version with tox + tox + +test-coverage: ## run tests and record test coverage + coverage run --rcfile=setup.cfg setup.py test + +test-coverage-report-console: ## print test coverage summary + coverage report --rcfile=setup.cfg -m + +test-coverage-report-html: ## generate test coverage HTML report + coverage html --rcfile=setup.cfg + +dist: clean ## builds source and wheel package + python setup.py sdist + python setup.py bdist_wheel + twine check dist/* + ls -l dist + +upload-release: ## upload dist packages + python -m twine upload 'dist/*' diff --git a/README.rst b/README.rst index 873c0f2b..49314c20 100644 --- a/README.rst +++ b/README.rst @@ -3,3 +3,10 @@ cl-sii Python lib ================= Python library for Servicio de Impuestos Internos (SII) of Chile. + +Status +------------- + +.. image:: https://circleci.com/gh/fyndata/lib-cl-sii-python/tree/develop.svg?style=shield + :target: https://circleci.com/gh/fyndata/lib-cl-sii-python/tree/develop + :alt: CI status diff --git a/cl_sii/__init__.py b/cl_sii/__init__.py new file mode 100644 index 00000000..27c7967f --- /dev/null +++ b/cl_sii/__init__.py @@ -0,0 +1,8 @@ +""" +cl-sii Python lib +================= + +""" + + +__version__ = '0.0.1' diff --git a/cl_sii/py.typed b/cl_sii/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..ddb01ab7 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,49 @@ +[bdist_wheel] +universal = 0 + +[coverage:run] +source = cl_sii/ +omit = + tests/* +branch = True + +[coverage:report] +exclude_lines = + pragma: no cover + if __name__ == .__main__. +show_missing = True + +[coverage:html] +directory = test-reports/coverage/html + +[mypy] +python_version = 3.7 +platform = linux + +follow_imports = normal +ignore_missing_imports = False +strict_optional = True +disallow_untyped_defs = True +check_untyped_defs = True +warn_return_any = True + +[flake8] +ignore = + # W503 line break before binary operator + W503 + +exclude = + *.egg-info/, + .git/, + .mypy_cache/, + .pyenvs/, + __pycache__/, + build/, + dist/, + docs/ + +max-line-length = 100 + +doctests = True +show-source = True +statistics = True diff --git a/setup.py b/setup.py new file mode 100755 index 00000000..4cb221d3 --- /dev/null +++ b/setup.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +import os +import re +from typing import Sequence + +from setuptools import find_packages, setup + + +def get_version(*file_paths: Sequence[str]) -> str: + filename = os.path.join(os.path.dirname(__file__), *file_paths) + version_file = open(filename).read() + version_match = re.search( + r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError('Unable to find version string.') + + +version = get_version('cl_sii', '__init__.py') + +readme = open('README.rst').read() +history = open('HISTORY.rst').read().replace('.. :changelog:', '') + +requirements = [ +] + +extras_requirements = { +} + +setup_requirements = [ +] + +test_requirements = [ + # note: include here only packages **imported** in test code (e.g. 'requests-mock'), NOT those + # like 'coverage' or 'tox'. +] + +# note: the "typing information" of this project's packages is not made available to its users +# automatically; it needs to be packaged and distributed. The way to do so is fairly new and +# it is specified in PEP 561 - "Distributing and Packaging Type Information". +# See: +# - https://www.python.org/dev/peps/pep-0561/#packaging-type-information +# - https://github.com/python/typing/issues/84 +# - https://github.com/python/mypy/issues/3930 +# warning: remember to replicate this in the manifest file for source distribution ('MANIFEST.in'). +_package_data = { + 'cl_sii': [ + # Indicates that the "typing information" of the package should be distributed. + 'py.typed', + ], +} + +setup( + author='Fyndata (Fynpal SpA)', + author_email='no-reply@fyndata.com', + classifiers=[ + # See https://pypi.org/classifiers/ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ], + description="""Python library for Servicio de Impuestos Internos (SII) of Chile.""", + extras_require=extras_requirements, + install_requires=requirements, + license="MIT", + long_description=readme + '\n\n' + history, + long_description_content_type='text/x-rst', # for Markdown: 'text/markdown' + include_package_data=True, + name='cl-sii', + package_data=_package_data, + packages=find_packages(exclude=['docs', 'tests*']), + python_requires='>=3.6, <3.8', + setup_requires=setup_requirements, + test_suite='tests', + tests_require=test_requirements, + url='https://github.com/fyndata/lib-cl-sii-python', + version=version, + zip_safe=False, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..4904145b --- /dev/null +++ b/tox.ini @@ -0,0 +1,15 @@ +[tox] +envlist = + py36 + py37 + +[testenv] +setenv = + PYTHONPATH = {toxinidir}:{toxinidir}/cl_sii +commands = coverage run --rcfile=setup.cfg setup.py test +deps = + -r{toxinidir}/requirements/test.txt + -r{toxinidir}/requirements/extras.txt +basepython = + py36: python3.6 + py37: python3.7 From 43ae28f623263f06b088ed2d1ca12c754814b411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Mon, 27 Aug 2018 10:57:34 -0300 Subject: [PATCH 07/13] drop Python 3.6 support Only Python 3.7 is supported. Python 3.6 and below will not work because we intend to use some features introduced in Python 3.7. For development and testing, Python 3.7.2 (released on 2018-12-24) will be used. More info: - https://docs.python.org/3.7/whatsnew/3.7.html - https://www.python.org/downloads/release/python-372/ --- README.rst | 7 +++++++ setup.py | 3 +-- tox.ini | 2 -- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 49314c20..654a6cd0 100644 --- a/README.rst +++ b/README.rst @@ -10,3 +10,10 @@ Status .. image:: https://circleci.com/gh/fyndata/lib-cl-sii-python/tree/develop.svg?style=shield :target: https://circleci.com/gh/fyndata/lib-cl-sii-python/tree/develop :alt: CI status + + +Supported Python versions +------------------------- + +Only Python 3.7. Python 3.6 and below will not work because we use some features introduced in +Python 3.7. diff --git a/setup.py b/setup.py index 4cb221d3..95b4c42a 100755 --- a/setup.py +++ b/setup.py @@ -60,7 +60,6 @@ def get_version(*file_paths: Sequence[str]) -> str: 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], description="""Python library for Servicio de Impuestos Internos (SII) of Chile.""", @@ -73,7 +72,7 @@ def get_version(*file_paths: Sequence[str]) -> str: name='cl-sii', package_data=_package_data, packages=find_packages(exclude=['docs', 'tests*']), - python_requires='>=3.6, <3.8', + python_requires='>=3.7, <3.8', setup_requires=setup_requirements, test_suite='tests', tests_require=test_requirements, diff --git a/tox.ini b/tox.ini index 4904145b..f6e6abb1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist = - py36 py37 [testenv] @@ -11,5 +10,4 @@ deps = -r{toxinidir}/requirements/test.txt -r{toxinidir}/requirements/extras.txt basepython = - py36: python3.6 py37: python3.7 From 6576aeee275b59df07b39853aed9646ff08338a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 4 Apr 2019 17:23:48 -0300 Subject: [PATCH 08/13] add sub-package `rut` Helpers and constants related to "RUT". Test have not been implemented. --- cl_sii/rut/__init__.py | 187 ++++++++++++++++++++++++++++++++++++++++ cl_sii/rut/constants.py | 20 +++++ tests/test_rut.py | 11 +++ 3 files changed, 218 insertions(+) create mode 100644 cl_sii/rut/__init__.py create mode 100644 cl_sii/rut/constants.py create mode 100644 tests/test_rut.py diff --git a/cl_sii/rut/__init__.py b/cl_sii/rut/__init__.py new file mode 100644 index 00000000..d6f1a979 --- /dev/null +++ b/cl_sii/rut/__init__.py @@ -0,0 +1,187 @@ +""" +Utilities for dealing with Chile's RUT ("Rol Único Tributario"). + +The terms RUT and RUN ("Rol Único Nacional") may be used interchangeably but +only when the holder is a natural person ("persona natural"); a legal person +("persona jurídica") does not have a RUN. + +RUT "canonical format": no dots ('.'), with dash ('-'), uppercase K e.g. +``'76042235-5'``, ``'96874030-K'``. + +""" +import itertools +import random + +from . import constants + + +class Rut: + + """ + Representation of a RUT. + + It verifies that the input is syntactically valid and, optionally, that the + "digito verificador" is correct. + + It does NOT check that the value is within boundaries deemed acceptable by + the SII (although the regex used does implicitly impose some) nor that the + RUT has actually been assigned to some person or entity. + + >>> Rut('96874030-K') + Rut('96874030-K')> + >>> str(Rut('96874030-K')) + '96874030-K' + >>> Rut('96874030-K').digits + '96874030' + >>> Rut('96874030-K').dv + 'K' + >>> Rut('96874030-K').canonical + '96874030-K' + >>> Rut('96874030-K').verbose + '96.874.030-K' + >>> Rut('96874030-K').digits_with_dots + '96.874.030' + + >>> Rut('77879240-0') == Rut('77.879.240-0') + True + >>> Rut('96874030-K') == Rut('9.68.7403.0-k') + True + + """ + + def __init__(self, value: str, validate_dv: bool = False) -> None: + """ + Constructor. + + :param value: a string that represents a syntactically valid RUT + :param validate_dv: whether to validate that the RUT's + "digito verificador" is correct + + """ + invalid_rut_msg = "Syntactically invalid RUT." + + clean_value = Rut.clean_str(value) + try: + match_obj = constants.RUT_CANONICAL_STRICT_REGEX.match(clean_value) + except Exception as exc: + raise ValueError(invalid_rut_msg, value) from exc + if match_obj is None: + raise ValueError(invalid_rut_msg, value) + + try: + match_groups = match_obj.groupdict() + self._digits = match_groups['digits'] + self._dv = match_groups['dv'] + except Exception as exc: + raise ValueError(invalid_rut_msg, value) from exc + + if validate_dv: + if Rut.calc_dv(self._digits) != self._dv: + raise ValueError("RUT's \"digito verificador\" is incorrect.", value) + + ############################################################################ + # properties + ############################################################################ + + @property + def canonical(self) -> str: + return f'{self._digits}-{self._dv}' + + @property + def verbose(self) -> str: + return f'{self.digits_with_dots}-{self._dv}' + + @property + def digits(self) -> str: + return self._digits + + @property + def digits_with_dots(self) -> str: + """Return RUT digits with a dot ('.') as thousands separator.""" + # > The ',' option signals the use of a comma for a thousands separator. + # https://docs.python.org/3/library/string.html#format-specification-mini-language + return '{:,}'.format(int(self.digits)).replace(',', '.') + + @property + def dv(self) -> str: + return self._dv + + ############################################################################ + # magic methods + ############################################################################ + + def __str__(self) -> str: + return self.canonical + + def __repr__(self) -> str: + return f"Rut('{self.canonical}')" + + def __eq__(self, other: object) -> bool: + if isinstance(other, Rut): + return self.canonical == other.canonical + return False + + def __hash__(self) -> int: + # Objects are hashable so they can be used in hashable collections. + return hash(self.canonical) + + ############################################################################ + # class methods + ############################################################################ + + @classmethod + def clean_str(cls, value: str) -> str: + # note: unfortunately `value.strip('.')` does not remove all the occurrences of '.' in + # 'value' (only the leading and trailing ones). + return value.strip().replace('.', '').upper() + + @classmethod + def calc_dv(cls, rut_digits: str) -> str: + """ + Calculate the "digito verificador" of a RUT's digits. + + >>> Rut.calc_dv('60910000') + '1' + >>> Rut.calc_dv('76555835') + '2' + >>> Rut.calc_dv('76177907') + '9' + >>> Rut.calc_dv('76369187') + 'K' + >>> Rut.calc_dv('77879240') + '0' + >>> Rut.calc_dv('96874030') + 'K' + + """ + if rut_digits != rut_digits.strip().lower(): + raise ValueError + try: + int(rut_digits) + except TypeError as exc: + raise ValueError from exc + + # Based on: + # https://gist.github.com/rbonvall/464824/4b07668b83ee45121345e4634ebce10dc6412ba3 + s = sum( + d * f + for d, f + in zip(map(int, reversed(rut_digits)), itertools.cycle(range(2, 8))) + ) + result_alg = 11 - (s % 11) + return {10: 'K', 11: '0'}.get(result_alg, str(result_alg)) + + @classmethod + def random(cls) -> 'Rut': + """ + Generate a random RUT. + + Value will be within proper boundaries and "digito verificador" + will be calculated appropriately i.e. it is not random. + + """ + rut_digits = str(random.randint( + constants.RUT_DIGITS_MIN_VALUE, + constants.RUT_DIGITS_MAX_VALUE)) + rut_dv = Rut.calc_dv(rut_digits) + return Rut(f'{rut_digits}-{rut_dv}') diff --git a/cl_sii/rut/constants.py b/cl_sii/rut/constants.py new file mode 100644 index 00000000..edb4f2da --- /dev/null +++ b/cl_sii/rut/constants.py @@ -0,0 +1,20 @@ +""" +RUT-related constants. + +Source: XML type 'RUTType' in official schema 'SiiTypes_v10.xsd'. +https://github.com/fynlabs/lib-cl-sii-python/blob/a80edd9/vendor/cl_sii/ref/factura_electronica/schema_dte/SiiTypes_v10.xsd#L121-L130 + +""" +import re + + +RUT_CANONICAL_STRICT_REGEX = re.compile(r'^(?P\d{1,8})-(?P[\dK])$') +"""RUT (strict) regex for canonical format.""" +RUT_CANONICAL_MAX_LENGTH = 10 +"""RUT max length for canonical format.""" +RUT_CANONICAL_MIN_LENGTH = 3 +"""RUT min length for canonical format.""" +RUT_DIGITS_MAX_VALUE = 99999999 +"""RUT digits max value.""" +RUT_DIGITS_MIN_VALUE = 50000000 +"""RUT digits min value.""" diff --git a/tests/test_rut.py b/tests/test_rut.py new file mode 100644 index 00000000..df310c99 --- /dev/null +++ b/tests/test_rut.py @@ -0,0 +1,11 @@ +import unittest + +from cl_sii import rut # noqa: F401 +from cl_sii.rut import constants # noqa: F401 + + +class RutTest(unittest.TestCase): + + # TODO: implement! + + pass From f110d815b3274c7408f76082bb57133d698c5374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Mu=C3=B1oz?= Date: Fri, 14 Dec 2018 11:08:33 -0300 Subject: [PATCH 09/13] rut: improve `Rut` - Remove unnecessary catches of exceptions. - Simplify `calc_dv`. --- cl_sii/rut/__init__.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/cl_sii/rut/__init__.py b/cl_sii/rut/__init__.py index d6f1a979..a58dc96b 100644 --- a/cl_sii/rut/__init__.py +++ b/cl_sii/rut/__init__.py @@ -61,19 +61,13 @@ def __init__(self, value: str, validate_dv: bool = False) -> None: invalid_rut_msg = "Syntactically invalid RUT." clean_value = Rut.clean_str(value) - try: - match_obj = constants.RUT_CANONICAL_STRICT_REGEX.match(clean_value) - except Exception as exc: - raise ValueError(invalid_rut_msg, value) from exc + match_obj = constants.RUT_CANONICAL_STRICT_REGEX.match(clean_value) if match_obj is None: raise ValueError(invalid_rut_msg, value) - try: - match_groups = match_obj.groupdict() - self._digits = match_groups['digits'] - self._dv = match_groups['dv'] - except Exception as exc: - raise ValueError(invalid_rut_msg, value) from exc + match_groups = match_obj.groupdict() + self._digits = match_groups['digits'] + self._dv = match_groups['dv'] if validate_dv: if Rut.calc_dv(self._digits) != self._dv: @@ -154,12 +148,8 @@ def calc_dv(cls, rut_digits: str) -> str: 'K' """ - if rut_digits != rut_digits.strip().lower(): - raise ValueError - try: - int(rut_digits) - except TypeError as exc: - raise ValueError from exc + if rut_digits.strip().isdigit() is False: + raise ValueError("Must be a sequence of digits.") # Based on: # https://gist.github.com/rbonvall/464824/4b07668b83ee45121345e4634ebce10dc6412ba3 From 734e71054d5c1be9c233352d743b18d70c37ca74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Mu=C3=B1oz?= Date: Fri, 14 Dec 2018 11:05:51 -0300 Subject: [PATCH 10/13] rut: add tests for `Rut` class --- tests/test_rut.py | 176 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 2 deletions(-) diff --git a/tests/test_rut.py b/tests/test_rut.py index df310c99..908b97e1 100644 --- a/tests/test_rut.py +++ b/tests/test_rut.py @@ -6,6 +6,178 @@ class RutTest(unittest.TestCase): - # TODO: implement! + valid_rut_canonical: str + valid_rut_dv: str + valid_rut_digits: str + valid_rut_digits_with_dots: str + valid_rut_verbose: str - pass + invalid_rut_canonical: str + invalid_rut_dv: str + + valid_rut_instance: rut.Rut + invalid_rut_instance: rut.Rut + + @classmethod + def setUpClass(cls) -> None: + cls.valid_rut_canonical = '6824160-K' + cls.valid_rut_dv = 'K' + cls.valid_rut_digits = '6824160' + cls.valid_rut_digits_with_dots = '6.824.160' + cls.valid_rut_verbose = '6.824.160-K' + + cls.invalid_rut_canonical = '6824160-0' + cls.invalid_rut_dv = '0' + + cls.valid_rut_instance = rut.Rut(cls.valid_rut_canonical) + cls.invalid_rut_instance = rut.Rut(cls.invalid_rut_canonical) + + ############################################################################ + # instance + ############################################################################ + + def test_instance_empty_string(self) -> None: + rut_value = '' + with self.assertRaises(ValueError) as context_manager: + rut.Rut(rut_value) + + exception = context_manager.exception + message, value = exception.args + self.assertEqual(message, 'Syntactically invalid RUT.') + self.assertEqual(value, rut_value, 'Different RUT value.') + + def test_instance_invalid_rut_format(self) -> None: + rut_value = 'invalid rut format' + with self.assertRaises(ValueError) as context_manager: + rut.Rut(rut_value) + + exception = context_manager.exception + message, value = exception.args + self.assertEqual(message, 'Syntactically invalid RUT.') + self.assertEqual(value, rut_value, 'Different RUT value.') + + def test_instance_short_rut(self) -> None: + rut_value = '1-0' + rut.Rut(rut_value) + + def test_instance_long_rut(self) -> None: + rut_value = '123456789-0' + with self.assertRaises(ValueError) as context_manager: + rut.Rut(rut_value) + + exception = context_manager.exception + message, value = exception.args + self.assertEqual(message, 'Syntactically invalid RUT.') + self.assertEqual(value, rut_value, 'Different RUT value.') + + def test_instance_validate_dv_ok(self) -> None: + rut.Rut(self.valid_rut_canonical, validate_dv=True) + + def test_instance_validate_dv_in_lowercase(self) -> None: + rut_instance = rut.Rut(self.valid_rut_canonical.lower(), validate_dv=True) + self.assertFalse(rut_instance.dv.isnumeric()) + self.assertEqual(rut_instance.dv, self.valid_rut_dv) + + def test_instance_validate_dv_raise_exception(self) -> None: + with self.assertRaises(ValueError) as context_manager: + rut.Rut(self.invalid_rut_canonical, validate_dv=True) + + exception = context_manager.exception + message, value = exception.args + self.assertEqual(message, "RUT's \"digito verificador\" is incorrect.") + self.assertEqual(value, self.invalid_rut_canonical, 'Different RUT value.') + + ############################################################################ + # properties + ############################################################################ + + def test_canonical(self) -> None: + self.assertEqual(self.valid_rut_instance.dv, self.valid_rut_dv) + + def test_verbose(self) -> None: + self.assertEqual(self.valid_rut_instance.verbose, self.valid_rut_verbose) + + def test_digits(self) -> None: + self.assertEqual(self.valid_rut_instance.digits, self.valid_rut_digits) + + def test_digits_with_dots(self) -> None: + self.assertEqual(self.valid_rut_instance.digits_with_dots, self.valid_rut_digits_with_dots) + + def test_dv(self) -> None: + self.assertEqual(self.valid_rut_instance.dv, self.valid_rut_dv) + + def test_dv_upper(self) -> None: + self.assertTrue(self.valid_rut_instance.dv.isupper()) + + ############################################################################ + # magic methods + ############################################################################ + + def test__str__(self) -> None: + self.assertEqual(self.valid_rut_instance.__str__(), self.valid_rut_canonical) + + def test__repr__(self) -> None: + rut_repr = f"Rut('{self.valid_rut_canonical}')" + self.assertEqual(self.valid_rut_instance.__repr__(), rut_repr) + + def test__eq__true(self) -> None: + rut_instance = rut.Rut(self.valid_rut_canonical) + self.assertTrue(self.valid_rut_instance.__eq__(rut_instance)) + + def test__eq__false(self) -> None: + self.assertFalse(self.valid_rut_instance.__eq__(self.invalid_rut_instance)) + + def test__eq__not_rut_instance(self) -> None: + self.assertFalse(self.valid_rut_instance.__eq__(self.valid_rut_canonical)) + + def test__hash__(self) -> None: + rut_hash = hash(self.valid_rut_instance.canonical) + self.assertEqual(self.valid_rut_instance.__hash__(), rut_hash) + + ############################################################################ + # class methods + ############################################################################ + + def test_clean_str_lowercase(self) -> None: + rut_value = f' {self.valid_rut_verbose.lower()} ' + clean_rut = rut.Rut.clean_str(rut_value) + self.assertEqual(clean_rut, self.valid_rut_canonical) + + def test_clean_type_error(self) -> None: + with self.assertRaises(AttributeError) as context_manager: + rut.Rut.clean_str(1) # type: ignore + + exception = context_manager.exception + self.assertEqual(len(exception.args), 1) + message = exception.args[0] + self.assertEqual(message, "'int' object has no attribute 'strip'") + + def test_calc_dv_ok(self) -> None: + dv = rut.Rut.calc_dv(self.valid_rut_digits) + self.assertEqual(dv, self.valid_rut_dv) + + def test_calc_dv_string_uppercase(self) -> None: + digits = 'A' + with self.assertRaises(ValueError) as context_manager: + rut.Rut.calc_dv(digits) + + self.assertListEqual( + list(context_manager.exception.args), + ["Must be a sequence of digits."] + ) + + def test_calc_dv_string_lowercase(self) -> None: + digits = 'a' + with self.assertRaises(ValueError) as context_manager: + rut.Rut.calc_dv(digits) + + self.assertListEqual( + list(context_manager.exception.args), + ["Must be a sequence of digits."] + ) + + def test_random(self) -> None: + rut_instance = rut.Rut.random() + self.assertIsInstance(rut_instance, rut.Rut) + dv = rut.Rut.calc_dv(rut_instance.digits) + self.assertEqual(rut_instance.dv, dv) From 3c49f5cf49c37603f52096b302782ce11c090c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 16 Jan 2019 19:23:06 -0300 Subject: [PATCH 11/13] rut: improve `Rut` class Constructor raises only `TypeError` and `ValueError` (not `AttributeError` anymore), and now supports its own type. --- cl_sii/rut/__init__.py | 8 ++++++++ tests/test_rut.py | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/cl_sii/rut/__init__.py b/cl_sii/rut/__init__.py index a58dc96b..1cd56f6f 100644 --- a/cl_sii/rut/__init__.py +++ b/cl_sii/rut/__init__.py @@ -57,9 +57,17 @@ def __init__(self, value: str, validate_dv: bool = False) -> None: :param validate_dv: whether to validate that the RUT's "digito verificador" is correct + :raises ValueError: + :raises TypeError: + """ invalid_rut_msg = "Syntactically invalid RUT." + if isinstance(value, Rut): + value = value.canonical + if not isinstance(value, str): + raise TypeError("Invalid type.") + clean_value = Rut.clean_str(value) match_obj = constants.RUT_CANONICAL_STRICT_REGEX.match(clean_value) if match_obj is None: diff --git a/tests/test_rut.py b/tests/test_rut.py index 908b97e1..5d7de911 100644 --- a/tests/test_rut.py +++ b/tests/test_rut.py @@ -36,6 +36,19 @@ def setUpClass(cls) -> None: # instance ############################################################################ + def test_fail_type_error(self) -> None: + with self.assertRaises(TypeError): + rut.Rut(object()) + with self.assertRaises(TypeError): + rut.Rut(1) + with self.assertRaises(TypeError): + rut.Rut(None) + + def test_ok_same_type(self) -> None: + self.assertEqual( + rut.Rut(rut.Rut('1-1')), + rut.Rut('1-1')) + def test_instance_empty_string(self) -> None: rut_value = '' with self.assertRaises(ValueError) as context_manager: From 35406a739864ae8eaa68faf92ffbeac3b706ad98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 4 Apr 2019 19:28:46 -0300 Subject: [PATCH 12/13] HISTORY: update for new version --- HISTORY.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index ed9b260d..5943cfc4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,4 +6,8 @@ History unreleased (YYYY-MM-DD) +++++++++++++++++++++++ -* TODO: abc +0.1.0 (2019-04-04) ++++++++++++++++++++++++ + +* (PR #2, 2019-04-04) Add class and constants for RUT +* (PR #1, 2019-04-04) Whole setup for a Python package/library From b2f5354f517ef0a1d5cfc0da9f8d9dc93a9d5607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 4 Apr 2019 19:30:40 -0300 Subject: [PATCH 13/13] =?UTF-8?q?Bump=20version:=200.0.1=20=E2=86=92=200.1?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cl_sii/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4e97be69..03bd12ae 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.0.1 +current_version = 0.1.0 commit = True tag = True diff --git a/cl_sii/__init__.py b/cl_sii/__init__.py index 27c7967f..42a55c1c 100644 --- a/cl_sii/__init__.py +++ b/cl_sii/__init__.py @@ -5,4 +5,4 @@ """ -__version__ = '0.0.1' +__version__ = '0.1.0'