Skip to content

Commit

Permalink
Merge pull request #13 from oribarilan/feature/at
Browse files Browse the repository at this point in the history
Adding `at`
  • Loading branch information
oribarilan authored Jan 13, 2024
2 parents 362dd53 + ff54a92 commit 5304095
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 28 deletions.
2 changes: 1 addition & 1 deletion docs/reference/code_api/materializer_methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
::: fliq.query.Query
options:
filters: [
"^__contains__$", "^_group_by$", "^_validate_partition_index$", "^aggregate$", "^all$", "^any$", "^contains$", "^count$", "^equals$", "^first$", "^max$", "^min$", "^sample$", "^single$", "^sum$", "^to_dict$", "^to_list$"
"^__contains__$", "^_group_by$", "^_validate_partition_index$", "^aggregate$", "^all$", "^any$", "^at$", "^contains$", "^count$", "^equals$", "^first$", "^max$", "^min$", "^sample$", "^single$", "^sum$", "^to_dict$", "^to_list$"
]
4 changes: 4 additions & 0 deletions docs/reference/release_notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release Notes (What's New) 🤩

## v1.13.0
* 🌟 **At** - new [at method](code_api/materializer_methods.md#fliq.query.Query.at) for getting the element at a specific index.


## v1.12.0

* 🌟 **Slide** - new [slide method](code_api/mapper_methods.md#fliq.query.Query.slide) for creating
Expand Down
9 changes: 7 additions & 2 deletions fliq/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ class BaseQueryException(Exception):
pass


class MultipleItemsFoundException(BaseQueryException):
class MultipleElementsFoundException(BaseQueryException):
pass


class NoItemsFoundException(BaseQueryException):
class QueryIsUnexpectedlyEmptyException(BaseQueryException):
pass


class NotEnoughElementsException(BaseQueryException):
pass


class ElementNotFoundException(BaseQueryException):
pass

73 changes: 57 additions & 16 deletions fliq/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
MISSING, MissingOrOptional
)
from fliq.exceptions import (
NoItemsFoundException, MultipleItemsFoundException, NotEnoughElementsException
QueryIsUnexpectedlyEmptyException, MultipleElementsFoundException, NotEnoughElementsException,
ElementNotFoundException
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -503,14 +504,20 @@ def take(self, n: int = 1, predicate: Optional[Predicate[T]] = None) -> Query[T]
def skip(self, n: int = 1) -> Query[T]:
"""
Yields the elements after skipping the first n (as returned from the iterator).
In case n is greater than the number of elements in the query, an empty query is returned.
Examples:
>>> from fliq import q
>>> q(range(10)).skip(n=5).to_list()
[5, 6, 7, 8, 9]
>>> q([1, 2, 3, 4]).skip(n=2).to_list()
[3, 4]
>>> q([1, 2, 3, 4]).skip(n=0).to_list()
[1, 2, 3, 4]
>>> q([1]).skip(n=5).to_list()
[]
Args:
n: Optional. The number of items to take. Defaults to 1.
If n=0, query is returned as is.
"""
query = self.slice(start=n)
return self._self(query._items)
Expand Down Expand Up @@ -893,15 +900,16 @@ def first(self,
"""
Returns the first element in the query that satisfies the predicate (if provided),
or a default value if the query is empty.
If default is not provided, raises NoItemsFoundException in case the query is empty.
If default is not provided, raises QueryIsUnexpectedlyEmptyException in case the query
is empty.
Args:
predicate: Optional. The predicate to filter the query by.
default: Optional. The default value to return in case the query is empty.
Defaults to raise an exception if the query is empty.
Raises:
NoItemsFoundException: In case the query is empty.
QueryIsUnexpectedlyEmptyException: In case the query is empty.
Examples:
>>> from fliq import q
Expand All @@ -910,7 +918,7 @@ def first(self,
>>> q([]).first()
Traceback (most recent call last):
...
fliq.exceptions.NoItemsFoundException
fliq.exceptions.QueryIsUnexpectedlyEmptyException
>>> q([]).first(default=-1)
-1
>>> q([]).first(default=None) # returns None
Expand All @@ -921,7 +929,7 @@ def first(self,
# query is empty
if default is MISSING:
# no default value provided
raise NoItemsFoundException()
raise QueryIsUnexpectedlyEmptyException()
else:
return default # type: ignore # default is not MISSING
else:
Expand All @@ -947,19 +955,19 @@ def single(self,
>>> q([]).single()
Traceback (most recent call last):
...
fliq.exceptions.NoItemsFoundException
fliq.exceptions.QueryIsUnexpectedlyEmptyException
>>> q([1]).single(default=None)
1
>>> q([]).single(default=None) # returns None
>>> q([1, 2, 3]).single(default=None)
Traceback (most recent call last):
...
fliq.exceptions.MultipleItemsFoundException
fliq.exceptions.MultipleElementsFoundException
Raises:
NoItemsFoundException: In case the query is empty.
MultipleItemsFoundException: In case the query has more than one element.
QueryIsUnexpectedlyEmptyException: In case the query is empty.
MultipleElementsFoundException: In case the query has more than one element.
"""
query = self.where(predicate)
first_item = next(query, default)
Expand All @@ -970,12 +978,45 @@ def single(self,
# query has 0 or 1 items
if default is MISSING and first_item is default:
# query is empty, user did not allow default
raise NoItemsFoundException()
raise QueryIsUnexpectedlyEmptyException()
else:
return first_item # type: ignore # MISSING isn't returned
else:
# query has more than 1 item
raise MultipleItemsFoundException()
raise MultipleElementsFoundException()

def at(self,
index: int,
default: MissingOrOptional[T] = MISSING) -> Optional[T]:
"""
Returns the element in the query at the specified index, or a default value if the query is
too short.
If default is not provided, raises ElementNotFoundException in case the query
is too short.
Args:
index: The index of the element to return.
default: Optional. The default value to return in case the query is too short.
Defaults to raise an exception if the query is too short.
Examples:
>>> from fliq import q
>>> q([1, 2, 3]).at(0)
1
>>> q([]).at(0)
Traceback (most recent call last):
...
fliq.exceptions.ElementNotFoundException
>>> q([1, 2, 3]).at(index=5, default=None) # returns None
Raises:
ElementNotFoundException: In case the query is too short.
QueryIsUnexpectedlyEmptyException: In case the query is empty.
"""
try:
return self.skip(index).first(default=default)
except QueryIsUnexpectedlyEmptyException:
raise ElementNotFoundException()

def sample(self,
n: int = 1,
Expand All @@ -997,12 +1038,12 @@ def sample(self,
n: Optional. The number of elements to sample. Defaults to 1.
seed: Optional. The seed to use for the random sample. Defaults to None.
budget_factor: Optional. Limits the number of attempts to sample n items,
as a factor of n. Defaults to 10x n.
as a factor of n. Defaults to 10, which means a budget of 10 * n.
None will disable budgeting, and may exhaust the iterable
(depending on stop_factor).
stop_factor: Optional. The probability to stop re-sampling once the sample is full,
as a factor of n: 1 / ( stop_factor * n). Defaults to 1 / (10 * n).
None will disable early stopping, and may exhaust the iterable
as a factor of n: 1 / ( stop_factor * n). Defaults to 10, which means a factor of
1 / (10 * n). None will disable early stopping, and may exhaust the iterable
(depending on budget_factor).
Note:
Expand Down
7 changes: 7 additions & 0 deletions fliq/tests/mappers/test_skip.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ def test_take_hasMultipleItems(self,
iterable,
iterable_list):
assert list(q(iterable).skip(n=3)) == iterable_list[3:]

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_multi())
def test_take_hasMultipleItems_skipIsZero(self,
iter_type,
iterable,
iterable_list):
assert list(q(iterable).skip(n=0)) == iterable_list
38 changes: 38 additions & 0 deletions fliq/tests/materializer/reducers/test_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest

from fliq import q
from fliq.exceptions import ElementNotFoundException
from fliq.tests.fliq_test_utils import Params


class TestAt:
@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_empty())
def test_at_withDefault_hasNoItems(self, iter_type, iterable, iterable_list):
assert q(iterable).at(0, default=None) is None

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_empty())
def test_at_withoutDefault_hasNoItems(self, iter_type, iterable, iterable_list):
with pytest.raises(ElementNotFoundException):
assert q(iterable).at(0)

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_single())
def test_at_withDefault_hasSingleItem(self, iter_type, iterable, iterable_list):
assert q(iterable).at(0, default=None) == iterable_list[0]

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_single())
def test_at_withDefault_hasSinlgeItem_indexIsTooBig(self, iter_type, iterable, iterable_list):
assert q(iterable).at(1, default=None) is None

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_empty())
def test_at_hasNoItems_raisesException(self, iter_type, iterable, iterable_list):
with pytest.raises(ElementNotFoundException):
q(iterable).at(0)

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_multi())
def test_at_hasItems_indexTooBig_raisesException(self, iter_type, iterable, iterable_list):
with pytest.raises(ElementNotFoundException):
q(iterable).at(6)

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_multi())
def test_at_hasNoItems(self, iter_type, iterable, iterable_list):
assert q(iterable).at(3) == iterable_list[3]
4 changes: 2 additions & 2 deletions fliq/tests/materializer/reducers/test_first.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from fliq import q
from fliq.exceptions import NoItemsFoundException
from fliq.exceptions import QueryIsUnexpectedlyEmptyException
from fliq.tests.fliq_test_utils import Params


Expand Down Expand Up @@ -52,7 +52,7 @@ def test_first_missingDefault_hasNoItems(self,
iter_type,
iterable,
iterable_list):
with pytest.raises(NoItemsFoundException):
with pytest.raises(QueryIsUnexpectedlyEmptyException):
q(iterable).first()

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_single())
Expand Down
8 changes: 4 additions & 4 deletions fliq/tests/materializer/reducers/test_single.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

from fliq import q
from fliq.exceptions import MultipleItemsFoundException, NoItemsFoundException
from fliq.exceptions import MultipleElementsFoundException, QueryIsUnexpectedlyEmptyException
from fliq.tests.fliq_test_utils import Params


Expand All @@ -16,12 +16,12 @@ def test_single_withDefault_hasSingleItem(self, iter_type, iterable, iterable_li

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_multi())
def test_single_withDefault_hasMultipleItems(self, iter_type, iterable, iterable_list):
with pytest.raises(MultipleItemsFoundException):
with pytest.raises(MultipleElementsFoundException):
q(iterable).single(default=None)

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_empty())
def test_single_hasNoItems(self, iter_type, iterable, iterable_list):
with pytest.raises(NoItemsFoundException):
with pytest.raises(QueryIsUnexpectedlyEmptyException):
q(iterable).single()

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_single())
Expand All @@ -30,5 +30,5 @@ def test_single_hasSingleItem(self, iter_type, iterable, iterable_list):

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_multi())
def test_single_hasMultipleItems(self, iter_type, iterable, iterable_list):
with pytest.raises(MultipleItemsFoundException):
with pytest.raises(MultipleElementsFoundException):
q(iterable).single()
4 changes: 2 additions & 2 deletions fliq/tests/special/test_snap.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from fliq.exceptions import NoItemsFoundException
from fliq.exceptions import QueryIsUnexpectedlyEmptyException
from fliq.tests.fliq_test_utils import Params
from fliq import q

Expand All @@ -10,7 +10,7 @@ class TestSnap:
def test_noSnap_differentPasses_iterableExhausted(self, iter_type, iterable, iterable_list):
evens = q(iterable).where(lambda x: int(x) % 2 == 0)
evens.single(lambda x: int(x) > 2)
with pytest.raises(NoItemsFoundException):
with pytest.raises(QueryIsUnexpectedlyEmptyException):
evens.single(lambda x: int(x) < 0)

@pytest.mark.parametrize(Params.sig_iterable, Params.iterable_multi())
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "fliq"
version = "1.12.0"
version = "1.13.0"
description = "Fluent-based Lazily-evaluated Integrated Query for Python"
readme = "README.md"
authors = [{ name = "Ori Bar-ilan", email = "python.oplog@gmail.com" }]
Expand Down

0 comments on commit 5304095

Please sign in to comment.