Skip to content

Commit 18f87c0

Browse files
committed
2.14.0 - new helpers.thread module + improved convert_datetime + fixes
- `privex.helpers.thread` (new module) - Added `lock_acquire_timeout` context manager function, for acquiring locks on `threading.Lock` objects using a `with lock_acquire_timeout(lock)` context manager, allowing the use of a context manager, while still being able to set a timeout / control blocking, plus the option to raise an exception on timeout. - Added `BetterEvent` - a modified version of `threading.Event` with more flexibility, such as the ability to wait for "clear" state AND "set" state - not just "set" state. - Added `StopperThread` - a `threading.Thread` sub-class which comes with thread instance events allowing you to signal a thread to stop/start/pause/unpause without having to constantly re-create stop/pause signalling. - Added `SafeLoopThread` - based on `StopperThread`, which is a looping thread with stop/pause signalling support, along with two queue's pre-included on the instance: `in_queue` for sending objects to the thread, and `out_queue` for receiving objects from the thread. - Added `event_multi_wait` which allows for waiting on multiple thread Event's using `threading.Event.wait`, and some extra features if you pass Privex Helper's `BetterEvent` events instead of standard events. - `privex.helpers.converters` - `convert_datetime` can now handle `datetime.date` objects, and also attempts to fallback to converting the passed object into a string and parsing the string result if it's not a supported type. - Added aliases `parse_datetime` and `parse_date` for `convert_datetime` - Added aliases `parse_unixtime`, `parse_epoch` and `convert_epoch_datetime` for `convert_unixtime_datetime` - `privex.helpers.exceptions` - Added `LockConflict` exception for failed attempts at acquiring `threading.Lock` or `asyncio.Lock` objects. - Added `LockWaitTimeout` - a more specific sub-class of `LockConflict` for lock acquisition timeouts - Added `EventWaitTimeout` - for timeouts related to `threading.Event` - Possibly some other minor changes **Unit Testing** - Adjusted timing for `tests.cache.test_async_memcached` to avoid race condition test bug where sometimes it would take too long to get the cache item to update it, and result in the item expiring before it can be updated. - Added more unit tests to `test_converters` - Test `convert_datetime` handling of `datetime.date` objects - Test `convert_datetime` handling of byte-strings - Test `convert_datetime` handling of just string dates without times - Added new `test_thread` module which tests a good portion of the new `privex.helpers.thread` module.
1 parent de0fb23 commit 18f87c0

File tree

8 files changed

+796
-7
lines changed

8 files changed

+796
-7
lines changed

local_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ USE_PYENV=$((USE_PYENV))
5252
# PY_VERS is an array which defines the local system python executables for each python version to run the tests for.
5353
[ -z ${PY_VERS+x} ] && PY_VERS=("python3.6" "python3.7" "python3.8")
5454
# PYENV_VERS is an array of Python version numbers to install & use for running tests if 'pyenv' is available.
55-
[ -z ${PYENV_VERS+x} ] && PYENV_VERS=("3.6.7" "3.7.1" "3.8.0")
55+
[ -z ${PYENV_VERS+x} ] && PYENV_VERS=("3.6.9" "3.7.1" "3.8.0")
5656

5757
has_command pyenv && HAS_PYENV=1 || HAS_PYENV=0
5858

privex/helpers/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
except ImportError:
123123
log.debug('privex.helpers __init__ failed to import "geoip", not loading GeoIP2 helpers')
124124

125+
try:
126+
from privex.helpers.thread import *
127+
except ImportError:
128+
log.debug('privex.helpers __init__ failed to import "thread", not loading python threading helpers')
129+
125130

126131
def _setup_logging(level=logging.WARNING):
127132
"""
@@ -143,7 +148,7 @@ def _setup_logging(level=logging.WARNING):
143148
log = _setup_logging()
144149
name = 'helpers'
145150

146-
VERSION = '2.13.0'
151+
VERSION = '2.14.0'
147152

148153

149154

privex/helpers/converters.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
2323
2424
"""
25-
from datetime import datetime
25+
from datetime import datetime, date
2626
from decimal import Decimal
27-
from typing import Optional, Union
27+
from typing import Optional, Union, AnyStr
2828

2929

3030
from privex.helpers.common import empty, is_true, stringify
@@ -39,6 +39,8 @@
3939
YEAR = DAY * 365
4040
DECADE = YEAR * 10
4141

42+
SUPPORTED_DT_TYPES = Union[str, bytes, int, datetime, date, AnyStr]
43+
4244

4345
def convert_datetime(d, if_empty=None, fail_empty=False, **kwargs) -> Optional[datetime]:
4446
"""
@@ -79,6 +81,10 @@ def convert_datetime(d, if_empty=None, fail_empty=False, **kwargs) -> Optional[d
7981
if d.tzinfo is None and _tzinfo is not None:
8082
d = d.replace(tzinfo=_tzinfo)
8183
return d
84+
85+
# For datetime.date objects, we first convert them into a string, then we can parse them into a datetime + set tzinfo
86+
if isinstance(d, date):
87+
d = str(d)
8288

8389
d = stringify(d) if isinstance(d, bytes) else d
8490

@@ -110,11 +116,25 @@ def convert_datetime(d, if_empty=None, fail_empty=False, **kwargs) -> Optional[d
110116
if empty(d):
111117
if fail_empty: raise AttributeError("Error converting datetime. Parameter 'd' was empty!")
112118
return if_empty
119+
120+
try:
121+
log.debug("Passed object is not a supported type. Object type: %s || object repr: %s", type(d), repr(d))
122+
log.debug("Calling convert_datetime with object casted to string: %s", str(d))
123+
_d = convert_datetime(str(d), fail_empty=True)
124+
d = _d
125+
except Exception as e:
126+
log.info("Converted passed object with str() to try and parse string version, but failed.")
127+
log.info("Exception thrown from convert_datetime(str(d)) was: %s %s", type(e), str(e))
128+
d = None # By setting d to None, it will trigger the ValueError code below.
129+
113130
if not isinstance(d, datetime):
114131
raise ValueError('Timestamp must be either a datetime object, or an ISO8601 string...')
115132
return d
116133

117134

135+
parse_datetime = parse_date = convert_datetime
136+
137+
118138
def convert_unixtime_datetime(d: Union[str, int, float, Decimal], if_empty=None, fail_empty=False) -> datetime:
119139
"""Convert a unix timestamp into a :class:`datetime.datetime` object"""
120140
from dateutil.tz import tzutc
@@ -134,6 +154,9 @@ def convert_unixtime_datetime(d: Union[str, int, float, Decimal], if_empty=None,
134154
return t
135155

136156

157+
parse_unixtime = parse_epoch = convert_epoch_datetime = convert_unixtime_datetime
158+
159+
137160
def convert_bool_int(d, if_empty=0, fail_empty=False) -> int:
138161
"""Convert a boolean ``d`` into an integer (``0`` for ``False``, ``1`` for ``True``)"""
139162
if type(d) is int: return 1 if d >= 1 else 0
@@ -153,6 +176,7 @@ def convert_int_bool(d, if_empty=False, fail_empty=False) -> bool:
153176

154177
__all__ = [
155178
'convert_datetime', 'convert_unixtime_datetime', 'convert_bool_int', 'convert_int_bool',
179+
'parse_date', 'parse_datetime', 'parse_epoch', 'parse_unixtime', 'convert_epoch_datetime',
156180
'MINUTE', 'HOUR', 'DAY', 'MONTH', 'YEAR', 'DECADE',
157181
]
158182

privex/helpers/exceptions.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,25 @@ class InvalidHost(PrivexException, ValueError):
141141
"""Raised when a passed IP address or hostname/domain is invalid."""
142142
pass
143143

144+
145+
class LockConflict(PrivexException):
146+
"""
147+
Raised when attempting to acquire a lock with a :class:`threading.Lock` or :class:`asyncio.Lock`, and the lock object
148+
is already locked.
149+
150+
This would only be raised either when non-blocking acquisition was requested, or the blocking wait timed out.
151+
"""
152+
153+
154+
class LockWaitTimeout(LockConflict):
155+
"""
156+
Sub-class of :class:`.LockConflict` - only to be raised when a timeout has been reached while waiting
157+
to acquire a :class:`threading.Lock`
158+
"""
159+
160+
161+
class EventWaitTimeout(PrivexException):
162+
"""
163+
Raised when a timeout has been reached while waiting for an event (:class:`threading.Event`) to be signalled.
164+
"""
165+

0 commit comments

Comments
 (0)