PythonIota 1.7.1__tar.gz → 1.9.0__tar.gz

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.
Files changed (69) hide show
  1. pythoniota-1.9.0/MANIFEST.in +2 -0
  2. pythoniota-1.9.0/PKG-INFO +659 -0
  3. pythoniota-1.9.0/README.md +642 -0
  4. {pythoniota-1.7.1 → pythoniota-1.9.0}/pyproject.toml +4 -1
  5. pythoniota-1.9.0/src/PythonIota.egg-info/PKG-INFO +659 -0
  6. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/PythonIota.egg-info/SOURCES.txt +13 -1
  7. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/__init__.py +3 -2
  8. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/__init__.py +13 -0
  9. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/_argument.py +34 -9
  10. pythoniota-1.9.0/src/pythoniota/cli/_completion.py +222 -0
  11. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/_formatter.py +12 -3
  12. pythoniota-1.9.0/src/pythoniota/cli/_parser.py +469 -0
  13. pythoniota-1.9.0/src/pythoniota/cli/_sources.py +147 -0
  14. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/enum.py +11 -1
  15. pythoniota-1.9.0/src/pythoniota/regex/__init__.py +74 -0
  16. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/regex/_compiler.py +27 -8
  17. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/regex/_parser.py +4 -0
  18. pythoniota-1.9.0/src/pythoniota/regex/_pattern.py +280 -0
  19. pythoniota-1.9.0/src/pythoniota/regex/_vm.py +214 -0
  20. pythoniota-1.9.0/src/pythoniota/sequence.py +803 -0
  21. pythoniota-1.9.0/tests/__init__.py +0 -0
  22. pythoniota-1.9.0/tests/test_cli.py +558 -0
  23. pythoniota-1.9.0/tests/test_cli_completion.py +271 -0
  24. pythoniota-1.9.0/tests/test_cli_groups_version.py +347 -0
  25. pythoniota-1.9.0/tests/test_cli_scripts.py +316 -0
  26. pythoniota-1.9.0/tests/test_cli_sources.py +431 -0
  27. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_enum_enhanced.py +41 -0
  28. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_integration.py +5 -1
  29. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_recipes.py +65 -1
  30. pythoniota-1.9.0/tests/test_regex_budget.py +163 -0
  31. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_regex_extended.py +39 -0
  32. pythoniota-1.9.0/tests/test_regex_search.py +169 -0
  33. pythoniota-1.9.0/tests/test_sequence.py +576 -0
  34. pythoniota-1.9.0/tests/test_sequence_typing.py +217 -0
  35. pythoniota-1.9.0/tests/test_sequence_utilities.py +507 -0
  36. pythoniota-1.9.0/tests/test_workflows.py +62 -0
  37. pythoniota-1.7.1/MANIFEST.in +0 -1
  38. pythoniota-1.7.1/PKG-INFO +0 -424
  39. pythoniota-1.7.1/README.md +0 -407
  40. pythoniota-1.7.1/src/PythonIota.egg-info/PKG-INFO +0 -424
  41. pythoniota-1.7.1/src/pythoniota/cli/_parser.py +0 -321
  42. pythoniota-1.7.1/src/pythoniota/regex/__init__.py +0 -66
  43. pythoniota-1.7.1/src/pythoniota/regex/_pattern.py +0 -210
  44. pythoniota-1.7.1/src/pythoniota/regex/_vm.py +0 -129
  45. pythoniota-1.7.1/src/pythoniota/sequence.py +0 -461
  46. pythoniota-1.7.1/tests/test_cli.py +0 -244
  47. pythoniota-1.7.1/tests/test_sequence.py +0 -153
  48. {pythoniota-1.7.1 → pythoniota-1.9.0}/setup.cfg +0 -0
  49. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/PythonIota.egg-info/dependency_links.txt +0 -0
  50. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/PythonIota.egg-info/top_level.txt +0 -0
  51. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/_bitflag.py +0 -0
  52. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/_compat.py +0 -0
  53. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/_safe_eval.py +0 -0
  54. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/_errors.py +0 -0
  55. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/_namespace.py +0 -0
  56. /pythoniota-1.7.1/tests/__init__.py → /pythoniota-1.9.0/src/pythoniota/py.typed +0 -0
  57. {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/recipes.py +0 -0
  58. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_advanced.py +0 -0
  59. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_bitflags.py +0 -0
  60. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_compat.py +0 -0
  61. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_coverage_fill.py +0 -0
  62. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_coverage_fill2.py +0 -0
  63. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_coverage_fill3.py +0 -0
  64. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_enum.py +0 -0
  65. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_new_features.py +0 -0
  66. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_regex.py +0 -0
  67. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_safe_eval.py +0 -0
  68. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_serialization.py +0 -0
  69. {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_string_enum.py +0 -0
@@ -0,0 +1,2 @@
1
+ recursive-include tests *.py
2
+ include src/pythoniota/py.typed
@@ -0,0 +1,659 @@
1
+ Metadata-Version: 2.2
2
+ Name: PythonIota
3
+ Version: 1.9.0
4
+ Summary: A zero-dependency Python toolkit: Go-style iota enums, sequence generators, a from-scratch regex engine, and an argparse-style CLI parser
5
+ Author: Equinox
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # PythonIota
19
+
20
+ A **zero-dependency Python toolkit** of from-scratch building blocks — Go-style
21
+ `iota` enumerations, lazy sequence generators, and a Pike-VM regex engine.
22
+
23
+ ```bash
24
+ pip install PythonIota
25
+ ```
26
+
27
+ Requires Python 3.10+.
28
+
29
+ ## 1.9.0
30
+
31
+ ### New features
32
+
33
+ - Regex searches skip impossible starts using a conservative literal prefix;
34
+ diagnostics expose the prefix and account for prefilter operations.
35
+ - CLI mutually exclusive groups, version output and static completion scripts for
36
+ Bash, Zsh and PowerShell extend the existing metadata-only completion API.
37
+ - Sequence pipelines add bounded `limit`, grouping, Top-K, partitioning, iterator
38
+ copies and combinatorics, with protection for known non-exhausting inputs.
39
+
40
+ ### Fixes
41
+
42
+ - Pattern backreferences now fail explicitly instead of silently matching digits.
43
+ - Out-of-range regex positions are clamped to the text, matching `re` semantics.
44
+ - Counted nullable repetitions stop after zero-width iterations without losing
45
+ the preferred non-empty alternative during iteration.
46
+
47
+ ## 1.8.0
48
+
49
+ ### New features
50
+
51
+ - Generic, single-pass `IotaIterator` pipelines support continuous chaining.
52
+ - CLI options support explicit JSON/mapping configuration, opt-in environment
53
+ values and metadata-only completion candidates.
54
+ - Regex operations support shared step/time budgets and structured diagnostics.
55
+
56
+ ### Fixes
57
+
58
+ - Mapping derived sequences preserves their lazy iteration, element types and
59
+ finite/infinite behavior; `filter` still returns an iterator.
60
+ - Empty regex matches, conditional capture states and zero-width repetitions
61
+ retain the appropriate alternatives and iteration order.
62
+ - Parent CLI positionals are validated before subcommand dispatch.
63
+ - Enum aliases cannot overwrite existing or reserved names; alias identifiers
64
+ must already be NFKC-normalized to agree with Python attribute syntax.
65
+ - Sequence APIs carry generic element types, and distributions include `py.typed`.
66
+
67
+ ## 1.7.1 fixes
68
+
69
+ - Full matching now considers complete alternatives instead of rejecting a
70
+ valid match after selecting a shorter branch.
71
+ - Ordered enum members now hash consistently with equal numeric values and
72
+ equal members of other enums, preserving dictionary and set lookups.
73
+
74
+ **What's inside**
75
+
76
+ - **Enums** — Go-style `iota`, bit flags, string enums (`pythoniota.IotaEnum`, ...)
77
+ - **Sequences** — lazy, chainable generators (`pythoniota.iota`)
78
+ - **Regex** — a from-scratch NFA (Pike VM) engine, no catastrophic backtracking (`pythoniota.regex`)
79
+ - **CLI** — a from-scratch `argparse`-style argument parser (`pythoniota.cli`)
80
+
81
+ ---
82
+
83
+ ## Enums
84
+
85
+ ### Go-style `iota`
86
+
87
+ `iota` starts at `0` and auto-increments each time it is read. Literal assignments do **not** consume it.
88
+
89
+ ```python
90
+ from pythoniota import IotaEnum
91
+
92
+ class Color(IotaEnum):
93
+ Red = iota # 0
94
+ Green = iota # 1
95
+ Blue = iota # 2
96
+
97
+ Color.Green # 1
98
+ Color.names() # ['Red', 'Green', 'Blue']
99
+ Color.values() # [0, 1, 2]
100
+ list(Color) # [('Red', 0), ('Green', 1), ('Blue', 2)]
101
+ 0 in Color # True (by value)
102
+ 'Red' in Color # True (by name)
103
+ Color.from_value(2) # 'Blue'
104
+ ```
105
+
106
+ `iota` works inside expressions — the value is the current counter:
107
+
108
+ ```python
109
+ class Perm(IotaEnum):
110
+ Read = 1 << iota # 1
111
+ Write = 1 << iota # 2
112
+ Exec = 1 << iota # 4
113
+ All = Read | Write | Exec # 7
114
+
115
+ class Size(IotaEnum):
116
+ KB = 1 << (iota + 10) # 1024
117
+ MB = 1 << (iota + 10) # 2048
118
+ GB = 1 << (iota + 10) # 4096
119
+ ```
120
+
121
+ ### Skipping values & custom start/step
122
+
123
+ ```python
124
+ class Errno(IotaEnum):
125
+ EPERM = iota # 0
126
+ skip(2) # skip 1, 2
127
+ EBADF = iota # 3
128
+
129
+ class Port(IotaEnum):
130
+ _iota_start_ = 8000
131
+ _iota_step_ = 10
132
+ HTTP = iota # 8000
133
+ HTTPS = iota # 8010
134
+ ```
135
+
136
+ `_ = iota` also skips a single value (Go's blank identifier).
137
+
138
+ ### Immutability, aliases, serialization
139
+
140
+ ```python
141
+ Color.Red = 99 # AttributeError: cannot modify enum member
142
+ Color.alias('R', 'Red') # Color.R == 0
143
+ Color.to_dict() # {'Red': 0, 'Green': 1, 'Blue': 2}
144
+ Color.to_json() # '{"Red": 0, "Green": 1, "Blue": 2}'
145
+ Color.from_json(s) # -> dict of members
146
+ Color.has('Red') # True
147
+ Color.get('Nope', -1) # -1
148
+ ```
149
+
150
+ Alias names must be public Python identifiers. Existing members, aliases,
151
+ methods and reserved/internal names cannot be overwritten; rejected aliases
152
+ leave the enum unchanged.
153
+
154
+ ### `@unique` — reject duplicate values
155
+
156
+ ```python
157
+ from pythoniota import unique
158
+
159
+ @unique
160
+ class Status(IotaEnum):
161
+ Active = iota
162
+ Inactive = iota
163
+ # raises ValueError if two members share a value
164
+ ```
165
+
166
+ ### Ordered enums
167
+
168
+ Add `_ordered_ = True` to make members comparable by declaration order:
169
+
170
+ ```python
171
+ class Priority(IotaEnum):
172
+ _ordered_ = True
173
+ Low = iota
174
+ Medium = iota
175
+ High = iota
176
+
177
+ Priority.Low < Priority.High # True
178
+ ```
179
+
180
+ ### Bit flags
181
+
182
+ ```python
183
+ from pythoniota import IotaBitFlags, FlagScope
184
+
185
+ class Access(IotaBitFlags):
186
+ Read = 1 << iota # 1
187
+ Write = 1 << iota # 2
188
+ Exec = 1 << iota # 4
189
+
190
+ flags = Access.Read | Access.Write
191
+ flags.has(Access.Read) # True
192
+ flags.has_all(Access.Read, Access.Exec) # False
193
+ flags.has_any(Access.Read, Access.Exec) # True
194
+ list(flags.decompose()) # [BitFlag(1), BitFlag(2)]
195
+
196
+ with FlagScope() as scope:
197
+ scope.grant(Access.Read, Access.Write)
198
+ scope.revoke(Access.Write)
199
+ scope.has(Access.Read) # True
200
+ ```
201
+
202
+ ### String enums
203
+
204
+ Members resolve to their own name, or a custom format:
205
+
206
+ ```python
207
+ from pythoniota import IotaStringEnum
208
+
209
+ class Color(IotaStringEnum):
210
+ Red = iota # 'Red'
211
+ Blue = iota # 'Blue'
212
+
213
+ class Code(IotaStringEnum):
214
+ _format_ = "{name}_{index:03d}"
215
+ OK = iota # 'OK_000'
216
+ Error = iota # 'Error_001'
217
+ ```
218
+
219
+ ### `@iota_enum` decorator
220
+
221
+ Turn a plain class into an enum:
222
+
223
+ ```python
224
+ from pythoniota import iota_enum
225
+
226
+ @iota_enum
227
+ class Color:
228
+ Red = 0
229
+ Green = 1
230
+ Blue = 2
231
+
232
+ @iota_enum(ordered=True)
233
+ class Priority:
234
+ Low = 0
235
+ High = 1
236
+ ```
237
+
238
+ ### `match` / `case`
239
+
240
+ Enum members match by value:
241
+
242
+ ```python
243
+ match color:
244
+ case Color.Red: ...
245
+ case Color.Blue: ...
246
+ case _: ...
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Sequences
252
+
253
+ `iota(...)` is a lazy, composable sequence. Construction mirrors `range`, plus an optional `map`:
254
+
255
+ ```python
256
+ from pythoniota import iota
257
+
258
+ iota() # 0, 1, 2, ... (infinite)
259
+ iota(5) # 0, 1, 2, 3, 4
260
+ iota(2, 10, 2) # 2, 4, 6, 8
261
+ iota(5, map=lambda i: i*i) # 0, 1, 4, 9, 16
262
+
263
+ s = iota(10)
264
+ len(s) # 10 (O(1))
265
+ s[7] # 7 (O(1) arithmetic indexing)
266
+ s[2:5] # [2, 3, 4]
267
+ list(reversed(s)) # 9, 8, ... 0 (lazy)
268
+ 5 in s # True (O(1))
269
+ s.take(3) # [0, 1, 2]
270
+ ```
271
+
272
+ ### Operators
273
+
274
+ ```python
275
+ iota(3) + iota(3, 6) # concat: 0,1,2,3,4,5
276
+ iota(3) * 2 # repeat: 0,1,2,0,1,2
277
+ iota(3) | iota(3, 6) # interleave: 0,3,1,4,2,5
278
+ iota(3) @ iota(3, 6) # zip: (0,3),(1,4),(2,5)
279
+ ```
280
+
281
+ ### Lazy combinators
282
+
283
+ ```python
284
+ iota(10).filter(lambda x: x % 2 == 0) # 0,2,4,6,8
285
+ iota().map(lambda x: x*x) # 0,1,4,9,...
286
+ iota().takewhile(lambda x: x < 5) # 0,1,2,3,4
287
+ iota(10).dropwhile(lambda x: x < 7) # 7,8,9
288
+ iota(4).pairwise() # (0,1),(1,2),(2,3)
289
+ iota(5).window(3) # (0,1,2),(1,2,3),(2,3,4)
290
+ iota(7).chunk(3) # [0,1,2],[3,4,5],[6]
291
+ iota(6).map(lambda x: x % 3).distinct() # 0,1,2
292
+ seq.flatten() # flatten one level
293
+ iota().enumerate() .accumulate() .zip(other)
294
+ ```
295
+
296
+ ### Chainable iterator pipelines
297
+
298
+ ```python
299
+ from pythoniota import IotaIterator, iota
300
+
301
+ windows = iota(20).filter(lambda x: x % 2 == 0).map(str).window(3)
302
+ windows.take(2) # [('0', '2', '4'), ('2', '4', '6')]
303
+ next(windows) # ('4', '6', '8'): continues the same stream
304
+ IotaIterator([1, 2, 3]).map(lambda x: x * 10).to_list() # [10, 20, 30]
305
+ iota().filter(lambda x: x % 2 == 0).map(str).take(3) # ['0', '2', '4']
306
+ ```
307
+
308
+ `filter`, `accumulate`, `enumerate`, `zip`, `chunk`, `window`, `takewhile`,
309
+ `dropwhile`, `pairwise`, `flatten` and `distinct` return `IotaIterator` pipelines,
310
+ which can continue chaining. They are lazy and single-pass (`iter(pipe) is pipe`),
311
+ with element types preserved through generic transformations. `take(n)` still
312
+ returns a list. Chunk/window sizes must be positive and are checked before consumption.
313
+
314
+ Base sequences and their `.map()` results remain reusable. Iterator pipelines
315
+ have no length, but expose `infinite`: `True` for known non-exhausting sources,
316
+ `False` for known finite sources, and `None` when unknown. Exhaustive terminals
317
+ reject known non-exhausting pipelines before consuming them. Filtering/distinct
318
+ can yield few values without ever exhausting their infinite input; these retain
319
+ the protection. Arbitrary generators and early-stopping `takewhile` may remain
320
+ unknown, so this is not a general termination detector.
321
+
322
+ Use `.limit(n)` to keep a bounded prefix chainable (`take(n)` still returns a list):
323
+
324
+ ```python
325
+ iota().filter(lambda x: x % 2 == 0).limit(5).sum() # 20
326
+ ```
327
+
328
+ `limit` bounds the number of returned elements, not the time to produce one:
329
+ `iota().filter(lambda x: False).limit(1)` still cannot yield a value. Short-circuit
330
+ operations (`first`, `find`, `any`, `all`, `take`) remain available on infinite input.
331
+
332
+ ### Grouping, selection and iterator utilities
333
+
334
+ ```python
335
+ iota(6).group_by(lambda x: x % 2) # {0: [0, 2, 4], 1: [1, 3, 5]}
336
+ iota(10).top_k(3) # [9, 8, 7]
337
+ iota(10).top_k(3, largest=False) # [0, 1, 2]
338
+ iota(2).zip_longest(["a"], fillvalue="-").to_list() # [(0, 'a'), (1, '-')]
339
+ iota(2).product(["a", "b"]).to_list() # [(0, 'a'), (0, 'b'), (1, 'a'), (1, 'b')]
340
+ iota(3).permutations(2).take(3) # [(0, 1), (0, 2), (1, 0)]
341
+
342
+ odds, evens = iota(6).partition(lambda x: x % 2 == 0)
343
+ odds.to_list(), evens.to_list() # ([1, 3, 5], [0, 2, 4])
344
+ left, right = iota(3).tee()
345
+ left.take(2), right.to_list() # ([0, 1], [0, 1, 2])
346
+ ```
347
+
348
+ `group_by(key=None)` groups globally into lists, preserving key/element insertion
349
+ order; unlike `itertools.groupby`, equal keys need not be adjacent. `top_k` supports
350
+ `key=`, preserves input order for ties and keeps O(k) selection storage, but must
351
+ consume the entire finite input. `top_k(0)` returns immediately without consuming.
352
+ Neither operation can compute a global result from an infinite stream.
353
+
354
+ `partition` returns false/true branches and evaluates its predicate once per item.
355
+ Like `tee`, it buffers when consumers advance at different rates; that buffer can
356
+ grow without bound. `product` and `permutations` defer input consumption until
357
+ iteration, then cache their inputs like `itertools`; they reject known infinite
358
+ inputs and are not constant-memory streaming operations.
359
+
360
+ ### Terminal operations
361
+
362
+ Reductions on plain arithmetic sequences are O(1); known-infinite base sequences
363
+ reject exhaustive terminals. Iterator pipelines consume their remaining elements.
364
+
365
+ ```python
366
+ iota(1, 101).sum() # 5050 (closed-form, O(1))
367
+ iota(3, 10).min() # 3
368
+ iota(3, 10).max() # 9
369
+ iota(0, 10, 2).count() # 5
370
+ iota(5).last() # 4
371
+ iota(10).nth(3) # 3
372
+ iota().first() # 0
373
+ iota().find(lambda x: x > 100) # 101
374
+ iota(1, 5).all(lambda x: x > 0) # True
375
+ iota(5).any(lambda x: x == 3) # True
376
+ iota(3).reduce(lambda a, b: a + b) # 3
377
+
378
+ iota(3).to_list() # [0, 1, 2]
379
+ iota(3).to_tuple() # (0, 1, 2)
380
+ iota(3).to_set() # {0, 1, 2}
381
+ iota(3).to_dict(value=lambda x: x*x) # {0: 0, 1: 1, 2: 4}
382
+ ```
383
+
384
+ ### Async iteration
385
+
386
+ ```python
387
+ async for x in iota(5):
388
+ ...
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Recipes
394
+
395
+ ```python
396
+ from pythoniota.recipes import (
397
+ fibonacci, lucas, factorial, catalan, harmonic,
398
+ triangle, powers, geometric, primes, primes_sieve,
399
+ pascal_row, collatz, repeat, cycle,
400
+ )
401
+
402
+ fibonacci(10).to_list() # 0,1,1,2,3,5,8,13,21,34
403
+ lucas(7).to_list() # 2,1,3,4,7,11,18
404
+ factorial(6).to_list() # 1,1,2,6,24,120
405
+ catalan(6).to_list() # 1,1,2,5,14,42
406
+ harmonic(4).to_list() # 1.0, 1.5, 1.833..., 2.083...
407
+ triangle(5).to_list() # 0,1,3,6,10
408
+ powers(2, 8).to_list() # 1,2,4,...,128
409
+ geometric(3, 2, 4).to_list() # 3,6,12,24
410
+ primes(5).to_list() # 2,3,5,7,11 (first n primes)
411
+ primes_sieve(20).to_list()# 2,3,5,7,11,13,17,19 (< limit, Eratosthenes)
412
+ pascal_row(4).to_list() # 1,4,6,4,1
413
+ collatz(6).to_list() # 6,3,10,5,16,8,4,2,1
414
+ repeat('x', 3).to_list() # ['x','x','x']
415
+ cycle([1,2], 5).to_list() # 1,2,1,2,1
416
+ ```
417
+
418
+ Recipes without a count argument produce infinite sequences: `fibonacci().take(20)`, `primes().takewhile(lambda p: p < 100)`.
419
+
420
+ ---
421
+
422
+ ## Safe expression evaluation
423
+
424
+ `safe_eval` evaluates arithmetic expressions over a whitelisted AST (no `eval`, no names/calls beyond provided variables):
425
+
426
+ ```python
427
+ from pythoniota import safe_eval
428
+
429
+ safe_eval("2 ** 10") # 1024
430
+ safe_eval("a * b + 1", {"a": 3, "b": 4}) # 13
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Regex
436
+
437
+ A from-scratch regular-expression engine that runs bytecode with an ordered
438
+ **Pike VM**. Plain patterns such as `(a+)+$` avoid catastrophic backtracking and
439
+ run in linear time in the text length for a fixed pattern. This is not a
440
+ universal complexity guarantee: lookaround can rescan suffixes, and conditionals
441
+ can retain multiple capture-participation states. Use explicit `max_steps` and
442
+ `timeout` limits when matching potentially expensive patterns.
443
+
444
+ ```python
445
+ from pythoniota import regex
446
+
447
+ m = regex.match(r"(\d{4})-(\d{2})-(\d{2})", "2026-08-02")
448
+ m.groups() # ('2026', '08', '02')
449
+
450
+ regex.findall(r"\w+", "hello world") # ['hello', 'world']
451
+ regex.sub(r"\s+", "_", "a b c") # 'a_b_c'
452
+ regex.split(r"[,;]\s*", "a, b; c,d") # ['a', 'b', 'c', 'd']
453
+
454
+ named = regex.search(r"(?P<user>\w+)@(?P<host>\w+)", "user@host")
455
+ named.groupdict() # {'user': 'user', 'host': 'host'}
456
+ ```
457
+
458
+ Supported: literals, `.`, `* + ?` and their lazy forms `*? +? ??`, alternation
459
+ `|`, groups `(...)` / non-capturing `(?:...)` / named `(?P<name>...)`, lookahead
460
+ `(?=...)` / `(?!...)`, fixed-width lookbehind `(?<=...)` / `(?<!...)`, conditionals
461
+ `(?(id/name)yes|no)`, character classes `[...]`, counted repeats `{m,n}`,
462
+ shorthands `\d \w \s` (and `\D \W \S`), word boundaries `\b \B`, anchors
463
+ `^ $ \A \Z`, and hex escapes `\xHH` / `\uHHHH`. Flags: `IGNORECASE`, `MULTILINE`,
464
+ `DOTALL`, `VERBOSE` (also inline `(?imsx)`). API mirrors `re`: `compile`, `match`,
465
+ `fullmatch`, `search`, `findall`, `finditer`, `sub`, `subn`, `split`, `escape`,
466
+ with `Match.group` / `groups` / `span` / `groupdict` / `expand`.
467
+
468
+ ```python
469
+ regex.findall(r"[a-z]+", "AbCdEf", regex.IGNORECASE) # ['AbCdEf']
470
+ regex.findall(r"\d+(?=px)", "10px 20em 30px") # ['10', '30'] (lookahead)
471
+ ```
472
+
473
+ Pattern backreferences (`\1`, `(?P=name)`) raise `RegexError`; this is an NFA-based
474
+ engine rather than a general backtracking engine. Replacement templates still
475
+ support backreferences. Patterns are text strings, not bytes. Empty matches follow
476
+ the iteration and splitting behavior of Python 3.10–3.13 `re`, including a non-empty
477
+ match immediately after an empty one at the same position.
478
+
479
+ ```python
480
+ regex.search(r"(a+)+$", "a" * 40 + "!") # None, without backtracking
481
+ ```
482
+
483
+ ### Execution limits and diagnostics
484
+
485
+ ```python
486
+ pattern = regex.compile(r"(?=a*$)")
487
+ try:
488
+ result = pattern.search("a" * 100 + "!", max_steps=1000, timeout=0.1)
489
+ except regex.RegexLimitError as error:
490
+ print(error.reason, error.steps)
491
+
492
+ structure = pattern.explain() # JSON-serializable bytecode, groups and nested assertions
493
+ report = pattern.diagnose("aaaa!", max_steps=1000)
494
+ print(report["status"], report["steps"], report["op_counts"])
495
+ ```
496
+
497
+ Search and iteration use a conservative mandatory literal prefix when available.
498
+ `explain()["literal_prefix"]` shows it; case-insensitive patterns and leading
499
+ branching/nullable patterns fall back to the normal VM. Existing higher-priority
500
+ threads keep running: the optimization does not restart the VM for each candidate.
501
+
502
+ All matching, iteration, splitting and replacement functions accept keyword-only
503
+ `max_steps` (nonnegative integer) and `timeout` (nonnegative seconds), defaulting
504
+ to no limits. A step is a bytecode analysis/visit or one native prefix lookup
505
+ (`PREFILTER`), including epsilon closure and consuming states; counts are
506
+ implementation-specific, not a portable time unit or a per-character work bound.
507
+ Nested lookarounds and all matches within one call share a single budget.
508
+ `finditer` starts its timeout when created, including pauses between yields.
509
+
510
+ Limit exhaustion raises `RegexLimitError`, never a false no-match result.
511
+ `diagnose` supports `match`, `fullmatch` and `search`, instead returning an explicit
512
+ `limit_exceeded` status with its reason, counters, peak pending threads and elapsed
513
+ time. `explain` and `diagnose` also exist as module-level functions.
514
+
515
+ Limits apply after pattern compilation. Timeouts are cooperative: they cannot
516
+ preempt Python replacement callbacks, compilation or a single long native
517
+ operation; elapsed time is checked again after callbacks. For hard isolation of
518
+ untrusted patterns/callbacks, use a separate process and external resource limits.
519
+
520
+ ---
521
+
522
+ ## CLI
523
+
524
+ A faithful subset of the standard library's `argparse`, built from scratch:
525
+
526
+ ```python
527
+ from pythoniota import cli
528
+
529
+ p = cli.ArgumentParser(prog="greet", description="say hello")
530
+ p.add_argument("name")
531
+ p.add_argument("-n", "--times", type=int, default=1)
532
+ p.add_argument("-v", "--verbose", action="store_true")
533
+
534
+ args = p.parse_args(["world", "--times", "3", "-v"])
535
+ args.name, args.times, args.verbose # ('world', 3, True)
536
+ ```
537
+
538
+ Supported:
539
+
540
+ - positional & optional arguments, `nargs` (`N` / `?` / `*` / `+`)
541
+ - `type` / `choices` / `default` / `required`
542
+ - actions: `store`, `store_true`, `store_false`, `store_const`, `append`, `count`
543
+ - `--opt=value`, clustered short flags (`-abc`), the `--` end-of-options marker
544
+ - an auto-generated `-h/--help`, and sub-commands via `add_subparsers`
545
+
546
+ Bad input prints a usage line and raises `SystemExit(2)` — the same contract as
547
+ argparse, so it drops into scripts and tests unchanged. `parse_known_args`
548
+ returns `(namespace, extras)` instead of erroring on unknown tokens.
549
+
550
+ ### Mutually exclusive options and version output
551
+
552
+ ```python
553
+ p = cli.ArgumentParser(prog="convert")
554
+ mode = p.add_mutually_exclusive_group(required=True)
555
+ mode.add_argument("--json", action="store_true")
556
+ mode.add_argument("--text", action="store_true")
557
+ p.add_argument("--version", action="version", version="%(prog)s 1.0")
558
+ p.parse_args(["--json"])
559
+ ```
560
+
561
+ Groups accept optional arguments only. A required group needs exactly one explicit
562
+ member; an optional group permits zero or one. CLI, environment and configuration
563
+ values count as explicit, even `False` or zero; defaults and caller `Namespace`
564
+ values do not. CLI overrides lower sources for the same destination, not another
565
+ group member: CLI `--json` plus config `text=true` is a conflict. Repeating the same
566
+ member is allowed. Group members cannot themselves be required or share a destination.
567
+
568
+ A valid version request prints to stdout and exits with status 0 without reading
569
+ configuration or converting unrelated values. It bypasses missing required options
570
+ but still respects argument consumption, subcommand boundaries and `--`.
571
+
572
+ ### Sub-commands
573
+
574
+ ```python
575
+ p = cli.ArgumentParser(prog="git")
576
+ sub = p.add_subparsers(dest="cmd")
577
+
578
+ add = sub.add_parser("add", help="stage files")
579
+ add.add_argument("path")
580
+ add.add_argument("-f", "--force", action="store_true")
581
+
582
+ rm = sub.add_parser("rm", help="remove files")
583
+ rm.add_argument("path")
584
+
585
+ args = p.parse_args(["add", "README.md", "-f"])
586
+ args.cmd, args.path, args.force # ('add', 'README.md', True)
587
+ ```
588
+
589
+ Parent positionals declared before `add_subparsers` are consumed and validated
590
+ before the command. Parent options may occur before the command; trailing
591
+ options are parsed only by the selected child. As with argparse, variable
592
+ `nargs` are greedy and command names are not special delimiters, so prefer
593
+ fixed-count parent positionals for predictable subcommand syntax.
594
+
595
+ ### Configuration and environment values
596
+
597
+ ```python
598
+ p = cli.ArgumentParser(config={"port": 8000, "mode": "fast"}, env_prefix="APP_")
599
+ p.add_argument("--port", type=int, default=3000)
600
+ p.add_argument("--mode", choices=["fast", "slow"])
601
+ p.add_argument("--token", env_var="SERVICE_TOKEN")
602
+ args = p.parse_args(["--port", "9000"]) # --port overrides APP_PORT and config
603
+ ```
604
+
605
+ `config` accepts a mapping or a UTF-8 JSON-object path, read again on each parse.
606
+ Keys are this parser's optional-argument destinations, not option spellings;
607
+ unknown keys and malformed files are errors. Sources do not fill positionals.
608
+ Precedence is explicit CLI > enabled environment > config > argument default;
609
+ only the winning value is converted and checked against `choices`. Required
610
+ options may be supplied by config/environment. Explicit caller `Namespace`
611
+ values are retained unless the CLI changes them, but do not by themselves
612
+ satisfy required options.
613
+
614
+ Environment lookup is opt-in via `env_prefix` or an exact `env_var`; children
615
+ configure their own sources with `add_parser(config=..., env_prefix=...)`.
616
+ Scalar environment values are literal strings. Multi-value options use JSON
617
+ arrays; `append` uses an array of occurrences (nested arrays for multi-value
618
+ occurrences). Boolean values accept true/false, yes/no, on/off and 1/0; counts
619
+ must be nonnegative integers. Explicit CLI append/count replaces lower sources,
620
+ while legacy accumulation from argument defaults is retained when no external
621
+ source applies. No configuration contents are executed as code or shell text.
622
+
623
+ ### Completion candidates
624
+
625
+ ```python
626
+ p.complete([], "--po") # ['--port']
627
+ p.complete(["--mode"], "s") # ['slow']
628
+ p.complete([], "--mode=f") # ['--mode=fast']
629
+ ```
630
+
631
+ `complete(tokens, prefix="")` accepts finished words without the program/current
632
+ word, then returns sorted, unique full replacement words for options, choices
633
+ and subcommands. It does not parse, read config/environment, call type converters,
634
+ execute shells or mutate parse state. Variable-length positionals are best-effort;
635
+ only built-in collections of plain scalar choices are enumerated, not generators
636
+ or custom objects. This is an adapter API for shells/editors: it does not install
637
+ shell hooks, quote candidates or perform filesystem completion.
638
+
639
+ ### Static shell completion scripts
640
+
641
+ ```python
642
+ script = p.completion_script("bash", command="convert")
643
+ # Also accepts "zsh" and "powershell"; save/load the returned text yourself.
644
+ ```
645
+
646
+ This generates a quoted static snapshot of options, subcommands and finite scalar
647
+ choices. It does not run the application, read sources, call converters or change
648
+ shell configuration. Unlike `complete(tokens, prefix)`, the snapshot is not a
649
+ context-sensitive parser: it may suggest candidates from other options or commands.
650
+ Regenerate it after changing parser metadata. Shell metacharacters are emitted as
651
+ literal data, not executable fragments. Command names containing control characters
652
+ are rejected; such candidates are omitted. Zsh requires an initialized completion
653
+ system (`compinit`).
654
+
655
+ ---
656
+
657
+ ## License
658
+
659
+ MIT