cs-seq 20250103__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cs/seq.py ADDED
@@ -0,0 +1,654 @@
1
+ #!/usr/bin/python -tt
2
+ #
3
+ # Stuff to do with counters, sequences and iterables.
4
+ # - Cameron Simpson <cs@cskk.id.au> 20jul2008
5
+ #
6
+
7
+ r'''
8
+ Stuff to do with counters, sequences and iterables.
9
+
10
+ Note that any function accepting an iterable
11
+ will consume some or all of the derived iterator
12
+ in the course of its function.
13
+ '''
14
+
15
+ import heapq
16
+ import itertools
17
+ from threading import Lock, Condition, Thread
18
+
19
+ from cs.deco import decorator
20
+ from cs.gimmicks import warning
21
+
22
+ __version__ = '20250103'
23
+
24
+ DISTINFO = {
25
+ 'description':
26
+ "Stuff to do with counters, sequences and iterables.",
27
+ 'keywords': ["python2", "python3"],
28
+ 'classifiers': [
29
+ "Programming Language :: Python",
30
+ "Programming Language :: Python :: 2",
31
+ "Programming Language :: Python :: 3",
32
+ ],
33
+ 'install_requires': [
34
+ 'cs.deco',
35
+ 'cs.gimmicks',
36
+ ],
37
+ }
38
+
39
+ class Seq(object):
40
+ ''' A numeric sequence implemented as a thread safe wrapper for
41
+ `itertools.count()`.
42
+
43
+ A `Seq` is iterable and both iterating and calling it return
44
+ the next number in the sequence.
45
+ '''
46
+
47
+ __slots__ = ('counter', '_lock')
48
+
49
+ def __init__(self, start=0, lock=None):
50
+ if lock is None:
51
+ lock = Lock()
52
+ self.counter = itertools.count(start)
53
+ self._lock = lock
54
+
55
+ def __iter__(self):
56
+ return self
57
+
58
+ def __next__(self):
59
+ with self._lock:
60
+ return next(self.counter)
61
+
62
+ next = __next__
63
+ __call__ = __next__
64
+
65
+ __seq = Seq()
66
+
67
+ def seq():
68
+ ''' Return a new sequential value.
69
+ '''
70
+ return next(__seq)
71
+
72
+ def the(iterable, context=None):
73
+ ''' Returns the first element of an iterable, but requires there to be
74
+ exactly one.
75
+ '''
76
+ icontext = "expected exactly one value"
77
+ if context is not None:
78
+ icontext = icontext + " for " + context
79
+ is_first = True
80
+ for elem in iterable:
81
+ if is_first:
82
+ it = elem
83
+ is_first = False
84
+ else:
85
+ raise IndexError(
86
+ "%s: got more than one element (%s, %s, ...)" % (icontext, it, elem)
87
+ )
88
+ if is_first:
89
+ raise IndexError("%s: got no elements" % (icontext,))
90
+
91
+ return it
92
+
93
+ def first(iterable):
94
+ ''' Return the first item from an iterable; raise `IndexError` on empty iterables.
95
+ '''
96
+ for item in iterable:
97
+ return item
98
+ raise IndexError("empty iterable %r" % (iterable,))
99
+
100
+ def last(iterable):
101
+ ''' Return the last item from an iterable; raise `IndexError` on empty iterables.
102
+ '''
103
+ nothing = True
104
+ for item in iterable:
105
+ nothing = False
106
+ if nothing:
107
+ raise IndexError("no items in iterable: %r" % (iterable,))
108
+ return item # pylint: disable=undefined-loop-variable
109
+
110
+ def get0(iterable, default=None):
111
+ ''' Return first element of an iterable, or the default.
112
+ '''
113
+ try:
114
+ i = first(iterable)
115
+ except IndexError:
116
+ return default
117
+ return i
118
+
119
+ def tee(iterable, *Qs):
120
+ ''' A generator yielding the items from an iterable
121
+ which also copies those items to a series of queues.
122
+
123
+ Parameters:
124
+ * `iterable`: the iterable to copy
125
+ * `Qs`: the queues, objects accepting a `.put` method.
126
+
127
+ Note: the item is `.put` onto every queue
128
+ before being yielded from this generator.
129
+ '''
130
+ for item in iterable:
131
+ for Q in Qs:
132
+ Q.put(item)
133
+ yield item
134
+
135
+ def imerge(*iters, **kw):
136
+ ''' Merge an iterable of ordered iterables in order.
137
+
138
+ Parameters:
139
+ * `iters`: an iterable of iterators
140
+ * `reverse`: keyword parameter: if true, yield items in reverse order.
141
+ This requires the iterables themselves to also be in
142
+ reversed order.
143
+
144
+ This function relies on the source iterables being ordered
145
+ and their elements being comparable, through slightly misordered
146
+ iterables (for example, as extracted from web server logs)
147
+ will produce only slightly misordered results, as the merging
148
+ is done on the basis of the front elements of each iterable.
149
+ '''
150
+ reverse = kw.get('reverse', False)
151
+ if kw:
152
+ raise ValueError("unexpected keyword arguments: %r" % (kw,))
153
+ if reverse:
154
+ # tuples that compare in reverse order
155
+ class _MergeHeapItem(tuple):
156
+
157
+ def __lt__(self, other):
158
+ return self[0] > other[0]
159
+ else:
160
+ # tuples that compare in forward order
161
+ class _MergeHeapItem(tuple):
162
+
163
+ def __lt__(self, other):
164
+ return self[0] < other[0]
165
+
166
+ # prime the list of head elements with (value, iter)
167
+ heap = []
168
+ for it in iters:
169
+ it = iter(it)
170
+ try:
171
+ head = next(it)
172
+ except StopIteration:
173
+ pass
174
+ else:
175
+ heapq.heappush(heap, _MergeHeapItem((head, it)))
176
+ while heap:
177
+ head, it = heapq.heappop(heap)
178
+ yield head
179
+ try:
180
+ head = next(it)
181
+ except StopIteration:
182
+ pass
183
+ else:
184
+ heapq.heappush(heap, _MergeHeapItem((head, it)))
185
+
186
+ def onetoone(func):
187
+ ''' A decorator for a method of a sequence to merge the results of
188
+ passing every element of the sequence to the function, expecting a
189
+ single value back.
190
+
191
+ Example:
192
+
193
+ class X(list):
194
+ @onetoone
195
+ def lower(self, item):
196
+ return item.lower()
197
+ strs = X(['Abc', 'Def'])
198
+ lower_strs = X.lower()
199
+ '''
200
+
201
+ def gather(self, *a, **kw):
202
+ ''' Yield the results of calling the function on each item.
203
+ '''
204
+ for item in self:
205
+ yield func(item, *a, **kw)
206
+
207
+ return gather
208
+
209
+ def onetomany(func):
210
+ ''' A decorator for a method of a sequence to merge the results of
211
+ passing every element of the sequence to the function, expecting
212
+ multiple values back.
213
+
214
+ Example:
215
+
216
+ class X(list):
217
+ @onetomany
218
+ def chars(self, item):
219
+ return item
220
+ strs = X(['Abc', 'Def'])
221
+ all_chars = X.chars()
222
+ '''
223
+
224
+ def gather(self, *a, **kw):
225
+ ''' Chain the function results together.
226
+ '''
227
+ return itertools.chain(*[func(item, *a, **kw) for item in self])
228
+
229
+ return gather
230
+
231
+ def isordered(items, reverse=False, strict=False):
232
+ ''' Test whether an iterable is ordered.
233
+ Note that the iterable is iterated, so this is a destructive
234
+ test for nonsequences.
235
+ '''
236
+ is_first = True
237
+ prev = None
238
+ for item in items:
239
+ if not is_first:
240
+ if reverse:
241
+ ordered = item < prev if strict else item <= prev
242
+ else:
243
+ ordered = item > prev if strict else item >= prev
244
+ if not ordered:
245
+ return False
246
+ prev = item
247
+ is_first = False
248
+ return True
249
+
250
+ def common_prefix_length(*seqs):
251
+ ''' Return the length of the common prefix of sequences `seqs`.
252
+ '''
253
+ if not seqs:
254
+ return 0
255
+ if len(seqs) == 1:
256
+ return len(seqs[0])
257
+ for i, items in enumerate(zip(*seqs)):
258
+ item0 = items[0]
259
+ # pylint: disable=cell-var-from-loop
260
+ if not all(map(lambda item: item == item0, items)):
261
+ return i
262
+ # return the length of the shorted sequence
263
+ return len(min(*seqs, key=len))
264
+
265
+ def common_suffix_length(*seqs):
266
+ ''' Return the length of the common suffix of sequences `seqs`.
267
+ '''
268
+ return common_prefix_length(list(map(lambda s: list(reversed(s)), *seqs)))
269
+
270
+ class TrackingCounter(object):
271
+ ''' A wrapper for a counter which can be incremented and decremented.
272
+
273
+ A facility is provided to wait for the counter to reach a specific value.
274
+ The .inc and .dec methods also accept a `tag` argument to keep
275
+ individual counts based on the tag to aid debugging.
276
+
277
+ TODO: add `strict` option to error and abort if any counter tries
278
+ to go below zero.
279
+ '''
280
+
281
+ def __init__(self, value=0, name=None, lock=None):
282
+ ''' Initialise the counter to `value` (default 0) with the optional `name`.
283
+ '''
284
+ if name is None:
285
+ name = "TrackingCounter-%d" % (seq(),)
286
+ if lock is None:
287
+ lock = Lock()
288
+ self.value = value
289
+ self.name = name
290
+ self._lock = lock
291
+ self._watched = {}
292
+ self._tag_up = {}
293
+ self._tag_down = {}
294
+
295
+ def __str__(self):
296
+ return "%s:%d" % (self.name, self.value)
297
+
298
+ def __repr__(self):
299
+ return "<TrackingCounter %r:%r>" % (str(self), self._watched)
300
+
301
+ def __nonzero__(self):
302
+ return self.value != 0
303
+
304
+ def __int__(self):
305
+ return self.value
306
+
307
+ def _notify(self):
308
+ ''' Notify any waiters on the current counter value.
309
+ This should be called inside self._lock.
310
+ '''
311
+ value = self.value
312
+ watcher = self._watched.get(value)
313
+ if watcher:
314
+ del self._watched[value]
315
+ watcher.acquire()
316
+ watcher.notify_all()
317
+ watcher.release()
318
+
319
+ def inc(self, tag=None):
320
+ ''' Increment the counter.
321
+ Wake up any threads waiting for its new value.
322
+ '''
323
+ with self._lock:
324
+ self.value += 1
325
+ if tag is not None:
326
+ tag = str(tag)
327
+ self._tag_up.setdefault(tag, 0)
328
+ self._tag_up[tag] += 1
329
+ self._notify()
330
+
331
+ def dec(self, tag=None):
332
+ ''' Decrement the counter.
333
+ Wake up any threads waiting for its new value.
334
+ '''
335
+ with self._lock:
336
+ self.value -= 1
337
+ if tag is not None:
338
+ tag = str(tag)
339
+ self._tag_down.setdefault(tag, 0)
340
+ self._tag_down[tag] += 1
341
+ if self._tag_up.get(tag, 0) < self._tag_down[tag]:
342
+ warning("%s.dec: more .decs than .incs for tag %r", self, tag)
343
+ if self.value < 0:
344
+ warning("%s.dec: value < 0!", self)
345
+ self._notify()
346
+
347
+ def check(self):
348
+ ''' Internal consistency check.
349
+ '''
350
+ for tag in sorted(self._tag_up.keys()):
351
+ ups = self._tag_up[tag]
352
+ downs = self._tag_down.get(tag, 0)
353
+ if ups != downs:
354
+ warning("%s: ups=%d, downs=%d: tag %r", self, ups, downs, tag)
355
+
356
+ def wait(self, value):
357
+ ''' Wait for the counter to reach the specified `value`.
358
+ '''
359
+ with self._lock:
360
+ if value == self.value:
361
+ return
362
+ if value not in self._watched:
363
+ watcher = self._watched[value] = Condition()
364
+ else:
365
+ watcher = self._watched[value]
366
+ watcher.acquire()
367
+ watcher.wait()
368
+
369
+ class StatefulIterator(object):
370
+ ''' A trivial iterator which wraps another iterator to expose some tracking state.
371
+
372
+ This has 2 attributes:
373
+ * `.it`: the internal iterator which should yield `(item,new_state)`
374
+ * `.state`: the last state value from the internal iterator
375
+
376
+ The originating use case is resuse of an iterator by independent
377
+ calls that are typically sequential, specificly the .read
378
+ method of file like objects. Naive sequential reads require
379
+ the underlying storage to locate the data on every call, even
380
+ though the previous call has just performed this task for the
381
+ previous read. Saving the iterator used from the preceeding
382
+ call allows the iterator to pick up directly if the file
383
+ offset hasn't been fiddled in the meantime.
384
+ '''
385
+
386
+ def __init__(self, it):
387
+ self.it = it
388
+ self.state = None
389
+
390
+ def __iter__(self):
391
+ return self
392
+
393
+ def __next__(self):
394
+ item, new_state = next(self.it)
395
+ self.state = new_state
396
+ return item
397
+
398
+ def splitoff(sq, *sizes):
399
+ ''' Split a sequence into (usually short) prefixes and a tail,
400
+ for example to construct subdirectory trees based on a UUID.
401
+
402
+ Example:
403
+
404
+ >>> from uuid import UUID
405
+ >>> uuid = 'd6d9c510-785c-468c-9aa4-b7bda343fb79'
406
+ >>> uu = UUID(uuid).hex
407
+ >>> uu
408
+ 'd6d9c510785c468c9aa4b7bda343fb79'
409
+ >>> splitoff(uu, 2, 2)
410
+ ['d6', 'd9', 'c510785c468c9aa4b7bda343fb79']
411
+ '''
412
+ if len(sizes) < 1:
413
+ raise ValueError("no sizes")
414
+ offset = 0
415
+ parts = []
416
+ for size in sizes:
417
+ if size < 1:
418
+ raise ValueError("size:%s < 1" % (size,))
419
+ end_offset = offset + size
420
+ if end_offset >= len(sq):
421
+ raise ValueError(
422
+ "size:%s consumes up to or beyond"
423
+ " the end of the sequence (length %d)" % (size, len(sq))
424
+ )
425
+ parts.append(sq[offset:end_offset])
426
+ offset = end_offset
427
+ parts.append(sq[offset:])
428
+ return parts
429
+
430
+ def unrepeated(it, seen=None, signature=None):
431
+ ''' A generator yielding items from the iterable `it` with no repetitions.
432
+
433
+ Parameters:
434
+ * `it`: the iterable to process
435
+ * `seen`: an optional setlike container supporting `in` and `.add()`
436
+ * `signature`: an optional signature function for items from `it`
437
+ which produces the value to compare to recognise repeated items;
438
+ its values are stored in the `seen` set
439
+
440
+ The default `signature` function is equality;
441
+ the items are stored n `seen` and compared.
442
+ This requires the items to be hashable and support equality tests.
443
+ The same applies to whatever values the `signature` function produces.
444
+
445
+ Another common signature is identity: `id`, useful for
446
+ traversing a graph which may have cycles.
447
+
448
+ Since `seen` accrues all the signature values for yielded items
449
+ generally it will grow monotonicly as iteration proceeeds.
450
+ If the items are complex or large it is well worth providing a signature
451
+ function even if the items themselves can be used in a set.
452
+ '''
453
+ if seen is None:
454
+ seen = set()
455
+ if signature is None:
456
+ signature = lambda item: item
457
+ for item in it:
458
+ sig = signature(item)
459
+ if sig in seen:
460
+ continue
461
+ seen.add(sig)
462
+ yield item
463
+
464
+ @decorator
465
+ def _greedy_decorator(g, queue_depth=0):
466
+
467
+ def greedy_generator(*a, **kw):
468
+ return greedy(g(*a, **kw), queue_depth=queue_depth)
469
+
470
+ return greedy_generator
471
+
472
+ def greedy(g=None, queue_depth=0):
473
+ ''' A decorator or function for greedy computation of iterables.
474
+
475
+ If `g` is omitted or callable
476
+ this is a decorator for a generator function
477
+ causing it to compute greedily,
478
+ capacity limited by `queue_depth`.
479
+
480
+ If `g` is iterable
481
+ this function dispatches it in a `Thread` to compute greedily,
482
+ capacity limited by `queue_depth`.
483
+
484
+ Example with an iterable:
485
+
486
+ for packet in greedy(parse_data_stream(stream)):
487
+ ... process packet ...
488
+
489
+ which does some readahead of the stream.
490
+
491
+ Example as a function decorator:
492
+
493
+ @greedy
494
+ def g(n):
495
+ for item in range(n):
496
+ yield n
497
+
498
+ This can also be used directly on an existing iterable:
499
+
500
+ for item in greedy(range(n)):
501
+ yield n
502
+
503
+ Normally a generator runs on demand.
504
+ This function dispatches a `Thread` to run the iterable
505
+ (typically a generator)
506
+ putting yielded values to a queue
507
+ and returns a new generator yielding from the queue.
508
+
509
+ The `queue_depth` parameter specifies the depth of the queue
510
+ and therefore how many values the original generator can compute
511
+ before blocking at the queue's capacity.
512
+
513
+ The default `queue_depth` is `0` which creates a `Channel`
514
+ as the queue - a zero storage buffer - which lets the generator
515
+ compute only a single value ahead of time.
516
+
517
+ A larger `queue_depth` allocates a `Queue` with that much storage
518
+ allowing the generator to compute as many as `queue_depth+1` values
519
+ ahead of time.
520
+
521
+ Here's a comparison of the behaviour:
522
+
523
+ Example without `@greedy`
524
+ where the "yield 1" step does not occur until after the "got 0":
525
+
526
+ >>> from time import sleep
527
+ >>> def g():
528
+ ... for i in range(2):
529
+ ... print("yield", i)
530
+ ... yield i
531
+ ... print("g done")
532
+ ...
533
+ >>> G = g(); sleep(0.1)
534
+ >>> for i in G:
535
+ ... print("got", i)
536
+ ... sleep(0.1)
537
+ ...
538
+ yield 0
539
+ got 0
540
+ yield 1
541
+ got 1
542
+ g done
543
+
544
+ Example with `@greedy`
545
+ where the "yield 1" step computes before the "got 0":
546
+
547
+ >>> from time import sleep
548
+ >>> @greedy
549
+ ... def g():
550
+ ... for i in range(2):
551
+ ... print("yield", i)
552
+ ... yield i
553
+ ... print("g done")
554
+ ...
555
+ >>> G = g(); sleep(0.1)
556
+ yield 0
557
+ >>> for i in G:
558
+ ... print("got", repr(i))
559
+ ... sleep(0.1)
560
+ ...
561
+ yield 1
562
+ got 0
563
+ g done
564
+ got 1
565
+
566
+ Example with `@greedy(queue_depth=1)`
567
+ where the "yield 1" step computes before the "got 0":
568
+
569
+ >>> from cs.x import X
570
+ >>> from time import sleep
571
+ >>> @greedy
572
+ ... def g():
573
+ ... for i in range(3):
574
+ ... X("Y")
575
+ ... print("yield", i)
576
+ ... yield i
577
+ ... print("g done")
578
+ ...
579
+ >>> G = g(); sleep(2)
580
+ yield 0
581
+ yield 1
582
+ >>> for i in G:
583
+ ... print("got", repr(i))
584
+ ... sleep(0.1)
585
+ ...
586
+ yield 2
587
+ got 0
588
+ yield 3
589
+ got 1
590
+ g done
591
+ got 2
592
+
593
+ '''
594
+ assert queue_depth >= 0
595
+
596
+ if g is None:
597
+ # the parameterised @greedy(queue_depth=n) form
598
+ # pylint: disable=no-value-for-parameter
599
+ return _greedy_decorator(queue_depth=queue_depth)
600
+
601
+ if callable(g):
602
+ # the direct @greedy form
603
+ return _greedy_decorator(g, queue_depth=queue_depth)
604
+
605
+ # presumably an iterator - dispatch it in a Thread
606
+ try:
607
+ it = iter(g)
608
+ except TypeError as e:
609
+ # pylint: disable=raise-missing-from
610
+ raise TypeError("g=%r: neither callable nor iterable: %s" % (g, e))
611
+
612
+ # pylint: disable=import-outside-toplevel
613
+ from cs.queues import Channel, IterableQueue
614
+ if queue_depth == 0:
615
+ q = Channel()
616
+ else:
617
+ q = IterableQueue(queue_depth)
618
+
619
+ def run_generator():
620
+ ''' Thread body for greedy generator.
621
+ '''
622
+ try:
623
+ for item in it:
624
+ q.put(item)
625
+ finally:
626
+ q.close()
627
+
628
+ Thread(target=run_generator).start()
629
+ return iter(q)
630
+
631
+ def skip_map(func, *iterables, except_types, quiet=False):
632
+ ''' A version of `map()` which will skip items where `func(item)`
633
+ raises an exception in `except_types`, a tuple of exception types.
634
+ If a skipped exception occurs a warning will be issued unless
635
+ `quiet` is true (default `False`).
636
+ '''
637
+ if not isinstance(except_types, tuple):
638
+ raise TypeError(
639
+ "except types must be a tuple of exception types but has type %s" %
640
+ (type(except_types),)
641
+ )
642
+ for iterable in iterables:
643
+ for item in iterable:
644
+ try:
645
+ yield func(item)
646
+ except except_types as e:
647
+ quiet or warning(
648
+ "skip_map(func=%s): item=%s: skip exception: %s", func, item, e
649
+ )
650
+
651
+ if __name__ == '__main__':
652
+ import sys
653
+ import cs.seq_tests
654
+ cs.seq_tests.selftest(sys.argv)
@@ -0,0 +1,399 @@
1
+ Metadata-Version: 2.1
2
+ Name: cs-seq
3
+ Version: 20250103
4
+ Summary: Stuff to do with counters, sequences and iterables.
5
+ Author-email: Cameron Simpson <cs@cskk.id.au>
6
+ License: GNU General Public License v3 or later (GPLv3+)
7
+ Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
8
+ Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
9
+ Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
10
+ Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/seq.py
11
+ Keywords: python2,python3
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 2
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: cs.deco>=20250103
22
+ Requires-Dist: cs.gimmicks>=20240316
23
+
24
+ Stuff to do with counters, sequences and iterables.
25
+
26
+ *Latest release 20250103*:
27
+ New skip_map(func, *iterables, except_types, quiet=False) generator function, like map() but skipping certain exceptions.
28
+
29
+ Note that any function accepting an iterable
30
+ will consume some or all of the derived iterator
31
+ in the course of its function.
32
+
33
+ ## <a name="common_prefix_length"></a>`common_prefix_length(*seqs)`
34
+
35
+ Return the length of the common prefix of sequences `seqs`.
36
+
37
+ ## <a name="common_suffix_length"></a>`common_suffix_length(*seqs)`
38
+
39
+ Return the length of the common suffix of sequences `seqs`.
40
+
41
+ ## <a name="first"></a>`first(iterable)`
42
+
43
+ Return the first item from an iterable; raise `IndexError` on empty iterables.
44
+
45
+ ## <a name="get0"></a>`get0(iterable, default=None)`
46
+
47
+ Return first element of an iterable, or the default.
48
+
49
+ ## <a name="greedy"></a>`greedy(g=None, queue_depth=0)`
50
+
51
+ A decorator or function for greedy computation of iterables.
52
+
53
+ If `g` is omitted or callable
54
+ this is a decorator for a generator function
55
+ causing it to compute greedily,
56
+ capacity limited by `queue_depth`.
57
+
58
+ If `g` is iterable
59
+ this function dispatches it in a `Thread` to compute greedily,
60
+ capacity limited by `queue_depth`.
61
+
62
+ Example with an iterable:
63
+
64
+ for packet in greedy(parse_data_stream(stream)):
65
+ ... process packet ...
66
+
67
+ which does some readahead of the stream.
68
+
69
+ Example as a function decorator:
70
+
71
+ @greedy
72
+ def g(n):
73
+ for item in range(n):
74
+ yield n
75
+
76
+ This can also be used directly on an existing iterable:
77
+
78
+ for item in greedy(range(n)):
79
+ yield n
80
+
81
+ Normally a generator runs on demand.
82
+ This function dispatches a `Thread` to run the iterable
83
+ (typically a generator)
84
+ putting yielded values to a queue
85
+ and returns a new generator yielding from the queue.
86
+
87
+ The `queue_depth` parameter specifies the depth of the queue
88
+ and therefore how many values the original generator can compute
89
+ before blocking at the queue's capacity.
90
+
91
+ The default `queue_depth` is `0` which creates a `Channel`
92
+ as the queue - a zero storage buffer - which lets the generator
93
+ compute only a single value ahead of time.
94
+
95
+ A larger `queue_depth` allocates a `Queue` with that much storage
96
+ allowing the generator to compute as many as `queue_depth+1` values
97
+ ahead of time.
98
+
99
+ Here's a comparison of the behaviour:
100
+
101
+ Example without `@greedy`
102
+ where the "yield 1" step does not occur until after the "got 0":
103
+
104
+ >>> from time import sleep
105
+ >>> def g():
106
+ ... for i in range(2):
107
+ ... print("yield", i)
108
+ ... yield i
109
+ ... print("g done")
110
+ ...
111
+ >>> G = g(); sleep(0.1)
112
+ >>> for i in G:
113
+ ... print("got", i)
114
+ ... sleep(0.1)
115
+ ...
116
+ yield 0
117
+ got 0
118
+ yield 1
119
+ got 1
120
+ g done
121
+
122
+ Example with `@greedy`
123
+ where the "yield 1" step computes before the "got 0":
124
+
125
+ >>> from time import sleep
126
+ >>> @greedy
127
+ ... def g():
128
+ ... for i in range(2):
129
+ ... print("yield", i)
130
+ ... yield i
131
+ ... print("g done")
132
+ ...
133
+ >>> G = g(); sleep(0.1)
134
+ yield 0
135
+ >>> for i in G:
136
+ ... print("got", repr(i))
137
+ ... sleep(0.1)
138
+ ...
139
+ yield 1
140
+ got 0
141
+ g done
142
+ got 1
143
+
144
+ Example with `@greedy(queue_depth=1)`
145
+ where the "yield 1" step computes before the "got 0":
146
+
147
+ >>> from cs.x import X
148
+ >>> from time import sleep
149
+ >>> @greedy
150
+ ... def g():
151
+ ... for i in range(3):
152
+ ... X("Y")
153
+ ... print("yield", i)
154
+ ... yield i
155
+ ... print("g done")
156
+ ...
157
+ >>> G = g(); sleep(2)
158
+ yield 0
159
+ yield 1
160
+ >>> for i in G:
161
+ ... print("got", repr(i))
162
+ ... sleep(0.1)
163
+ ...
164
+ yield 2
165
+ got 0
166
+ yield 3
167
+ got 1
168
+ g done
169
+ got 2
170
+
171
+ ## <a name="imerge"></a>`imerge(*iters, **kw)`
172
+
173
+ Merge an iterable of ordered iterables in order.
174
+
175
+ Parameters:
176
+ * `iters`: an iterable of iterators
177
+ * `reverse`: keyword parameter: if true, yield items in reverse order.
178
+ This requires the iterables themselves to also be in
179
+ reversed order.
180
+
181
+ This function relies on the source iterables being ordered
182
+ and their elements being comparable, through slightly misordered
183
+ iterables (for example, as extracted from web server logs)
184
+ will produce only slightly misordered results, as the merging
185
+ is done on the basis of the front elements of each iterable.
186
+
187
+ ## <a name="isordered"></a>`isordered(items, reverse=False, strict=False)`
188
+
189
+ Test whether an iterable is ordered.
190
+ Note that the iterable is iterated, so this is a destructive
191
+ test for nonsequences.
192
+
193
+ ## <a name="last"></a>`last(iterable)`
194
+
195
+ Return the last item from an iterable; raise `IndexError` on empty iterables.
196
+
197
+ ## <a name="onetomany"></a>`onetomany(func)`
198
+
199
+ A decorator for a method of a sequence to merge the results of
200
+ passing every element of the sequence to the function, expecting
201
+ multiple values back.
202
+
203
+ Example:
204
+
205
+ class X(list):
206
+ @onetomany
207
+ def chars(self, item):
208
+ return item
209
+ strs = X(['Abc', 'Def'])
210
+ all_chars = X.chars()
211
+
212
+ ## <a name="onetoone"></a>`onetoone(func)`
213
+
214
+ A decorator for a method of a sequence to merge the results of
215
+ passing every element of the sequence to the function, expecting a
216
+ single value back.
217
+
218
+ Example:
219
+
220
+ class X(list):
221
+ @onetoone
222
+ def lower(self, item):
223
+ return item.lower()
224
+ strs = X(['Abc', 'Def'])
225
+ lower_strs = X.lower()
226
+
227
+ ## <a name="Seq"></a>Class `Seq`
228
+
229
+ A numeric sequence implemented as a thread safe wrapper for
230
+ `itertools.count()`.
231
+
232
+ A `Seq` is iterable and both iterating and calling it return
233
+ the next number in the sequence.
234
+
235
+ ## <a name="seq"></a>`seq()`
236
+
237
+ Return a new sequential value.
238
+
239
+ ## <a name="skip_map"></a>`skip_map(func, *iterables, except_types, quiet=False)`
240
+
241
+ A version of `map()` which will skip items where `func(item)`
242
+ raises an exception in `except_types`, a tuple of exception types.
243
+ If a skipped exception occurs a warning will be issued unless
244
+ `quiet` is true (default `False`).
245
+
246
+ ## <a name="splitoff"></a>`splitoff(sq, *sizes)`
247
+
248
+ Split a sequence into (usually short) prefixes and a tail,
249
+ for example to construct subdirectory trees based on a UUID.
250
+
251
+ Example:
252
+
253
+ >>> from uuid import UUID
254
+ >>> uuid = 'd6d9c510-785c-468c-9aa4-b7bda343fb79'
255
+ >>> uu = UUID(uuid).hex
256
+ >>> uu
257
+ 'd6d9c510785c468c9aa4b7bda343fb79'
258
+ >>> splitoff(uu, 2, 2)
259
+ ['d6', 'd9', 'c510785c468c9aa4b7bda343fb79']
260
+
261
+ ## <a name="StatefulIterator"></a>Class `StatefulIterator`
262
+
263
+ A trivial iterator which wraps another iterator to expose some tracking state.
264
+
265
+ This has 2 attributes:
266
+ * `.it`: the internal iterator which should yield `(item,new_state)`
267
+ * `.state`: the last state value from the internal iterator
268
+
269
+ The originating use case is resuse of an iterator by independent
270
+ calls that are typically sequential, specificly the .read
271
+ method of file like objects. Naive sequential reads require
272
+ the underlying storage to locate the data on every call, even
273
+ though the previous call has just performed this task for the
274
+ previous read. Saving the iterator used from the preceeding
275
+ call allows the iterator to pick up directly if the file
276
+ offset hasn't been fiddled in the meantime.
277
+
278
+ ## <a name="tee"></a>`tee(iterable, *Qs)`
279
+
280
+ A generator yielding the items from an iterable
281
+ which also copies those items to a series of queues.
282
+
283
+ Parameters:
284
+ * `iterable`: the iterable to copy
285
+ * `Qs`: the queues, objects accepting a `.put` method.
286
+
287
+ Note: the item is `.put` onto every queue
288
+ before being yielded from this generator.
289
+
290
+ ## <a name="the"></a>`the(iterable, context=None)`
291
+
292
+ Returns the first element of an iterable, but requires there to be
293
+ exactly one.
294
+
295
+ ## <a name="TrackingCounter"></a>Class `TrackingCounter`
296
+
297
+ A wrapper for a counter which can be incremented and decremented.
298
+
299
+ A facility is provided to wait for the counter to reach a specific value.
300
+ The .inc and .dec methods also accept a `tag` argument to keep
301
+ individual counts based on the tag to aid debugging.
302
+
303
+ TODO: add `strict` option to error and abort if any counter tries
304
+ to go below zero.
305
+
306
+ *`TrackingCounter.__init__(self, value=0, name=None, lock=None)`*:
307
+ Initialise the counter to `value` (default 0) with the optional `name`.
308
+
309
+ *`TrackingCounter.check(self)`*:
310
+ Internal consistency check.
311
+
312
+ *`TrackingCounter.dec(self, tag=None)`*:
313
+ Decrement the counter.
314
+ Wake up any threads waiting for its new value.
315
+
316
+ *`TrackingCounter.inc(self, tag=None)`*:
317
+ Increment the counter.
318
+ Wake up any threads waiting for its new value.
319
+
320
+ *`TrackingCounter.wait(self, value)`*:
321
+ Wait for the counter to reach the specified `value`.
322
+
323
+ ## <a name="unrepeated"></a>`unrepeated(it, seen=None, signature=None)`
324
+
325
+ A generator yielding items from the iterable `it` with no repetitions.
326
+
327
+ Parameters:
328
+ * `it`: the iterable to process
329
+ * `seen`: an optional setlike container supporting `in` and `.add()`
330
+ * `signature`: an optional signature function for items from `it`
331
+ which produces the value to compare to recognise repeated items;
332
+ its values are stored in the `seen` set
333
+
334
+ The default `signature` function is equality;
335
+ the items are stored n `seen` and compared.
336
+ This requires the items to be hashable and support equality tests.
337
+ The same applies to whatever values the `signature` function produces.
338
+
339
+ Another common signature is identity: `id`, useful for
340
+ traversing a graph which may have cycles.
341
+
342
+ Since `seen` accrues all the signature values for yielded items
343
+ generally it will grow monotonicly as iteration proceeeds.
344
+ If the items are complex or large it is well worth providing a signature
345
+ function even if the items themselves can be used in a set.
346
+
347
+ # Release Log
348
+
349
+
350
+
351
+ *Release 20250103*:
352
+ New skip_map(func, *iterables, except_types, quiet=False) generator function, like map() but skipping certain exceptions.
353
+
354
+ *Release 20221118*:
355
+ Small doc improvement.
356
+
357
+ *Release 20220530*:
358
+ Seq: calling a Seq is like next(seq).
359
+
360
+ *Release 20210924*:
361
+ New greedy(iterable) or @greedy(generator_function) to let generators precompute.
362
+
363
+ *Release 20210913*:
364
+ New unrepeated() generator removing duplicates from an iterable.
365
+
366
+ *Release 20201025*:
367
+ New splitoff() function to split a sequence into (usually short) prefixes and a tail.
368
+
369
+ *Release 20200914*:
370
+ New common_prefix_length and common_suffix_length for comparing prefixes and suffixes of sequences.
371
+
372
+ *Release 20190103*:
373
+ Documentation update.
374
+
375
+ *Release 20190101*:
376
+ * New and UNTESTED class StatefulIterator to associate some externally visible state with an iterator.
377
+ * Seq: accept optional `lock` parameter.
378
+
379
+ *Release 20171231*:
380
+ * Python 2 backport for imerge().
381
+ * New tee function to duplicate an iterable to queues.
382
+ * Function isordered() is now a test instead of an assertion.
383
+ * Drop NamedTuple, NamedTupleClassFactory (unused).
384
+
385
+ *Release 20160918*:
386
+ * New function isordered() to test ordering of a sequence.
387
+ * imerge: accept new `reverse` parameter for merging reversed iterables.
388
+
389
+ *Release 20160828*:
390
+ Modify DISTINFO to say "install_requires", fixes pypi requirements.
391
+
392
+ *Release 20160827*:
393
+ TrackingCounter: accept presupplied lock object. Python 3 exec fix.
394
+
395
+ *Release 20150118*:
396
+ metadata update
397
+
398
+ *Release 20150111*:
399
+ Initial PyPI release.
@@ -0,0 +1,5 @@
1
+ cs/seq.py,sha256=p9k6zwoF-wRvPKfFKS7MWPygalhqc0CohCcLmC0vFrI,18384
2
+ cs_seq-20250103.dist-info/METADATA,sha256=C9MINzOVG0z-icxM6Iy0_hUzvn7ErbP3KJzsZy5l9aY,12134
3
+ cs_seq-20250103.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
4
+ cs_seq-20250103.dist-info/top_level.txt,sha256=MJn10B_hUOb4f5hIJP5wKj_mMYsOQ6NUWqo9vSLmwcQ,3
5
+ cs_seq-20250103.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ cs