2014-07-25 14:14:19 +02:00
|
|
|
#!/usr/bin/env python
|
2015-10-19 05:32:10 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2014-07-25 14:14:19 +02:00
|
|
|
|
2016-05-20 14:46:10 +02:00
|
|
|
import hashlib
|
|
|
|
import numpy as np
|
2016-07-12 15:31:59 +02:00
|
|
|
from scipy.interpolate import splrep, splev
|
2015-02-07 09:12:58 +01:00
|
|
|
import os
|
|
|
|
import pwd
|
2014-07-25 14:14:19 +02:00
|
|
|
import re
|
2016-05-20 14:46:10 +02:00
|
|
|
import subprocess
|
2015-02-09 13:24:55 +01:00
|
|
|
from obspy.core import UTCDateTime
|
2015-12-03 17:20:05 +01:00
|
|
|
|
2016-05-25 14:22:08 +02:00
|
|
|
|
2016-05-24 14:20:59 +02:00
|
|
|
def _pickle_method(m):
|
|
|
|
if m.im_self is None:
|
|
|
|
return getattr, (m.im_class, m.im_func.func_name)
|
|
|
|
else:
|
|
|
|
return getattr, (m.im_self, m.im_func.func_name)
|
|
|
|
|
2016-07-12 15:31:59 +02:00
|
|
|
def fit_curve(x, y):
|
|
|
|
return splev, splrep(x, y)
|
|
|
|
|
|
|
|
def getindexbounds(f, eta):
|
|
|
|
mi = f.argmax()
|
|
|
|
m = max(f)
|
|
|
|
b = m * eta
|
|
|
|
l = find_nearest(f[:mi], b)
|
|
|
|
u = find_nearest(f[mi:], b) + mi
|
|
|
|
return mi, l, u
|
2016-05-25 14:22:08 +02:00
|
|
|
|
|
|
|
def worker(func, input, cores='max', async=False):
|
2016-05-24 14:20:59 +02:00
|
|
|
import multiprocessing
|
|
|
|
|
|
|
|
if cores == 'max':
|
|
|
|
cores = multiprocessing.cpu_count()
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2016-05-27 06:49:20 +02:00
|
|
|
pool = multiprocessing.Pool(cores)
|
2016-05-24 14:20:59 +02:00
|
|
|
if async == True:
|
|
|
|
result = pool.map_async(func, input)
|
|
|
|
else:
|
|
|
|
result = pool.map(func, input)
|
|
|
|
pool.close()
|
2016-05-27 06:49:20 +02:00
|
|
|
return result
|
|
|
|
|
2016-06-15 14:52:42 +02:00
|
|
|
def clims(lim1, lim2):
|
|
|
|
"""
|
|
|
|
takes two pairs of limits and returns one pair of common limts
|
|
|
|
:param lim1:
|
|
|
|
:param lim2:
|
|
|
|
:return:
|
|
|
|
|
|
|
|
>>> clims([0, 4], [1, 3])
|
|
|
|
[0, 4]
|
|
|
|
>>> clims([1, 4], [0, 3])
|
|
|
|
[0, 4]
|
|
|
|
>>> clims([1, 3], [0, 4])
|
|
|
|
[0, 4]
|
|
|
|
>>> clims([0, 3], [1, 4])
|
|
|
|
[0, 4]
|
|
|
|
>>> clims([0, 3], [0, 4])
|
|
|
|
[0, 4]
|
|
|
|
>>> clims([1, 4], [0, 4])
|
|
|
|
[0, 4]
|
|
|
|
>>> clims([0, 4], [0, 4])
|
|
|
|
[0, 4]
|
|
|
|
>>> clims([0, 4], [1, 4])
|
|
|
|
[0, 4]
|
|
|
|
>>> clims([0, 4], [0, 3])
|
|
|
|
[0, 4]
|
|
|
|
"""
|
|
|
|
lim = [None, None]
|
|
|
|
if lim1[0] < lim2[0]:
|
|
|
|
lim[0] = lim1[0]
|
|
|
|
else:
|
|
|
|
lim[0] = lim2[0]
|
|
|
|
if lim1[1] > lim2[1]:
|
|
|
|
lim[1] = lim1[1]
|
|
|
|
else:
|
|
|
|
lim[1] = lim2[1]
|
|
|
|
return lim
|
|
|
|
|
2016-05-25 14:22:08 +02:00
|
|
|
|
2015-12-03 17:20:05 +01:00
|
|
|
def demeanTrace(trace, window):
|
|
|
|
"""
|
2016-08-12 09:50:36 +02:00
|
|
|
takes a trace object and returns the same trace object but with data
|
|
|
|
demeaned within a certain time window
|
2015-12-03 17:20:05 +01:00
|
|
|
:param trace: waveform trace object
|
|
|
|
:type trace: `~obspy.core.stream.Trace`
|
|
|
|
:param window:
|
|
|
|
:type window: tuple
|
|
|
|
:return: trace
|
|
|
|
:rtype: `~obspy.core.stream.Trace`
|
|
|
|
"""
|
|
|
|
trace.data -= trace.data[window].mean()
|
|
|
|
return trace
|
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2015-12-03 17:20:05 +01:00
|
|
|
def findComboBoxIndex(combo_box, val):
|
|
|
|
"""
|
|
|
|
Function findComboBoxIndex takes a QComboBox object and a string and
|
|
|
|
returns either 0 or the index throughout all QComboBox items.
|
|
|
|
:param combo_box: Combo box object.
|
2016-08-12 09:50:36 +02:00
|
|
|
:type combo_box: `~QComboBox`
|
2015-12-03 17:20:05 +01:00
|
|
|
:param val: Name of a combo box to search for.
|
2016-08-12 09:50:36 +02:00
|
|
|
:type val: basestring
|
2015-12-03 17:20:05 +01:00
|
|
|
:return: index value of item with name val or 0
|
|
|
|
"""
|
|
|
|
return combo_box.findText(val) if combo_box.findText(val) is not -1 else 0
|
2014-07-25 14:14:19 +02:00
|
|
|
|
2016-08-19 16:09:29 +02:00
|
|
|
def find_in_list(list, str):
|
|
|
|
"""
|
|
|
|
takes a list of strings and a string and returns the first list item
|
|
|
|
matching the string pattern
|
|
|
|
:param list: list to search in
|
|
|
|
:param str: pattern to search for
|
|
|
|
:return: first list item containing pattern
|
|
|
|
|
|
|
|
.. example::
|
|
|
|
|
|
|
|
>>> l = ['/dir/e1234.123.12', '/dir/e2345.123.12', 'abc123', 'def456']
|
|
|
|
>>> find_in_list(l, 'dir')
|
|
|
|
'/dir/e1234.123.12'
|
|
|
|
>>> find_in_list(l, 'e1234')
|
|
|
|
'/dir/e1234.123.12'
|
|
|
|
>>> find_in_list(l, 'e2')
|
|
|
|
'/dir/e2345.123.12'
|
|
|
|
>>> find_in_list(l, 'ABC')
|
|
|
|
'abc123'
|
|
|
|
>>> find_in_list(l, 'f456')
|
|
|
|
'def456'
|
|
|
|
>>> find_in_list(l, 'gurke')
|
|
|
|
|
|
|
|
"""
|
|
|
|
rlist = [s for s in list if str.lower() in s.lower()]
|
|
|
|
if rlist:
|
|
|
|
return rlist[0]
|
|
|
|
return None
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2016-03-04 06:25:17 +01:00
|
|
|
def find_nearest(array, value):
|
|
|
|
'''
|
2016-08-12 09:50:36 +02:00
|
|
|
function find_nearest takes an array and a value and returns the
|
|
|
|
index of the nearest value found in the array
|
|
|
|
:param array: array containing values
|
|
|
|
:type array: `~numpy.ndarray`
|
|
|
|
:param value: number searched for
|
|
|
|
:return: index of the array item being nearest to the value
|
|
|
|
|
|
|
|
>>> a = np.array([ 1.80339578, -0.72546654, 0.95769195, -0.98320759, 0.85922623])
|
|
|
|
>>> find_nearest(a, 1.3)
|
|
|
|
2
|
|
|
|
>>> find_nearest(a, 0)
|
|
|
|
1
|
|
|
|
>>> find_nearest(a, 2)
|
|
|
|
0
|
|
|
|
>>> find_nearest(a, -1)
|
|
|
|
3
|
|
|
|
>>> a = np.array([ 1.1, -0.7, 0.9, -0.9, 0.8])
|
|
|
|
>>> find_nearest(a, 0.849)
|
|
|
|
4
|
2016-03-04 06:25:17 +01:00
|
|
|
'''
|
2016-03-30 08:14:58 +02:00
|
|
|
return (np.abs(array - value)).argmin()
|
|
|
|
|
2016-03-04 06:25:17 +01:00
|
|
|
|
2015-02-09 13:24:55 +01:00
|
|
|
def fnConstructor(s):
|
2015-12-03 17:20:05 +01:00
|
|
|
'''
|
2016-08-12 09:50:36 +02:00
|
|
|
takes a string and returns a valid filename (especially on windows machines)
|
|
|
|
:param s: desired filename
|
|
|
|
:type s: str
|
|
|
|
:return: valid filename
|
2015-12-03 17:20:05 +01:00
|
|
|
'''
|
2015-06-10 15:43:43 +02:00
|
|
|
if type(s) is str:
|
2015-07-08 14:58:42 +02:00
|
|
|
s = s.split(':')[-1]
|
2015-06-10 15:43:43 +02:00
|
|
|
else:
|
|
|
|
s = getHash(UTCDateTime())
|
2014-12-01 12:43:49 +01:00
|
|
|
|
2014-07-25 14:14:19 +02:00
|
|
|
badchars = re.compile(r'[^A-Za-z0-9_. ]+|^\.|\.$|^ | $|^$')
|
|
|
|
badsuffix = re.compile(r'(aux|com[1-9]|con|lpt[1-9]|prn)(\.|$)')
|
|
|
|
|
|
|
|
fn = badchars.sub('_', s)
|
|
|
|
|
|
|
|
if badsuffix.match(fn):
|
|
|
|
fn = '_' + fn
|
|
|
|
return fn
|
2015-01-22 16:41:52 +01:00
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2016-05-27 11:25:47 +02:00
|
|
|
def four_digits(year):
|
2016-08-12 09:50:36 +02:00
|
|
|
"""
|
|
|
|
takes a two digit year integer and returns the correct four digit equivalent
|
|
|
|
from the last 100 years
|
|
|
|
:param year: two digit year
|
|
|
|
:type year: int
|
|
|
|
:return: four digit year correspondant
|
|
|
|
|
|
|
|
>>> four_digits(20)
|
|
|
|
1920
|
|
|
|
>>> four_digits(16)
|
|
|
|
2016
|
|
|
|
>>> four_digits(00)
|
|
|
|
2000
|
|
|
|
"""
|
|
|
|
if year + 2000 <= UTCDateTime.utcnow().year:
|
2016-05-27 11:25:47 +02:00
|
|
|
year += 2000
|
|
|
|
else:
|
|
|
|
year += 1900
|
|
|
|
return year
|
|
|
|
|
|
|
|
|
2015-12-03 17:20:05 +01:00
|
|
|
def getGlobalTimes(stream):
|
|
|
|
'''
|
2016-08-12 09:50:36 +02:00
|
|
|
takes a stream object and returns the latest end and the earliest start
|
|
|
|
time of all contained trace objects
|
|
|
|
:param stream: seismological data stream
|
|
|
|
:type stream: `~obspy.core.stream.Stream`
|
|
|
|
:return: minimum start time and maximum end time
|
2015-12-03 17:20:05 +01:00
|
|
|
'''
|
|
|
|
min_start = UTCDateTime()
|
|
|
|
max_end = None
|
|
|
|
for trace in stream:
|
|
|
|
if trace.stats.starttime < min_start:
|
|
|
|
min_start = trace.stats.starttime
|
|
|
|
if max_end is None or trace.stats.endtime > max_end:
|
|
|
|
max_end = trace.stats.endtime
|
|
|
|
return min_start, max_end
|
2015-03-30 05:55:54 +02:00
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2015-02-09 13:24:55 +01:00
|
|
|
def getHash(time):
|
|
|
|
'''
|
2016-08-12 09:50:36 +02:00
|
|
|
takes a time object and returns the corresponding SHA1 hash of the
|
|
|
|
formatted date string
|
2015-02-09 13:24:55 +01:00
|
|
|
:param time: time object for which a hash should be calculated
|
|
|
|
:type time: :class: `~obspy.core.utcdatetime.UTCDateTime` object
|
|
|
|
:return: str
|
|
|
|
'''
|
|
|
|
hg = hashlib.sha1()
|
|
|
|
hg.update(time.strftime('%Y-%m-%d %H:%M:%S.%f'))
|
|
|
|
return hg.hexdigest()
|
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2015-12-03 17:20:05 +01:00
|
|
|
def getLogin():
|
|
|
|
'''
|
2016-08-12 09:50:36 +02:00
|
|
|
returns the actual user's login ID
|
|
|
|
:return: login ID
|
2015-12-03 17:20:05 +01:00
|
|
|
'''
|
|
|
|
return pwd.getpwuid(os.getuid())[0]
|
2015-03-30 05:55:54 +02:00
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2015-03-30 05:55:54 +02:00
|
|
|
def getOwner(fn):
|
2015-12-03 17:20:05 +01:00
|
|
|
'''
|
2016-08-12 09:50:36 +02:00
|
|
|
takes a filename and return the login ID of the actual owner of the file
|
|
|
|
:param fn: filename of the file tested
|
2016-08-10 16:30:28 +02:00
|
|
|
:type fn: str
|
2016-08-12 09:50:36 +02:00
|
|
|
:return: login ID of the file's owner
|
2015-12-03 17:20:05 +01:00
|
|
|
'''
|
2015-03-30 05:55:54 +02:00
|
|
|
return pwd.getpwuid(os.stat(fn).st_uid).pw_name
|
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2015-11-12 09:23:28 +01:00
|
|
|
def getPatternLine(fn, pattern):
|
|
|
|
"""
|
2016-08-12 09:50:36 +02:00
|
|
|
takes a file name and a pattern string to search for in the file and
|
|
|
|
returns the first line which contains the pattern string otherwise 'None'
|
2015-11-12 09:23:28 +01:00
|
|
|
:param fn: file name
|
|
|
|
:type fn: str
|
|
|
|
:param pattern: pattern string to search for
|
|
|
|
:type pattern: str
|
2016-08-10 16:30:28 +02:00
|
|
|
:return: the complete line containing the pattern string or None
|
2015-11-12 09:23:28 +01:00
|
|
|
|
|
|
|
>>> getPatternLine('utils.py', 'python')
|
|
|
|
'#!/usr/bin/env python\\n'
|
|
|
|
>>> print(getPatternLine('version.py', 'palindrome'))
|
|
|
|
None
|
|
|
|
"""
|
|
|
|
fobj = open(fn, 'r')
|
|
|
|
for line in fobj.readlines():
|
|
|
|
if pattern in line:
|
|
|
|
fobj.close()
|
|
|
|
return line
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2016-08-25 13:31:51 +02:00
|
|
|
def is_executable(fn):
|
|
|
|
"""
|
|
|
|
takes a filename and returns True if the file is executable on the system
|
|
|
|
and False otherwise
|
|
|
|
:param fn: path to the file to be tested
|
|
|
|
:return: True or False
|
|
|
|
"""
|
|
|
|
return os.path.isfile(fn) and os.access(fn, os.X_OK)
|
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2015-12-03 17:20:05 +01:00
|
|
|
def isSorted(iterable):
|
|
|
|
'''
|
2016-08-12 09:50:36 +02:00
|
|
|
takes an iterable and returns 'True' if the items are in order otherwise
|
|
|
|
'False'
|
|
|
|
:param iterable: an iterable object
|
2015-12-03 17:20:05 +01:00
|
|
|
:type iterable:
|
2016-08-11 16:13:53 +02:00
|
|
|
:return: Boolean
|
2016-08-12 09:50:36 +02:00
|
|
|
|
|
|
|
>>> isSorted(1)
|
|
|
|
Traceback (most recent call last):
|
|
|
|
...
|
|
|
|
AssertionError: object is not iterable; object: 1
|
|
|
|
>>> isSorted([1,2,3,4])
|
|
|
|
True
|
|
|
|
>>> isSorted('abcd')
|
|
|
|
True
|
|
|
|
>>> isSorted('bcad')
|
|
|
|
False
|
|
|
|
>>> isSorted([2,3,1,4])
|
|
|
|
False
|
2015-12-03 17:20:05 +01:00
|
|
|
'''
|
2016-08-12 09:50:36 +02:00
|
|
|
assert isIterable(iterable), 'object is not iterable; object: {' \
|
|
|
|
'0}'.format(iterable)
|
|
|
|
if type(iterable) is str:
|
|
|
|
iterable = [s for s in iterable]
|
2015-12-03 17:20:05 +01:00
|
|
|
return sorted(iterable) == iterable
|
2015-03-30 05:55:54 +02:00
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2016-08-12 09:50:36 +02:00
|
|
|
def isIterable(obj):
|
|
|
|
"""
|
|
|
|
takes a python object and returns 'True' is the object is iterable and
|
|
|
|
'False' otherwise
|
|
|
|
:param obj: a python object
|
|
|
|
:return: True of False
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
iterator = iter(obj)
|
|
|
|
except TypeError as te:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2015-03-30 05:55:54 +02:00
|
|
|
def prepTimeAxis(stime, trace):
|
2015-12-03 17:20:05 +01:00
|
|
|
'''
|
2016-08-12 09:50:36 +02:00
|
|
|
takes a starttime and a trace object and returns a valid time axis for
|
|
|
|
plotting
|
|
|
|
:param stime: start time of the actual seismogram as UTCDateTime
|
|
|
|
:param trace: seismic trace object
|
|
|
|
:return: valid numpy array with time stamps for plotting
|
2015-12-03 17:20:05 +01:00
|
|
|
'''
|
2015-03-30 05:55:54 +02:00
|
|
|
nsamp = trace.stats.npts
|
|
|
|
srate = trace.stats.sampling_rate
|
|
|
|
tincr = trace.stats.delta
|
|
|
|
etime = stime + nsamp / srate
|
|
|
|
time_ax = np.arange(stime, etime, tincr)
|
|
|
|
if len(time_ax) < nsamp:
|
2016-06-21 09:32:37 +02:00
|
|
|
print('elongate time axes by one datum')
|
2015-03-30 05:55:54 +02:00
|
|
|
time_ax = np.arange(stime, etime + tincr, tincr)
|
|
|
|
elif len(time_ax) > nsamp:
|
2016-06-21 09:32:37 +02:00
|
|
|
print('shorten time axes by one datum')
|
2015-03-30 05:55:54 +02:00
|
|
|
time_ax = np.arange(stime, etime - tincr, tincr)
|
|
|
|
if len(time_ax) != nsamp:
|
|
|
|
raise ValueError('{0} samples of data \n '
|
|
|
|
'{1} length of time vector \n'
|
|
|
|
'delta: {2}'.format(nsamp, len(time_ax), tincr))
|
|
|
|
return time_ax
|
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2015-07-02 10:29:50 +02:00
|
|
|
def scaleWFData(data, factor=None, components='all'):
|
|
|
|
"""
|
|
|
|
produce scaled waveforms from given waveform data and a scaling factor,
|
|
|
|
waveform may be selected by their components name
|
|
|
|
:param data: waveform data to be scaled
|
|
|
|
:type data: `~obspy.core.stream.Stream` object
|
|
|
|
:param factor: scaling factor
|
|
|
|
:type factor: float
|
|
|
|
:param components: components labels for the traces in data to be scaled by
|
|
|
|
the scaling factor (optional, default: 'all')
|
|
|
|
:type components: tuple
|
|
|
|
:return: scaled waveform data
|
|
|
|
:rtype: `~obspy.core.stream.Stream` object
|
|
|
|
"""
|
|
|
|
if components is not 'all':
|
|
|
|
for comp in components:
|
|
|
|
if factor is None:
|
|
|
|
max_val = np.max(np.abs(data.select(component=comp)[0].data))
|
|
|
|
data.select(component=comp)[0].data /= 2 * max_val
|
|
|
|
else:
|
|
|
|
data.select(component=comp)[0].data /= 2 * factor
|
|
|
|
else:
|
|
|
|
for tr in data:
|
|
|
|
if factor is None:
|
|
|
|
max_val = float(np.max(np.abs(tr.data)))
|
|
|
|
tr.data /= 2 * max_val
|
|
|
|
else:
|
|
|
|
tr.data /= 2 * factor
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2015-12-03 17:20:05 +01:00
|
|
|
def runProgram(cmd, parameter=None):
|
2015-07-02 10:29:50 +02:00
|
|
|
"""
|
2015-12-03 17:20:05 +01:00
|
|
|
run an external program specified by cmd with parameters input returning the
|
|
|
|
stdout output
|
|
|
|
:param cmd: name of the command to run
|
|
|
|
:type cmd: str
|
|
|
|
:param parameter: filename of parameter file or parameter string
|
|
|
|
:type parameter: str
|
|
|
|
:return: stdout output
|
|
|
|
:rtype: str
|
2015-11-20 09:06:52 +01:00
|
|
|
"""
|
|
|
|
|
2015-12-03 17:20:05 +01:00
|
|
|
if parameter:
|
|
|
|
cmd.strip()
|
|
|
|
cmd += ' %s 2>&1' % parameter
|
|
|
|
|
2016-08-25 13:31:51 +02:00
|
|
|
subprocess.check_output('{} | tee /dev/stderr'.format(cmd), shell=True)
|
|
|
|
|
|
|
|
def which(program):
|
|
|
|
"""
|
|
|
|
takes a program name and returns the full path to the executable or None
|
2016-08-26 10:48:16 +02:00
|
|
|
modified after: http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
|
2016-08-25 13:31:51 +02:00
|
|
|
:param program: name of the desired external program
|
|
|
|
:return: full path of the executable file
|
|
|
|
"""
|
2016-08-26 10:48:16 +02:00
|
|
|
try:
|
|
|
|
from PySide.QtCore import QSettings
|
|
|
|
settings = QSettings()
|
|
|
|
for key in settings.allKeys():
|
|
|
|
if 'binPath' in key:
|
|
|
|
os.environ['PATH'] += ':{0}'.format(settings.value(key))
|
|
|
|
except ImportError as e:
|
|
|
|
print(e.message)
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2016-08-25 13:31:51 +02:00
|
|
|
def is_exe(fpath):
|
|
|
|
return os.path.exists(fpath) and os.access(fpath, os.X_OK)
|
|
|
|
|
|
|
|
def ext_candidates(fpath):
|
|
|
|
yield fpath
|
|
|
|
for ext in os.environ.get("PATHEXT", "").split(os.pathsep):
|
|
|
|
yield fpath + ext
|
|
|
|
|
|
|
|
fpath, fname = os.path.split(program)
|
|
|
|
if fpath:
|
|
|
|
if is_exe(program):
|
|
|
|
return program
|
|
|
|
else:
|
|
|
|
for path in os.environ["PATH"].split(os.pathsep):
|
|
|
|
exe_file = os.path.join(path, program)
|
|
|
|
for candidate in ext_candidates(exe_file):
|
|
|
|
if is_exe(candidate):
|
|
|
|
return candidate
|
|
|
|
|
|
|
|
return None
|
2016-05-25 14:22:08 +02:00
|
|
|
|
2015-11-12 09:23:28 +01:00
|
|
|
if __name__ == "__main__":
|
|
|
|
import doctest
|
2016-03-30 08:14:58 +02:00
|
|
|
|
2015-11-12 09:23:28 +01:00
|
|
|
doctest.testmod()
|