Initial commit.

This commit is contained in:
Phil Gilmore 2025-02-02 19:25:14 -07:00
commit 1b47458efa
10 changed files with 546 additions and 0 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
root=true
[*.py]
tab_width = 4
indent_size = 4
indent_style = tab
[{*.yml, *.yaml}]
tab_width = 2
indent_size = 2
indent_style = space

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# Others
~$*
*~
*.pfx
# Python
__pycache__/
*.pyc
venv/
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Local dev files I don't want checked in.
run.py

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

189
README.md Normal file
View File

@ -0,0 +1,189 @@
# EzSequence
A quick and dirty sequence generator for Python.
## About
- EzSequence uses a fluent interface to strive for a self-documenting code approach. It works like the _Builder_ pattern.
- EzSequence produces iterators that you can use in your FOR loops, your list comprehensions or to be converted to a list all at once.
- EzSequence supports infinite sequences but has safety built in.
## Basic Usage
```
from ezsequence import *
seq = sequence_from(1).to(5)
print(seq)
```
## Full Usage
### Static Methods
There is only one!
`sequence_from()`
This function starts your sequence with an initial value and some default behavior. From there, you can customize it with any of the fluent methods listed below.
### Fluent Methods
|Method| Description| Example| Output|
|---- |---- |---- |---- |
|.by(increment: int) |Optional. Specifies a gap between values. Default is 1. Cannot be used with `.with_increment()`.|`sequence_from(1) .to(5) .by(2)`|[1, 3, 5]|
|.with_increment(increment_func: Callable[[int, int], int])| Optional. Specifies the translation of the value on each iteration. By default, value is incremented by 1. |`sequence .from(1) .to(5) .with_increment(lambda index, value: value + 2)` | [1, 3, 5] |
|.until(condition: Callable[[int, int], int]) |Optional. Specifies a terminating condition in the sequence. Value is inclusive. In other words, once the condition is met, the next iteration will end the sequence. Multiple .until() calls are permitted. Otherwise, it is the same as `.to()`. |`sequence_from(1) .until(lambda index, value: value == 5)`| [1, 2, 3, 4, 5]|
|.as_long_as(condition: Callable[[int, int], int]) |Optional. Specifies a terminating condition in the sequence. Value is exclusive. In other words, once the condition is violated, the current iteration will end the sequence. Multiple .as_long_as() calls are permitted. |`sequence_from(1) .as_long_as(lambda index, value: value <= 5)`| [1, 2, 3, 4, 5]|
|.when(condition: Callable[[int, int], int]) |Optional. Specifies a condition for including this value in the sequence output. By default, all values are included. Multiple .when() calls are supported. Evluations do not terminate the sequence. |`sequence_from(1) .to(5) .when(lambda index, value: value % 2 == 1)`| [1, 3, 5]|
|.not_when(condition: Callable[[int, int], int]) |Optional. Specifies a condition for excluding values, overriding any .when() condition that included them. By default, there are no exclusions. Multiple .not_when() calls are supported. Evaluations do not terminate the sequence.|`sequence_from(1) .to(5) .not_when(lambda index, value: value % 2 == 0)`| [1, 3, 5]
|.safely(max_attempts: int)|Optional. Prevents unintended infinite loops caused by .when() and .not_when() filters which will hang in the _while_ loop that searches for the next unfiltered vlaue in the sequence. Takes an optional parameter for the max number of attempts to find an unfiltered value. Default max attempts is 10000. |`sequence_from(1) .to(5) .when(lambda index, value: value < 0) .safely(100)`|Exception: Safe iteration limit exceeded. 100 attempts exceeded.|
|.unsafely()|Optional. Allows unintended infinite loops caused by `.when()` and `.not_when()` filters which may hang in the _while_loop that searchs for the next unfiltered value in the sequence. |`sequence_from(1) .with_increment(lambda index, value: random.Random() .randint(1, 1000)) .as_long_as(lambda index, value: value != 50) .unsafely()` |(No exception but may hang indefinitely)|
|.translate_as(projection: Callable[[int, int], any]) |Optional. Specifies a function to convert each element that is returned by the iterator. | `sequence_from(5) .to(9) .translate_as(lambda index, value: value - 4)`| [1, 2, 3, 4, 5]|
|.reset() |Rewinds the sequence to the beginning so it can be reused. |`seq.reset()` | N/A|
## More Examples
Odds
```
seq = sequence_from(1).to(5) \
.when(lambda index, value: value % 2 == 1)
```
Evens
```
seq = sequence_from(1).to(5) \
.when(lambda index, value: value % 2 == 0)
```
Backward
```
seq = sequence_from(1) \
.by(-1) \
.until(lambda index, value: value == -5)
```
Random Bytes
```
import random
rnd = random.Random()
seq = sequence_from(1).to(8) \
.translate_as(lambda index, value: rnd.randint(0, 255))
data = list(seq)
```
Random AlphaNumeric Text
```
import random
rnd = random.Random()
seq = sequence_from(1).to(8) \
.translate_as(lambda index, value: chr(rnd.randint(1, 26) + 64))
data = "".join(seq)
```
Reset Example
```
import random
rnd = random.Random()
seq = sequence_from(1).to(8) \
.translate_as(lambda index, value: chr(rnd.randint(1, 26) + 64))
data1 = "".join(seq)
seq.reset()
data2 = "".join(seq)
```
Random CSV values
```
import random
rnd = random.Random()
field_count = 5
row_count = 3
def create_field_data(data_seq):
data_seq.reset()
return "".join(data_seq)
def create_row(field_seq):
field_seq.reset()
return ",".join(field_seq)
data_seq = sequence_from(1).to(8) \
.translate_as(lambda index, value: chr(rnd.randint(1, 26) + 64))
field_seq = sequence_from(1).to(field_count) \
.translate_as(lambda index, value: "\"" + create_field_data(data_seq) + "\"")
row_seq = sequence_from(1).to(row_count)
for row in row_seq:
print(create_row(field_seq))
```
Guid
```
import random
rnd = random.Random()
section_generators = \
[
sequence_from(1).to(8).translate_as(lambda index, value: hex(rnd.randint(0, 15))[2:3]),
sequence_from(1).to(4).translate_as(lambda index, value: hex(rnd.randint(0, 15))[2:3]),
sequence_from(1).to(4).translate_as(lambda index, value: hex(rnd.randint(0, 15))[2:3]),
sequence_from(1).to(4).translate_as(lambda index, value: hex(rnd.randint(0, 15))[2:3]),
sequence_from(1).to(12).translate_as(lambda index, value: hex(rnd.randint(0, 15))[2:3])
]
sections = ["".join(generator) for generator in section_generators]
guid = "-".join(sections)
print(guid)
```
Factorials
```
from functools import reduce
factorial_of = 5
factors = sequence_from(factorial_of) \
.with_increment(lambda index, value: value - 1) \
.until(lambda index, value: value == 1)
factorial = reduce(lambda running, value: running * value, factors)
print(list(factors.reset()))
print(factorial)
```
Fibonacci
```
import itertools
class Fibonacci:
def __init__(self):
self.previous2 = 0
self.previous1 = 1
def calculate_fibonacci(self, value):
next_value = self.previous1 + self.previous2
self.previous2 = self.previous1
self.previous1 = next_value
return next_value
def sequence(self):
seq = sequence_from(1).to(20) \
.translate_as(lambda index, value: self.calculate_fibonacci(value)) \
.as_long_as(lambda index, value: value < 500000000)
return itertools.chain([1], seq)
fib = list(Fibonacci().sequence())
print(fib)
```
Multiple _when_, _not_when_, _until_ and _as_long_as_
```
seq = sequence_from(1).to(50) \
.by(3) \
.when(lambda index, value: value >= 10) \
.when(lambda index, value: value % 2 == 1) \
.when_not(lambda index, value: value == 25) \
.as_long_as(lambda index, value: value <= 40) \
.as_long_as(lambda index, value: value != 38) \
.safely(10)
print(list(seq))
```

1
ezsequence/__init__.py Normal file
View File

@ -0,0 +1 @@
from .ezsequence import *

139
ezsequence/ezsequence.py Normal file
View File

@ -0,0 +1,139 @@
from typing import Callable
class EzSequence:
def __init__(self, start_value):
self.start_value = start_value
self.value = start_value
self.index = 0
self.when_funcs = []
self.not_when_funcs = []
self.until_funcs = []
self.as_long_as_funcs = []
self.is_last = False
self.by(1)
self.consecutive_attempts = 0
self.safely(100)
self.is_first_element = True
self.translate_as(lambda index, value: value)
def reset(self):
self.value = self.start_value
self.index = 0
self.is_first_element = True
self.is_last = False
return self
def __iter__(self):
return self
def __next__(self) -> int:
if self.is_first_element:
self.is_first_element = False
if self.until_funcs != None and len(self.until_funcs) > 0:
if any(f(self.index, self.value) for f in self.until_funcs):
self.is_last = True
if ((self.when_funcs != None and len(self.when_funcs) > 0) or (self.not_when_funcs != None and len(self.not_when_funcs) > 0)):
if (all(f(self.index, self.value) for f in self.when_funcs)) and (not any(f(self.index, self.value) for f in self.not_when_funcs)):
return self.translate_as_func(self.index, self.value)
else:
pass
else:
return self.translate_as_func(self.index, self.value)
self.index -= 1 # Because it will be incremented below and we are still in the first element.
#current_index = self.index
current_value = self.value
# Handle UNTIL
if self.is_last:
raise StopIteration
# Find next value.
self.consecutive_attempts = 1
next_value = self.increment_func(self.index + 1, current_value)
next_index = self.index + 1
# Keep looking for a new value until we get a value that can be output or until we violate the safety limit.
if ((self.when_funcs != None and len(self.when_funcs) > 0) or (self.not_when_funcs != None and len(self.not_when_funcs) > 0)):
while True:
if (not all(f(next_index, next_value) for f in self.when_funcs)) or (any(f(next_index, next_value) for f in self.not_when_funcs)):
if self.is_last:
raise StopIteration
# This value will not be output. Find a new one that will.
self.consecutive_attempts += 1
if self.use_safety and (self.consecutive_attempts > self.max_consective_attempts):
raise Exception(f"Safe iteration limit exceeded. {self.max_consective_attempts} attempts exceeded.")
next_value = self.increment_func(next_index, next_value)
# Stop if we encountered the end of the sequence.
if self.as_long_as_funcs != None and len(self.as_long_as_funcs) > 0:
if not all(f(next_index, next_value) for f in self.as_long_as_funcs):
raise StopIteration
if self.until_funcs != None and len(self.until_funcs) > 0:
if any(f(next_index, next_value) for f in self.until_funcs):
self.is_last = True
else:
break
# Stop if we encountered the end of the sequence.
if self.as_long_as_funcs != None and len(self.as_long_as_funcs) > 0:
if not all(f(next_index, next_value) for f in self.as_long_as_funcs):
raise StopIteration
# Return the value THIS time, but prepare to stop NEXT time if UNTIL is encountered here.
if self.until_funcs != None and len(self.until_funcs) > 0:
if any(f(next_index, next_value) for f in self.until_funcs):
self.is_last = True
self.index + next_index
self.value = next_value
return self.translate_as_func(self.index, self.value)
def by(self, increment: Callable[[int, int], int]):
self.with_increment(lambda index, value: value + increment)
return self
def with_increment(self, increment_func: Callable[[int, int], int] ):
self.increment_func = increment_func
return self
def until(self, until_func: Callable[[int, int], bool]):
self.until_funcs.append(until_func)
return self
def as_long_as(self, as_long_as_func: Callable[[int, int], bool]):
self.as_long_as_funcs.append(as_long_as_func)
return self
def to(self, last_value):
self.until(lambda index, value: value == last_value)
return self
def when(self, when_func: Callable[[int, int], bool]):
self.when_funcs.append(when_func)
return self
def not_when(self, not_when_func: Callable[[int, int], bool]):
self.not_when_funcs.append(not_when_func)
return self
def translate_as(self, translate_as_func: Callable[[int, int], int]):
self.translate_as_func = translate_as_func
return self
def safely(self, max_attempts: int = 10000):
self.use_safety = True
self.max_consective_attempts = max_attempts
return self
def unsafely(self):
self.use_safety = False
return self
def sequence_from(start_value: int):
return EzSequence(start_value)

22
pyproject.toml Normal file
View File

@ -0,0 +1,22 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "EzSequence"
version = "0.0.1"
authors = [{ name = "Phil Gilmore", email="spgilmore+pypi@gmail.com"}]
description = "A simple iterable sequence generator."
readme = "README.md"
requires-python = ">=3.6"
classifiers = [
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
"License :: OSI Approved :: MIT License"
]
[project.urls]
Homepage = "https://git.pillidar.com/PillidarPublic/EzSequencePy"
Repository = "https://git.pillidar.com/PillidarPublic/EzSequencePy"
Issues = "https://git.pillidar.com/PillidarPublic/EzSequencePy/issues"

5
runtests.py Normal file
View File

@ -0,0 +1,5 @@
import unittest
from tests.test_ezsequence import *
if __name__ == "__main__":
unittest.main()

0
tests/__init__.py Normal file
View File

135
tests/test_ezsequence.py Normal file
View File

@ -0,0 +1,135 @@
import unittest
from ezsequence import *
class TestAll(unittest.TestCase):
def number_names(self):
return \
{
1: "One",
2: "Two",
3: "Three",
4: "Four",
5: "Five"
}
def setUp(self):
pass
def test_contains_test(self):
full_list = [1, 2, 3]
sub_list = [1, 2]
list_intersection = list(set(full_list) & set(sub_list))
self.assertListEqual(sub_list, list_intersection)
def test_from_to(self):
expected = [1, 2, 3, 4, 5]
actual = list(sequence_from(1).to(5))
self.assertListEqual(expected, actual)
def test_until(self):
expected = [1, 2, 3, 4, 5]
sequence = \
sequence_from(1) \
.until(lambda index, value: value == 5)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_as_long_as(self):
expected = [1, 2, 3, 4, 5]
sequence = \
sequence_from(1) \
.as_long_as(lambda index, value: value < 6)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_when(self):
expected = [1, 3, 5, 7, 9]
sequence = \
sequence_from(1) \
.when(lambda index, value: value % 2 == 1) \
.as_long_as(lambda index, value: value <= 10)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_with_increment(self):
expected = [1, 3, 5, 7, 9]
sequence = \
sequence_from(1) \
.with_increment(lambda index, value: value + 2) \
.as_long_as(lambda index, value: value <= 10)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_with_increment_backward(self):
expected = [1, -1, -3, -5, -7, -9]
sequence = \
sequence_from(1) \
.with_increment(lambda index, value: value - 2) \
.as_long_as(lambda index, value: value >= -10)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_multiple_when(self):
expected = [5, 7, 9]
sequence = \
sequence_from(1) \
.when(lambda index, value: value % 2 == 1) \
.when(lambda index, value: value >= 5) \
.as_long_as(lambda index, value: value <= 10)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_not_when(self):
expected = [1, 3, 5, 7, 9]
sequence = \
sequence_from(1) \
.not_when(lambda index, value: value % 2 == 0) \
.as_long_as(lambda index, value: value <= 10)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_multiple_not_when(self):
expected = [5, 7, 9]
sequence = \
sequence_from(1) \
.not_when(lambda index, value: value % 2 == 0) \
.not_when(lambda index, value: value < 5) \
.as_long_as(lambda index, value: value <= 10)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_mixed_when_and_not_when(self):
expected = [3, 9]
sequence = \
sequence_from(1) \
.when(lambda index, value: value % 2 == 1) \
.when(lambda index, value: value > 1) \
.not_when(lambda index, value: value == 7) \
.not_when(lambda index, value: value == 5) \
.as_long_as(lambda index, value: value <= 10)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_translate_on_one_item(self):
expected = [4]
sequence = sequence_from(1).to(1) \
.translate_as(lambda index, value: value * 3 + 1)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_translate_on_multiple_items(self):
expected = [4, 7, 10, 13, 16]
sequence = sequence_from(1).to(5) \
.translate_as(lambda index, value: value * 3 + 1)
actual = list(sequence)
self.assertListEqual(expected, actual)
def test_translate_on_disparate_types(self):
expected = ["ONE", "TWO", "THREE", "FOUR", "FIVE"]
sequence = sequence_from(1).to(5) \
.translate_as(lambda index, value: self.number_names()[value].upper())
actual = list(sequence)
self.assertListEqual(expected, actual)
# if __name__ == "__main__":
# unittest.main()