2 # This file is part of my.gpodder.org.
4 # my.gpodder.org is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
9 # my.gpodder.org is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12 # License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
22 from datetime
import datetime
, timedelta
, date
25 from django
.core
.cache
import cache
28 def daterange(from_date
, to_date
=None, leap
=timedelta(days
=1)):
30 >>> from_d = datetime(2010, 01, 01)
31 >>> to_d = datetime(2010, 01, 05)
32 >>> list(daterange(from_d, to_d))
33 [datetime.datetime(2010, 1, 1, 0, 0), datetime.datetime(2010, 1, 2, 0, 0), datetime.datetime(2010, 1, 3, 0, 0), datetime.datetime(2010, 1, 4, 0, 0), datetime.datetime(2010, 1, 5, 0, 0)]
37 if isinstance(from_date
, datetime
):
38 to_date
= datetime
.now()
40 to_date
= date
.today()
42 while from_date
<= to_date
:
44 from_date
= from_date
+ leap
47 def format_time(value
):
48 """Format an offset (in seconds) to a string
50 The offset should be an integer or float value.
58 >>> format_time(10921)
62 dt
= datetime
.utcfromtimestamp(value
)
67 return dt
.strftime('%M:%S')
69 return dt
.strftime('%H:%M:%S')
71 def parse_time(value
):
76 >>> parse_time('05:10') #5*60+10
79 >>> parse_time('1:05:10') #60*60+5*60+10
83 raise ValueError('None value in parse_time')
85 if isinstance(value
, int):
86 # Don't need to parse already-converted time value
90 raise ValueError('Empty valueing in parse_time')
92 for format
in ('%H:%M:%S', '%M:%S'):
94 t
= time
.strptime(value
, format
)
95 return t
.tm_hour
* 60*60 + t
.tm_min
* 60 + t
.tm_sec
104 >>> parse_bool('True')
107 >>> parse_bool('true')
113 if isinstance(val
, bool):
115 if val
.lower() == 'true':
120 def iterate_together(lists
, key
=lambda x
: x
, reverse
=False):
122 takes ordered, possibly sparse, lists with similar items
123 (some items have a corresponding item in the other lists, some don't).
125 It then yield tuples of corresponding items, where one element is None is
126 there is no corresponding entry in one of the lists.
128 Tuples where both elements are None are skipped.
130 The results of the key method are used for the comparisons.
132 If reverse is True, the lists are expected to be sorted in reverse order
133 and the results will also be sorted reverse
135 >>> list(iterate_together([range(1, 3), range(1, 4, 2)]))
136 [(1, 1), (2, None), (None, 3)]
138 >>> list(iterate_together([[], []]))
141 >>> list(iterate_together([range(1, 3), range(3, 5)]))
142 [(1, None), (2, None), (None, 3), (None, 4)]
144 >>> list(iterate_together([range(1, 3), []]))
145 [(1, None), (2, None)]
147 >>> list(iterate_together([[1, None, 3], [None, None, 3]]))
151 Next
= collections
.namedtuple('Next', 'item more')
152 min_
= min if not reverse
else max
153 lt_
= operator
.lt
if not reverse
else operator
.gt
155 lists
= [iter(l
) for l
in lists
]
163 except StopIteration:
164 return Next(None, False)
167 return [None]*len(lists
)
169 # take first bunch of items
170 items
= [_take(l
) for l
in lists
]
172 while any(i
.item
is not None or i
.more
for i
in items
):
176 for n
, item
in enumerate(items
):
178 if item
.item
is None:
181 if all(x
is None for x
in res
):
185 min_v
= min_(filter(lambda x
: x
is not None, res
), key
=key
)
187 if key(item
.item
) == key(min_v
):
190 elif lt_(key(item
.item
), key(min_v
)):
194 for n
, x
in enumerate(res
):
196 items
[n
] = _take(lists
[n
])
201 def progress(val
, max_val
, status_str
='', max_width
=50, stream
=sys
.stdout
):
202 print >> stream
, '\r',
203 print >> stream
, '[ %s ] %s / %s | %s' % (
204 '#'*int(float(val
)/max_val
*max_width
) +
205 ' ' * (max_width
-(int(float(val
)/max_val
*max_width
))),
212 def set_cmp(list, simplify
):
214 Builds a set out of a list but uses the results of simplify to determine equality between items
216 simpl
= lambda x
: (simplify(x
), x
)
217 lst
= dict(map(simpl
, list))
223 returns the first not-None object or None if the iterator is exhausted
232 return list(set(a
) & set(b
))
236 def multi_request_view(cls
, view
, wrap
=True, auto_advance
=True,
239 splits up a view request into several requests, which reduces
240 the server load of the number of returned objects is large.
242 NOTE: As such a split request is obviously not atomical anymore, results
243 might skip some elements of contain some twice
245 If auto_advance is False the method will always request the same range.
246 This can be useful when the view contain unprocessed items and the caller
247 processes the items, thus removing them from the view before the next
251 per_page
= kwargs
.get('limit', 1000)
252 kwargs
['limit'] = per_page
+ 1
254 wrapper
= kwargs
.pop('wrapper', cls
.wrap
)
259 resp
= db
.view(view
, *args
, **kwargs
)
262 for n
, obj
in enumerate(resp
.iterator()):
267 doc
= wrapper(obj
['doc']) if wrapper
else obj
['doc']
268 docid
= doc
._id
if wrapper
else obj
['id']
270 docid
= obj
.get('id', None)
275 kwargs
['startkey'] = key
276 if docid
is not None:
277 kwargs
['startkey_docid'] = docid
281 # we reached the end of the page, load next one
288 def remove_control_chars(s
):
289 import unicodedata
, re
291 all_chars
= (unichr(i
) for i
in xrange(0x110000))
292 control_chars
= ''.join(map(unichr, range(0,32) + range(127,160)))
293 control_char_re
= re
.compile('[%s]' % re
.escape(control_chars
))
295 return control_char_re
.sub('', s
)
299 return tuple(map(list,zip(*a
)))
302 def parse_range(s
, min, max, default
=None):
304 Parses the string and returns its value. If the value is outside the given
305 range, its closest number within the range is returned
307 >>> parse_range('5', 0, 10)
310 >>> parse_range('0', 5, 10)
313 >>> parse_range('15',0, 10)
316 >>> parse_range('x', 0, 20)
319 >>> parse_range('x', 0, 20, 20)
330 except (ValueError, TypeError):
331 return default
if default
is not None else (max-min)/2
334 def get_to_dict(cls
, ids
, get_id
=lambda x
: x
._id
, use_cache
=False):
344 cache_objs
.append(obj
)
347 db_objs
= list(cls
.get_multi(ids
))
349 for obj
in (cache_objs
+ db_objs
):
351 # get_multi returns dict {'key': _id, 'error': 'not found'}
352 # for non-existing objects
353 if isinstance(obj
, dict) and 'error' in obj
:
358 ids
= obj
.get_ids() if hasattr(obj
, 'get_ids') else [get_id(obj
)]
364 cache
.set(get_id(obj
), obj
)
370 return [item
for sublist
in l
for item
in sublist
]
373 def linearize(key
, iterators
, reverse
=False):
375 Linearizes a number of iterators, sorted by some comparison function
378 iters
= [iter(i
) for i
in iterators
]
383 vals
. append( (v
, i
) )
384 except StopIteration:
388 vals
= sorted(vals
, key
=lambda x
: key(x
[0]), reverse
=reverse
)
389 val
, it
= vals
.pop(0)
393 vals
.append( (next_val
, it
) )
394 except StopIteration:
398 def skip_pairs(iterator
, cmp=cmp):
399 """ Skips pairs of equal items
401 >>> list(skip_pairs([]))
404 >>> list(skip_pairs([1]))
407 >>> list(skip_pairs([1, 2, 3]))
410 >>> list(skip_pairs([1, 1]))
413 >>> list(skip_pairs([1, 2, 2]))
416 >>> list(skip_pairs([1, 2, 2, 3]))
419 >>> list(skip_pairs([1, 2, 2, 2]))
422 >>> list(skip_pairs([1, 2, 2, 2, 2, 3]))
426 iterator
= iter(iterator
)
427 next
= iterator
.next()
432 next
= iterator
.next()
433 except StopIteration as e
:
437 if cmp(item
, next
) == 0:
438 next
= iterator
.next()
443 def get_timestamp(datetime_obj
):
444 """ Returns the timestamp as an int for the given datetime object
446 >>> get_timestamp(datetime(2011, 4, 7, 9, 30, 6))
449 >>> get_timestamp(datetime(1970, 1, 1, 0, 0, 0))
452 return int(time
.mktime(datetime_obj
.timetuple()))
456 re_url
= re
.compile('^https?://')
459 """ Returns true if a string looks like an URL
461 >>> is_url('http://example.com/some-path/file.xml')
464 >>> is_url('something else')
468 return bool(re_url
.match(string
))
471 def is_couchdb_id(id_str
):
475 f
= functools
.partial(operator
.contains
, string
.hexdigits
)
476 return len(id_str
) == 32 and all(map(f
, id_str
))
479 # from http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python
480 # this does not increase asymptotical complexity
481 # but can still waste more time than it saves.
482 def shortest_of(strings
):
483 return min(strings
, key
=len)
485 def longest_substr(strings
):
487 Returns the longest common substring of the given strings
493 reference
= shortest_of(strings
) #strings[0]
494 length
= len(reference
)
495 #find a suitable slice i:j
496 for i
in xrange(length
):
497 #only consider strings long at least len(substr) + 1
498 for j
in xrange(i
+ len(substr
) + 1, length
):
499 candidate
= reference
[i
:j
]
500 if all(candidate
in text
for text
in strings
):
506 def additional_value(it
, gen_val
, val_changed
=lambda _
: True):
507 """ Provides an additional value to the elements, calculated when needed
509 For the elements from the iterator, some additional value can be computed
510 by gen_val (which might be an expensive computation).
512 If the elements in the iterator are ordered so that some subsequent
513 elements would generate the same additional value, val_changed can be
514 provided, which receives the next element from the iterator and the
515 previous additional value. If the element would generate the same
516 additional value (val_changed returns False), its computation is skipped.
518 >>> # get the next full hundred higher than x
519 >>> # this will probably be an expensive calculation
520 >>> next_hundred = lambda x: x + 100-(x % 100)
522 >>> # returns True if h is not the value that next_hundred(x) would provide
523 >>> # this should be a relatively cheap calculation, compared to the above
524 >>> diff_hundred = lambda x, h: (h-x) < 0 or (h - x) > 100
526 >>> xs = [0, 50, 100, 101, 199, 200, 201]
527 >>> list(additional_value(xs, next_hundred, diff_hundred))
528 [(0, 100), (50, 100), (100, 100), (101, 200), (199, 200), (200, 200), (201, 300)]
535 if current
is _none
or val_changed(x
, current
):