Browse Source

first commit

Radu Boncea 6 years ago
commit
35896bc350
7 changed files with 527 additions and 0 deletions
  1. 249 0
      .gitignore
  2. 4 0
      AUTHORS
  3. 22 0
      LICENSE
  4. 48 0
      README.md
  5. 0 0
      pyVies/__init__.py
  6. 158 0
      pyVies/api.py
  7. 46 0
      setup.py

+ 249 - 0
.gitignore

@@ -0,0 +1,249 @@
+
+# Created by https://www.gitignore.io/api/python,sublimetext,pycharm+all,visualstudiocode
+# Edit at https://www.gitignore.io/?templates=python,sublimetext,pycharm+all,visualstudiocode
+
+### PyCharm+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# 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/modules.xml
+# .idea/*.iml
+# .idea/modules
+
+# 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
+
+### PyCharm+all Patch ###
+# Ignores the whole .idea folder and all .iml files
+# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
+
+.idea/
+
+# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
+
+*.iml
+modules.xml
+.idea/misc.xml
+*.ipr
+
+### Python ###
+# 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
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# 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/
+
+### Python Patch ###
+.venv/
+
+### SublimeText ###
+# 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
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+
+# End of https://www.gitignore.io/api/python,sublimetext,pycharm+all,visualstudiocode

+ 4 - 0
AUTHORS

@@ -0,0 +1,4 @@
+Developers:
+Radu Boncea <radu.boncea@gmail.com>
+
+Contributors:

+ 22 - 0
LICENSE

@@ -0,0 +1,22 @@
+Copyright (c) 2008 Mike Verdone
+
+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.

+ 48 - 0
README.md

@@ -0,0 +1,48 @@
+# A wrapper API of VIES VAT web service
+
+## Compatibility
+Requires Python 2.7 or greater.
+Has been tested on 2.7, 3.4 and 3.6
+
+## Installation
+
+    $ pip install pyVies
+
+## Usage
+
+##### Python API:
+
+```python
+from pyVies import api
+
+try:
+    vies = api.Vies()
+    result = vies.request('2785503', 'RO')
+
+    # works as well
+    # result = vies.request('RO2785503')
+    # result = vies.request('RO2785503', 'RO')
+
+except api.ViesValidationError as e:
+    print (e)
+except api.ViesHTTPError as e:
+    print (e)
+except api.ViesError as e:
+    print (e)
+else:
+    print (result)
+
+"""
+You may also use `clean` to extract vat number and country code
+The line bellow would print ('2785503', 'RO')
+"""
+print (vies.clean('RO2785503'))
+
+```
+
+##### From console:
+
+	$ pyvies <vat_number>
+
+The number should start with the country code.
+For python3 you might have to set python encoding for your environment (e.g. export PYTHONIOENCODING=utf-8).

+ 0 - 0
pyVies/__init__.py


+ 158 - 0
pyVies/api.py

@@ -0,0 +1,158 @@
+from __future__ import unicode_literals, print_function
+
+import re
+import sys
+import zeep
+import pprint
+
+VAT_NUMBER_REGEXPS = {
+    'AT': re.compile(r'^U\d{8}$', re.IGNORECASE),
+    'BE': re.compile(r'^\d{9,10}$'),
+    'BG': re.compile(r'^\d{9,10}$'),
+    'CY': re.compile(r'^\d{8}[a-z]$', re.IGNORECASE),
+    'CZ': re.compile(r'^\d{8,10}$'),
+    'DE': re.compile(r'^\d{9}$'),
+    'DK': re.compile(r'^\d{8}$'),
+    'EE': re.compile(r'^\d{9}$'),
+    'ES': re.compile(r'^[\da-z]\d{7}[\da-z]$', re.IGNORECASE),
+    'FI': re.compile(r'^\d{8}$'),
+    'FR': re.compile(r'^[\da-hj-np-z]{2}\d{9}$', re.IGNORECASE),
+    'GB': re.compile(r'^((\d{9})|(\d{12})|(GD\d{3})|(HA\d{3}))$',
+                     re.IGNORECASE),
+    'GR': re.compile(r'^\d{9}$'),
+    'HR': re.compile(r'^\d{11}$'),
+    'HU': re.compile(r'^\d{8}$'),
+    'IE': re.compile(r'^((\d{7}[a-z])|(\d[a-z]\d{5}[a-z])|(\d{6,7}[a-z]{2}))$',
+                     re.IGNORECASE),
+    'IT': re.compile(r'^\d{11}$'),
+    'LT': re.compile(r'^((\d{9})|(\d{12}))$'),
+    'LU': re.compile(r'^\d{8}$'),
+    'LV': re.compile(r'^\d{11}$'),
+    'MT': re.compile(r'^\d{8}$'),
+    'NL': re.compile(r'^\d{9}B\d{2,3}$', re.IGNORECASE),
+    'PL': re.compile(r'^\d{10}$'),
+    'PT': re.compile(r'^\d{9}$'),
+    'RO': re.compile(r'^\d{2,10}$'),
+    'SE': re.compile(r'^\d{12}$'),
+    'SI': re.compile(r'^\d{8}$'),
+    'SK': re.compile(r'^\d{10}$'),
+}
+
+
+class ViesValidationError(Exception):
+    """
+    Exception thrown by the Vies object when there is a
+    validation problem.
+    """
+    pass
+
+
+class ViesHTTPError(Exception):
+    """
+    Exception thrown by the Vies object when there is an
+    HTTP error interacting with Vies.
+    """
+    pass
+
+
+class ViesError(Exception):
+    """
+    Base Exception thrown by the Vies object when there is a
+    general problem when interacting with Vies service.
+    """
+    pass
+
+
+class Vies(object):
+    WS_ENDPOINT = 'http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl'
+
+
+    def __init__(self):
+        super(Vies, self).__init__()
+
+    def clean(self, vat_number, vat_country_code=None):
+        try:
+            vat_number = str(vat_number)
+        except:
+            ViesValidationError('Invalid VAT number provided')
+        vat_number = vat_number.replace(' ', '')
+
+        if vat_country_code is not None:
+            try:
+                vat_country_code = str(vat_country_code)
+            except:
+                ViesValidationError('Invalid VAT country provided')
+            vat_country_code = vat_country_code.replace(' ', '')
+            vat_country_code = vat_country_code.upper()
+
+        # if no vat_country_code provided we try to extract it from vat_number
+        else:
+            try:
+                vat_country_code = vat_number[:2]
+            except:
+                raise ViesValidationError('Invalid VAT number provided')
+
+            vat_country_code = vat_country_code.upper()
+            if vat_country_code== 'EL':
+                vat_country_code = 'GR'
+            else:
+                vat_number = vat_number[2:]
+
+        if vat_country_code not in VAT_NUMBER_REGEXPS.keys():
+            raise ViesValidationError('Invalid VAT country')
+
+        if len(vat_number)>2:
+            if vat_number[:2].upper() == vat_country_code:
+                vat_number = vat_number[2:]
+
+        # validate the vat number against VAT_NUMBER_REGEXPS
+        if not VAT_NUMBER_REGEXPS[vat_country_code].match(vat_number):
+            raise ViesValidationError('Invalid VAT number')
+
+        return vat_number,vat_country_code
+
+    def request(self, vat_number, vat_country_code=None):
+        result = None
+
+        vat_number,vat_country_code = self.clean(vat_number,vat_country_code)
+
+        # check VIES
+        try:
+            client = zeep.Client(wsdl=self.WS_ENDPOINT)
+        except Exception as e:
+            raise ViesHTTPError('VIES service unreachable. %s' % str(e))
+
+        try:
+            result = client.service.checkVat(vat_country_code, vat_number)
+        except Exception as e:
+            raise ViesError('Got error from vies: %s' % str(e))
+
+        return result
+
+
+def console():
+
+    def print_err(*args, **kwargs):
+        print(*args, file=sys.stderr, **kwargs)
+
+    if len(sys.argv) < 2:
+        print_err("usage: %s <vat_number>\n" % sys.argv[0])
+        sys.exit(-255)
+
+    vat_number = sys.argv[1]
+
+    try:
+        vies = Vies()
+        result = vies.request(vat_number)
+    except Exception as e:
+        if hasattr(e, 'message'):
+            print_err(e.message)
+        else:
+            print_err(e)
+    else:
+        pp = pprint.PrettyPrinter(indent=8)
+        pp.pprint(result)
+
+
+if __name__ == '__main__':
+    console()

+ 46 - 0
setup.py

@@ -0,0 +1,46 @@
+import setuptools
+
+with open("README.md", "r") as fh:
+    long_description = fh.read()
+
+
+setuptools.setup(
+    name="pyVies",
+    version="0.0.1",
+    author="Radu Boncea",
+    author_email="radu.boncea@gmail.com",
+    description="A wrapper API of VIES VAT web service",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    url="https://github.com/agilegeeks/pyVies.git",
+    packages=setuptools.find_packages(exclude=['tests']),
+    entry_points={
+        "console_scripts": [
+            "pyvies = pyVies.api:console",
+        ]
+    },
+    install_requires=[
+          "zeep>=3.1.0"
+    ],
+    python_requires='>=2.7',
+    classifiers=[
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Development Status :: 4 - Beta",
+        "Intended Audience :: Developers",
+        "Intended Audience :: Financial and Insurance Industry",
+        "License :: OSI Approved :: MIT License",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3.0",
+        "Programming Language :: Python :: 3.1",
+        "Programming Language :: Python :: 3.2",
+        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
+        "Topic :: Database :: Front-Ends",
+        "Topic :: Office/Business :: Financial :: Accounting",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        "Topic :: Utilities",
+    ],
+)