lists.arthurdejong.org
RSS feed

python-stdnum branch master updated. 1.7-11-g6be1754

[Date Prev][Date Next] [Thread Prev][Thread Next]

python-stdnum branch master updated. 1.7-11-g6be1754



This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "python-stdnum".

The branch, master has been updated
       via  6be1754ace72b849bbd382cfc1f23ab79d880ed8 (commit)
       via  9ab1d661f78784b59dfb7118e58f2ddecc59bf8e (commit)
       via  ab21159de6210da7746fc56268f34d2bee210e7b (commit)
       via  6b09c5d744d983da96c3c46a1e081e29db6c9f28 (commit)
       via  665bf7a403df0e2e61b5f4a245675b0c641170a2 (commit)
       via  4ab1e3b538460078c87878b730b2ac06e6d1f9cd (commit)
      from  cecd35cbce73ab166394352f75f85b4f83de367f (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
https://arthurdejong.org/git/python-stdnum/commit/?id=6be1754ace72b849bbd382cfc1f23ab79d880ed8

commit 6be1754ace72b849bbd382cfc1f23ab79d880ed8
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Oct 22 21:03:42 2017 +0200

    Support zeep as preferred SOAP library
    
    This tries zeep, suds (suds-jurko) and falls back to using pysimplesoap
    for performing the SOAP requests. From those zeep seems to be the best
    supported implementation.

diff --git a/stdnum/util.py b/stdnum/util.py
index b410d4f..7033047 100644
--- a/stdnum/util.py
+++ b/stdnum/util.py
@@ -169,7 +169,7 @@ def get_cc_module(cc, name):
 _soap_clients = {}
 
 
-def get_soap_client(wsdlurl):  # pragma: no cover (no tests for this function)
+def get_soap_client(wsdlurl):  # pragma: no cover (not part of normal test 
suite)
     """Get a SOAP client for performing requests. The client is cached."""
     # this function isn't automatically tested because the functions using
     # it are not automatically tested
@@ -178,13 +178,18 @@ def get_soap_client(wsdlurl):  # pragma: no cover (no 
tests for this function)
             from urllib import getproxies
         except ImportError:
             from urllib.request import getproxies
-        # try suds first
+        # try zeep first
         try:
-            from suds.client import Client
-            client = Client(wsdlurl, proxy=getproxies()).service
+            from zeep import CachingClient
+            client = CachingClient(wsdlurl).service
         except ImportError:
-            # fall back to using pysimplesoap
-            from pysimplesoap.client import SoapClient
-            client = SoapClient(wsdl=wsdlurl, proxy=getproxies())
+            # fall back to suds
+            try:
+                from suds.client import Client
+                client = Client(wsdlurl, proxy=getproxies()).service
+            except ImportError:
+                # use pysimplesoap as last resort
+                from pysimplesoap.client import SoapClient
+                client = SoapClient(wsdl=wsdlurl, proxy=getproxies())
         _soap_clients[wsdlurl] = client
     return _soap_clients[wsdlurl]

https://arthurdejong.org/git/python-stdnum/commit/?id=9ab1d661f78784b59dfb7118e58f2ddecc59bf8e

commit 9ab1d661f78784b59dfb7118e58f2ddecc59bf8e
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Oct 22 15:54:30 2017 +0200

    Add tests for the VIES VAT validation functions
    
    These tests are not normally run as part of the normal test suite and
    have to be explicitly enabled by setting the ONLINE_TESTS environment
    variable to avoid overloading these online services.

diff --git a/stdnum/eu/vat.py b/stdnum/eu/vat.py
index 8f25841..d91a137 100644
--- a/stdnum/eu/vat.py
+++ b/stdnum/eu/vat.py
@@ -109,7 +109,7 @@ def guess_country(number):
             if _get_cc_module(cc).is_valid(number)]
 
 
-def check_vies(number):  # pragma: no cover (no tests for this function)
+def check_vies(number):  # pragma: no cover (not part of normal test suite)
     """Query the online European Commission VAT Information Exchange System
     (VIES) for validity of the provided number. Note that the service has
     usage limitations (see the VIES website for details). This returns a
diff --git a/tests/test_eu_vat.py b/tests/test_eu_vat.py
new file mode 100644
index 0000000..ff0f5dc
--- /dev/null
+++ b/tests/test_eu_vat.py
@@ -0,0 +1,49 @@
+# test_eu_vat.py - functions for testing the online VIES VAT validation
+# coding: utf-8
+#
+# Copyright (C) 2017 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+# This is a separate test file because it should not be run regularly
+# because it could negatively impact the VIES service.
+
+"""Extra tests for the stdnum.eu.vat module."""
+
+import os
+import unittest
+
+from stdnum.eu import vat
+
+
+@unittest.skipIf(
+    not os.environ.get('ONLINE_TESTS'),
+    'Do not overload online services')
+class TestVies(unittest.TestCase):
+    """Test the VIES web service provided by the European commission for
+    validation VAT numbers of European countries."""
+
+    def test_check_vies(self):
+        """Test stdnum.eu.vat.check_vies()"""
+        result = vat.check_vies('BE555445')
+        self.assertEqual(result['countryCode'], 'BE')
+        self.assertEqual(result['vatNumber'], '555445')
+
+    def test_check_vies_approx(self):
+        """Test stdnum.eu.vat.check_vies_approx()"""
+        result = vat.check_vies_approx('BE555445', 'BE555445')
+        self.assertEqual(result['countryCode'], 'BE')
+        self.assertEqual(result['vatNumber'], '555445')

https://arthurdejong.org/git/python-stdnum/commit/?id=ab21159de6210da7746fc56268f34d2bee210e7b

commit ab21159de6210da7746fc56268f34d2bee210e7b
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Wed Oct 18 23:42:47 2017 +0200

    Add stdnum.do.ncf.check_dgii()
    
    This adds functions for querying the Dirección General de Impuestos
    Internos (DGII) API to check if the RNC and NCF combination provided is
    valid.

diff --git a/stdnum/do/ncf.py b/stdnum/do/ncf.py
index b0959d4..9f37a4c 100644
--- a/stdnum/do/ncf.py
+++ b/stdnum/do/ncf.py
@@ -40,8 +40,10 @@ Traceback (most recent call last):
 InvalidFormat: ...
 """
 
+import json
+
 from stdnum.exceptions import *
-from stdnum.util import clean
+from stdnum.util import clean, get_soap_client
 
 
 def compact(number):
@@ -80,3 +82,51 @@ def is_valid(number):
         return bool(validate(number))
     except ValidationError:
         return False
+
+
+def _convert_result(result):  # pragma: no cover
+    """Translate SOAP result entries into dictionaries."""
+    translation = {
+        'NOMBRE': 'name',
+        'COMPROBANTE': 'proof',
+        'ES_VALIDO': 'is_valid',
+        'MENSAJE_VALIDACION': 'validation_message',
+        'RNC': 'rnc',
+        'NCF': 'ncf',
+    }
+    return {
+        translation.get(key, key): value
+        for key, value in json.loads(result.replace('\t', '\\t')).items()}
+
+
+def check_dgii(rnc, ncf):  # pragma: no cover
+    """Validate the RNC, NCF combination on using the DGII online web service.
+
+    This uses the validation service run by the the Dirección General de
+    Impuestos Internos, the Dominican Republic tax department to check
+    whether the combination of RNC and NCF is valid.
+
+    Returns a dict with the following structure::
+
+        {
+            'name': 'The registered name',
+            'proof': 'Source of the information',
+            'is_valid': '1',
+            'validation_message': '',
+            'rnc': '123456789',
+            'ncf': 'A020010210100000005'
+        }
+
+    Will return None if the number is invalid or unknown."""
+    from stdnum.do.rnc import compact as rnc_compact
+    from stdnum.do.rnc import dgii_wsdl
+    rnc = rnc_compact(rnc)
+    ncf = compact(ncf)
+    client = get_soap_client(dgii_wsdl)
+    result = client.GetNCF(
+        RNC=rnc,
+        NCF=ncf,
+        IMEI='')
+    if result == '0':
+        return
+    return _convert_result(result)
diff --git a/tests/test_do_ncf.py b/tests/test_do_ncf.py
new file mode 100644
index 0000000..5f57bb0
--- /dev/null
+++ b/tests/test_do_ncf.py
@@ -0,0 +1,56 @@
+# test_do_ncf.py - functions for testing the online NCF validation
+# coding: utf-8
+#
+# Copyright (C) 2017 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+# This is a separate test file because it should not be run regularly
+# because it could negatively impact the online service.
+
+"""Extra tests for the stdnum.do.ncf module."""
+
+import os
+import unittest
+
+from stdnum.do import ncf
+from stdnum.exceptions import *
+
+
+@unittest.skipIf(
+    not os.environ.get('ONLINE_TESTS'),
+    'Do not overload online services')
+class TestDGII(unittest.TestCase):
+    """Test the web services provided by the the Dirección General de
+    Impuestos Internos (DGII), the Dominican Republic tax department."""
+
+    def test_check_dgii(self):
+        """Test stdnum.do.ncf.check_dgii()"""
+        # Test a normal valid number
+        result = ncf.check_dgii('130546312', 'A010010011500000038')
+        self.assertTrue(all(
+            key in result.keys()
+            for key in ['name', 'proof', 'is_valid', 'validation_message', 
'rnc', 'ncf']))
+        self.assertEqual(result['rnc'], '130546312')
+        self.assertEqual(result['ncf'], 'A010010011500000038')
+        # Test an invalid combination
+        self.assertIsNone(ncf.check_dgii('501620371', 'A010010011500000038'))
+        # Another valid example
+        self.assertTrue(ncf.check_dgii('1-31-56633-2', 'A010010010100000001'))
+        self.assertTrue(ncf.check_dgii('1-31-56633-2', 'A010010010100000100'))
+        # These types have not been requested with the regulator
+        self.assertFalse(ncf.check_dgii('1-31-56633-2', 'A030010010100000001'))
+        self.assertFalse(ncf.check_dgii('1-31-56633-2', 'A010020010100000001'))

https://arthurdejong.org/git/python-stdnum/commit/?id=6b09c5d744d983da96c3c46a1e081e29db6c9f28

commit 6b09c5d744d983da96c3c46a1e081e29db6c9f28
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Wed Oct 18 22:16:20 2017 +0200

    Add stdnum.do.rnc.check_dgii() and search_dgii()
    
    This adds functions for querying the Dirección General de Impuestos
    Internos (DGII) API to validate the RNC and search the register by
    keyword.

diff --git a/stdnum/do/rnc.py b/stdnum/do/rnc.py
index b19d8ea..0e0fde9 100644
--- a/stdnum/do/rnc.py
+++ b/stdnum/do/rnc.py
@@ -1,4 +1,5 @@
 # rnc.py - functions for handling Dominican Republic tax registration
+# coding: utf-8
 #
 # Copyright (C) 2015-2017 Arthur de Jong
 #
@@ -17,6 +18,8 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 # 02110-1301 USA
 
+# Development of this functionality was funded by iterativo | 
http://iterativo.do
+
 """RNC (Registro Nacional del Contribuyente, Dominican Republic tax number).
 
 The RNC is the Dominican Republic taxpayer registration number for
@@ -36,8 +39,10 @@ InvalidChecksum: ...
 '1-31-24679-6'
 """
 
+import json
+
 from stdnum.exceptions import *
-from stdnum.util import clean
+from stdnum.util import clean, get_soap_client
 
 
 # list of RNCs that do not match the checksum but are nonetheless valid
@@ -49,6 +54,10 @@ whitelist = set('''
 '''.split())
 
 
+dgii_wsdl = 'http://www.dgii.gov.do/wsMovilDGII/WSMovilDGII.asmx?WSDL'
+"""The WSDL URL of DGII validation service."""
+
+
 def compact(number):
     """Convert the number to the minimal representation. This strips the
     number of any valid separators and removes surrounding whitespace."""
@@ -88,3 +97,93 @@ def format(number):
     """Reformat the number to the standard presentation format."""
     number = compact(number)
     return '-'.join((number[:1], number[1:3], number[3:-1], number[-1]))
+
+
+def _convert_result(result):  # pragma: no cover
+    """Translate SOAP result entries into dicts."""
+    translation = {
+        'RGE_RUC': 'rnc',
+        'RGE_NOMBRE': 'name',
+        'NOMBRE_COMERCIAL': 'commercial_name',
+        'CATEGORIA': 'category',
+        'REGIMEN_PAGOS': 'payment_regime',
+        'ESTATUS': 'status',
+        'RNUM': 'result_number',
+    }
+    return {
+        translation.get(key, key): value
+        for key, value in json.loads(result.replace('\t', '\\t')).items()}
+
+
+def check_dgii(number):  # pragma: no cover
+    """Lookup the number using the DGII online web service.
+
+    This uses the validation service run by the the Dirección General de
+    Impuestos Internos, the Dominican Republic tax department to lookup
+    registration information for the number.
+
+    Returns a dict with the following structure::
+
+        {
+            'rnc': '123456789',     # The requested number
+            'name': 'The registered name',
+            'commercial_name': 'An additional commercial name',
+            'status': '2',          # 1: inactive, 2: active
+            'category': '0',        # always 0?
+            'payment_regime': '2',  # 1: N/D, 2: NORMAL, 3: PST
+        }
+
+    Will return none if the number is invalid or unknown."""
+    # this function isn't automatically tested because it would require
+    # network access for the tests and unnecessarily load the online service
+    number = compact(number)
+    client = get_soap_client(dgii_wsdl)
+    result = '%s' % client.GetContribuyentes(
+        value=number,
+        patronBusqueda=0,   # search type: 0=by number, 1=by name
+        inicioFilas=1,      # start result (1-based)
+        filaFilas=1,        # end result
+        IMEI='')
+    if result == '0':
+        return
+    return _convert_result(result)
+
+
+def search_dgii(keyword, end_at=10, start_at=1):  # pragma: no cover
+    """Search the DGII online web service using the keyword.
+
+    This uses the validation service run by the the Dirección General de
+    Impuestos Internos, the Dominican Republic tax department to search the
+    registration information using the keyword.
+
+    The number of entries returned can be tuned with the `end_at` and
+    `start_at` arguments.
+
+    Returns a list of dicts with the following structure::
+
+        [
+            {
+                'rnc': '123456789',     # The found number
+                'name': 'The registered name',
+                'commercial_name': 'An additional commercial name',
+                'status': '2',          # 1: inactive, 2: active
+                'category': '0',        # always 0?
+                'payment_regime': '2',  # 1: N/D, 2: NORMAL, 3: PST
+                'result_number': '1',   # index of the result
+            },
+            ...
+        ]
+
+    Will return an empty list if the number is invalid or unknown."""
+    # this function isn't automatically tested because it would require
+    # network access for the tests and unnecessarily load the online service
+    client = get_soap_client(dgii_wsdl)
+    results = '%s' % client.GetContribuyentes(
+        value=keyword,
+        patronBusqueda=1,       # search type: 0=by number, 1=by name
+        inicioFilas=start_at,   # start result (1-based)
+        filaFilas=end_at,       # end result
+        IMEI='')
+    if results == '0':
+        return []
+    return [_convert_result(result) for result in results.split('@@@')]
diff --git a/tests/test_do_rnc.py b/tests/test_do_rnc.py
new file mode 100644
index 0000000..52e2393
--- /dev/null
+++ b/tests/test_do_rnc.py
@@ -0,0 +1,76 @@
+# test_do_rnc.py - functions for testing the online RNC validation
+# coding: utf-8
+#
+# Copyright (C) 2017 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+# This is a separate test file because it should not be run regularly
+# because it could negatively impact the online service.
+
+"""Extra tests for the stdnum.do.rnc module."""
+
+import os
+import unittest
+
+from stdnum.do import rnc
+from stdnum.exceptions import *
+
+
+@unittest.skipIf(
+    not os.environ.get('ONLINE_TESTS'),
+    'Do not overload online services')
+class TestDGII(unittest.TestCase):
+    """Test the web services provided by the the Dirección General de
+    Impuestos Internos (DGII), the Dominican Republic tax department."""
+
+    def test_check_dgii(self):
+        """Test stdnum.do.rnc.check_dgii()"""
+        # Test a normal valid number
+        result = rnc.check_dgii('131098193')
+        self.assertTrue(all(
+            key in result.keys()
+            for key in ['rnc', 'name', 'commercial_name', 'category', 
'status']))
+        self.assertEqual(result['rnc'], '131098193')
+        # Test an invalid length number
+        self.assertIsNone(rnc.check_dgii('123'))
+        # Test a number with an invalid checksum
+        self.assertIsNone(rnc.check_dgii('112031226'))
+        # Valid number but unknown
+        self.assertIsNone(rnc.check_dgii('814387152'))
+        # Test a number on the whitelist
+        result = rnc.check_dgii('501658167')
+        self.assertEqual(result['rnc'], '501658167')
+
+    def test_search_dgii(self):
+        """Test stdnum.do.rnc.search_dgii()"""
+        # Search for some existing companies
+        results = rnc.search_dgii('EXPORT DE')
+        self.assertGreaterEqual(len(results), 3)
+        self.assertRegexpMatches(results[0]['rnc'], r'\d{9}')
+        self.assertRegexpMatches(results[1]['rnc'], r'\d{9}')
+        self.assertRegexpMatches(results[2]['rnc'], r'\d{9}')
+        # Check maximum rows parameter
+        two_results = rnc.search_dgii('EXPORT DE', end_at=2)
+        self.assertEqual(len(two_results), 2)
+        self.assertEqual(two_results, results[:2])
+        # Check the start_at parameter
+        two_results = rnc.search_dgii('EXPORT DE', end_at=3, start_at=2)
+        self.assertEqual(len(two_results), 2)
+        self.assertEqual(two_results, results[1:3])
+        # Check non-existing company
+        results = rnc.search_dgii('NON-EXISTING COMPANY')
+        self.assertEqual(results, [])
diff --git a/tox.ini b/tox.ini
index d67119e..40a3f1c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,4 +26,4 @@ deps = flake8
        flake8-tidy-imports
        flake8-tuple
        pep8-naming
-commands = flake8 stdnum *.py
+commands = flake8 stdnum tests *.py

https://arthurdejong.org/git/python-stdnum/commit/?id=665bf7a403df0e2e61b5f4a245675b0c641170a2

commit 665bf7a403df0e2e61b5f4a245675b0c641170a2
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Mon Oct 16 22:45:32 2017 +0200

    Add Dominican Republic receipt number (NCF)
    
    This number does not have a check digit but uses a distinctive enough
    format that it should not be too great of a problem.

diff --git a/stdnum/do/ncf.py b/stdnum/do/ncf.py
new file mode 100644
index 0000000..b0959d4
--- /dev/null
+++ b/stdnum/do/ncf.py
@@ -0,0 +1,82 @@
+# ncf.py - functions for handling Dominican Republic invoice numbers
+# coding: utf-8
+#
+# Copyright (C) 2017 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+# Development of this functionality was funded by iterativo | 
http://iterativo.do
+
+"""NCF (Números de Comprobante Fiscal, Dominican Republic receipt number).
+
+The NCF is used to number invoices and other documents for the purposes of
+tax filing. The number is 19 digits long and consists of a letter (A or P) to
+indicate that the number was assigned by the taxpayer or the DGIT, followed a
+2-digit business unit number, a 3-digit location number, a 3-digit mechanism
+identifier, a 2-digit document type and a 8-digit serial number.
+
+More information:
+
+ * 
https://www.dgii.gov.do/et/nivelContribuyentes/Presentaciones%20contribuyentes/Número%20de%20Comprobantes%20Fiscales%20(NCF).pdf
+
+>>> validate('A020010210100000005')
+'A020010210100000005'
+>>> validate('Z020010210100000005')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...
+"""
+
+from stdnum.exceptions import *
+from stdnum.util import clean
+
+
+def compact(number):
+    """Convert the number to the minimal representation. This strips the
+    number of any valid separators and removes surrounding whitespace."""
+    return clean(number, ' ').strip().upper()
+
+
+# The following document types are known:
+#  01 invoices for fiscal declaration (or tax reporting)
+#  02 invoices for final consumer
+#  03 debit note
+#  04 credit note (refunds)
+#  11 informal supplier invoices (purchases)
+#  12 single income record
+#  13 minor expenses invoices (purchases)
+#  14 invoices for special customers (tourists, free zones)
+#  15 invoices for the government
+
+def validate(number):
+    """Check if the number provided is a valid NCF."""
+    number = compact(number)
+    if len(number) != 19:
+        raise InvalidLength()
+    if number[0] not in 'AP' or not number[1:].isdigit():
+        raise InvalidFormat()
+    if number[9:11] not in (
+            '01', '02', '03', '04', '11', '12', '13', '14', '15'):
+        raise InvalidComponent()
+    return number
+
+
+def is_valid(number):
+    """Check if the number provided is a valid NCF."""
+    try:
+        return bool(validate(number))
+    except ValidationError:
+        return False
diff --git a/tests/test_do_ncf.doctest b/tests/test_do_ncf.doctest
new file mode 100644
index 0000000..1c815cf
--- /dev/null
+++ b/tests/test_do_ncf.doctest
@@ -0,0 +1,159 @@
+test_do_ncf.doctest - more detailed doctests for stdnum.do.ncf module
+
+Copyright (C) 2017 Arthur de Jong
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301 USA
+
+
+This file contains more detailed doctests for the stdnum.do.ncf module. It
+tries to cover more corner cases and detailed functionality that is not
+really useful as module documentation.
+
+>>> from stdnum.do import ncf
+
+
+Some basic tests for invalid numbers:
+
+>>> ncf.validate('FJ10010010100000004')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...
+>>> ncf.validate('A0100100101000000')
+Traceback (most recent call last):
+    ...
+InvalidLength: ...
+>>> ncf.validate('A01001001010000003234')
+Traceback (most recent call last):
+    ...
+InvalidLength: ...
+>>> ncf.validate('A010010010500000004')
+Traceback (most recent call last):
+    ...
+InvalidComponent: ...
+>>> ncf.validate('Z010010011600000004')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...
+
+
+These have been found online and should all be valid numbers.
+
+>>> numbers = '''
+...
+... A010010010100000052
+... A010010010100001688
+... A010010010100003508
+... A010010010100003509
+... A010010011500000008
+... A010010011500000012
+... A010010011500000019
+... A010010011500000025
+... A010010011500000027
+... A010010011500000031
+... A010010011500000037
+... A010010011500000042
+... A010010011500000043
+... A010010011500000059
+... A010010011500000063
+... A010010011500000066
+... A010010011500000071
+... A010010011500000081
+... A010010011500000084
+... A010010011500000105
+... A010010011500000108
+... A010010011500000128
+... A010010011500000149
+... A010010011500000154
+... A010010011500000200
+... A010010011500000205
+... A010010011500000232
+... A010010011500000293
+... A010010011500000320
+... A010010011500000329
+... A010010011500000535
+... A010010011500000547
+... A010010011500000557
+... A010010011500000828
+... A010010011500000829
+... A010010011500000830
+... A010010011500000832
+... A010010011500000840
+... A010010011500000841
+... A010010011500000843
+... A010010011500000896
+... A010010011500000925
+... A010010011500000942
+... A010010011500001003
+... A010010011500001082
+... A010010011500001101
+... A010010011500001495
+... A010010011500002048
+... A010010011500002056
+... A010010011500002061
+... A010010011500002246
+... A010010011500002309
+... A010010011500002314
+... A010010011500002321
+... A010010011500002392
+... A010010011500003273
+... A010010011500003274
+... A010010011500003515
+... A010010011500003677
+... A010010011500004073
+... A010010011500004151
+... A010010011500004343
+... A010010011500004745
+... A010010011500005192
+... A010010011500005248
+... A010010011500005445
+... A010010011500005640
+... A010010011500005727
+... A010010011500006427
+... A010010011500007175
+... A010010011500007508
+... A010010011500007510
+... A010010011500008501
+... A010010011500012641
+... A010010011500012649
+... A010010011500013333
+... A010010011500042367
+... A010010011501344909
+... A010010011501344910
+... A010010011501500383
+... A010010031500051556
+... A010020011500000727
+... A010020011500000734
+... A010070051500004287
+... A010070051500004896
+... A010070051500004909
+... A010070051500004929
+... A020010011500002311
+... A020010011500003095
+... A020010011500024073
+... A020010011500431422
+... A020010230100003922
+... A020020011500000180
+... A020020011500000181
+... A030030011500002297
+... A040010011500010708
+... A040010011500012279
+... P010010011501235539
+... P010010011501528319
+... P010010011501528378
+...
+... '''
+>>> [x for x in numbers.splitlines() if x and not ncf.is_valid(x)]
+[]

https://arthurdejong.org/git/python-stdnum/commit/?id=4ab1e3b538460078c87878b730b2ac06e6d1f9cd

commit 4ab1e3b538460078c87878b730b2ac06e6d1f9cd
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sat Oct 14 18:55:20 2017 +0200

    Cache SOAP client in get_soap_client()
    
    This caches the instantiated SOAP client classes in the util module
    instead of doing the caching in every module that performs requests.

diff --git a/stdnum/eu/vat.py b/stdnum/eu/vat.py
index 3a6a2eb..8f25841 100644
--- a/stdnum/eu/vat.py
+++ b/stdnum/eu/vat.py
@@ -56,9 +56,6 @@ _country_modules = dict()
 vies_wsdl = 'http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl'
 """The WSDL URL of the VAT Information Exchange System (VIES)."""
 
-# a cached version of the suds client for VIES
-_vies_client = None
-
 
 def _get_cc_module(cc):
     """Get the VAT number module based on the country code."""
@@ -112,16 +109,6 @@ def guess_country(number):
             if _get_cc_module(cc).is_valid(number)]
 
 
-def _get_client():  # pragma: no cover (no tests for this function)
-    """Get a SOAP client for performing VIES requests."""
-    # this function isn't automatically tested because the functions using
-    # it are not automatically tested
-    global _vies_client
-    if _vies_client is None:
-        _vies_client = get_soap_client(vies_wsdl)
-    return _vies_client
-
-
 def check_vies(number):  # pragma: no cover (no tests for this function)
     """Query the online European Commission VAT Information Exchange System
     (VIES) for validity of the provided number. Note that the service has
@@ -130,7 +117,8 @@ def check_vies(number):  # pragma: no cover (no tests for 
this function)
     # this function isn't automatically tested because it would require
     # network access for the tests and unnecessarily load the VIES website
     number = compact(number)
-    return _get_client().checkVat(number[:2], number[2:])
+    client = get_soap_client(vies_wsdl)
+    return client.checkVat(number[:2], number[2:])
 
 
 def check_vies_approx(number, requester):  # pragma: no cover
@@ -143,6 +131,7 @@ def check_vies_approx(number, requester):  # pragma: no 
cover
     # network access for the tests and unnecessarily load the VIES website
     number = compact(number)
     requester = compact(requester)
-    return _get_client().checkVatApprox(
+    client = get_soap_client(vies_wsdl)
+    return client.checkVatApprox(
         countryCode=number[:2], vatNumber=number[2:],
         requesterCountryCode=requester[:2], requesterVatNumber=requester[2:])
diff --git a/stdnum/tr/tckimlik.py b/stdnum/tr/tckimlik.py
index 26eebf3..dc35922 100644
--- a/stdnum/tr/tckimlik.py
+++ b/stdnum/tr/tckimlik.py
@@ -53,10 +53,6 @@ tckimlik_wsdl = 
'https://tckimlik.nvi.gov.tr/Service/KPSPublic.asmx?WSDL'
 """The WSDL URL of the T.C. Kimlik validation service."""
 
 
-# a cached version of the SOAP client for Kimlik validation
-_tckimlik_client = None
-
-
 def compact(number):
     """Convert the number to the minimal representation. This strips the
     number of any valid separators and removes surrounding whitespace."""
@@ -93,16 +89,6 @@ def is_valid(number):
         return False
 
 
-def _get_client():  # pragma: no cover (no tests for this function)
-    """Get a SOAP client for performing T.C. Kimlik validation."""
-    # this function isn't automatically tested because the functions using
-    # it are not automatically tested
-    global _tckimlik_client
-    if _tckimlik_client is None:
-        _tckimlik_client = get_soap_client(tckimlik_wsdl)
-    return _tckimlik_client
-
-
 def check_kps(number, name, surname, birth_year):  # pragma: no cover
     """Query the online T.C. Kimlik validation service run by the Directorate
     of Population and Citizenship Affairs. This returns a boolean but may
@@ -110,7 +96,8 @@ def check_kps(number, name, surname, birth_year):  # pragma: 
no cover
     # this function isn't automatically tested because it would require
     # network access for the tests and unnecessarily load the online service
     number = compact(number)
-    result = _get_client().TCKimlikNoDogrula(
+    client = get_soap_client(tckimlik_wsdl)
+    result = client.TCKimlikNoDogrula(
         TCKimlikNo=number, Ad=name, Soyad=surname, DogumYili=birth_year)
     if hasattr(result, 'get'):
         return result.get('TCKimlikNoDogrulaResult')
diff --git a/stdnum/util.py b/stdnum/util.py
index 7338557..b410d4f 100644
--- a/stdnum/util.py
+++ b/stdnum/util.py
@@ -165,19 +165,26 @@ def get_cc_module(cc, name):
         return
 
 
+# this is a cache of SOAP clients
+_soap_clients = {}
+
+
 def get_soap_client(wsdlurl):  # pragma: no cover (no tests for this function)
-    """Get a SOAP client for performing requests."""
+    """Get a SOAP client for performing requests. The client is cached."""
     # this function isn't automatically tested because the functions using
     # it are not automatically tested
-    try:
-        from urllib import getproxies
-    except ImportError:
-        from urllib.request import getproxies
-    # try suds first
-    try:
-        from suds.client import Client
-        return Client(wsdlurl, proxy=getproxies()).service
-    except ImportError:
-        # fall back to using pysimplesoap
-        from pysimplesoap.client import SoapClient
-        return SoapClient(wsdl=wsdlurl, proxy=getproxies())
+    if wsdlurl not in _soap_clients:
+        try:
+            from urllib import getproxies
+        except ImportError:
+            from urllib.request import getproxies
+        # try suds first
+        try:
+            from suds.client import Client
+            client = Client(wsdlurl, proxy=getproxies()).service
+        except ImportError:
+            # fall back to using pysimplesoap
+            from pysimplesoap.client import SoapClient
+            client = SoapClient(wsdl=wsdlurl, proxy=getproxies())
+        _soap_clients[wsdlurl] = client
+    return _soap_clients[wsdlurl]

-----------------------------------------------------------------------

Summary of changes:
 stdnum/do/ncf.py          | 132 ++++++++++++++++++++++++++++++++++++++
 stdnum/do/rnc.py          | 101 ++++++++++++++++++++++++++++-
 stdnum/eu/vat.py          |  21 ++----
 stdnum/tr/tckimlik.py     |  17 +----
 stdnum/util.py            |  40 ++++++++----
 tests/test_do_ncf.doctest | 159 ++++++++++++++++++++++++++++++++++++++++++++++
 tests/test_do_ncf.py      |  56 ++++++++++++++++
 tests/test_do_rnc.py      |  76 ++++++++++++++++++++++
 tests/test_eu_vat.py      |  49 ++++++++++++++
 tox.ini                   |   2 +-
 10 files changed, 606 insertions(+), 47 deletions(-)
 create mode 100644 stdnum/do/ncf.py
 create mode 100644 tests/test_do_ncf.doctest
 create mode 100644 tests/test_do_ncf.py
 create mode 100644 tests/test_do_rnc.py
 create mode 100644 tests/test_eu_vat.py


hooks/post-receive
-- 
python-stdnum
-- 
To unsubscribe send an email to
python-stdnum-commits-unsubscribe@lists.arthurdejong.org or see
https://lists.arthurdejong.org/python-stdnum-commits/