Fixes #85, optional skipping of the autoupdate

This commit is contained in:
László Károlyi 2021-12-06 17:58:28 +01:00
parent 07512c45db
commit 91fb131abf
Signed by untrusted user: karolyi
GPG Key ID: 2DCAF25E55735BFE
8 changed files with 38 additions and 24 deletions

View File

@ -1,3 +1,6 @@
1.0.4:
- Skipping the startup update with setting an environment variable called `PY3VE_IGNORE_UPDATER`
1.0.3: 1.0.3:
- Moving project off github - Moving project off github
- Static type check fixes - Static type check fixes

View File

@ -172,5 +172,7 @@ The update can be triggered manually:
`callback`: An optional Callable (function/method) to be called when the update is done. `callback`: An optional Callable (function/method) to be called when the update is done.
You can completely skip the auto-update on startup by setting the environment variable `PY3VE_IGNORE_UPDATER` to any value.
# Read the [FAQ](https://gitea.ksol.io/karolyi/py3-validate-email/src/branch/master/FAQ.md)! # Read the [FAQ](https://gitea.ksol.io/karolyi/py3-validate-email/src/branch/master/FAQ.md)!

View File

@ -57,7 +57,7 @@ class BuildPyCommand(build_py):
setup( setup(
name='py3-validate-email', name='py3-validate-email',
version='1.0.3', version='1.0.4',
packages=find_packages(exclude=['tests']), packages=find_packages(exclude=['tests']),
install_requires=['dnspython~=2.1', 'idna~=3.0', 'filelock~=3.0'], install_requires=['dnspython~=2.1', 'idna~=3.0', 'filelock~=3.0'],
author='László Károlyi', author='László Károlyi',

View File

@ -11,11 +11,13 @@ from validate_email.validate_email import (
class BlacklistCheckTestCase(TestCase): class BlacklistCheckTestCase(TestCase):
'Testing if the included blacklist filtering works.' 'Testing if the included blacklist filtering works.'
def setUpClass(): def setUpClass(): # type: ignore
update_builtin_blacklist(force=False, background=False) update_builtin_blacklist(force=False, background=False)
def test_blacklist_positive(self): def test_blacklist_positive(self):
'Disallows blacklist item: mailinator.com.' 'Disallows blacklist item: mailinator.com.'
# The setting of the PY3VE_IGNORE_UPDATER variable doesn't
# matter here as the module has already download an initial list
with self.assertRaises(DomainBlacklistedError): with self.assertRaises(DomainBlacklistedError):
domainlist_check(EmailAddress('pm2@mailinator.com')) domainlist_check(EmailAddress('pm2@mailinator.com'))
with self.assertRaises(DomainBlacklistedError): with self.assertRaises(DomainBlacklistedError):

View File

@ -16,11 +16,11 @@ class InstallTest(TestCase):
'Testing package installation.' 'Testing package installation.'
def test_datadir_is_in_place(self): def test_datadir_is_in_place(self):
'Data directory should be in the virtualenv.' 'Data directory should be in the virtualenv *after installation*.'
output = check_output([ output = check_output([
executable, '-c', ( executable, '-c', (
'import sys;sys.path.remove("");import validate_email;' 'import sys;sys.path.remove("");import validate_email;' +
'print(validate_email.updater.BLACKLIST_FILEPATH_INSTALLED);' 'print(validate_email.updater.BLACKLIST_FILEPATH_INSTALLED);' +
'print(validate_email.updater.ETAG_FILEPATH_INSTALLED, end="")' 'print(validate_email.updater.ETAG_FILEPATH_INSTALLED, end="")'
)]).decode('ascii') )]).decode('ascii')
bl_path, etag_path = output.split('\n') bl_path, etag_path = output.split('\n')

View File

@ -3,14 +3,14 @@ from re import compile as re_compile
HOST_REGEX = re_compile( HOST_REGEX = re_compile(
# max length for domain name labels is 63 characters per RFC 1034 # max length for domain name labels is 63 characters per RFC 1034
r'((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)' r'((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)' +
r'(?:[A-Z0-9-]{2,63}(?<!-))\Z', IGNORECASE) r'(?:[A-Z0-9-]{2,63}(?<!-))\Z', IGNORECASE)
LITERAL_REGEX = re_compile( LITERAL_REGEX = re_compile(
# literal form, ipv4 or ipv6 address (SMTP 4.1.3) # literal form, ipv4 or ipv6 address (SMTP 4.1.3)
r'\[([A-f0-9:\.]+)\]\Z', IGNORECASE) r'\[([A-f0-9:\.]+)\]\Z', IGNORECASE)
USER_REGEX = re_compile( USER_REGEX = re_compile(
# dot-atom # dot-atom
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" +
# quoted-string # quoted-string
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013' r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013' +
r'\014\016-\177])*"\Z)', IGNORECASE) r'\014\016-\177])*"\Z)', IGNORECASE)

View File

@ -1,4 +1,5 @@
from logging import getLogger from logging import getLogger
from pathlib import Path
from typing import Optional from typing import Optional
from filelock import FileLock from filelock import FileLock
@ -6,8 +7,8 @@ from filelock import FileLock
from .email_address import EmailAddress from .email_address import EmailAddress
from .exceptions import DomainBlacklistedError from .exceptions import DomainBlacklistedError
from .updater import ( from .updater import (
BLACKLIST_FILEPATH_INSTALLED, BLACKLIST_FILEPATH_TMP, LOCK_PATH, BLACKLIST_FILEPATH_INSTALLED, BLACKLIST_FILEPATH_TMP, ENV_IGNORE_UPDATER,
update_builtin_blacklist) LOCK_PATH, TMP_PATH, update_builtin_blacklist)
SetOrNone = Optional[set] SetOrNone = Optional[set]
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -17,7 +18,6 @@ class DomainListValidator(object):
'Check the provided email against domain lists.' 'Check the provided email against domain lists.'
domain_whitelist = set() domain_whitelist = set()
domain_blacklist = set('localhost') domain_blacklist = set('localhost')
_is_builtin_bl_used: bool = False
def __init__( def __init__(
self, whitelist: SetOrNone = None, blacklist: SetOrNone = None): self, whitelist: SetOrNone = None, blacklist: SetOrNone = None):
@ -26,11 +26,10 @@ class DomainListValidator(object):
if blacklist: if blacklist:
self.domain_blacklist = set(x.lower() for x in blacklist) self.domain_blacklist = set(x.lower() for x in blacklist)
else: else:
self._is_builtin_bl_used = True
self.reload_builtin_blacklist() self.reload_builtin_blacklist()
@property @property
def _blacklist_path(self) -> str: def _blacklist_path(self) -> Path:
'Return the path of the `blacklist.txt` that should be loaded.' 'Return the path of the `blacklist.txt` that should be loaded.'
try: try:
# Zero size: file is touched to indicate the preinstalled # Zero size: file is touched to indicate the preinstalled
@ -43,9 +42,8 @@ class DomainListValidator(object):
def reload_builtin_blacklist(self): def reload_builtin_blacklist(self):
'(Re)load our built-in blacklist.' '(Re)load our built-in blacklist.'
if not self._is_builtin_bl_used: TMP_PATH.mkdir(exist_ok=True)
return with FileLock(lock_file=str(LOCK_PATH)):
with FileLock(lock_file=LOCK_PATH):
bl_path = self._blacklist_path bl_path = self._blacklist_path
LOGGER.debug(msg=f'(Re)loading blacklist from {bl_path}') LOGGER.debug(msg=f'(Re)loading blacklist from {bl_path}')
try: try:
@ -57,7 +55,10 @@ class DomainListValidator(object):
x.strip().lower() for x in lines if x.strip()) x.strip().lower() for x in lines if x.strip())
def __call__(self, email_address: EmailAddress) -> bool: def __call__(self, email_address: EmailAddress) -> bool:
'Do the checking here.' """
Check if the email domain is valid, raise
`DomainBlacklistedError` if not.
"""
if email_address.domain in self.domain_whitelist: if email_address.domain in self.domain_whitelist:
return True return True
if email_address.domain in self.domain_blacklist: if email_address.domain in self.domain_blacklist:

View File

@ -1,5 +1,6 @@
from http.client import HTTPResponse from http.client import HTTPResponse
from logging import getLogger from logging import getLogger
from os import environ
from pathlib import Path from pathlib import Path
from tempfile import gettempdir, gettempprefix from tempfile import gettempdir, gettempprefix
from threading import Thread from threading import Thread
@ -19,9 +20,9 @@ except ImportError:
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
TMP_PATH = Path(gettempdir()).joinpath( TMP_PATH = Path(gettempdir()).joinpath(
f'{gettempprefix()}-py3-validate-email-{geteuid()}') f'{gettempprefix()}-py3-validate-email-{geteuid()}')
TMP_PATH.mkdir(exist_ok=True) ENV_IGNORE_UPDATER = environ.get('PY3VE_IGNORE_UPDATER')
BLACKLIST_URL = ( BLACKLIST_URL = (
'https://raw.githubusercontent.com/disposable-email-domains/' 'https://raw.githubusercontent.com/disposable-email-domains/' +
'disposable-email-domains/master/disposable_email_blocklist.conf') 'disposable-email-domains/master/disposable_email_blocklist.conf')
LIB_PATH_DEFAULT = Path(__file__).resolve().parent.joinpath('data') LIB_PATH_DEFAULT = Path(__file__).resolve().parent.joinpath('data')
BLACKLIST_FILEPATH_INSTALLED = LIB_PATH_DEFAULT.joinpath('blacklist.txt') BLACKLIST_FILEPATH_INSTALLED = LIB_PATH_DEFAULT.joinpath('blacklist.txt')
@ -117,9 +118,11 @@ class BlacklistUpdater(object):
self, force: bool = False, callback: Optional[Callable] = None): self, force: bool = False, callback: Optional[Callable] = None):
'Start optionally updating the blacklist.txt file.' 'Start optionally updating the blacklist.txt file.'
# Locking to avoid multi-process update on multi-process startup # Locking to avoid multi-process update on multi-process startup
# Import filelock locally because this module is als used by setup.py # Import filelock locally because this module is als used by
# setup.py
from filelock import FileLock from filelock import FileLock
with FileLock(lock_file=LOCK_PATH): TMP_PATH.mkdir(exist_ok=True)
with FileLock(lock_file=str(LOCK_PATH)):
self._process(force=force) self._process(force=force)
# Always execute callback because multiple processes can have # Always execute callback because multiple processes can have
# different versions of blacklists (one before, one after # different versions of blacklists (one before, one after
@ -135,12 +138,15 @@ def update_builtin_blacklist(
Update and reload the built-in blacklist. Return the `Thread` used Update and reload the built-in blacklist. Return the `Thread` used
to do the background update, so it can be `join()`-ed. to do the background update, so it can be `join()`-ed.
""" """
if ENV_IGNORE_UPDATER:
LOGGER.debug(msg='Skipping update of built-in blacklist.')
return
LOGGER.debug(msg='Starting optional update of built-in blacklist.') LOGGER.debug(msg='Starting optional update of built-in blacklist.')
blacklist_updater = BlacklistUpdater() blacklist_updater = BlacklistUpdater()
kwargs = dict(force=force, callback=callback) kwargs = dict(force=force, callback=callback)
if not background: if not background:
blacklist_updater.process(**kwargs) blacklist_updater.process(**kwargs)
return return
bl_thread = Thread(target=blacklist_updater.process, kwargs=kwargs) updater_thread = Thread(target=blacklist_updater.process, kwargs=kwargs)
bl_thread.start() updater_thread.start()
return bl_thread return updater_thread