diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d3a57a --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.log +*.swp +__pycache__ +.idea +.vscode + +# PyPi build files +build +dist +*.egg-info + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d7b23da --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ ++===================================================+ +| © 2019 Privex Inc. | +| https://www.privex.io | ++===================================================+ +| | +| Privex's Python Helpers | +| License: X11/MIT | +| | +| Core Developer(s): | +| | +| (+) Chris (@someguy123) [Privex] | +| (+) Kale (@kryogenic) [Privex] | +| | ++===================================================+ + +Privex's Python Helpers - a variety of python functions and classes that are useful in many projects +Copyright (c) 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. + +Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Software without prior written authorization. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..be0dbe2 --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +# Privex's Python Helpers + +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 +packaged, and so we've amalgamated them into this PyPi package, `privex-helpers`. + + +``` + +===================================================+ + | © 2019 Privex Inc. | + | https://www.privex.io | + +===================================================+ + | | + | Originally Developed by Privex Inc. | + | | + | Core Developer(s): | + | | + | (+) Chris (@someguy123) [Privex] | + | (+) Kale (@kryogenic) [Privex] | + | | + +===================================================+ +``` + +# Install + +### Download and install from PyPi using pip (recommended) + +```sh +pip3 install privex-helpers +``` + +### (Alternative) Manual install from Git + +**Option 1 - Use pip to install straight from Github** + +```sh +pip3 install git+https://github.com/Privex/python-helpers +``` + +**Option 2 - Clone and install manually** + +```bash +# Clone the repository from Github +git clone https://github.com/Privex/python-helpers +cd python-helpers + +# RECOMMENDED MANUAL INSTALL METHOD +# Use pip to install the source code +pip3 install . + +# ALTERNATIVE MANUAL INSTALL METHOD +# If you don't have pip, or have issues with installing using it, then you can use setuptools instead. +python3 setup.py install +``` + +# License + +**Python Log Helper** was created by [Privex Inc. of Belize City](https://www.privex.io), and licensed under the X11/MIT License. +See the file [LICENSE](https://github.com/Privex/python-loghelper/blob/master/LICENSE) for the license text. + +**TL;DR; license:** + +We offer no warranty. You can copy it, modify it, use it in projects with a different license, and even in commercial (paid for) software. + +The most important rule is - you **MUST** keep the original license text visible (see `LICENSE`) in any copies. + + + +# Example uses + +We export all of the submodule's contents in `privex/helpers/__init__.py`, so you can import any +function/class/attribute straight from `privex.helper` without needing several import lines. + +Here are some of the most useful examples (part of our `.common` module, no dependencies) + +```python +from privex.helpers import empty, is_true, random_str, ip_is_v4, ip_is_v6 + +#### +# Our empty() helper is very convenient and easy to remember. It allows you to quick check if a variable is "empty" +# (a blank string, None, zero, or an empty list/dict/tuple). +# +# empty(v, zero: bool = False, itr: bool = False) -> bool +# +# For safety, it only returns True for empty iterables / integer zero (0) if you enable `zero` and/or `itr` respectively. +#### + +x = '' +if empty(x): + print('Var x is empty: either None or empty string') + +y = [] +if empty(y, itr=True): + print('Var y is empty: either None, empty string, or empty iterable') + +#### +# Our is_true() / is_false() helpers are designed to ease checking boolean values from plain text config files +# such as .env files, or values passed in an API call +#### + +# The strings 'true' / 'y' / 'yes' / '1' are all considered truthy, plus int 1 / bool True +enable_x = 'YES' # String values are automatically cast to lowercase, so even 'YeS' and 'TrUe' are fine. +if is_true(enable_x): + print('Enabling feature X') + +#### +# Need to generate a random alphanumeric string for a password / API key? Try random_str(), which uses SystemRandom() +# for cryptographically secure randomness, and by default uses our SAFE_CHARS character set, removing look-alike +# characters such as 1 and l, or o and 0 +#### + +# Default random string - 50 character alphanum without easily mistaken chars +random_str() # outputs: 'MrCWLYMYtT9A7bHc5ZNE4hn7PxHPmsWaT9GpfCkmZASK7ApN8r' + +# Customised random string - 12 characters using only the characters `abcdef12345` +random_str(12, chars='abcdef12345') # outputs: 'aba4cc14a43d' + +#### +# As a server hosting provider, we deal with IP addresses a lot :) +# The helper functions ip_is_v4 and ip_is_v6 do exactly as their name says, they return a boolean +# if an IP is IPv4 or IPv6 respectively. +#### + +ip_is_v4('192.168.1.1') # True +ip_is_v4('2a07:e00::1') # False + +ip_is_v6('192.168.1.1') # False +ip_is_v6('2a07:e00::1') # True + +``` + +# Minimal dependencies + +Most of our helper code is independant, and does not result in any extra dependencies being installed. + +Some of our helpers are dependant on external libraries or frameworks, such as Django or Flask. To avoid +large Python packages such as Django being installed needlessly, we programatically enable/disable some +of the helpers based on whether you have the required dependency installed. + +This package only requires (and automatically installs if needed) a single dependency - our +[privex-loghelper](https://github.com/Privex/python-loghelper) package, which itself is lightweight +and dependency free. + + +Optional requirements (just `pip3 install` them depending on the helpers you require): + +``` +# For all Django-specific helpers in privex.helpers.django +Django +# For certain DNS dependant helpers in privex.helpers.net +dnspython>=1.16.0 +``` + +# Contributing + +We're happy to accept pull requests, no matter how small. + +Please make sure any changes you make meet these basic requirements: + + - No additional dependencies. We want our helper package to be lightweight and painless to install. + - Any code taken from other projects should be compatible with the MIT License + - This is a new project, and as such, supporting Python versions prior to 3.4 is very low priority. + - However, we're happy to accept PRs to improve compatibility with older versions of Python, as long as it doesn't: + - drastically increase the complexity of the code + - OR cause problems for those on newer versions of Python. + +**Legal Disclaimer for Contributions** + +Nobody wants to read a long document filled with legal text, so we've summed up the important parts here. + +If you contribute content that you've created/own to projects that are created/owned by Privex, such as code or +documentation, then you might automatically grant us unrestricted usage of your content, regardless of the open source +license that applies to our project. + +If you don't want to grant us unlimited usage of your content, you should make sure to place your content +in a separate file, making sure that the license of your content is clearly displayed at the start of the file +(e.g. code comments), or inside of it's containing folder (e.g. a file named LICENSE). + +You should let us know in your pull request or issue that you've included files which are licensed +separately, so that we can make sure there's no license conflicts that might stop us being able +to accept your contribution. + +If you'd rather read the whole legal text, it should be included as `privex_contribution_agreement.txt`. + + +# Thanks for reading! + +**If this project has helped you, consider [grabbing a VPS or Dedicated Server from Privex](https://www.privex.io) - prices start at as little as US$8/mo (we take cryptocurrency!)** \ No newline at end of file diff --git a/privex/__init__.py b/privex/__init__.py new file mode 100644 index 0000000..d72c164 --- /dev/null +++ b/privex/__init__.py @@ -0,0 +1,36 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) + +""" + +===================================================+ + | © 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. +""" \ No newline at end of file diff --git a/privex/helpers/__init__.py b/privex/helpers/__init__.py new file mode 100644 index 0000000..bf330fc --- /dev/null +++ b/privex/helpers/__init__.py @@ -0,0 +1,61 @@ +""" + +===================================================+ + | © 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 +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' + +# Only import the Django functions if Django is actually installed +try: + import django + from privex.helpers.django import * +except ImportError: + log.debug('privex.helpers __init__ failed to import "django", not loading django helpers') + pass + + +from privex.helpers.common import * +from privex.helpers.decorators import * +from privex.helpers.net import * diff --git a/privex/helpers/common.py b/privex/helpers/common.py new file mode 100644 index 0000000..e7c828a --- /dev/null +++ b/privex/helpers/common.py @@ -0,0 +1,179 @@ +""" +Common functions and classes that don't fit into a specific category + + +===================================================+ + | © 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 random +import string +import argparse +import logging +import sys +from typing import Sequence + +log = logging.getLogger(__name__) + +SAFE_CHARS = 'abcdefhkmnprstwxyz23456789ACDEFGHJKLMNPRSTWXYZ' +"""Characters that shouldn't be mistaken, avoiding users confusing an o with a 0 or an l with a 1 or I""" + +ALPHANUM = string.ascii_uppercase + string.digits + string.ascii_lowercase +"""All characters from a-z, A-Z, and 0-9 - for random strings where there's no risk of user font confusion""" + +def random_str(size:int = 50, chars: Sequence = SAFE_CHARS) -> str: + """ + Generate a random string of arbitrary length using a given character set (string / list / tuple). Uses Python's + SystemRandom class to provide relatively secure randomness from the OS. (On Linux, uses /dev/urandom) + + By default, uses the character set :py:attr:`.SAFE_CHARS` which contains letters a-z / A-Z and numbers 2-9 + with commonly misread characters removed (such as 1, l, L, 0 and o). Pass :py:attr:`.ALPHANUM` as `chars` if + you needthe full set of upper/lowercase + numbers. + + Usage: + + >>> from privex.helpers import random_str + >>> # Default random string - 50 character alphanum without easily mistaken chars + >>> password = random_str() + 'MrCWLYMYtT9A7bHc5ZNE4hn7PxHPmsWaT9GpfCkmZASK7ApN8r' + >>> # Customised random string - 12 characters using only the characters `abcdef12345` + >>> custom = random_str(12, chars='abcdef12345') + 'aba4cc14a43d' + + Warning: As this relies on the OS's entropy features, it may not be cryptographically secure on non-Linux platforms: + + > The returned data should be unpredictable enough for cryptographic applications, though its exact quality + > depends on the OS implementation. + + :param int size: Length of random string to generate (default 50 characters) + :param str chars: Characterset to generate with ( default is :py:attr:`.SAFE_CHARS` - a-z/A-Z/0-9 with + often misread chars removed) + + """ + return ''.join(random.SystemRandom().choice(chars) for _ in range(size)) + +def empty(v, zero: bool = False, itr: bool = False) -> bool: + """ + Quickly check if a variable is empty or not. By default only '' and None are checked, use `itr` and `zero` to + test for empty iterable's and zeroed variables. + + Returns True if a variable is None or '', returns False if variable passes the tests + + Example usage: + + >>> x, y = [], None + >>> if empty(y): + ... print('Var y is None or a blank string') + ... + >>> if empty(x, itr=True): + ... print('Var x is None, blank string, or an empty dict/list/iterable') + + :param v: The variable to check if it's empty + :param zero: if zero=True, then return True if the variable is 0 + :param itr: if itr=True, then return True if the variable is ``[]``, ``{}``, or is an iterable and has 0 length + :return bool is_blank: True if a variable is blank (``None``, ``''``, ``0``, ``[]`` etc.) + :return bool is_blank: False if a variable has content (or couldn't be checked properly) + """ + + _check = [None, ''] + if zero: _check.append(0) + if v in _check: return True + if itr: + if v == [] or v == {}: return True + if hasattr(v, '__len__') and len(v) == 0: return True + + return False + +def is_true(v) -> bool: + """ + Check if a given bool/str/int value is some form of True: + boolean: True // string: 'true', 'yes', 'y', '1' // integer: 1 + + (note: strings are automatically .lower()'d) + + Usage: + + >>> is_true('true') + True + >>> is_true('no') + False + + :param Any v: The value to check for truthfulness + :return bool is_true: True if the value appears to be truthy, otherwise False. + """ + v = v.lower() if type(v) is str else v + return v in [True, 'true', 'yes', 'y', '1', 1] + +def is_false(v, chk_none: bool = True) -> bool: + """ + **Warning:** Unless you specifically need to verify a value is Falsey, it's usually safer to + check for truth :py:func:`.is_true` and invert the result, i.e. ``if not is_true(v)`` + + Check if a given bool/str/int value is some form of False:: + + boolean: False // string: 'false', 'no', 'n', '0' // integer: 0 + + If ``chk_none`` is True (default), will also consider the below values to be Falsey:: + + boolean: None // string: 'null', 'none', '' + + (note: strings are automatically .lower()'d) + + Usage: + + >>> is_false(0) + True + >>> is_false('yes') + False + + :param Any v: The value to check for falseyness + :param bool chk_none: If True, treat None/'none'/'null' as Falsey (default True) + :return bool is_False: True if the value appears to be falsey, otherwise False. + """ + v = v.lower() if type(v) is str else v + chk = [False, 'false', 'no', 'n', '0', 0] + chk += [None, 'none', 'null', ''] if chk_none else [] + return v in chk + + +class ErrHelpParser(argparse.ArgumentParser): + """ + ErrHelpParser - Use this instead of :py:class:`argparse.ArgumentParser` to automatically get full + help output as well as the error message when arguments are invalid, instead of just an error message. + + >>> parser = ErrHelpParser(description='My command line app') + >>> parser.add_argument('nums', metavar='N', type=int, nargs='+') + + """ + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) diff --git a/privex/helpers/decorators.py b/privex/helpers/decorators.py new file mode 100644 index 0000000..d585099 --- /dev/null +++ b/privex/helpers/decorators.py @@ -0,0 +1,113 @@ +""" +Class Method / Function decorators + + +===================================================+ + | © 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 functools +import logging +from time import sleep + +DEF_RETRY_MSG = "Exception while running '%s', will retry %d more times." +DEF_FAIL_MSG = "Giving up after attempting to retry function '%s' %d times." + +log = logging.getLogger(__name__) + + +def retry_on_err(max_retries: int = 3, delay: int = 3, **retry_conf): + """ + Decorates a function or class method, wraps the function/method with a try/catch block, and will automatically + re-run the function with the same arguments up to `max_retries` time after any exception is raised, with a + ``delay`` second delay between re-tries. + + If it still throws an exception after ``max_retries`` retries, it will log the exception details with ``fail_msg``, + and then re-raise it. + + Usage (retry up to 5 times, 1 second between retries, stop immediately if IOError is detected): + + >>> @retry_on_err(5, 1, fail_on=[IOError]) + ... def my_func(self, some=None, args=None): + ... if some == 'io': raise IOError() + ... raise FileExistsError() + + This will be re-ran 5 times, 1 second apart after each exception is raised, before giving up: + + >>> my_func() + + Where-as this one will immediately re-raise the caught IOError on the first attempt, as it's passed in ``fail_on``: + + >>> my_func('io') + + + :param int max_retries: Maximum total retry attempts before giving up + :param int delay: Amount of time in seconds to sleep before re-trying the wrapped function + :param retry_conf: Less frequently used arguments, pass in as keyword args: + + - (list) fail_on: A list() of Exception types that should result in immediate failure (don't retry, raise) + + - (str) retry_msg: Override the log message used for retry attempts. First message param %s is func name, + second message param %d is retry attempts remaining + + - (str) fail_msg: Override the log message used after all retry attempts are exhausted. First message param %s + is func name, and second param %d is amount of times retried. + + """ + retry_msg = retry_conf['retry_msg'] if 'retry_msg' in retry_conf else DEF_RETRY_MSG + fail_msg = retry_conf['fail_msg'] if 'fail_msg' in retry_conf else DEF_FAIL_MSG + fail_on = list(retry_conf['fail_on']) if 'fail_on' in retry_conf else [] + + def _decorator(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + retries = 0 + if 'retry_attempts' in kwargs: + retries = int(kwargs['retry_attempts']) + del kwargs['retry_attempts'] + + try: + return f(*args, **kwargs) + except Exception as e: + if type(e) in fail_on: + log.warning('Giving up. Re-raising exception %s (as requested by `fail_on` arg)', type(e)) + raise e + if retries < max_retries: + log.exception(retry_msg, f.__name__, max_retries - retries) + sleep(delay) + kwargs['retry_attempts'] = retries + 1 + return wrapper(*args, **kwargs) + log.exception(fail_msg, f.__name__, max_retries) + raise e + return wrapper + return _decorator + + diff --git a/privex/helpers/django.py b/privex/helpers/django.py new file mode 100644 index 0000000..482febb --- /dev/null +++ b/privex/helpers/django.py @@ -0,0 +1,106 @@ +""" +This module file contains Django-specific helper functions, to help save time +when developing with the Django framework. + +handle_error - Redirects normal web page requests with a session error, + outputs JSON with a status code for API queries. +is_database_synchronized - Check if all migrations have been ran before running code. +model_to_dict - Extract an individual Django model instance into a dict (with display names) +to_json - Convert a model Queryset into a plain string JSON array with display names + + + +===================================================+ + | © 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 json +from django.contrib import messages +from django.contrib.messages import add_message +from django.http import JsonResponse, HttpRequest, HttpResponseRedirectBase +from django.db.migrations.executor import MigrationExecutor +from django.db import connections + +def handle_error(request: HttpRequest, err: str, rdr: HttpResponseRedirectBase, status=400): + """ + Output an error as either a Django session message + redirect, or a JSON response + based on whether the request was for the API readable version (?format=json) or not. + + Usage: + + >>> from django.shortcuts import redirect + >>> def my_view(request): + ... return handle_error(request, "Invalid password", redirect('/login'), 403) + + :param HttpRequest request: The Django request object from your view + :param str err: An error message as a string to display to the user / api call + :param HttpResponseRedirectBase rdr: A redirect() for normal browsers to follow after adding the session error. + :param int status: The HTTP status code to return if the request is an API call (default: 400 bad request) + """ + if request.GET.get('format', '') == 'json': + return JsonResponse(dict(error=True, message=err), status=status) + else: + add_message(request, messages.ERROR, err) + return rdr + +def is_database_synchronized(database: str) -> bool: + """ + Check if all migrations have been ran. Useful for preventing auto-running code accessing models before the + tables even exist, thus preventing you from migrating... + + >>> from django.db import DEFAULT_DB_ALIAS + >>> if not is_database_synchronized(DEFAULT_DB_ALIAS): + >>> log.warning('Cannot run reload_handlers because there are unapplied migrations!') + >>> return + + :param str database: Which Django database config is being used? Generally just pass django.db.DEFAULT_DB_ALIAS + :return bool: True if all migrations have been ran, False if not. + """ + + connection = connections[database] + connection.prepare_database() + executor = MigrationExecutor(connection) + targets = executor.loader.graph.leaf_nodes() + return False if executor.migration_plan(targets) else True + +def model_to_dict(model) -> dict: + """1 dimensional json-ifyer for any Model""" + from django.forms import model_to_dict as mtd + # gets a field on the model, using the display name if available + def get_display(model, field, default): + method_name = 'get_{}_display'.format(field) + return getattr(model, method_name)() if method_name in dir(model) else default + + return {k: get_display(model, k, v) for k, v in mtd(model).items()} + +def to_json(query_set) -> str: + """Iterate a Django query set and dump to json str""" + return json.dumps([model_to_dict(e) for e in query_set], default=str) diff --git a/privex/helpers/net.py b/privex/helpers/net.py new file mode 100644 index 0000000..8e610d4 --- /dev/null +++ b/privex/helpers/net.py @@ -0,0 +1,103 @@ +""" +Network related helper code + + +===================================================+ + | © 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 +from ipaddress import ip_address, IPv4Address, IPv6Address +from typing import Union + +log = logging.getLogger(__name__) + +try: + from dns.resolver import Resolver + 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 + name of the organization. + + Usage: + + >>> asn_to_name(210083) + 'PRIVEX, SE' + >>> asn_to_name('13335') + 'CLOUDFLARENET - Cloudflare, Inc., US' + + This helper function requires ``dnspython>=1.16.0``, it will not be visible unless + you install the dnspython package in your virtualenv, or systemwide:: + + pip3 install dnspython + + + :param int/str as_number: The AS number as a string or integer, e.g. 210083 or '210083' + :param bool quiet: (default True) If True, returns 'Unknown ASN' if a lookup fails. + If False, raises a KeyError if no results are found. + :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)) + +except ImportError: + log.debug('privex.helpers.net failed to import "dns.resolver" (pypi package "dnspython"), skipping some helpers') + pass + + +def ip_is_v4(ip: str) -> bool: + """ + Determines whether an IP address is IPv4 or not + + :param str ip: An IP address as a string, e.g. 192.168.1.1 + :raises ValueError: When the given IP address ``ip`` is invalid + :return bool: True if IPv6, False if not (i.e. probably IPv4) + """ + return type(ip_address(ip)) == IPv4Address + +def ip_is_v6(ip: str) -> bool: + """ + Determines whether an IP address is IPv6 or not + + :param str ip: An IP address as a string, e.g. 192.168.1.1 + :raises ValueError: When the given IP address ``ip`` is invalid + :return bool: True if IPv6, False if not (i.e. probably IPv4) + """ + return type(ip_address(ip)) == IPv6Address diff --git a/privex_contribution_agreement.txt b/privex_contribution_agreement.txt new file mode 100644 index 0000000..0a615fd --- /dev/null +++ b/privex_contribution_agreement.txt @@ -0,0 +1,90 @@ +Legal agreement for contributions made to projects owned/created by Privex Inc. +Revision: 1.0 +Revision date: 2019-03-12 +--------------------------- + * Contribution - Content such as code (additions/changes to application source code), + documentation (such as the README), media (images, music, video), + and ideas sent to Privex with the intention of the content being included + within a project that Privex has created, or owns. + + * Privex / Privex Inc. - Refers to the corporate entity Privex Inc. registered in Belize City, Belize + and may also include persons employed by Privex Inc. + Official website: https://www.privex.io + + * Core Project - Refers to content within the project that is being contributed to, which + Privex has created, or holds ownership rights to (e.g. contributed content + that paragraph 2.1 applies to) + +SECTION 1 - Contributions to Open Source projects + +1.1 By contributing to open source projects created by Privex Inc., you agree that your + contributions will become licensed under the same license as the project, and may be used + within the scope of that license. + +SECTION 2 - Contributions to any Privex owned/created project + +2.1 By submitting a contribution to a Privex project, you grant unlimited usage rights + of included content which you have created, or have ownership rights of, to Privex Inc. + unless the content has been exempted by paragraph 2.2. + + This includes, but is not limited to, the right for Privex Inc. to use, + modify, re-license, re-distribute, and/or sell the content. + + This does not affect your existing rights to the content. You still retain any prior + ownership of the submitted content. + + Privex does not claim unlimited usage rights on any file with a clearly visible license + contained within the file, nor the content of folders which contain a license file such + as LICENSE, LICENSE.TXT, LICENCE etc. + + +2.1.1 (PLAIN ENGLISH) While the above paragraph sounds scary, it's a necessary legal + protection. + + Many of our open source projects are actively used by Privex, and sometimes + used as components of other projects, which may not fall under the same license + as the project you're contributing to. + + To ensure we don't get into any licensing issues because we accepted your contribution, + we need to ensure that any contributions submitted to **our repo** are fully licensed + to us, so that we can continue using our own project legally. + + + +2.2 The following sub-paragraphs detail exemptions of contributions from paragraph 2.2 + +2.2.1 If you are submitting content which you have created and/or own, and require that parts of, + or all of your contribution to be exempted from paragraph 2.1, you must follow ALL of the below + requirements before submitting your contribution: + + 1. Submit the affected content as individual files, or using a reference (e.g. a + link to the file(s) on a website) + 2. Ensure that the license of the content is made clearly visible through one + or more of the following methods: + 1. Within the affected file(s) (e.g. a code comment) + 2. If affected content is submitted within a new folder, which contains only + files that are under the same license (or set of licenses), then the + license may be placed in a clearly named license file, such as `LICENSE`. + 3. A notice displayed within the README and/or LICENSE file of the project + which the content is being contributed to, clearly stating the files + that are unaffected by paragraph 2.1 + 3. Ensure that you clearly state in the contribution request (e.g. git pull requests + / issues), that the contribution contains content that does not fall under + paragraph 2.1. You should reference the files/folders that are exempt. + + Example: "This pull request includes content that is exempt from paragraph 2.1 + of the Privex contribution legal agreement, see the folder `lib/libexample`" + + Example: "I retain my ownership rights for the files `abc.py` and `myfile.sh`, + I have included the appropriate license/copyright text at the start of the files." + +2.2.2 If you are submitting third-party content which falls under an existing license, including + content under the same license as the project, you must: + + 1. Follow requirements 1 (individual files or links) and 2 (clear license) of paragraph 2.2.1 + 2. If it is not clear from the names of the submitted files/folders that the content is + not part of the Base Project (content owned by Privex), you must ensure that you clearly + state in the contribution request (e.g. git pull requests / issues) the files and/or folders + which are licensed separately. + +2.3 Privex may decline a contribution for any reason it chooses. \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..809c42c --- /dev/null +++ b/setup.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" + +===================================================+ + | © 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 setuptools import setup, find_packages + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name='privex_helpers', + + version='1.0.0', + + description='A variety of helper functions and classes, useful for many different projects', + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/Privex/python-helpers", + author='Chris (Someguy123) @ Privex', + author_email='chris@privex.io', + + license='MIT', + install_requires=[ + 'privex-loghelper>=1.0.4' + ], + packages=find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], +)