diff --git a/.gitignore b/.gitignore index 8e010b6..b022024 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ __pycache__ build dist *.egg-info +venv/ diff --git a/.travis.yml b/.travis.yml index adada43..3b30845 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,6 @@ python: install: - pip install -r docs/requirements.txt - pip install . -script: pytest --cov=./privex tests.py +script: pytest --cov=./privex -v after_success: - codecov diff --git a/README.md b/README.md index e5f18ac..859195f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Documentation Status](https://readthedocs.org/projects/python-helpers/badge/?version=latest)](https://python-helpers.readthedocs.io/en/latest/?badge=latest) [![Build Status](https://travis-ci.com/Privex/python-helpers.svg?branch=master)](https://travis-ci.com/Privex/python-helpers) -[![Codecov](https://img.shields.io/codecov/c/github/Privex/python-helpers)](https://codecov.io/gh/Privex/python-helpers) +[![Codecov](https://img.shields.io/codecov/c/github/Privex/python-helpers)](https://codecov.io/gh/Privex/python-helpers) [![PyPi Version](https://img.shields.io/pypi/v/privex-helpers.svg)](https://pypi.org/project/privex-helpers/) ![License Button](https://img.shields.io/pypi/l/privex-helpers) ![PyPI - Downloads](https://img.shields.io/pypi/dm/privex-helpers) @@ -12,7 +12,7 @@ This small Python 3 module is comprised of various small functions and classes that were often copied and pasted across our projects. -Each of these "helper" functions, decorators or classes are otherwise too small to be independantly +Each of these "helper" functions, decorators or classes are otherwise too small to be independently packaged, and so we've amalgamated them into this PyPi package, `privex-helpers`. @@ -34,7 +34,15 @@ packaged, and so we've amalgamated them into this PyPi package, `privex-helpers` # Install -### Download and install from PyPi using pip (recommended) +### Download and install from PyPi + +**Using [Pipenv](https://pipenv.kennethreitz.org/en/latest/) (recommended)** + +```sh +pipenv install privex-helpers +``` + +**Using standard Python pip** ```sh pip3 install privex-helpers diff --git a/docs/source/examples.rst b/docs/source/examples.rst new file mode 100644 index 0000000..8268493 --- /dev/null +++ b/docs/source/examples.rst @@ -0,0 +1,101 @@ +############## +Example Usages +############## + +Boolean testing +=============== + +The ``empty`` function +----------------------- + +The :func:`empty` function in our opinion, is one of most useful functions in this library. It allows for a clean, +readable method of checking if a variable is "empty", e.g. when checking keyword arguments to a function. + +With a single argument, it simply tests if a variable is ``""`` (empty string) or ``None``. + +The argument ``itr`` can be set to ``True`` if you consider an empty iterable such as ``[]`` or ``{}`` as "empty". This +functionality also supports objects which implement ``__len__``, and also checks to ensure ``__len__`` is available, +avoiding an exception if an object doesn't support it. + +The argument ``zero`` can be set to ``True`` if you want to consider ``0`` (integer) and ``'0'`` (string) as "empty". + +.. code-block:: python + + from privex.helpers import empty + + x, y = "", None + z, a = [], 0 + + empty(x) # True + empty(y) # True + empty(z) # False + empty(z, itr=True) # True + empty(a) # False + empty(a, zero=True) # True + + +The ``is_true`` and ``is_false`` functions +------------------------------------------ + +When handling user input, whether from an environment file (``.env``), or from data passed to a web API, it can be +a pain attempting to check for booleans. + +A boolean ``True`` could be represented as the string ``'true'``, ``'1'``, ``'YES'``, as an integer ``1``, or even +an actual boolean ``True``. Trying to test for all of those cases requires a rather long ``if`` statement... + +Thus :func:`.is_true` and :func:`.is_false` were created. + +.. code-block:: python + + from privex.helpers import is_true, is_false + + is_true(0) # False + is_true(1) # True + is_true('1') # True + is_true('true') # True + is_true('false') # False + is_true('orange') # False + is_true('YeS') # True + + is_false(0) # True + is_false('false') # True + is_false('true') # False + is_false(False) # True + + +Handling environmental variables in different formats +===================================================== + +Using ``env_csv`` to support lists contained within an env var +--------------------------------------------------------------- + +The function :func:`.env_csv` parses a CSV-like environment variable into a list + +.. code-block:: python + + from privex.helpers import env_csv + import os + os.environ['EXAMPLE'] = "this, is, an,example " + + env_csv('EXAMPLE', ['error']) + # returns: ['this', 'is', 'an', 'example'] + env_csv('NOEXIST', ['non-existent']) + # returns: ['non-existent'] + +Using ``env_keyval`` to support dictionaries contained within an env var +------------------------------------------------------------------------ + +The function :func:`.env_keyval` parses an environment variable into a ordered list of tuple pairs, which can be +easily converted into a dictionary using ``dict()``. + +.. code-block:: python + + from privex.helpers import env_keyval + import os + os.environ['EXAMPLE'] = "John: Doe , Jane : Doe, Aaron:Smith" + + env_keyval('EXAMPLE') + # returns: [('John', 'Doe'), ('Jane', 'Doe'), ('Aaron', 'Smith')] + env_keyval('NOEXIST', {}) + # returns: {} + diff --git a/docs/source/helpers/common/privex.helpers.common.Dictable.rst b/docs/source/helpers/common/privex.helpers.common.Dictable.rst new file mode 100644 index 0000000..a705a1e --- /dev/null +++ b/docs/source/helpers/common/privex.helpers.common.Dictable.rst @@ -0,0 +1,23 @@ +Dictable +======== + +.. currentmodule:: privex.helpers.common + +.. autoclass:: Dictable + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~Dictable.__init__ + ~Dictable.from_dict + + + + + + \ No newline at end of file diff --git a/docs/source/helpers/common/privex.helpers.common.ErrHelpParser.rst b/docs/source/helpers/common/privex.helpers.common.ErrHelpParser.rst new file mode 100644 index 0000000..bfede4a --- /dev/null +++ b/docs/source/helpers/common/privex.helpers.common.ErrHelpParser.rst @@ -0,0 +1,40 @@ +ErrHelpParser +============= + +.. currentmodule:: privex.helpers.common + +.. autoclass:: ErrHelpParser + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~ErrHelpParser.__init__ + ~ErrHelpParser.add_argument + ~ErrHelpParser.add_argument_group + ~ErrHelpParser.add_mutually_exclusive_group + ~ErrHelpParser.add_subparsers + ~ErrHelpParser.convert_arg_line_to_args + ~ErrHelpParser.error + ~ErrHelpParser.exit + ~ErrHelpParser.format_help + ~ErrHelpParser.format_usage + ~ErrHelpParser.get_default + ~ErrHelpParser.parse_args + ~ErrHelpParser.parse_intermixed_args + ~ErrHelpParser.parse_known_args + ~ErrHelpParser.parse_known_intermixed_args + ~ErrHelpParser.print_help + ~ErrHelpParser.print_usage + ~ErrHelpParser.register + ~ErrHelpParser.set_defaults + + + + + + \ No newline at end of file diff --git a/docs/source/helpers/common/privex.helpers.common.inject_items.rst b/docs/source/helpers/common/privex.helpers.common.inject_items.rst new file mode 100644 index 0000000..3da94f5 --- /dev/null +++ b/docs/source/helpers/common/privex.helpers.common.inject_items.rst @@ -0,0 +1,6 @@ +inject\_items +============= + +.. currentmodule:: privex.helpers.common + +.. autofunction:: inject_items \ No newline at end of file diff --git a/docs/source/helpers/index.rst b/docs/source/helpers/index.rst index 0c4632a..5aeea23 100644 --- a/docs/source/helpers/index.rst +++ b/docs/source/helpers/index.rst @@ -1,5 +1,3 @@ -Code Docs for each helper module -================================= .. autosummary:: :toctree: @@ -14,10 +12,4 @@ Code Docs for each helper module privex.helpers.plugin privex.helpers.settings -Unit Tests -========== -.. autosummary:: - :toctree: - - tests diff --git a/docs/source/helpers/modules.rst b/docs/source/helpers/modules.rst deleted file mode 100644 index f2e4551..0000000 --- a/docs/source/helpers/modules.rst +++ /dev/null @@ -1,8 +0,0 @@ -helpers -======= - -.. toctree:: - :maxdepth: 4 - - privex - tests diff --git a/docs/source/helpers/privex.helpers.cache.rst b/docs/source/helpers/privex.helpers.cache.rst index b557452..e17cfaf 100644 --- a/docs/source/helpers/privex.helpers.cache.rst +++ b/docs/source/helpers/privex.helpers.cache.rst @@ -1,5 +1,10 @@ -privex.helpers.cache -==================== +Cache Abstraction Layer +======================= + +**Module:** ``privex.helpers.cache`` + +Classes +^^^^^^^ .. autosummary:: :toctree: cache @@ -10,7 +15,7 @@ privex.helpers.cache Functions ---------- +^^^^^^^^^ .. automodule:: privex.helpers.cache diff --git a/docs/source/helpers/privex.helpers.common.rst b/docs/source/helpers/privex.helpers.common.rst index b6b7754..ed738ff 100644 --- a/docs/source/helpers/privex.helpers.common.rst +++ b/docs/source/helpers/privex.helpers.common.rst @@ -21,6 +21,7 @@ privex.helpers.common env_keyval is_false is_true + inject_items parse_csv parse_keyval random_str @@ -32,7 +33,8 @@ privex.helpers.common .. rubric:: Classes .. autosummary:: - + :toctree: common + ErrHelpParser Dictable diff --git a/docs/source/helpers/privex.helpers.rst b/docs/source/helpers/privex.helpers.rst deleted file mode 100644 index 69621fc..0000000 --- a/docs/source/helpers/privex.helpers.rst +++ /dev/null @@ -1,93 +0,0 @@ -privex.helpers package -====================== - -Subpackages ------------ - -.. toctree:: - - privex.helpers.cache - -Submodules ----------- - -privex.helpers.asyncx module ----------------------------- - -.. automodule:: privex.helpers.asyncx - :members: - :undoc-members: - :show-inheritance: - -privex.helpers.cache module ---------------------------- - -.. automodule:: privex.helpers.cache - :members: - :undoc-members: - :show-inheritance: - -privex.helpers.common module ----------------------------- - -.. automodule:: privex.helpers.common - :members: - :undoc-members: - :show-inheritance: - -privex.helpers.decorators module --------------------------------- - -.. automodule:: privex.helpers.decorators - :members: - :undoc-members: - :show-inheritance: - -privex.helpers.django module ----------------------------- - -.. automodule:: privex.helpers.django - :members: - :undoc-members: - :show-inheritance: - -privex.helpers.exceptions module --------------------------------- - -.. automodule:: privex.helpers.exceptions - :members: - :undoc-members: - :show-inheritance: - -privex.helpers.net module -------------------------- - -.. automodule:: privex.helpers.net - :members: - :undoc-members: - :show-inheritance: - -privex.helpers.plugin module ----------------------------- - -.. automodule:: privex.helpers.plugin - :members: - :undoc-members: - :show-inheritance: - -privex.helpers.settings module ------------------------------- - -.. automodule:: privex.helpers.settings - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: privex.helpers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/helpers/tests.rst b/docs/source/helpers/tests.rst index bf854c0..ffe428d 100644 --- a/docs/source/helpers/tests.rst +++ b/docs/source/helpers/tests.rst @@ -1,26 +1,23 @@ -tests -===== +How to use the unit tests +------------------------- .. automodule:: tests - - - .. rubric:: Classes - - .. autosummary:: - :toctree: tests - - EmptyIter - TestBoolHelpers - TestGeneral - TestIPReverseDNS - TestParseHelpers - TestCacheDecoratorMemory - TestCacheDecoratorRedis - TestMemoryCache - TestRedisCache - PrivexBaseCase - - + +Unit Test List / Overview +------------------------- + +.. autosummary:: + :toctree: tests + + tests.base + tests.test_bool + tests.test_cache + tests.test_general + tests.test_parse + tests.test_rdns + + + diff --git a/docs/source/helpers/tests/tests.EmptyIter.rst b/docs/source/helpers/tests/tests.EmptyIter.rst deleted file mode 100644 index b15f4e7..0000000 --- a/docs/source/helpers/tests/tests.EmptyIter.rst +++ /dev/null @@ -1,22 +0,0 @@ -EmptyIter -========= - -.. currentmodule:: tests - -.. autoclass:: EmptyIter - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~EmptyIter.__init__ - - - - - - \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.PrivexBaseCase.rst b/docs/source/helpers/tests/tests.PrivexBaseCase.rst deleted file mode 100644 index 9e2dc49..0000000 --- a/docs/source/helpers/tests/tests.PrivexBaseCase.rst +++ /dev/null @@ -1,99 +0,0 @@ -PrivexBaseCase -============== - -.. currentmodule:: tests - -.. autoclass:: PrivexBaseCase - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~PrivexBaseCase.__init__ - ~PrivexBaseCase.addCleanup - ~PrivexBaseCase.addTypeEqualityFunc - ~PrivexBaseCase.assertAlmostEqual - ~PrivexBaseCase.assertAlmostEquals - ~PrivexBaseCase.assertCountEqual - ~PrivexBaseCase.assertDictContainsSubset - ~PrivexBaseCase.assertDictEqual - ~PrivexBaseCase.assertEqual - ~PrivexBaseCase.assertEquals - ~PrivexBaseCase.assertFalse - ~PrivexBaseCase.assertGreater - ~PrivexBaseCase.assertGreaterEqual - ~PrivexBaseCase.assertIn - ~PrivexBaseCase.assertIs - ~PrivexBaseCase.assertIsInstance - ~PrivexBaseCase.assertIsNone - ~PrivexBaseCase.assertIsNot - ~PrivexBaseCase.assertIsNotNone - ~PrivexBaseCase.assertLess - ~PrivexBaseCase.assertLessEqual - ~PrivexBaseCase.assertListEqual - ~PrivexBaseCase.assertLogs - ~PrivexBaseCase.assertMultiLineEqual - ~PrivexBaseCase.assertNotAlmostEqual - ~PrivexBaseCase.assertNotAlmostEquals - ~PrivexBaseCase.assertNotEqual - ~PrivexBaseCase.assertNotEquals - ~PrivexBaseCase.assertNotIn - ~PrivexBaseCase.assertNotIsInstance - ~PrivexBaseCase.assertNotRegex - ~PrivexBaseCase.assertNotRegexpMatches - ~PrivexBaseCase.assertRaises - ~PrivexBaseCase.assertRaisesRegex - ~PrivexBaseCase.assertRaisesRegexp - ~PrivexBaseCase.assertRegex - ~PrivexBaseCase.assertRegexpMatches - ~PrivexBaseCase.assertSequenceEqual - ~PrivexBaseCase.assertSetEqual - ~PrivexBaseCase.assertTrue - ~PrivexBaseCase.assertTupleEqual - ~PrivexBaseCase.assertWarns - ~PrivexBaseCase.assertWarnsRegex - ~PrivexBaseCase.assert_ - ~PrivexBaseCase.countTestCases - ~PrivexBaseCase.debug - ~PrivexBaseCase.defaultTestResult - ~PrivexBaseCase.doCleanups - ~PrivexBaseCase.fail - ~PrivexBaseCase.failIf - ~PrivexBaseCase.failIfAlmostEqual - ~PrivexBaseCase.failIfEqual - ~PrivexBaseCase.failUnless - ~PrivexBaseCase.failUnlessAlmostEqual - ~PrivexBaseCase.failUnlessEqual - ~PrivexBaseCase.failUnlessRaises - ~PrivexBaseCase.id - ~PrivexBaseCase.run - ~PrivexBaseCase.setUp - ~PrivexBaseCase.setUpClass - ~PrivexBaseCase.shortDescription - ~PrivexBaseCase.skipTest - ~PrivexBaseCase.subTest - ~PrivexBaseCase.tearDown - ~PrivexBaseCase.tearDownClass - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~PrivexBaseCase.empty_lst - ~PrivexBaseCase.empty_vals - ~PrivexBaseCase.empty_zero - ~PrivexBaseCase.falsey - ~PrivexBaseCase.falsey_empty - ~PrivexBaseCase.longMessage - ~PrivexBaseCase.maxDiff - ~PrivexBaseCase.truthy - - \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.TestBoolHelpers.rst b/docs/source/helpers/tests/tests.TestBoolHelpers.rst deleted file mode 100644 index 091a056..0000000 --- a/docs/source/helpers/tests/tests.TestBoolHelpers.rst +++ /dev/null @@ -1,108 +0,0 @@ -TestBoolHelpers -=============== - -.. currentmodule:: tests - -.. autoclass:: TestBoolHelpers - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~TestBoolHelpers.__init__ - ~TestBoolHelpers.addCleanup - ~TestBoolHelpers.addTypeEqualityFunc - ~TestBoolHelpers.assertAlmostEqual - ~TestBoolHelpers.assertAlmostEquals - ~TestBoolHelpers.assertCountEqual - ~TestBoolHelpers.assertDictContainsSubset - ~TestBoolHelpers.assertDictEqual - ~TestBoolHelpers.assertEqual - ~TestBoolHelpers.assertEquals - ~TestBoolHelpers.assertFalse - ~TestBoolHelpers.assertGreater - ~TestBoolHelpers.assertGreaterEqual - ~TestBoolHelpers.assertIn - ~TestBoolHelpers.assertIs - ~TestBoolHelpers.assertIsInstance - ~TestBoolHelpers.assertIsNone - ~TestBoolHelpers.assertIsNot - ~TestBoolHelpers.assertIsNotNone - ~TestBoolHelpers.assertLess - ~TestBoolHelpers.assertLessEqual - ~TestBoolHelpers.assertListEqual - ~TestBoolHelpers.assertLogs - ~TestBoolHelpers.assertMultiLineEqual - ~TestBoolHelpers.assertNotAlmostEqual - ~TestBoolHelpers.assertNotAlmostEquals - ~TestBoolHelpers.assertNotEqual - ~TestBoolHelpers.assertNotEquals - ~TestBoolHelpers.assertNotIn - ~TestBoolHelpers.assertNotIsInstance - ~TestBoolHelpers.assertNotRegex - ~TestBoolHelpers.assertNotRegexpMatches - ~TestBoolHelpers.assertRaises - ~TestBoolHelpers.assertRaisesRegex - ~TestBoolHelpers.assertRaisesRegexp - ~TestBoolHelpers.assertRegex - ~TestBoolHelpers.assertRegexpMatches - ~TestBoolHelpers.assertSequenceEqual - ~TestBoolHelpers.assertSetEqual - ~TestBoolHelpers.assertTrue - ~TestBoolHelpers.assertTupleEqual - ~TestBoolHelpers.assertWarns - ~TestBoolHelpers.assertWarnsRegex - ~TestBoolHelpers.assert_ - ~TestBoolHelpers.countTestCases - ~TestBoolHelpers.debug - ~TestBoolHelpers.defaultTestResult - ~TestBoolHelpers.doCleanups - ~TestBoolHelpers.fail - ~TestBoolHelpers.failIf - ~TestBoolHelpers.failIfAlmostEqual - ~TestBoolHelpers.failIfEqual - ~TestBoolHelpers.failUnless - ~TestBoolHelpers.failUnlessAlmostEqual - ~TestBoolHelpers.failUnlessEqual - ~TestBoolHelpers.failUnlessRaises - ~TestBoolHelpers.id - ~TestBoolHelpers.run - ~TestBoolHelpers.setUp - ~TestBoolHelpers.setUpClass - ~TestBoolHelpers.shortDescription - ~TestBoolHelpers.skipTest - ~TestBoolHelpers.subTest - ~TestBoolHelpers.tearDown - ~TestBoolHelpers.tearDownClass - ~TestBoolHelpers.test_empty_combined - ~TestBoolHelpers.test_empty_lst - ~TestBoolHelpers.test_empty_vals - ~TestBoolHelpers.test_empty_zero - ~TestBoolHelpers.test_isfalse_falsey - ~TestBoolHelpers.test_isfalse_truthy - ~TestBoolHelpers.test_istrue_falsey - ~TestBoolHelpers.test_istrue_truthy - ~TestBoolHelpers.test_notempty - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~TestBoolHelpers.empty_lst - ~TestBoolHelpers.empty_vals - ~TestBoolHelpers.empty_zero - ~TestBoolHelpers.falsey - ~TestBoolHelpers.falsey_empty - ~TestBoolHelpers.longMessage - ~TestBoolHelpers.maxDiff - ~TestBoolHelpers.truthy - - \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.TestCacheDecoratorMemory.rst b/docs/source/helpers/tests/tests.TestCacheDecoratorMemory.rst deleted file mode 100644 index 9f55873..0000000 --- a/docs/source/helpers/tests/tests.TestCacheDecoratorMemory.rst +++ /dev/null @@ -1,34 +0,0 @@ -TestCacheDecoratorMemory -======================== - -.. currentmodule:: tests - -.. autoclass:: TestCacheDecoratorMemory - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~TestCacheDecoratorMemory.setUp - ~TestCacheDecoratorMemory.setUpClass - ~TestCacheDecoratorMemory.tearDown - ~TestCacheDecoratorMemory.tearDownClass - ~TestCacheDecoratorMemory.test_rcache_callable - ~TestCacheDecoratorMemory.test_rcache_rand - ~TestCacheDecoratorMemory.test_rcache_rand_dynamic - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~TestCacheDecoratorMemory.cache - - \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.TestCacheDecoratorRedis.rst b/docs/source/helpers/tests/tests.TestCacheDecoratorRedis.rst deleted file mode 100644 index 1ce9c24..0000000 --- a/docs/source/helpers/tests/tests.TestCacheDecoratorRedis.rst +++ /dev/null @@ -1,34 +0,0 @@ -TestCacheDecoratorRedis -======================= - -.. currentmodule:: tests - -.. autoclass:: TestCacheDecoratorRedis - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~TestCacheDecoratorRedis.setUp - ~TestCacheDecoratorRedis.setUpClass - ~TestCacheDecoratorRedis.tearDown - ~TestCacheDecoratorRedis.tearDownClass - ~TestCacheDecoratorRedis.test_rcache_callable - ~TestCacheDecoratorRedis.test_rcache_rand - ~TestCacheDecoratorRedis.test_rcache_rand_dynamic - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~TestCacheDecoratorRedis.cache - - \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.TestGeneral.rst b/docs/source/helpers/tests/tests.TestGeneral.rst deleted file mode 100644 index 2f9ba78..0000000 --- a/docs/source/helpers/tests/tests.TestGeneral.rst +++ /dev/null @@ -1,106 +0,0 @@ -TestGeneral -=========== - -.. currentmodule:: tests - -.. autoclass:: TestGeneral - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~TestGeneral.__init__ - ~TestGeneral.addCleanup - ~TestGeneral.addTypeEqualityFunc - ~TestGeneral.assertAlmostEqual - ~TestGeneral.assertAlmostEquals - ~TestGeneral.assertCountEqual - ~TestGeneral.assertDictContainsSubset - ~TestGeneral.assertDictEqual - ~TestGeneral.assertEqual - ~TestGeneral.assertEquals - ~TestGeneral.assertFalse - ~TestGeneral.assertGreater - ~TestGeneral.assertGreaterEqual - ~TestGeneral.assertIn - ~TestGeneral.assertIs - ~TestGeneral.assertIsInstance - ~TestGeneral.assertIsNone - ~TestGeneral.assertIsNot - ~TestGeneral.assertIsNotNone - ~TestGeneral.assertLess - ~TestGeneral.assertLessEqual - ~TestGeneral.assertListEqual - ~TestGeneral.assertLogs - ~TestGeneral.assertMultiLineEqual - ~TestGeneral.assertNotAlmostEqual - ~TestGeneral.assertNotAlmostEquals - ~TestGeneral.assertNotEqual - ~TestGeneral.assertNotEquals - ~TestGeneral.assertNotIn - ~TestGeneral.assertNotIsInstance - ~TestGeneral.assertNotRegex - ~TestGeneral.assertNotRegexpMatches - ~TestGeneral.assertRaises - ~TestGeneral.assertRaisesRegex - ~TestGeneral.assertRaisesRegexp - ~TestGeneral.assertRegex - ~TestGeneral.assertRegexpMatches - ~TestGeneral.assertSequenceEqual - ~TestGeneral.assertSetEqual - ~TestGeneral.assertTrue - ~TestGeneral.assertTupleEqual - ~TestGeneral.assertWarns - ~TestGeneral.assertWarnsRegex - ~TestGeneral.assert_ - ~TestGeneral.countTestCases - ~TestGeneral.debug - ~TestGeneral.defaultTestResult - ~TestGeneral.doCleanups - ~TestGeneral.fail - ~TestGeneral.failIf - ~TestGeneral.failIfAlmostEqual - ~TestGeneral.failIfEqual - ~TestGeneral.failUnless - ~TestGeneral.failUnlessAlmostEqual - ~TestGeneral.failUnlessEqual - ~TestGeneral.failUnlessRaises - ~TestGeneral.id - ~TestGeneral.run - ~TestGeneral.setUp - ~TestGeneral.setUpClass - ~TestGeneral.shortDescription - ~TestGeneral.skipTest - ~TestGeneral.subTest - ~TestGeneral.tearDown - ~TestGeneral.tearDownClass - ~TestGeneral.test_async_decorator - ~TestGeneral.test_async_decorator_return - ~TestGeneral.test_chunked - ~TestGeneral.test_ping - ~TestGeneral.test_retry_on_err - ~TestGeneral.test_retry_on_err_return - ~TestGeneral.test_run_sync - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~TestGeneral.empty_lst - ~TestGeneral.empty_vals - ~TestGeneral.empty_zero - ~TestGeneral.falsey - ~TestGeneral.falsey_empty - ~TestGeneral.longMessage - ~TestGeneral.maxDiff - ~TestGeneral.truthy - - diff --git a/docs/source/helpers/tests/tests.TestIPReverseDNS.rst b/docs/source/helpers/tests/tests.TestIPReverseDNS.rst deleted file mode 100644 index b5423f1..0000000 --- a/docs/source/helpers/tests/tests.TestIPReverseDNS.rst +++ /dev/null @@ -1,113 +0,0 @@ -TestIPReverseDNS -================ - -.. currentmodule:: tests - -.. autoclass:: TestIPReverseDNS - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~TestIPReverseDNS.__init__ - ~TestIPReverseDNS.addCleanup - ~TestIPReverseDNS.addTypeEqualityFunc - ~TestIPReverseDNS.assertAlmostEqual - ~TestIPReverseDNS.assertAlmostEquals - ~TestIPReverseDNS.assertCountEqual - ~TestIPReverseDNS.assertDictContainsSubset - ~TestIPReverseDNS.assertDictEqual - ~TestIPReverseDNS.assertEqual - ~TestIPReverseDNS.assertEquals - ~TestIPReverseDNS.assertFalse - ~TestIPReverseDNS.assertGreater - ~TestIPReverseDNS.assertGreaterEqual - ~TestIPReverseDNS.assertIn - ~TestIPReverseDNS.assertIs - ~TestIPReverseDNS.assertIsInstance - ~TestIPReverseDNS.assertIsNone - ~TestIPReverseDNS.assertIsNot - ~TestIPReverseDNS.assertIsNotNone - ~TestIPReverseDNS.assertLess - ~TestIPReverseDNS.assertLessEqual - ~TestIPReverseDNS.assertListEqual - ~TestIPReverseDNS.assertLogs - ~TestIPReverseDNS.assertMultiLineEqual - ~TestIPReverseDNS.assertNotAlmostEqual - ~TestIPReverseDNS.assertNotAlmostEquals - ~TestIPReverseDNS.assertNotEqual - ~TestIPReverseDNS.assertNotEquals - ~TestIPReverseDNS.assertNotIn - ~TestIPReverseDNS.assertNotIsInstance - ~TestIPReverseDNS.assertNotRegex - ~TestIPReverseDNS.assertNotRegexpMatches - ~TestIPReverseDNS.assertRaises - ~TestIPReverseDNS.assertRaisesRegex - ~TestIPReverseDNS.assertRaisesRegexp - ~TestIPReverseDNS.assertRegex - ~TestIPReverseDNS.assertRegexpMatches - ~TestIPReverseDNS.assertSequenceEqual - ~TestIPReverseDNS.assertSetEqual - ~TestIPReverseDNS.assertTrue - ~TestIPReverseDNS.assertTupleEqual - ~TestIPReverseDNS.assertWarns - ~TestIPReverseDNS.assertWarnsRegex - ~TestIPReverseDNS.assert_ - ~TestIPReverseDNS.countTestCases - ~TestIPReverseDNS.debug - ~TestIPReverseDNS.defaultTestResult - ~TestIPReverseDNS.doCleanups - ~TestIPReverseDNS.fail - ~TestIPReverseDNS.failIf - ~TestIPReverseDNS.failIfAlmostEqual - ~TestIPReverseDNS.failIfEqual - ~TestIPReverseDNS.failUnless - ~TestIPReverseDNS.failUnlessAlmostEqual - ~TestIPReverseDNS.failUnlessEqual - ~TestIPReverseDNS.failUnlessRaises - ~TestIPReverseDNS.id - ~TestIPReverseDNS.run - ~TestIPReverseDNS.setUp - ~TestIPReverseDNS.setUpClass - ~TestIPReverseDNS.shortDescription - ~TestIPReverseDNS.skipTest - ~TestIPReverseDNS.subTest - ~TestIPReverseDNS.tearDown - ~TestIPReverseDNS.tearDownClass - ~TestIPReverseDNS.test_v4_arpa_boundary_16bit - ~TestIPReverseDNS.test_v4_arpa_boundary_24bit - ~TestIPReverseDNS.test_v4_inv_boundary - ~TestIPReverseDNS.test_v4_inv_boundary_2 - ~TestIPReverseDNS.test_v4_invalid - ~TestIPReverseDNS.test_v4_invalid_2 - ~TestIPReverseDNS.test_v4_to_arpa - ~TestIPReverseDNS.test_v6_arpa_boundary_16bit - ~TestIPReverseDNS.test_v6_arpa_boundary_32bit - ~TestIPReverseDNS.test_v6_inv_boundary - ~TestIPReverseDNS.test_v6_inv_boundary_2 - ~TestIPReverseDNS.test_v6_invalid - ~TestIPReverseDNS.test_v6_invalid_2 - ~TestIPReverseDNS.test_v6_to_arpa - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~TestIPReverseDNS.empty_lst - ~TestIPReverseDNS.empty_vals - ~TestIPReverseDNS.empty_zero - ~TestIPReverseDNS.falsey - ~TestIPReverseDNS.falsey_empty - ~TestIPReverseDNS.longMessage - ~TestIPReverseDNS.maxDiff - ~TestIPReverseDNS.truthy - - \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.TestMemoryCache.rst b/docs/source/helpers/tests/tests.TestMemoryCache.rst deleted file mode 100644 index 56a324f..0000000 --- a/docs/source/helpers/tests/tests.TestMemoryCache.rst +++ /dev/null @@ -1,105 +0,0 @@ -TestMemoryCache -=============== - -.. currentmodule:: tests - -.. autoclass:: TestMemoryCache - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~TestMemoryCache.__init__ - ~TestMemoryCache.addCleanup - ~TestMemoryCache.addTypeEqualityFunc - ~TestMemoryCache.assertAlmostEqual - ~TestMemoryCache.assertAlmostEquals - ~TestMemoryCache.assertCountEqual - ~TestMemoryCache.assertDictContainsSubset - ~TestMemoryCache.assertDictEqual - ~TestMemoryCache.assertEqual - ~TestMemoryCache.assertEquals - ~TestMemoryCache.assertFalse - ~TestMemoryCache.assertGreater - ~TestMemoryCache.assertGreaterEqual - ~TestMemoryCache.assertIn - ~TestMemoryCache.assertIs - ~TestMemoryCache.assertIsInstance - ~TestMemoryCache.assertIsNone - ~TestMemoryCache.assertIsNot - ~TestMemoryCache.assertIsNotNone - ~TestMemoryCache.assertLess - ~TestMemoryCache.assertLessEqual - ~TestMemoryCache.assertListEqual - ~TestMemoryCache.assertLogs - ~TestMemoryCache.assertMultiLineEqual - ~TestMemoryCache.assertNotAlmostEqual - ~TestMemoryCache.assertNotAlmostEquals - ~TestMemoryCache.assertNotEqual - ~TestMemoryCache.assertNotEquals - ~TestMemoryCache.assertNotIn - ~TestMemoryCache.assertNotIsInstance - ~TestMemoryCache.assertNotRegex - ~TestMemoryCache.assertNotRegexpMatches - ~TestMemoryCache.assertRaises - ~TestMemoryCache.assertRaisesRegex - ~TestMemoryCache.assertRaisesRegexp - ~TestMemoryCache.assertRegex - ~TestMemoryCache.assertRegexpMatches - ~TestMemoryCache.assertSequenceEqual - ~TestMemoryCache.assertSetEqual - ~TestMemoryCache.assertTrue - ~TestMemoryCache.assertTupleEqual - ~TestMemoryCache.assertWarns - ~TestMemoryCache.assertWarnsRegex - ~TestMemoryCache.assert_ - ~TestMemoryCache.countTestCases - ~TestMemoryCache.debug - ~TestMemoryCache.defaultTestResult - ~TestMemoryCache.doCleanups - ~TestMemoryCache.fail - ~TestMemoryCache.failIf - ~TestMemoryCache.failIfAlmostEqual - ~TestMemoryCache.failIfEqual - ~TestMemoryCache.failUnless - ~TestMemoryCache.failUnlessAlmostEqual - ~TestMemoryCache.failUnlessEqual - ~TestMemoryCache.failUnlessRaises - ~TestMemoryCache.id - ~TestMemoryCache.run - ~TestMemoryCache.setUp - ~TestMemoryCache.setUpClass - ~TestMemoryCache.shortDescription - ~TestMemoryCache.skipTest - ~TestMemoryCache.subTest - ~TestMemoryCache.tearDown - ~TestMemoryCache.tearDownClass - ~TestMemoryCache.test_cache_expire - ~TestMemoryCache.test_cache_remove - ~TestMemoryCache.test_cache_set - ~TestMemoryCache.test_cache_update_timeout - ~TestMemoryCache.test_cache_update_timeout_raise - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~TestMemoryCache.cache_keys - ~TestMemoryCache.empty_lst - ~TestMemoryCache.empty_vals - ~TestMemoryCache.empty_zero - ~TestMemoryCache.falsey - ~TestMemoryCache.falsey_empty - ~TestMemoryCache.longMessage - ~TestMemoryCache.maxDiff - ~TestMemoryCache.truthy - - \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.TestParseHelpers.rst b/docs/source/helpers/tests/tests.TestParseHelpers.rst deleted file mode 100644 index 56b0219..0000000 --- a/docs/source/helpers/tests/tests.TestParseHelpers.rst +++ /dev/null @@ -1,109 +0,0 @@ -TestParseHelpers -================ - -.. currentmodule:: tests - -.. autoclass:: TestParseHelpers - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~TestParseHelpers.__init__ - ~TestParseHelpers.addCleanup - ~TestParseHelpers.addTypeEqualityFunc - ~TestParseHelpers.assertAlmostEqual - ~TestParseHelpers.assertAlmostEquals - ~TestParseHelpers.assertCountEqual - ~TestParseHelpers.assertDictContainsSubset - ~TestParseHelpers.assertDictEqual - ~TestParseHelpers.assertEqual - ~TestParseHelpers.assertEquals - ~TestParseHelpers.assertFalse - ~TestParseHelpers.assertGreater - ~TestParseHelpers.assertGreaterEqual - ~TestParseHelpers.assertIn - ~TestParseHelpers.assertIs - ~TestParseHelpers.assertIsInstance - ~TestParseHelpers.assertIsNone - ~TestParseHelpers.assertIsNot - ~TestParseHelpers.assertIsNotNone - ~TestParseHelpers.assertLess - ~TestParseHelpers.assertLessEqual - ~TestParseHelpers.assertListEqual - ~TestParseHelpers.assertLogs - ~TestParseHelpers.assertMultiLineEqual - ~TestParseHelpers.assertNotAlmostEqual - ~TestParseHelpers.assertNotAlmostEquals - ~TestParseHelpers.assertNotEqual - ~TestParseHelpers.assertNotEquals - ~TestParseHelpers.assertNotIn - ~TestParseHelpers.assertNotIsInstance - ~TestParseHelpers.assertNotRegex - ~TestParseHelpers.assertNotRegexpMatches - ~TestParseHelpers.assertRaises - ~TestParseHelpers.assertRaisesRegex - ~TestParseHelpers.assertRaisesRegexp - ~TestParseHelpers.assertRegex - ~TestParseHelpers.assertRegexpMatches - ~TestParseHelpers.assertSequenceEqual - ~TestParseHelpers.assertSetEqual - ~TestParseHelpers.assertTrue - ~TestParseHelpers.assertTupleEqual - ~TestParseHelpers.assertWarns - ~TestParseHelpers.assertWarnsRegex - ~TestParseHelpers.assert_ - ~TestParseHelpers.countTestCases - ~TestParseHelpers.debug - ~TestParseHelpers.defaultTestResult - ~TestParseHelpers.doCleanups - ~TestParseHelpers.fail - ~TestParseHelpers.failIf - ~TestParseHelpers.failIfAlmostEqual - ~TestParseHelpers.failIfEqual - ~TestParseHelpers.failUnless - ~TestParseHelpers.failUnlessAlmostEqual - ~TestParseHelpers.failUnlessEqual - ~TestParseHelpers.failUnlessRaises - ~TestParseHelpers.id - ~TestParseHelpers.run - ~TestParseHelpers.setUp - ~TestParseHelpers.setUpClass - ~TestParseHelpers.shortDescription - ~TestParseHelpers.skipTest - ~TestParseHelpers.subTest - ~TestParseHelpers.tearDown - ~TestParseHelpers.tearDownClass - ~TestParseHelpers.test_csv_single - ~TestParseHelpers.test_csv_spaced - ~TestParseHelpers.test_env_bool_false - ~TestParseHelpers.test_env_bool_true - ~TestParseHelpers.test_env_nonexist_bool - ~TestParseHelpers.test_kval_clean - ~TestParseHelpers.test_kval_custom_clean - ~TestParseHelpers.test_kval_custom_spaced - ~TestParseHelpers.test_kval_single - ~TestParseHelpers.test_kval_spaced - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~TestParseHelpers.empty_lst - ~TestParseHelpers.empty_vals - ~TestParseHelpers.empty_zero - ~TestParseHelpers.falsey - ~TestParseHelpers.falsey_empty - ~TestParseHelpers.longMessage - ~TestParseHelpers.maxDiff - ~TestParseHelpers.truthy - - \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.TestRedisCache.rst b/docs/source/helpers/tests/tests.TestRedisCache.rst deleted file mode 100644 index bd607fb..0000000 --- a/docs/source/helpers/tests/tests.TestRedisCache.rst +++ /dev/null @@ -1,105 +0,0 @@ -TestRedisCache -============== - -.. currentmodule:: tests - -.. autoclass:: TestRedisCache - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~TestRedisCache.__init__ - ~TestRedisCache.addCleanup - ~TestRedisCache.addTypeEqualityFunc - ~TestRedisCache.assertAlmostEqual - ~TestRedisCache.assertAlmostEquals - ~TestRedisCache.assertCountEqual - ~TestRedisCache.assertDictContainsSubset - ~TestRedisCache.assertDictEqual - ~TestRedisCache.assertEqual - ~TestRedisCache.assertEquals - ~TestRedisCache.assertFalse - ~TestRedisCache.assertGreater - ~TestRedisCache.assertGreaterEqual - ~TestRedisCache.assertIn - ~TestRedisCache.assertIs - ~TestRedisCache.assertIsInstance - ~TestRedisCache.assertIsNone - ~TestRedisCache.assertIsNot - ~TestRedisCache.assertIsNotNone - ~TestRedisCache.assertLess - ~TestRedisCache.assertLessEqual - ~TestRedisCache.assertListEqual - ~TestRedisCache.assertLogs - ~TestRedisCache.assertMultiLineEqual - ~TestRedisCache.assertNotAlmostEqual - ~TestRedisCache.assertNotAlmostEquals - ~TestRedisCache.assertNotEqual - ~TestRedisCache.assertNotEquals - ~TestRedisCache.assertNotIn - ~TestRedisCache.assertNotIsInstance - ~TestRedisCache.assertNotRegex - ~TestRedisCache.assertNotRegexpMatches - ~TestRedisCache.assertRaises - ~TestRedisCache.assertRaisesRegex - ~TestRedisCache.assertRaisesRegexp - ~TestRedisCache.assertRegex - ~TestRedisCache.assertRegexpMatches - ~TestRedisCache.assertSequenceEqual - ~TestRedisCache.assertSetEqual - ~TestRedisCache.assertTrue - ~TestRedisCache.assertTupleEqual - ~TestRedisCache.assertWarns - ~TestRedisCache.assertWarnsRegex - ~TestRedisCache.assert_ - ~TestRedisCache.countTestCases - ~TestRedisCache.debug - ~TestRedisCache.defaultTestResult - ~TestRedisCache.doCleanups - ~TestRedisCache.fail - ~TestRedisCache.failIf - ~TestRedisCache.failIfAlmostEqual - ~TestRedisCache.failIfEqual - ~TestRedisCache.failUnless - ~TestRedisCache.failUnlessAlmostEqual - ~TestRedisCache.failUnlessEqual - ~TestRedisCache.failUnlessRaises - ~TestRedisCache.id - ~TestRedisCache.run - ~TestRedisCache.setUp - ~TestRedisCache.setUpClass - ~TestRedisCache.shortDescription - ~TestRedisCache.skipTest - ~TestRedisCache.subTest - ~TestRedisCache.tearDown - ~TestRedisCache.tearDownClass - ~TestRedisCache.test_cache_expire - ~TestRedisCache.test_cache_remove - ~TestRedisCache.test_cache_set - ~TestRedisCache.test_cache_update_timeout - ~TestRedisCache.test_cache_update_timeout_raise - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~TestRedisCache.cache_keys - ~TestRedisCache.empty_lst - ~TestRedisCache.empty_vals - ~TestRedisCache.empty_zero - ~TestRedisCache.falsey - ~TestRedisCache.falsey_empty - ~TestRedisCache.longMessage - ~TestRedisCache.maxDiff - ~TestRedisCache.truthy - - \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.base.rst b/docs/source/helpers/tests/tests.base.rst new file mode 100644 index 0000000..38541d0 --- /dev/null +++ b/docs/source/helpers/tests/tests.base.rst @@ -0,0 +1,23 @@ +tests.base +========== + +.. automodule:: tests.base + + + + + + + + .. rubric:: Classes + + .. autosummary:: + + EmptyIter + PrivexBaseCase + + + + + + \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.test_bool.rst b/docs/source/helpers/tests/tests.test_bool.rst new file mode 100644 index 0000000..1155c97 --- /dev/null +++ b/docs/source/helpers/tests/tests.test_bool.rst @@ -0,0 +1,22 @@ +tests.test\_bool +================ + +.. automodule:: tests.test_bool + + + + + + + + .. rubric:: Classes + + .. autosummary:: + + TestBoolHelpers + + + + + + \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.test_cache.rst b/docs/source/helpers/tests/tests.test_cache.rst new file mode 100644 index 0000000..5f4f0f5 --- /dev/null +++ b/docs/source/helpers/tests/tests.test_cache.rst @@ -0,0 +1,25 @@ +tests.test\_cache +================= + +.. automodule:: tests.test_cache + + + + + + + + .. rubric:: Classes + + .. autosummary:: + + TestCacheDecoratorMemory + TestCacheDecoratorRedis + TestMemoryCache + TestRedisCache + + + + + + \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.test_general.rst b/docs/source/helpers/tests/tests.test_general.rst new file mode 100644 index 0000000..8c70009 --- /dev/null +++ b/docs/source/helpers/tests/tests.test_general.rst @@ -0,0 +1,22 @@ +tests.test\_general +=================== + +.. automodule:: tests.test_general + + + + + + + + .. rubric:: Classes + + .. autosummary:: + + TestGeneral + + + + + + \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.test_parse.rst b/docs/source/helpers/tests/tests.test_parse.rst new file mode 100644 index 0000000..91012e5 --- /dev/null +++ b/docs/source/helpers/tests/tests.test_parse.rst @@ -0,0 +1,22 @@ +tests.test\_parse +================= + +.. automodule:: tests.test_parse + + + + + + + + .. rubric:: Classes + + .. autosummary:: + + TestParseHelpers + + + + + + \ No newline at end of file diff --git a/docs/source/helpers/tests/tests.test_rdns.rst b/docs/source/helpers/tests/tests.test_rdns.rst new file mode 100644 index 0000000..cf9eba4 --- /dev/null +++ b/docs/source/helpers/tests/tests.test_rdns.rst @@ -0,0 +1,22 @@ +tests.test\_rdns +================ + +.. automodule:: tests.test_rdns + + + + + + + + .. rubric:: Classes + + .. autosummary:: + + TestIPReverseDNS + + + + + + \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 401fab8..1d398c9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,5 +1,7 @@ .. _Privex Python Helpers documentation: + + Privex Python Helpers's documentation ================================================= @@ -20,9 +22,61 @@ a new commit is pushed to the `Github Project`_ .. _Privex's Python Helpers: https://github.com/Privex/python-helpers .. _Github Project: https://github.com/Privex/python-helpers +.. contents:: + + +Quick install +------------- + +**Installing with** `Pipenv`_ **(recommended)** + +.. code-block:: bash + + pipenv install privex-helpers + + +**Installing with standard** ``pip3`` + +.. code-block:: bash + + pip3 install privex-helpers + + + +.. _Pipenv: https://pipenv.kennethreitz.org/en/latest/ + + -Contents -========= + +Python Module Overview +---------------------- + +Privex's Python Helpers is organised into various sub-modules to make it easier to find the +functions/classes you want to use, and to avoid having to load the entire module (though it's lightweight). + +With the exception of :mod:`privex.helpers.django` (Django gets upset if certain django modules are imported before +it's initialised), **all functions/classes are imported within the** ``__init__`` **file,** allowing you to simply type: + +.. code-block:: python + + from privex.helpers import empty, run_sync, asn_to_name + +Instead of having to import the functions from each individual module: + +.. code-block:: python + + from privex.helpers.common import empty + from privex.helpers.asyncx import run_sync + from privex.helpers.net import asn_to_name + +Below is a listing of the sub-modules available in ``privex-helpers`` with a short description of what each module +contains. + +.. include:: ./helpers/index.rst + + +All Documentation +================= .. toctree:: :maxdepth: 8 @@ -30,6 +84,7 @@ Contents self install + examples .. toctree:: @@ -38,6 +93,11 @@ Contents helpers/index +.. toctree:: + :caption: Unit Testing + + helpers/tests + Indices and tables ================== diff --git a/privex/helpers/__init__.py b/privex/helpers/__init__.py index 20e9d78..bbaf97a 100644 --- a/privex/helpers/__init__.py +++ b/privex/helpers/__init__.py @@ -51,54 +51,13 @@ import logging from privex.loghelper import LogHelper - - -# Set up logging for the entire module ``privex.helpers`` . Since this is a package, we don't add any -# console or file logging handlers, we purely just set our minimum logging level to WARNING to avoid -# spamming the logs of any application importing it. -def _setup_logging(level=logging.WARNING): - lh = LogHelper(__name__, level=level) - return lh.get_logger() - - -log = _setup_logging() -name = 'helpers' - -VERSION = '1.3.4' - - -class ImproperlyConfigured(Exception): - """Placeholder in-case this fails to import from django.core.exceptions""" - pass - - -class AppRegistryNotReady(Exception): - """Placeholder in-case this fails to import from django.core.exceptions""" - pass - - -# Only import the Django functions if Django is actually installed -try: - import django - from django.core.exceptions import ImproperlyConfigured, AppRegistryNotReady - from privex.helpers.django import * -except ImportError: - log.debug('privex.helpers __init__ failed to import "django", not loading django helpers') - pass -except (ImproperlyConfigured, AppRegistryNotReady): - log.debug('privex.helpers __init__ failed to import "django", not loading django helpers') - pass -except Exception: - log.debug('privex.helpers __init__ failed to import "django", (unknown exception) not loading django helpers') - pass - - from privex.helpers.common import * from privex.helpers.decorators import * from privex.helpers.net import * from privex.helpers.exceptions import * from privex.helpers.plugin import * from privex.helpers.cache import CacheNotFound, CacheAdapter, CacheWrapper, MemoryCache, cached + try: from privex.helpers.cache.RedisCache import RedisCache except ImportError: @@ -110,3 +69,21 @@ class AppRegistryNotReady(Exception): log.debug('privex.helpers __init__ failed to import "asyncx", not loading async helpers') pass + +def _setup_logging(level=logging.WARNING): + """ + Set up logging for the entire module ``privex.helpers`` . Since this is a package, we don't add any + console or file logging handlers, we purely just set our minimum logging level to WARNING to avoid + spamming the logs of any application importing it. + """ + lh = LogHelper(__name__, level=level) + return lh.get_logger() + + +log = _setup_logging() +name = 'helpers' + +VERSION = '1.4.0' + + + diff --git a/privex/helpers/cache/RedisCache.py b/privex/helpers/cache/RedisCache.py index 0b73b87..46cb5bf 100644 --- a/privex/helpers/cache/RedisCache.py +++ b/privex/helpers/cache/RedisCache.py @@ -15,7 +15,7 @@ class RedisCache(CacheAdapter): """ - An Redis backed implementation of :class:`.CacheAdapter`. Uses the global Redis instance from + A Redis backed implementation of :class:`.CacheAdapter`. Uses the global Redis instance from :py:mod:`privex.helpers.plugin` by default, however custom Redis instances can be passed in via the constructor argument ``redis_instance``. diff --git a/privex/helpers/common.py b/privex/helpers/common.py index ec27f24..cca28f4 100644 --- a/privex/helpers/common.py +++ b/privex/helpers/common.py @@ -376,6 +376,31 @@ def chunked(iterable, n): return (iterable[i * chunksize:i * chunksize + chunksize] for i in range(n)) +def inject_items(items: list, dest_list: list, position: int) -> List[str]: + """ + Inject a list ``items`` after a certain element in ``dest_list``. + + **NOTE:** This does NOT alter ``dest_list`` - it returns a **NEW list** with ``items`` injected after the + given ``position`` in ``dest_list``. + + **Example Usage**:: + + >>> x = ['a', 'b', 'e', 'f', 'g'] + >>> y = ['c', 'd'] + >>> # Inject the list 'y' into list 'x' after element 1 (b) + >>> inject_items(y, x, 1) + ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + + :param list items: A list of items to inject into ``dest_list`` + :param list dest_list: The list to inject ``items`` into + :param int position: Inject ``items`` after this element (0 = 1st item) in ``dest_list`` + :return List[str] injected: :py:attr:`.dest_list` with the passed ``items`` list injected at ``position`` + """ + before = list(dest_list[0:position+1]) + after = list(dest_list[position+1:]) + return before + items + after + + class ErrHelpParser(argparse.ArgumentParser): """ ErrHelpParser - Use this instead of :py:class:`argparse.ArgumentParser` to automatically get full diff --git a/privex/helpers/net.py b/privex/helpers/net.py index ee4adf9..19b5ad7 100644 --- a/privex/helpers/net.py +++ b/privex/helpers/net.py @@ -38,15 +38,18 @@ """ import logging +import platform import subprocess from privex.helpers.exceptions import BoundaryException +from privex.helpers import plugin from ipaddress import ip_address, IPv4Address, IPv6Address from typing import Union log = logging.getLogger(__name__) try: - from dns.resolver import Resolver + from dns.resolver import Resolver, NoAnswer + def asn_to_name(as_number: Union[int, str], quiet: bool = True) -> str: """ Look up an integer Autonomous System Number and return the human readable @@ -71,21 +74,27 @@ def asn_to_name(as_number: Union[int, str], quiet: bool = True) -> str: :raises KeyError: Raised when a lookup returns no results, and ``quiet`` is set to False. :return str as_name: The name and country code of the ASN, e.g. 'PRIVEX, SE' """ - - res = Resolver().query('AS{}.asn.cymru.com'.format(as_number), "TXT") - if len(res) > 0: - # res[0] is formatted like such: "15169 | US | arin | 2000-03-30 | GOOGLE - Google LLC, US" - # with literal quotes. we need to strip them, split by pipe, extract the last element, then strip spaces. - asname = str(res[0]).strip('"').split('|')[-1:][0].strip() - return str(asname) - if quiet: - return 'Unknown ASN' - raise KeyError('ASN {} was not found, or server did not respond.'.format(as_number)) + + try: + res = Resolver().query('AS{}.asn.cymru.com'.format(as_number), "TXT") + if len(res) > 0: + # res[0] is formatted like such: "15169 | US | arin | 2000-03-30 | GOOGLE - Google LLC, US" with + # literal quotes. we need to strip them, split by pipe, extract the last element, then strip spaces. + asname = str(res[0]).strip('"').split('|')[-1:][0].strip() + return str(asname) + raise NoAnswer('privex.helpers.net.asn_to_name returned no results.') + except NoAnswer: + if quiet: + return 'Unknown ASN' + raise KeyError('ASN {} was not found, or server did not respond.'.format(as_number)) + + plugin.HAS_DNSPYTHON = True except ImportError: log.debug('privex.helpers.net failed to import "dns.resolver" (pypi package "dnspython"), skipping some helpers') pass + def ip_to_rdns(ip: str, boundary: bool = False, v6_boundary: int = 32, v4_boundary: int = 24) -> str: """ Converts an IPv4 or IPv6 address into an in-addr domain @@ -176,7 +185,7 @@ def ip6_to_rdns(ip_obj: IPv6Address, v6_boundary: int = 32, boundary: bool = Fal you have a specific need for this one. :param IPv6Address ip_obj: An IPv4 ip_address() object to get the rDNS domain for - :param int v4_boundary: 8-128 bits. If ``boundary`` is True, return the base rDNS domain at this boundary. + :param int v6_boundary: 8-128 bits. If ``boundary`` is True, return the base rDNS domain at this boundary. :param bool boundary: If True, cut off the rDNS domain to the given ``v6_boundary`` :return str rdns_domain: ip6.arpa format, e.g. ``0.8.e.f.ip6.arpa`` """ @@ -204,6 +213,7 @@ def ip_is_v4(ip: str) -> bool: """ return type(ip_address(ip)) == IPv4Address + def ip_is_v6(ip: str) -> bool: """ Determines whether an IP address is IPv6 or not @@ -214,17 +224,43 @@ def ip_is_v6(ip: str) -> bool: """ return type(ip_address(ip)) == IPv6Address + def ping(ip: str, timeout: int = 30) -> bool: """ - Sends a ping to a given IP + Sends a ping to a given IPv4 / IPv6 address. Tested with IPv4+IPv6 using ``iputils-ping`` on Linux, as well as the + default IPv4 ``ping`` utility on Mac OSX (Mojave, 10.14.6). + + Fully supported when using Linux with the ``iputils-ping`` package. Only IPv4 support on Mac OSX. + + **Example Usage**:: + + >>> from privex.helpers import ping + >>> if ping('127.0.0.1', 5) and ping('::1', 10): + ... print('Both 127.0.0.1 and ::1 are up') + ... else: + ... print('127.0.0.1 or ::1 failed to respond to a ping within the given timeout.') + + **Known Incompatibilities**: + + * NOT compatible with IPv6 addresses on OSX due to the lack of a timeout argument with ``ping6`` + * NOT compatible with IPv6 addresses when using ``inetutils-ping`` on Linux due to separate ``ping6`` command - :param str ip: An IP address as a string, e.g. 192.168.1.1 - :param int timeout: Number of seconds to wait for a response from the ping before timing out + :param str ip: An IP address as a string, e.g. ``192.168.1.1`` or ``2a07:e00::1`` + :param int timeout: (Default: 30) Number of seconds to wait for a response from the ping before timing out :raises ValueError: When the given IP address ``ip`` is invalid or ``timeout`` < 1 - :return bool: True if ping got a response from the given IP, False if not + :return bool: ``True`` if ping got a response from the given IP, ``False`` if not """ ip_obj = ip_address(ip) # verify IP is valid (this will throw if it isn't) if timeout < 1: raise ValueError('timeout value cannot be less than 1 second') - with subprocess.Popen(["/bin/ping", "-c1", "-w{}".format(timeout), ip], stdout=subprocess.PIPE) as proc: + opts4 = { + 'Linux': ["/bin/ping", "-c1", f"-w{timeout}"], + 'Darwin': ["/sbin/ping", "-c1", f"-t{timeout}"] + } + opts6 = {'Linux': ["/bin/ping", "-c1", f"-w{timeout}"]} + opts = opts4 if ip_is_v4(ip_obj) else opts6 + if platform.system() not in opts: + raise NotImplementedError(f"{__name__}.ping is not fully supported on platform '{platform.system()}'...") + + with subprocess.Popen(opts[platform.system()] + [ip], stdout=subprocess.PIPE) as proc: return 'bytes from {}'.format(ip) in proc.stdout.read().decode('utf-8') diff --git a/privex/helpers/plugin.py b/privex/helpers/plugin.py index 947551a..df08f0e 100644 --- a/privex/helpers/plugin.py +++ b/privex/helpers/plugin.py @@ -52,6 +52,9 @@ HAS_REDIS = False """If the ``redis`` module was imported successfully, this will change to True.""" +HAS_DNSPYTHON = False +"""If the ``dns.resolver`` module was imported successfully, this will change to True.""" + __STORE = {} """This ``dict`` is used to store initialised classes for connections to databases, APIs etc.""" diff --git a/tests.py b/tests.py deleted file mode 100755 index 24158d3..0000000 --- a/tests.py +++ /dev/null @@ -1,659 +0,0 @@ -#!/usr/bin/env python3.7 -""" -This file contains test cases for Privex's Python Helper's (privex-helpers). - -Before running the tests: - - - Ensure you have any mandatory requirements installed (see setup.py's install_requires) - - You may wish to install any optional requirements listed in README.md for best results - - Python 3.7 is recommended at the time of writing this. See README.md in-case this has changed. - -To run the tests, simply execute ``./tests.py`` in your shell:: - - user@the-matrix ~/privex-helpers $ ./tests.py - ............................ - ---------------------------------------------------------------------- - Ran 28 tests in 0.001s - - OK - -If for some reason you don't have the executable ``python3.7`` in your PATH, try running by hand with ``python3`` :: - - user@the-matrix ~/privex-helpers $ python3 tests.py - ............................ - ---------------------------------------------------------------------- - Ran 28 tests in 0.001s - - OK - -For more verbosity, simply add ``-v`` to the end of the command:: - - user@the-matrix ~/privex-helpers $ ./tests.py -v - test_empty_combined (__main__.TestBoolHelpers) ... ok - test_isfalse_truthy (__main__.TestBoolHelpers) ... ok - test_v4_arpa_boundary_16bit (__main__.TestIPReverseDNS) - Test generating 16-bit v4 boundary ... ok - test_v4_arpa_boundary_24bit (__main__.TestIPReverseDNS) - Test generating 24-bit v4 boundary ... ok - test_kval_single (__main__.TestParseHelpers) - Test that a single value still returns a list ... ok - test_kval_spaced (__main__.TestParseHelpers) - Test key:val csv parsing with excess outer whitespace, and value whitespace ... ok - # Truncated excess output in this PyDoc example, as there are many more lines showing - # the results of each individual testcase, wasting space and adding bloat... - ---------------------------------------------------------------------- - Ran 28 tests in 0.001s - - OK - -You can also use the ``pytest`` tool (used by default for our Travis CI):: - - user@host: ~/privex-helpers $ pip3 install pytest - # You can add `-v` for more detailed output, just like when running tests.py directly. - user@host: ~/privex-helpers $ pytest tests.py - - ===================================== test session starts ===================================== - platform darwin -- Python 3.7.0, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 - rootdir: /home/user/privex-helpers - collected 33 items - - tests.py ................................. [100%] - - ====================================== warnings summary ======================================= - /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/jinja2/utils.py:485 - /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/jinja2/utils.py:485: - DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' - is deprecated, and in 3.8 it will stop working - from collections import MutableMapping - ============================ 33 passed, 2 warnings in 0.17 seconds ============================ - - -**Copyright**:: - - Copyright 2019 Privex Inc. ( https://www.privex.io ) - License: X11 / MIT Github: https://github.com/Privex/python-helpers - - -""" -import unittest -import logging -import os -from time import sleep - -from privex import helpers -from privex.loghelper import LogHelper -from privex.helpers import ping, ip_to_rdns, BoundaryException, plugin, r_cache, random_str, CacheAdapter, env_bool - -if env_bool('DEBUG', False) is True: - LogHelper('privex.helpers', level=logging.DEBUG).add_console_handler(logging.DEBUG) -else: - LogHelper('privex.helpers', level=logging.CRITICAL) # Silence non-critical log messages - - -class EmptyIter(object): - """A mock iterable object with zero length for testing empty()""" - def __len__(self): - return 0 - - -class PrivexBaseCase(unittest.TestCase): - """ - Base test-case for module test cases to inherit. - - Contains useful class attributes such as ``falsey`` and ``empty_vals`` that are used - across different unit tests. - """ - - falsey = ['false', 'FALSE', False, 0, '0', 'no'] - """Normal False-y values, as various types""" - - falsey_empty = falsey + [None, '', 'null'] - """False-y values, plus 'empty' values like '' and None""" - - truthy = [True, 'TRUE', 'true', 'yes', 'y', '1', 1] - """Truthful values, as various types""" - - empty_vals = [None, ''] - empty_lst = empty_vals + [[], (), set(), {}, EmptyIter()] - empty_zero = empty_vals + [0, '0'] - - -class TestParseHelpers(PrivexBaseCase): - """Test the parsing functions parse_csv and parse_keyval""" - - def test_csv_spaced(self): - """Test csv parsing with excess outer whitespace, and value whitespace""" - c = helpers.parse_csv(' valid , spaced out, csv ') - self.assertListEqual(c, ['valid', 'spaced out', 'csv']) - - def test_csv_single(self): - """Test that a single value still returns a list""" - self.assertListEqual(helpers.parse_csv('single'), ['single']) - - def test_kval_clean(self): - """Test that a clean key:val csv is parsed correctly""" - self.assertListEqual( - helpers.parse_keyval('John:Doe,Jane:Smith'), - [('John', 'Doe'), ('Jane', 'Smith')] - ) - - def test_kval_spaced(self): - """Test key:val csv parsing with excess outer whitespace, and value whitespace""" - self.assertListEqual( - helpers.parse_keyval(' John : Doe , Jane : Smith '), - [('John', 'Doe'), ('Jane', 'Smith')] - ) - - def test_kval_single(self): - """Test that a single value still returns a list""" - self.assertListEqual( - helpers.parse_keyval('John:Doe'), - [('John', 'Doe')] - ) - - def test_kval_custom_clean(self): - """ - Test that a clean key:val csv with custom split characters is parsed correctly - (pipe for kv, semi-colon for pair separation) - """ - self.assertListEqual( - helpers.parse_keyval('John|Doe;Jane|Smith', valsplit='|', csvsplit=';'), - [('John', 'Doe'), ('Jane', 'Smith')] - ) - - def test_kval_custom_spaced(self): - """Test key:val csv parsing with excess outer/value whitespace, and custom split characters.""" - self.assertListEqual( - helpers.parse_keyval(' John | Doe ; Jane |Smith ', valsplit='|', csvsplit=';'), - [('John', 'Doe'), ('Jane', 'Smith')] - ) - - def test_env_nonexist_bool(self): - """Test env_bool returns default with non-existant env var""" - k = 'EXAMPLE_NOEXIST' - if k in os.environ: del os.environ[k] # Ensure the env var we're testing definitely does not exist. - self.assertIsNone(helpers.env_bool(k)) - self.assertEqual(helpers.env_bool(k, 'error'), 'error') - - def test_env_bool_true(self): - """Test env_bool returns True boolean with valid env var""" - k = 'EXAMPLE_EXIST' - for v in self.truthy: - os.environ[k] = str(v) - self.assertTrue(helpers.env_bool(k, 'fail'), msg=f'env_bool({v}) === True') - - def test_env_bool_false(self): - """Test env_bool returns False boolean with valid env var""" - k = 'EXAMPLE_EXIST' - for v in self.falsey: - os.environ[k] = str(v) - self.assertFalse(helpers.env_bool(k, 'fail'), msg=f'env_bool({v}) === False') - - -class TestBoolHelpers(PrivexBaseCase): - """Test the boolean check functions is_true, is_false, as well as empty()""" - - def test_isfalse_falsey(self): - for f in self.falsey_empty: - self.assertTrue(helpers.is_false(f), msg=f"is_false({repr(f)}") - - def test_isfalse_truthy(self): - for f in self.truthy: - self.assertFalse(helpers.is_false(f), msg=f"!is_false({repr(f)}") - - def test_istrue_truthy(self): - for f in self.truthy: - self.assertTrue(helpers.is_true(f), msg=f"is_true({repr(f)}") - - def test_istrue_falsey(self): - for f in self.falsey_empty: - self.assertFalse(helpers.is_true(f), msg=f"!is_true({repr(f)}") - - def test_empty_vals(self): - for f in self.empty_vals: - self.assertTrue(helpers.empty(f), msg=f"empty({repr(f)})") - - def test_empty_lst(self): - for f in self.empty_lst: - self.assertTrue(helpers.empty(f, itr=True), msg=f"empty({repr(f)})") - - def test_empty_zero(self): - for f in self.empty_zero: - self.assertTrue(helpers.empty(f, zero=True), msg=f"empty({repr(f)})") - - def test_empty_combined(self): - for f in self.empty_zero + self.empty_lst: - self.assertTrue(helpers.empty(f, zero=True, itr=True), msg=f"empty({repr(f)})") - - def test_notempty(self): - # Basic string test - self.assertFalse(helpers.empty('hello')) - # Integer test - self.assertFalse(helpers.empty(1, zero=True)) - # Iterable tests - self.assertFalse(helpers.empty(['world'], itr=True)) - self.assertFalse(helpers.empty(('world',), itr=True)) - self.assertFalse(helpers.empty({'hello': 'world'}, itr=True)) - - -VALID_V4_1 = '172.131.22.17' -VALID_V4_1_16BOUND = '131.172.in-addr.arpa' -VALID_V4_1_24BOUND = '22.131.172.in-addr.arpa' - -VALID_V4_2 = '127.0.0.1' -VALID_V4_2_RDNS = '1.0.0.127.in-addr.arpa' - -VALID_V6_1 = '2001:dead:beef::1' -VALID_V6_1_RDNS = '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.1.0.0.2.ip6.arpa' -VALID_V6_1_16BOUND = '1.0.0.2.ip6.arpa' -VALID_V6_1_32BOUND = 'd.a.e.d.1.0.0.2.ip6.arpa' - - -class TestIPReverseDNS(PrivexBaseCase): - """ - Unit testing for the reverse DNS functions in :py:mod:`privex.helpers.net` - - Covers: - - positive resolution tests (generate standard rDNS domain from clean input) - - positive boundary tests (confirm valid results with range of boundaries) - - negative address tests (ensure errors thrown for invalid v4/v6 addresses) - - negative boundary tests (ensure errors thrown for invalid v4/v6 rDNS boundaries) - - """ - - #### - # Positive tests (normal resolution) - #### - - def test_v4_to_arpa(self): - """Test generating rDNS for standard v4""" - rdns = ip_to_rdns(VALID_V4_2) - self.assertEqual(rdns, VALID_V4_2_RDNS) - - def test_v6_to_arpa(self): - """Test generating rDNS for standard v6""" - rdns = ip_to_rdns(VALID_V6_1) - self.assertEqual(rdns, VALID_V6_1_RDNS) - - #### - # Positive tests (boundaries) - #### - - def test_v4_arpa_boundary_24bit(self): - """Test generating 24-bit v4 boundary""" - rdns = ip_to_rdns(VALID_V4_1, boundary=True, v4_boundary=24) - self.assertEqual(rdns, VALID_V4_1_24BOUND) - - def test_v4_arpa_boundary_16bit(self): - """Test generating 16-bit v4 boundary""" - rdns = ip_to_rdns(VALID_V4_1, boundary=True, v4_boundary=16) - self.assertEqual(rdns, VALID_V4_1_16BOUND) - - def test_v6_arpa_boundary_16bit(self): - """Test generating 16-bit v6 boundary""" - rdns = ip_to_rdns(VALID_V6_1, boundary=True, v6_boundary=16) - self.assertEqual(rdns, VALID_V6_1_16BOUND) - - def test_v6_arpa_boundary_32bit(self): - """Test generating 32-bit v6 boundary""" - rdns = ip_to_rdns(VALID_V6_1, boundary=True, v6_boundary=32) - self.assertEqual(rdns, VALID_V6_1_32BOUND) - - #### - # Negative tests (invalid addresses) - #### - def test_v4_invalid(self): - """Raise if IPv4 address has < 4 octets""" - with self.assertRaises(ValueError): - ip_to_rdns('127.0.0') - - def test_v4_invalid_2(self): - """Raise if IPv4 address has octet out of range""" - with self.assertRaises(ValueError): - ip_to_rdns('127.0.0.373') - - def test_v6_invalid(self): - """Raise if IPv6 address has invalid block formatting""" - with self.assertRaises(ValueError): - ip_to_rdns('2001::ff::a') - - def test_v6_invalid_2(self): - """Raise if v6 address has invalid chars""" - with self.assertRaises(ValueError): - ip_to_rdns('2001::fh') - - #### - # Negative tests (invalid boundaries) - #### - - def test_v4_inv_boundary(self): - """Raise if IPv4 boundary isn't divisable by 8""" - with self.assertRaises(BoundaryException): - ip_to_rdns(VALID_V4_2, boundary=True, v4_boundary=7) - - def test_v4_inv_boundary_2(self): - """Raise if IPv4 boundary is too short""" - with self.assertRaises(BoundaryException): - ip_to_rdns(VALID_V4_2, boundary=True, v4_boundary=0) - - def test_v6_inv_boundary(self): - """Raise if IPv6 boundary isn't dividable by 4""" - with self.assertRaises(BoundaryException): - ip_to_rdns(VALID_V6_1, boundary=True, v6_boundary=9) - - def test_v6_inv_boundary_2(self): - """Raise if IPv6 boundary is too short""" - with self.assertRaises(BoundaryException): - ip_to_rdns(VALID_V6_1, boundary=True, v6_boundary=0) - - -class TestCacheDecoratorMemory(PrivexBaseCase): - """ - Test that the decorator :py:func:`privex.helpers.decorators.r_cache` caches correctly, with adapter - :class:`helpers.MemoryCache` and also verifies dynamic cache key generation works as expected. - """ - - cache = helpers.cache.cached - - @classmethod - def setUpClass(cls): - helpers.cache.adapter_set(helpers.MemoryCache()) - - def test_rcache_rand(self): - """Decorate random string function with r_cache - test that two calls return the same string""" - @r_cache('pxhelpers_test_rand') - def r_test(): - return random_str() - - val1 = r_test() - val2 = r_test() - self.assertEqual(val1, val2, msg=f"'{repr(val1)}' == '{repr(val2)}'") - - def test_rcache_rand_dynamic(self): - """Decorate random string function with r_cache and use format_args for dynamic cache string testing""" - - @r_cache('pxhelpers_rand_dyn:{}:{}', format_args=[0, 1, 'x', 'y']) - def r_test(x, y): - return random_str() - - a, b, c, d = r_test(1, 1), r_test(1, 2), r_test(x=1, y=2), r_test(y=2, x=1) - e, f, g, h = r_test(1, 1), r_test(1, 2), r_test(x=1, y=2), r_test(y=2, x=1) - - # Test random cached strings using 1,1 and 1,2 as positional args - self.assertEqual(a, e, msg="r_test(1,1) == r_test(1,1)") - self.assertEqual(b, f, msg="r_test(1,2) == r_test(1,2)") - # Test positional arg cache is equivalent to kwarg cache - self.assertEqual(b, c, msg="r_test(1,2) == r_test(y=1,x=2)") - self.assertEqual(b, g, msg="r_test(1,2) == r_test(x=1,y=2)") - self.assertEqual(b, d, msg="r_test(1,2) == r_test(y=2,x=1)") - # Test kwarg cache is equivalent to inverted kwarg cache - self.assertEqual(h, c, msg="r_test(y=2, x=1) == r_test(x=1, y=2)") - # To be sure they aren't all producing the same string, make sure that 1,2 and 1,1 - # (positional and kwarg) are not equal - self.assertNotEqual(a, b, msg="r_test(1,1) != r_test(1,2)") - self.assertNotEqual(g, a, msg="r_test(x=1, y=2) != r_test(1,1)") - - def test_rcache_callable(self): - """Decorate random string function - use a lambda callable to determine a cache key""" - @r_cache(lambda x, y: f"{x}") - def r_test(x, y): - return random_str() - - a, b = r_test(1, 1), r_test(1, 1) - c, d = r_test(2, 1), r_test(1, 2) - - # If the first argument is the same, then we'll get the same result. The second argument is ignored. - self.assertEqual(a, b, msg='r_test(1,1) == r_test(1,1)') - self.assertEqual(a, d, msg='r_test(1,1) == r_test(1,2)') - # If the first argument is different (1 and 2), then we should get a different result. - self.assertNotEqual(a, c, msg='r_test(1,1) != r_test(2,1)') - - def tearDown(self): - """Remove any Redis keys used during test, to avoid failure on re-run""" - self.cache.remove('pxhelpers_test_rand', 'pxhelpers_rand_dyn:1:1', 'pxhelpers_rand_dyn:1:2', - 'pxhelpers_rand_dyn:2:1') - super(TestCacheDecoratorMemory, self).tearDown() - - -class TestCacheDecoratorRedis(TestCacheDecoratorMemory): - """ - Test decorator :py:func:`privex.helpers.decorators.r_cache` with adapter - :class:`helpers.RedisCache` - - (See :class:`.TestCacheDecoratorMemory`) - """ - - @classmethod - def setUpClass(cls): - if not plugin.HAS_REDIS: - print(f'The package "redis" is not installed, skipping Redis dependent tests ({cls.__name__}).') - return cls.tearDownClass() - helpers.cache.adapter_set(helpers.RedisCache()) - - -class TestGeneral(PrivexBaseCase): - """General test cases that don't fit under a specific category""" - - def setUp(self): - self.tries = 0 - - def test_ping(self): - """Test success & failure cases for ping function, as well as input validation""" - with self.assertRaises(ValueError): - ping('127.0.0.1', -1) - with self.assertRaises(ValueError): - ping('127.0.0.1', 0) - with self.assertRaises(ValueError): - ping('notavalidip', 1) - self.assertTrue(ping('127.0.0.1', 3)) - self.assertFalse(ping('192.0.2.0', 3)) - - def test_chunked(self): - """Create a 20 element long list, split it into 4 chunks, and verify the chunks are correctly made""" - x = list(range(0, 20)) - c = list(helpers.chunked(x, 4)) - self.assertEqual(len(c), 4) - self.assertEqual(c[0], [0, 1, 2, 3, 4]) - self.assertEqual(c[1], [5, 6, 7, 8, 9]) - - async def _tst_async(self, a, b): - """Basic async function used for testing async code""" - return a * 2, b * 3 - - def test_run_sync(self): - """Test helpers.async.run_sync by running an async function from this synchronous test""" - x, y = helpers.run_sync(self._tst_async, 5, 10) - d, e = helpers.run_sync(self._tst_async, 1, 2) - self.assertEqual(x, 10) - self.assertEqual(y, 30) - self.assertEqual(d, 2) - self.assertEqual(e, 6) - - @helpers.async_sync - def test_async_decorator(self): - """Test the async_sync decorator by wrapping this unit test""" - - x, y = yield from self._tst_async(5, 10) - d, e = yield from self._tst_async(1, 2) - - self.assertEqual(x, 10) - self.assertEqual(y, 30) - self.assertEqual(d, 2) - self.assertEqual(e, 6) - - def test_async_decorator_return(self): - """Test the async_sync decorator handles returning async data from synchronous function""" - - async_func = self._tst_async - - @helpers.async_sync - def non_async(a, b): - f, g = yield from async_func(a, b) - return f, g - - x, y = non_async(5, 10) - d, e = non_async(1, 2) - - self.assertEqual(x, 10) - self.assertEqual(y, 30) - self.assertEqual(d, 2) - self.assertEqual(e, 6) - - def test_retry_on_err(self): - """Test that the :class:`helpers.retry_on_err` decorator retries a function 3 times as expected""" - - @helpers.retry_on_err(max_retries=3, delay=0.2) - def retry_func(cls): - cls.tries += 1 - raise Exception - - with self.assertRaises(Exception): - retry_func(self) - - # The first run should cause tries = 1, then after 3 re-tries it should reach 4 tries in total. - self.assertEqual(self.tries, 4) - - def test_retry_on_err_return(self): - """Test that the :class:`helpers.retry_on_err` decorator can return correctly after some retries""" - - @helpers.retry_on_err(max_retries=3, delay=0.2) - def retry_func(cls): - if cls.tries < 3: - cls.tries += 1 - raise Exception - return 'success' - - ret = retry_func(self) - - # retry_func stops raising exceptions after the 2nd retry (try 3), thus 3 tries in total - self.assertEqual(self.tries, 3) - self.assertEqual(ret, 'success') - - -class TestMemoryCache(PrivexBaseCase): - """:class:`.MemoryCache` Test cases for caching related functions/classes in :py:mod:`privex.helpers.cache`""" - - cache_keys = [ - 'test_cache_set', - 'test_expire', - 'test_update_timeout', - 'test_update_timeout_noexist', - 'test_cache_remove', - ] - """A list of all cache keys used during the test case, so they can be removed by :py:meth:`.tearDown` once done.""" - - cache: CacheAdapter - - @classmethod - def setUpClass(cls): - """Set the current cache adapter to an instance of MemoryCache() and make it available through ``self.cache``""" - helpers.cache.adapter_set(helpers.MemoryCache()) - cls.cache = helpers.cache - - @classmethod - def tearDownClass(cls): - for k in cls.cache_keys: - cls.cache.remove(k) - sleep(0.1) # A small sleep to give cache backends time to fully remove each item. - - def test_cache_set(self): - """Test basic cache.set and cache.get""" - key, c = self.cache_keys[0], self.cache - self.assertIs(c.get(key), None) - - c.set(key=key, value='TestingValue') - self.assertEqual(c.get(key), 'TestingValue') - - def test_cache_expire(self): - """Test that cache keys are removed after the specified timeout""" - key, c = self.cache_keys[1], self.cache - self.assertIs(c.get(key), None) - - c.set(key, 'ExpiryTest', timeout=2) - self.assertEqual(c.get(key), 'ExpiryTest') - sleep(3) - self.assertEqual(c.get(key), None) - - def test_cache_update_timeout(self): - """Test that cache.update_timeout extends timeouts correctly""" - key, c = self.cache_keys[2], self.cache - c.set(key, 'UpdateExpiryTest', timeout=3) - self.assertEqual(c.get(key), 'UpdateExpiryTest') - sleep(1.5) - c.update_timeout(key, timeout=10) - sleep(2.5) - self.assertEqual(c.get(key), 'UpdateExpiryTest') - - def test_cache_update_timeout_raise(self): - """Test that cache.update_timeout raises :class:`.helpers.CacheNotFound` if the key does not exist""" - key, c = self.cache_keys[3], self.cache - with self.assertRaises(helpers.CacheNotFound): - c.update_timeout(key, timeout=10) - - def test_cache_remove(self): - """Test that cache.remove correctly removes cache keys""" - key, c = self.cache_keys[4], self.cache - c.set(key, 'RemoveTest', timeout=30) - self.assertEqual(c.get(key), 'RemoveTest') - c.remove(key) - self.assertIs(c.get(key), None) - - -class TestRedisCache(TestMemoryCache): - """ - :class:`.RedisCache` Test cases for caching related functions/classes in :py:mod:`privex.helpers.cache` - - This is **simply a child class** for :class:`.TestMemoryCache` - but with an overridden :class:`.setUpClass` - to ensure the cache adapter is set to :class:`.RedisCache` for this re-run. - """ - - cache_keys: list - """A list of all cache keys used during the test case, so they can be removed by :py:meth:`.tearDown` once done.""" - - @classmethod - def setUpClass(cls): - """Set the current cache adapter to an instance of RedisCache() and make it available through ``self.cache``""" - if not plugin.HAS_REDIS: - print(f'The package "redis" is not installed, skipping Redis dependent tests ({cls.__name__}).') - return cls.tearDownClass() - helpers.cache.adapter_set(helpers.RedisCache()) - cls.cache = helpers.cache - - -if __name__ == '__main__': - unittest.main() - -""" - +===================================================+ - | © 2019 Privex Inc. | - | https://www.privex.io | - +===================================================+ - | | - | Originally Developed by Privex Inc. | - | | - | Core Developer(s): | - | | - | (+) Chris (@someguy123) [Privex] | - | (+) Kale (@kryogenic) [Privex] | - | | - +===================================================+ - -Copyright 2019 Privex Inc. ( https://www.privex.io ) - -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/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a8fe019 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,128 @@ +""" + + +This module contains test cases for Privex's Python Helper's (privex-helpers). + +Testing pre-requisites +---------------------- + + - Ensure you have any mandatory requirements installed (see setup.py's install_requires) + - You may wish to install any optional requirements listed in README.md for best results + - Python 3.7 is recommended at the time of writing this. See README.md in-case this has changed. + +Running via PyTest +------------------ + +To run the tests, we strongly recommend using the ``pytest`` tool (used by default for our Travis CI):: + + # Install PyTest if you don't already have it. + user@host: ~/privex-helpers $ pip3 install pytest + # You can add `-v` for more detailed output, just like when running the tests directly. + user@host: ~/privex-helpers $ pytest + + ===================================== test session starts ===================================== + platform darwin -- Python 3.7.0, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 + rootdir: /home/user/privex-helpers + collected 56 items + + tests/test_bool.py ......... [ 16%] + tests/test_cache.py ................ [ 44%] + tests/test_general.py ....... [ 57%] + tests/test_parse.py .......... [ 75%] + tests/test_rdns.py .............. [100%] + + ============================ 56 passed, 1 warnings in 0.17 seconds ============================ + +Running directly using Python Unittest +-------------------------------------- + +Alternatively, you can run the tests by hand with ``python3.7`` ( or just ``python3`` ) :: + + user@the-matrix ~/privex-helpers $ python3.7 -m tests + ............................ + ---------------------------------------------------------------------- + Ran 28 tests in 0.001s + + OK + +For more verbosity, simply add ``-v`` to the end of the command:: + + user@the-matrix ~/privex-helpers $ python3 -m tests -v + test_empty_combined (__main__.TestBoolHelpers) ... ok + test_isfalse_truthy (__main__.TestBoolHelpers) ... ok + test_v4_arpa_boundary_16bit (__main__.TestIPReverseDNS) + Test generating 16-bit v4 boundary ... ok + test_v4_arpa_boundary_24bit (__main__.TestIPReverseDNS) + Test generating 24-bit v4 boundary ... ok + test_kval_single (__main__.TestParseHelpers) + Test that a single value still returns a list ... ok + test_kval_spaced (__main__.TestParseHelpers) + Test key:val csv parsing with excess outer whitespace, and value whitespace ... ok + # Truncated excess output in this PyDoc example, as there are many more lines showing + # the results of each individual testcase, wasting space and adding bloat... + ---------------------------------------------------------------------- + Ran 28 tests in 0.001s + + OK + + + +**Copyright**:: + + Copyright 2019 Privex Inc. ( https://www.privex.io ) + License: X11 / MIT Github: https://github.com/Privex/python-helpers + + + +===================================================+ + | © 2019 Privex Inc. | + | https://www.privex.io | + +===================================================+ + | | + | Originally Developed by Privex Inc. | + | | + | Core Developer(s): | + | | + | (+) Chris (@someguy123) [Privex] | + | (+) Kale (@kryogenic) [Privex] | + | | + +===================================================+ + + Copyright 2019 Privex Inc. ( https://www.privex.io ) + + 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. + + +""" +import logging +import unittest +from privex.loghelper import LogHelper +from privex.helpers import env_bool +from tests.base import PrivexBaseCase, EmptyIter +from tests.test_cache import * +from tests.test_general import * +from tests.test_bool import TestBoolHelpers +from tests.test_rdns import TestIPReverseDNS +from tests.test_parse import TestParseHelpers + +if env_bool('DEBUG', False) is True: + LogHelper('privex.helpers', level=logging.DEBUG).add_console_handler(logging.DEBUG) +else: + LogHelper('privex.helpers', level=logging.CRITICAL) # Silence non-critical log messages + +if __name__ == '__main__': + unittest.main() diff --git a/tests/__main__.py b/tests/__main__.py new file mode 100644 index 0000000..a744411 --- /dev/null +++ b/tests/__main__.py @@ -0,0 +1,8 @@ +""" +This file exists to allow for ``python3 -m tests`` to work, as python's module execution option +attempts to load ``__main__`` from a package. +""" +from tests import * + +if __name__ == '__main__': + unittest.main() diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 0000000..28487b2 --- /dev/null +++ b/tests/base.py @@ -0,0 +1,68 @@ +""" +Various classes / functions / attributes used by test cases (no actual test cases in here) + +**Copyright**:: + + +===================================================+ + | © 2019 Privex Inc. | + | https://www.privex.io | + +===================================================+ + | | + | Originally Developed by Privex Inc. | + | | + | Core Developer(s): | + | | + | (+) Chris (@someguy123) [Privex] | + | (+) Kale (@kryogenic) [Privex] | + | | + +===================================================+ + + Copyright 2019 Privex Inc. ( https://www.privex.io ) + + 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. + +""" +import unittest + + +class EmptyIter(object): + """A mock iterable object with zero length for testing empty()""" + def __len__(self): + return 0 + + +class PrivexBaseCase(unittest.TestCase): + """ + Base test-case for module test cases to inherit. + + Contains useful class attributes such as ``falsey`` and ``empty_vals`` that are used + across different unit tests. + """ + + falsey = ['false', 'FALSE', False, 0, '0', 'no'] + """Normal False-y values, as various types""" + + falsey_empty = falsey + [None, '', 'null'] + """False-y values, plus 'empty' values like '' and None""" + + truthy = [True, 'TRUE', 'true', 'yes', 'y', '1', 1] + """Truthful values, as various types""" + + empty_vals = [None, ''] + empty_lst = empty_vals + [[], (), set(), {}, EmptyIter()] + empty_zero = empty_vals + [0, '0'] \ No newline at end of file diff --git a/tests/test_bool.py b/tests/test_bool.py new file mode 100644 index 0000000..a29ef1b --- /dev/null +++ b/tests/test_bool.py @@ -0,0 +1,97 @@ +""" +Test cases for boolean helper functions, such as :py:func:`.is_true`, :py:func:`.is_false`, and :py:func:`.empty` + +**Copyright**:: + + +===================================================+ + | © 2019 Privex Inc. | + | https://www.privex.io | + +===================================================+ + | | + | Originally Developed by Privex Inc. | + | | + | Core Developer(s): | + | | + | (+) Chris (@someguy123) [Privex] | + | (+) Kale (@kryogenic) [Privex] | + | | + +===================================================+ + + Copyright 2019 Privex Inc. ( https://www.privex.io ) + + 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. + + +""" +from privex import helpers +from tests.base import PrivexBaseCase + + +class TestBoolHelpers(PrivexBaseCase): + """Test the boolean check functions is_true, is_false, as well as empty()""" + + def test_isfalse_falsey(self): + """Test :py:func:`.is_false` with falsey values""" + for f in self.falsey_empty: + self.assertTrue(helpers.is_false(f), msg=f"is_false({repr(f)}") + + def test_isfalse_truthy(self): + """Test :py:func:`.is_false` with truthy values""" + for f in self.truthy: + self.assertFalse(helpers.is_false(f), msg=f"!is_false({repr(f)}") + + def test_istrue_truthy(self): + """Test :py:func:`.is_true` with truthy values""" + for f in self.truthy: + self.assertTrue(helpers.is_true(f), msg=f"is_true({repr(f)}") + + def test_istrue_falsey(self): + """Test :py:func:`.is_true` with falsey values""" + for f in self.falsey_empty: + self.assertFalse(helpers.is_true(f), msg=f"!is_true({repr(f)}") + + def test_empty_vals(self): + """Test :py:func:`.empty` with empty values""" + for f in self.empty_vals: + self.assertTrue(helpers.empty(f), msg=f"empty({repr(f)})") + + def test_empty_lst(self): + """Test :py:func:`.empty` with empty iterables""" + for f in self.empty_lst: + self.assertTrue(helpers.empty(f, itr=True), msg=f"empty({repr(f)})") + + def test_empty_zero(self): + """Test :py:func:`.empty` with different representations of ``0``""" + for f in self.empty_zero: + self.assertTrue(helpers.empty(f, zero=True), msg=f"empty({repr(f)})") + + def test_empty_combined(self): + """Test :py:func:`.empty` with empty iterables AND different representations of ``0``""" + for f in self.empty_zero + self.empty_lst: + self.assertTrue(helpers.empty(f, zero=True, itr=True), msg=f"empty({repr(f)})") + + def test_notempty(self): + """Test :py:func:`.empty` with non-empty values""" + # Basic string test + self.assertFalse(helpers.empty('hello')) + # Integer test + self.assertFalse(helpers.empty(1, zero=True)) + # Iterable tests + self.assertFalse(helpers.empty(['world'], itr=True)) + self.assertFalse(helpers.empty(('world',), itr=True)) + self.assertFalse(helpers.empty({'hello': 'world'}, itr=True)) diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..4347c79 --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,219 @@ +""" +Test cases for the cache decorator :py:func:`.r_cache` plus cache layers :class:`.RedisCache` and :class:`.MemoryCache` + +**Copyright**:: + + +===================================================+ + | © 2019 Privex Inc. | + | https://www.privex.io | + +===================================================+ + | | + | Originally Developed by Privex Inc. | + | | + | Core Developer(s): | + | | + | (+) Chris (@someguy123) [Privex] | + | (+) Kale (@kryogenic) [Privex] | + | | + +===================================================+ + + Copyright 2019 Privex Inc. ( https://www.privex.io ) + + 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. + + +""" +import warnings +from time import sleep +from privex import helpers +from privex.helpers import r_cache, random_str, plugin, CacheAdapter +from tests.base import PrivexBaseCase + + +class TestCacheDecoratorMemory(PrivexBaseCase): + """ + Test that the decorator :py:func:`privex.helpers.decorators.r_cache` caches correctly, with adapter + :class:`helpers.MemoryCache` and also verifies dynamic cache key generation works as expected. + """ + + cache = helpers.cache.cached + + @classmethod + def setUpClass(cls): + helpers.cache.adapter_set(helpers.MemoryCache()) + + def test_rcache_rand(self): + """Decorate random string function with r_cache - test that two calls return the same string""" + @r_cache('pxhelpers_test_rand') + def r_test(): + return random_str() + + val1 = r_test() + val2 = r_test() + self.assertEqual(val1, val2, msg=f"'{repr(val1)}' == '{repr(val2)}'") + + def test_rcache_rand_dynamic(self): + """Decorate random string function with r_cache and use format_args for dynamic cache string testing""" + + @r_cache('pxhelpers_rand_dyn:{}:{}', format_args=[0, 1, 'x', 'y']) + def r_test(x, y): + return random_str() + + a, b, c, d = r_test(1, 1), r_test(1, 2), r_test(x=1, y=2), r_test(y=2, x=1) + e, f, g, h = r_test(1, 1), r_test(1, 2), r_test(x=1, y=2), r_test(y=2, x=1) + + # Test random cached strings using 1,1 and 1,2 as positional args + self.assertEqual(a, e, msg="r_test(1,1) == r_test(1,1)") + self.assertEqual(b, f, msg="r_test(1,2) == r_test(1,2)") + # Test positional arg cache is equivalent to kwarg cache + self.assertEqual(b, c, msg="r_test(1,2) == r_test(y=1,x=2)") + self.assertEqual(b, g, msg="r_test(1,2) == r_test(x=1,y=2)") + self.assertEqual(b, d, msg="r_test(1,2) == r_test(y=2,x=1)") + # Test kwarg cache is equivalent to inverted kwarg cache + self.assertEqual(h, c, msg="r_test(y=2, x=1) == r_test(x=1, y=2)") + # To be sure they aren't all producing the same string, make sure that 1,2 and 1,1 + # (positional and kwarg) are not equal + self.assertNotEqual(a, b, msg="r_test(1,1) != r_test(1,2)") + self.assertNotEqual(g, a, msg="r_test(x=1, y=2) != r_test(1,1)") + + def test_rcache_callable(self): + """Decorate random string function - use a lambda callable to determine a cache key""" + @r_cache(lambda x, y: f"{x}") + def r_test(x, y): + return random_str() + + a, b = r_test(1, 1), r_test(1, 1) + c, d = r_test(2, 1), r_test(1, 2) + + # If the first argument is the same, then we'll get the same result. The second argument is ignored. + self.assertEqual(a, b, msg='r_test(1,1) == r_test(1,1)') + self.assertEqual(a, d, msg='r_test(1,1) == r_test(1,2)') + # If the first argument is different (1 and 2), then we should get a different result. + self.assertNotEqual(a, c, msg='r_test(1,1) != r_test(2,1)') + + def tearDown(self): + """Remove any Redis keys used during test, to avoid failure on re-run""" + self.cache.remove('pxhelpers_test_rand', 'pxhelpers_rand_dyn:1:1', 'pxhelpers_rand_dyn:1:2', + 'pxhelpers_rand_dyn:2:1') + super(TestCacheDecoratorMemory, self).tearDown() + + +class TestCacheDecoratorRedis(TestCacheDecoratorMemory): + """ + Test decorator :py:func:`privex.helpers.decorators.r_cache` with adapter + :class:`helpers.RedisCache` + + (See :class:`.TestCacheDecoratorMemory`) + """ + + @classmethod + def setUpClass(cls): + if not plugin.HAS_REDIS: + warnings.warn(f'The package "redis" is not installed, skipping Redis dependent tests ({cls.__name__}).') + return cls.tearDownClass() + helpers.cache.adapter_set(helpers.RedisCache()) + + +class TestMemoryCache(PrivexBaseCase): + """:class:`.MemoryCache` Test cases for caching related functions/classes in :py:mod:`privex.helpers.cache`""" + + cache_keys = [ + 'test_cache_set', + 'test_expire', + 'test_update_timeout', + 'test_update_timeout_noexist', + 'test_cache_remove', + ] + """A list of all cache keys used during the test case, so they can be removed by :py:meth:`.tearDown` once done.""" + + cache: CacheAdapter + + @classmethod + def setUpClass(cls): + """Set the current cache adapter to an instance of MemoryCache() and make it available through ``self.cache``""" + helpers.cache.adapter_set(helpers.MemoryCache()) + cls.cache = helpers.cached + + @classmethod + def tearDownClass(cls): + for k in cls.cache_keys: + cls.cache.remove(k) + sleep(0.1) # A small sleep to give cache backends time to fully remove each item. + + def test_cache_set(self): + """Test basic cache.set and cache.get""" + key, c = self.cache_keys[0], self.cache + self.assertIs(c.get(key), None) + + c.set(key=key, value='TestingValue') + self.assertEqual(c.get(key), 'TestingValue') + + def test_cache_expire(self): + """Test that cache keys are removed after the specified timeout""" + key, c = self.cache_keys[1], self.cache + self.assertIs(c.get(key), None) + + c.set(key, 'ExpiryTest', timeout=2) + self.assertEqual(c.get(key), 'ExpiryTest') + sleep(3) + self.assertEqual(c.get(key), None) + + def test_cache_update_timeout(self): + """Test that cache.update_timeout extends timeouts correctly""" + key, c = self.cache_keys[2], self.cache + c.set(key, 'UpdateExpiryTest', timeout=3) + self.assertEqual(c.get(key), 'UpdateExpiryTest') + sleep(1.5) + c.update_timeout(key, timeout=10) + sleep(2.5) + self.assertEqual(c.get(key), 'UpdateExpiryTest') + + def test_cache_update_timeout_raise(self): + """Test that cache.update_timeout raises :class:`.helpers.CacheNotFound` if the key does not exist""" + key, c = self.cache_keys[3], self.cache + with self.assertRaises(helpers.CacheNotFound): + c.update_timeout(key, timeout=10) + + def test_cache_remove(self): + """Test that cache.remove correctly removes cache keys""" + key, c = self.cache_keys[4], self.cache + c.set(key, 'RemoveTest', timeout=30) + self.assertEqual(c.get(key), 'RemoveTest') + c.remove(key) + self.assertIs(c.get(key), None) + + +class TestRedisCache(TestMemoryCache): + """ + :class:`.RedisCache` Test cases for caching related functions/classes in :py:mod:`privex.helpers.cache` + + This is **simply a child class** for :class:`.TestMemoryCache` - but with an overridden :class:`.setUpClass` + to ensure the cache adapter is set to :class:`.RedisCache` for this re-run. + """ + + cache_keys: list + """A list of all cache keys used during the test case, so they can be removed by :py:meth:`.tearDown` once done.""" + + @classmethod + def setUpClass(cls): + """Set the current cache adapter to an instance of RedisCache() and make it available through ``self.cache``""" + if not plugin.HAS_REDIS: + warnings.warn(f'The package "redis" is not installed, skipping Redis dependent tests ({cls.__name__}).') + return cls.tearDownClass() + helpers.cache.adapter_set(helpers.RedisCache()) + cls.cache = helpers.cached diff --git a/tests/test_general.py b/tests/test_general.py new file mode 100644 index 0000000..c09358b --- /dev/null +++ b/tests/test_general.py @@ -0,0 +1,207 @@ +""" +General test cases for various un-categorized functions / classes e.g. :py:func:`.chunked` and :py:func:`.ping` + +**Copyright**:: + + +===================================================+ + | © 2019 Privex Inc. | + | https://www.privex.io | + +===================================================+ + | | + | Originally Developed by Privex Inc. | + | | + | Core Developer(s): | + | | + | (+) Chris (@someguy123) [Privex] | + | (+) Kale (@kryogenic) [Privex] | + | | + +===================================================+ + + Copyright 2019 Privex Inc. ( https://www.privex.io ) + + 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. + + +""" +import platform +import warnings + +from privex import helpers +from privex.helpers import ping +from tests.base import PrivexBaseCase + + +class TestGeneral(PrivexBaseCase): + """General test cases that don't fit under a specific category""" + + def setUp(self): + self.tries = 0 + + def test_ping(self): + """Test success & failure cases for ping function with IPv4, as well as input validation""" + try: + with self.assertRaises(ValueError): + ping('127.0.0.1', -1) + with self.assertRaises(ValueError): + ping('127.0.0.1', 0) + with self.assertRaises(ValueError): + ping('notavalidip', 1) + self.assertTrue(ping('127.0.0.1', 3)) + self.assertFalse(ping('192.0.2.0', 3)) + except NotImplementedError as e: + warnings.warn(f"Skipping test TestGeneral.test_ping as platform is not supported: {str(e)}") + return + + def test_ping_v6(self): + """Test success & failure cases for ping function with IPv6, as well as input validation""" + try: + with self.assertRaises(ValueError): + ping('::1', -1) + with self.assertRaises(ValueError): + ping('::1', 0) + with self.assertRaises(ValueError): + ping('notavalidip', 1) + self.assertTrue(ping('::1', 3)) + self.assertFalse(ping('fd06:dead::beef:ab12', 3)) + except NotImplementedError as e: + warnings.warn(f"Skipping test TestGeneral.test_ping_v6 as platform is not supported: \"{str(e)}\"") + return + + def _check_asn(self, asn, expected_name): + if not helpers.plugin.HAS_DNSPYTHON: + return warnings.warn(f"Skipping asn_to_name tests as dnspython is not installed...") + name = helpers.asn_to_name(asn) + self.assertEqual(name, expected_name, msg=f"asn_to_name({asn}) '{name}' == '{expected_name}'") + + def test_asn_to_name_int(self): + """Test Privex's ASN (as an int) 210083 resolves to 'PRIVEX, SE'""" + self._check_asn(210083, 'PRIVEX, SE') + + def test_asn_to_name_str(self): + """Test Cloudflare's ASN (as a str) '13335' resolves to 'CLOUDFLARENET - Cloudflare, Inc., US'""" + self._check_asn('13335', 'CLOUDFLARENET - Cloudflare, Inc., US') + + def test_asn_to_name_erroneous(self): + """Test asn_to_name returns 'Unknown ASN' when quiet, otherwise throws a KeyError for ASN 'nonexistent'""" + self.assertEqual(helpers.asn_to_name('nonexistent'), 'Unknown ASN') + with self.assertRaises(KeyError): + helpers.asn_to_name('nonexistent', quiet=False) + + def test_asn_to_name_erroneous_2(self): + """Test asn_to_name returns 'Unknown ASN' when quiet, otherwise throws KeyError for the ASN 999999999""" + self.assertEqual(helpers.asn_to_name(999999999), 'Unknown ASN') + with self.assertRaises(KeyError): + helpers.asn_to_name(999999999, quiet=False) + + def test_chunked(self): + """Create a 20 element long list, split it into 4 chunks, and verify the chunks are correctly made""" + x = list(range(0, 20)) + c = list(helpers.chunked(x, 4)) + self.assertEqual(len(c), 4) + self.assertEqual(c[0], [0, 1, 2, 3, 4]) + self.assertEqual(c[1], [5, 6, 7, 8, 9]) + + async def _tst_async(self, a, b): + """Basic async function used for testing async code""" + return a * 2, b * 3 + + def test_run_sync(self): + """Test helpers.async.run_sync by running an async function from this synchronous test""" + x, y = helpers.run_sync(self._tst_async, 5, 10) + d, e = helpers.run_sync(self._tst_async, 1, 2) + self.assertEqual(x, 10) + self.assertEqual(y, 30) + self.assertEqual(d, 2) + self.assertEqual(e, 6) + + @helpers.async_sync + def test_async_decorator(self): + """Test the async_sync decorator by wrapping this unit test""" + + x, y = yield from self._tst_async(5, 10) + d, e = yield from self._tst_async(1, 2) + + self.assertEqual(x, 10) + self.assertEqual(y, 30) + self.assertEqual(d, 2) + self.assertEqual(e, 6) + + def test_async_decorator_return(self): + """Test the async_sync decorator handles returning async data from synchronous function""" + + async_func = self._tst_async + + @helpers.async_sync + def non_async(a, b): + f, g = yield from async_func(a, b) + return f, g + + x, y = non_async(5, 10) + d, e = non_async(1, 2) + + self.assertEqual(x, 10) + self.assertEqual(y, 30) + self.assertEqual(d, 2) + self.assertEqual(e, 6) + + def test_retry_on_err(self): + """Test that the :class:`helpers.retry_on_err` decorator retries a function 3 times as expected""" + + @helpers.retry_on_err(max_retries=3, delay=0.2) + def retry_func(cls): + cls.tries += 1 + raise Exception + + with self.assertRaises(Exception): + retry_func(self) + + # The first run should cause tries = 1, then after 3 re-tries it should reach 4 tries in total. + self.assertEqual(self.tries, 4) + + def test_retry_on_err_return(self): + """Test that the :class:`helpers.retry_on_err` decorator can return correctly after some retries""" + + @helpers.retry_on_err(max_retries=3, delay=0.2) + def retry_func(cls): + if cls.tries < 3: + cls.tries += 1 + raise Exception + return 'success' + + ret = retry_func(self) + + # retry_func stops raising exceptions after the 2nd retry (try 3), thus 3 tries in total + self.assertEqual(self.tries, 3) + self.assertEqual(ret, 'success') + + def test_inject_items(self): + """Test :py:func:`helpers.inject_items` injecting into a list after position 1""" + a = ['a', 'b', 'g'] + b = ['c', 'd', 'e', 'f'] + # Position 1 is the 2nd element of ``a`` - which is the letter 'b' + c = helpers.inject_items(b, a, 1) + self.assertListEqual(c, ['a', 'b', 'c', 'd', 'e', 'f', 'g']) + + def test_inject_items_2(self): + """Test :py:func:`helpers.inject_items` injecting into a list after position 3""" + a = ['a', 'b', 'c', 'd', 'h'] + b = ['e', 'f', 'g'] + # Position 3 is the 4th element of ``a`` - which is the letter 'd' + c = helpers.inject_items(b, a, 3) + self.assertListEqual(c, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']) + diff --git a/tests/test_parse.py b/tests/test_parse.py new file mode 100644 index 0000000..697c311 --- /dev/null +++ b/tests/test_parse.py @@ -0,0 +1,117 @@ +""" +Test cases for parsing functions, such as :py:func:`.parse_csv`, :py:func:`.env_keyval` etc. + +**Copyright**:: + + +===================================================+ + | © 2019 Privex Inc. | + | https://www.privex.io | + +===================================================+ + | | + | Originally Developed by Privex Inc. | + | | + | Core Developer(s): | + | | + | (+) Chris (@someguy123) [Privex] | + | (+) Kale (@kryogenic) [Privex] | + | | + +===================================================+ + + Copyright 2019 Privex Inc. ( https://www.privex.io ) + + 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. + + +""" +import os + +from privex import helpers +from tests.base import PrivexBaseCase + + +class TestParseHelpers(PrivexBaseCase): + """Test the parsing functions parse_csv and parse_keyval""" + + def test_csv_spaced(self): + """Test csv parsing with excess outer whitespace, and value whitespace""" + c = helpers.parse_csv(' valid , spaced out, csv ') + self.assertListEqual(c, ['valid', 'spaced out', 'csv']) + + def test_csv_single(self): + """Test that a single value still returns a list""" + self.assertListEqual(helpers.parse_csv('single'), ['single']) + + def test_kval_clean(self): + """Test that a clean key:val csv is parsed correctly""" + self.assertListEqual( + helpers.parse_keyval('John:Doe,Jane:Smith'), + [('John', 'Doe'), ('Jane', 'Smith')] + ) + + def test_kval_spaced(self): + """Test key:val csv parsing with excess outer whitespace, and value whitespace""" + self.assertListEqual( + helpers.parse_keyval(' John : Doe , Jane : Smith '), + [('John', 'Doe'), ('Jane', 'Smith')] + ) + + def test_kval_single(self): + """Test that a single value still returns a list""" + self.assertListEqual( + helpers.parse_keyval('John:Doe'), + [('John', 'Doe')] + ) + + def test_kval_custom_clean(self): + """ + Test that a clean key:val csv with custom split characters is parsed correctly + (pipe for kv, semi-colon for pair separation) + """ + self.assertListEqual( + helpers.parse_keyval('John|Doe;Jane|Smith', valsplit='|', csvsplit=';'), + [('John', 'Doe'), ('Jane', 'Smith')] + ) + + def test_kval_custom_spaced(self): + """Test key:val csv parsing with excess outer/value whitespace, and custom split characters.""" + self.assertListEqual( + helpers.parse_keyval(' John | Doe ; Jane |Smith ', valsplit='|', csvsplit=';'), + [('John', 'Doe'), ('Jane', 'Smith')] + ) + + def test_env_nonexist_bool(self): + """Test env_bool returns default with non-existant env var""" + k = 'EXAMPLE_NOEXIST' + if k in os.environ: del os.environ[k] # Ensure the env var we're testing definitely does not exist. + self.assertIsNone(helpers.env_bool(k)) + self.assertEqual(helpers.env_bool(k, 'error'), 'error') + + def test_env_bool_true(self): + """Test env_bool returns True boolean with valid env var""" + k = 'EXAMPLE_EXIST' + for v in self.truthy: + os.environ[k] = str(v) + self.assertTrue(helpers.env_bool(k, 'fail'), msg=f'env_bool({v}) === True') + + def test_env_bool_false(self): + """Test env_bool returns False boolean with valid env var""" + k = 'EXAMPLE_EXIST' + for v in self.falsey: + os.environ[k] = str(v) + self.assertFalse(helpers.env_bool(k, 'fail'), msg=f'env_bool({v}) === False') + diff --git a/tests/test_rdns.py b/tests/test_rdns.py new file mode 100644 index 0000000..89c0f8b --- /dev/null +++ b/tests/test_rdns.py @@ -0,0 +1,150 @@ +""" +A thorough test case for :py:func:`.ip_to_rdns` - which converts IPv4/v6 addresses into ARPA reverse DNS domains. + +**Copyright**:: + + +===================================================+ + | © 2019 Privex Inc. | + | https://www.privex.io | + +===================================================+ + | | + | Originally Developed by Privex Inc. | + | | + | Core Developer(s): | + | | + | (+) Chris (@someguy123) [Privex] | + | (+) Kale (@kryogenic) [Privex] | + | | + +===================================================+ + + Copyright 2019 Privex Inc. ( https://www.privex.io ) + + 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. + + +""" +from privex.helpers import ip_to_rdns, BoundaryException +from tests.base import PrivexBaseCase + +VALID_V4_1 = '172.131.22.17' +VALID_V4_1_16BOUND = '131.172.in-addr.arpa' +VALID_V4_1_24BOUND = '22.131.172.in-addr.arpa' +VALID_V4_2 = '127.0.0.1' +VALID_V4_2_RDNS = '1.0.0.127.in-addr.arpa' +VALID_V6_1 = '2001:dead:beef::1' +VALID_V6_1_RDNS = '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.1.0.0.2.ip6.arpa' +VALID_V6_1_16BOUND = '1.0.0.2.ip6.arpa' +VALID_V6_1_32BOUND = 'd.a.e.d.1.0.0.2.ip6.arpa' + + +class TestIPReverseDNS(PrivexBaseCase): + """ + Unit testing for the reverse DNS functions in :py:mod:`privex.helpers.net` + + Covers: + - positive resolution tests (generate standard rDNS domain from clean input) + - positive boundary tests (confirm valid results with range of boundaries) + - negative address tests (ensure errors thrown for invalid v4/v6 addresses) + - negative boundary tests (ensure errors thrown for invalid v4/v6 rDNS boundaries) + + """ + + #### + # Positive tests (normal resolution) + #### + + def test_v4_to_arpa(self): + """Test generating rDNS for standard v4""" + rdns = ip_to_rdns(VALID_V4_2) + self.assertEqual(rdns, VALID_V4_2_RDNS) + + def test_v6_to_arpa(self): + """Test generating rDNS for standard v6""" + rdns = ip_to_rdns(VALID_V6_1) + self.assertEqual(rdns, VALID_V6_1_RDNS) + + #### + # Positive tests (boundaries) + #### + + def test_v4_arpa_boundary_24bit(self): + """Test generating 24-bit v4 boundary""" + rdns = ip_to_rdns(VALID_V4_1, boundary=True, v4_boundary=24) + self.assertEqual(rdns, VALID_V4_1_24BOUND) + + def test_v4_arpa_boundary_16bit(self): + """Test generating 16-bit v4 boundary""" + rdns = ip_to_rdns(VALID_V4_1, boundary=True, v4_boundary=16) + self.assertEqual(rdns, VALID_V4_1_16BOUND) + + def test_v6_arpa_boundary_16bit(self): + """Test generating 16-bit v6 boundary""" + rdns = ip_to_rdns(VALID_V6_1, boundary=True, v6_boundary=16) + self.assertEqual(rdns, VALID_V6_1_16BOUND) + + def test_v6_arpa_boundary_32bit(self): + """Test generating 32-bit v6 boundary""" + rdns = ip_to_rdns(VALID_V6_1, boundary=True, v6_boundary=32) + self.assertEqual(rdns, VALID_V6_1_32BOUND) + + #### + # Negative tests (invalid addresses) + #### + def test_v4_invalid(self): + """Raise if IPv4 address has < 4 octets""" + with self.assertRaises(ValueError): + ip_to_rdns('127.0.0') + + def test_v4_invalid_2(self): + """Raise if IPv4 address has octet out of range""" + with self.assertRaises(ValueError): + ip_to_rdns('127.0.0.373') + + def test_v6_invalid(self): + """Raise if IPv6 address has invalid block formatting""" + with self.assertRaises(ValueError): + ip_to_rdns('2001::ff::a') + + def test_v6_invalid_2(self): + """Raise if v6 address has invalid chars""" + with self.assertRaises(ValueError): + ip_to_rdns('2001::fh') + + #### + # Negative tests (invalid boundaries) + #### + + def test_v4_inv_boundary(self): + """Raise if IPv4 boundary isn't divisable by 8""" + with self.assertRaises(BoundaryException): + ip_to_rdns(VALID_V4_2, boundary=True, v4_boundary=7) + + def test_v4_inv_boundary_2(self): + """Raise if IPv4 boundary is too short""" + with self.assertRaises(BoundaryException): + ip_to_rdns(VALID_V4_2, boundary=True, v4_boundary=0) + + def test_v6_inv_boundary(self): + """Raise if IPv6 boundary isn't dividable by 4""" + with self.assertRaises(BoundaryException): + ip_to_rdns(VALID_V6_1, boundary=True, v6_boundary=9) + + def test_v6_inv_boundary_2(self): + """Raise if IPv6 boundary is too short""" + with self.assertRaises(BoundaryException): + ip_to_rdns(VALID_V6_1, boundary=True, v6_boundary=0)