Collections and pagination
get() returns the framework-wide Collection. from orionis.orm import Collection, ModelCollection exports two names for the same type. Operations work in memory: collection.where(...) does not add SQL WHERE and collection.count() does not issue COUNT. Filter and paginate in the builder before loading large tables.
Controller example
Section titled “Controller example”from orionis.http import JSONResponsefrom app.models.article import Article
class ArticleListController: async def __call__(self) -> JSONResponse: page = await Article.where("published", True).orderBy("id").paginate(per_page=20) return JSONResponse(page.toDict())Query pagination
Section titled “Query pagination”paginate
Section titled “paginate”async def paginate(self, page: int=1, per_page: int=_DEFAULT_PER_PAGE) -> PaginatorAsync terminal with page=1, per_page=15 (_DEFAULT_PER_PAGE in source): requires positive integers excluding bool, otherwise InvalidQueryException. Applies OFFSET/LIMIT to the builder, counts/retrieves rows and returns Paginator. Inside a transaction runs both sequentially on the transactional connection; outside runs them through gather with pooled connections. No consistent snapshot between the two is guaranteed outside a transaction.
async def article_page(page_number: int = 1): page = await Article.orderBy("id").paginate(page=page_number, per_page=20) return page.toDict()Order by a deterministic key, especially across pages. Pagination uses offset, not cursor/keyset. Out-of-range pages return empty items and keep the requested page number. COUNT and items can observe concurrent changes; use a transaction with appropriate engine isolation when a consistent view is required.
Paginator API
Section titled “Paginator API”Property examples assume page = Paginator(Collection([{"id": 1}]), total=11, page=1, per_page=10). len(page) is the current item count, not total; repr shows page/perPage/total. It is neither a builder nor a Response.
__init__
Section titled “__init__”def __init__(self, items: Collection, total: int, page: int, per_page: int) -> NoneWraps a Collection without querying SQL. page and per_page must be positive int values excluding bool (ValueError). total is converted to int and clamped to zero. Does not clamp a page above lastPage.
from orionis.orm import Collection, Paginator
page = Paginator(Collection([{"id": 1}]), total=11, page=1, per_page=10)def items(self) -> CollectionRead-only property: the same page Collection; its mutators can still change it.
items = page.itemsdef total(self) -> intInt property: total counted rows, not current page length.
assert page.total == 11def page(self) -> intInt property: current page starting at 1.
assert page.page == 1perPage
Section titled “perPage”def perPage(self) -> intInt property: configured size even if the last page contains fewer rows.
assert page.perPage == 10lastPage
Section titled “lastPage”def lastPage(self) -> intInt property: ceil(total/perPage), at least 1 even for total=0.
assert page.lastPage == 2hasNext
Section titled “hasNext”def hasNext(self) -> boolBool property: page*perPage < total. Does not build links or retrieve another page.
assert page.hasNext is TruehasPrevious
Section titled “hasPrevious”def hasPrevious(self) -> boolBool property: page > 1, including when the current page is out of range.
assert page.hasPrevious is FalsetoDict
Section titled “toDict”def toDict(self) -> dict[str, Any]Returns a dict with serialized items and total, page, perPage, lastPage, hasNext, hasPrevious. Does not include URLs or cursors.
assert page.toDict()["items"] == [{"id": 1}]toJson
Section titled “toJson”def toJson(self, **kwargs: Any) -> strSerializes toDict with json.dumps and default=str unless overridden in kwargs. Returns JSON text.
text = page.toJson(indent=2, ensure_ascii=False)Complete Collection API used by ORM
Section titled “Complete Collection API used by ORM”All examples operate in memory. Article and Team are defined in the model/relationship guides. Callbacks are synchronous: map/filter/each do not await coroutines.
Mutators such as merge, push, forget, put, sort, reverse, reject, transform, each and random(count>0) change the same collection; contained models remain the same instances. They do not automatically persist SQL changes. New collections are shallow copies of their elements.
__init__
Section titled “__init__”def __init__(self, items: list[Any] | None=None) -> NoneBuilds an in-memory collection; items=None makes it empty. A nonempty list is retained by reference: pass .copy() to isolate it. ModelCollection is the same type, not a persistence subclass.
values = Collection([1, 2, 3])def take(self, number: int) -> CollectionReturns a new collection of the first number items; a negative value takes from the end and zero returns empty. Does not query or limit SQL.
assert Collection([1, 2, 3]).take(-2).all() == [2, 3]def first(self, callback: Callable | None=None) -> objectFirst item or None; with a callback returns the first match. Noncallable callbacks raise ValueError; callback exceptions propagate.
assert Collection([1, 2, 3]).first(lambda n: n > 1) == 2def last(self, callback: Callable | None=None) -> objectLast item/match or None. With a callback scans in order and remembers the last matching item.
assert Collection([1, 2, 3]).last(lambda n: n < 3) == 2def all(self) -> list[Any]Returns the underlying list, not a copy or serialization. For pluck(..., key)/groupBy, returns the stored mapping despite the list annotation.
raw = Collection([1, 2]).all()assert raw == [1, 2]def avg(self, key: str | None=None) -> floatIn-memory numeric average with an optional dict/attribute key; empty input or TypeError returns 0. Not an SQL aggregate and does not load absent columns.
assert Collection([{"score": 2}, {"score": 4}]).avg("score") == 3def max(self, key: str | None=None) -> objectIn-memory maximum, optionally by key; empty input or incompatible comparison returns 0.
assert Collection([2, 4]).max() == 4def min(self, key: str | None=None) -> objectIn-memory minimum, optionally by key; empty input or incompatible comparison returns 0.
assert Collection([2, 4]).min() == 2def chunk(self, size: int) -> CollectionNew collection of subcollections of size; nonpositive sizes raise ValueError. Splits loaded results, not SQL batches.
chunks = Collection([1, 2, 3]).chunk(2)assert chunks[0].all() == [1, 2]collapse
Section titled “collapse”def collapse(self) -> CollectionFlattens one level of lists/collections into a new collection. Noniterable scalar entries may raise TypeError; use flatten for deeply nested lists/dicts.
assert Collection([[1, 2], [3]]).collapse().all() == [1, 2, 3]contains
Section titled “contains”def contains(self, key: str | Callable, value: object=None) -> boolMembership bool for a direct value, callback or key/value. value=None means no second argument; use a callback to match a null attribute.
assert Collection([{"id": 2}]).contains("id", 2)def count(self) -> intCount of already loaded items. Does not issue SQL COUNT.
assert Collection([1, 2]).count() == 2def diff(self, items: list[Any] | Collection) -> CollectionNew collection of items absent from the compared list/collection. Uses equality; handles unhashable values through sequential comparison.
assert Collection([1, 2]).diff([2]).all() == [1]def each(self, callback: Callable) -> CollectionReplaces each position with the callback’s truthy return and stops at its first falsy return without replacing that position. Returns self. It is not a side-effect loop for callbacks returning None.
values = Collection([1, 2, 3])assert values.each(lambda n: n * 2).all() == [2, 4, 6]def every(self, callback: Callable) -> boolBool indicating every item satisfies the callback; empty input returns True. Invalid callbacks raise ValueError.
assert Collection([2, 4]).every(lambda n: n % 2 == 0)filter
Section titled “filter”def filter(self, callback: Callable) -> CollectionNew collection of items with truthy callback results. Callback is required; invalid callbacks raise ValueError.
assert Collection([1, 2, 3]).filter(lambda n: n > 1).all() == [2, 3]flatten
Section titled “flatten”def flatten(self) -> CollectionNew collection recursively flattening lists and dictionary values. Nested tuples/Collections remain items rather than being expanded.
assert Collection([1, [2, {"x": 3}]]).flatten().all() == [1, 2, 3]forget
Section titled “forget”def forget(self, *keys: int | str) -> CollectionDeletes indices/keys in descending order and returns self. Missing indices/keys propagate IndexError/KeyError; lists require integer indices.
assert Collection([1, 2, 3]).forget(0, 2).all() == [2]forPage
Section titled “forPage”def forPage(self, page: int, number: int) -> CollectionNew sliced collection: clamps page to at least 1 and requires number>0 (ValueError). Does not produce metadata or query extra rows.
assert Collection([1, 2, 3]).forPage(2, 2).all() == [3]def get(self, key: int | str, default: object=None) -> objectReads an index/key or default; callable defaults are called with no arguments only when missing. Catches IndexError/KeyError, not invalid index types.
assert Collection([1]).get(7, lambda: "missing") == "missing"implode
Section titled “implode”def implode(self, glue: str=',', key: str | None=None) -> strJoins string representations using glue=","; key=None uses complete items. With objects/dicts and a key extracts that value. Empty returns "".
assert Collection([{"name": "A"}, {"name": "B"}]).implode(" / ", "name") == "A / B"isEmpty
Section titled “isEmpty”def isEmpty(self) -> boolLength-based bool; does not query SQL existence.
assert Collection().isEmpty()def map(self, callback: Callable) -> CollectionNew collection with every callback result, including None/falsy values; does not mutate the original list.
assert Collection([1, 2]).map(lambda n: n * 3).all() == [3, 6]mapInto
Section titled “mapInto”def mapInto(self, cls: type, method: str | None=None, **kwargs: object) -> CollectionMaps through cls(item) or cls.method(item, **kwargs); kwargs are passed only for a method. cls must be a type (TypeError); missing methods or per-item TypeError/AttributeError/ValueError become None. Does not persist models.
assert Collection(["1", "bad"]).mapInto(int).all() == [1, None]def merge(self, items: list[Any] | Collection) -> CollectionAppends a list/Collection to the same object and returns self; no PK merge or deduplication. Unsupported types raise TypeError.
assert Collection([1]).merge([1, 2]).all() == [1, 1, 2]def pluck(self, value: str, key: str | None=None) -> CollectionExtracts a dict path/attribute; without key returns a list omitting None. With key returns a dict-backed Collection, overwriting duplicate keys and preserving null values. Read that mapping with .all()/.get().
assert Collection([{"id": 1, "name": "A"}]).pluck("name", "id").all() == {1: "A"}def pop(self) -> objectRemoves and returns the last item; empty returns None.
values = Collection([1, 2])assert values.pop() == 2prepend
Section titled “prepend”def prepend(self, value: object) -> CollectionPrepends to the same object and returns self.
assert Collection([2]).prepend(1).all() == [1, 2]def pull(self, key: int | str) -> objectReads then removes an index/key. Although it starts with get, removing an absent key subsequently raises IndexError/KeyError; check presence first.
values = Collection([1, 2])assert values.pull(0) == 1def push(self, value: object) -> CollectionAppends to the same object and returns self. Does not save entities.
assert Collection([1]).push(2).all() == [1, 2]def put(self, key: int | str, value: object) -> CollectionReplaces an index/key on the same object and returns self. Does not extend a list past its range: raises IndexError.
assert Collection([1]).put(0, 2).all() == [2]random
Section titled “random”def random(self, count: int | None=None) -> object | Collection | NoneEmpty returns None. Omitted count or 0 returns one item using secrets.choice. Positive count replaces items with a sample and returns self; negative or oversized count raises ValueError. Do not use it for unique persisted identities.
sample = Collection([1, 2, 3]).random(2)assert len(sample) == 2reduce
Section titled “reduce”def reduce(self, callback: Callable, initial: object=0) -> objectReduces with callback(accumulator, item) and initial=0. Returns a scalar/object; callback errors propagate.
assert Collection([1, 2, 3]).reduce(lambda total, n: total + n, 0) == 6reject
Section titled “reject”def reject(self, callback: Callable) -> CollectionRemoves items with truthy callback results from the same object and returns self. Unlike filter, mutates the original.
assert Collection([1, 2, 3]).reject(lambda n: n == 2).all() == [1, 3]reverse
Section titled “reverse”def reverse(self) -> CollectionReverses the same object and returns self.
assert Collection([1, 2]).reverse().all() == [2, 1]serialize
Section titled “serialize”def serialize(self) -> list[Any]Returns a list of item.serialize(), otherwise to_dict(), otherwise the original item. Models honor hidden/appends; relationships are not automatically included. Dict-backed collections iterate keys: use all() to obtain the mapping.
assert Collection([{"id": 1}]).serialize() == [{"id": 1}]def shift(self) -> objectRemoves index 0 through pull. Empty collections can raise IndexError; check isEmpty first.
values = Collection([1, 2])assert values.shift() == 1def sort(self, key: str | None=None) -> CollectionSorts the same object ascending by optional key or complete items. Returns self; incomparable values propagate TypeError. No descending argument exists.
assert Collection([3, 1, 2]).sort().all() == [1, 2, 3]def sum(self, key: str | None=None) -> floatIn-memory sum, optionally by key; TypeError returns 0. An empty key extraction falls back to full items, so avoid missing keys.
assert Collection([{"points": 2}, {"points": 4}]).sum("points") == 6toJson
Section titled “toJson”def toJson(self, **kwargs: object) -> strjson.dumps(serialize(), **kwargs); does not automatically add default=str. Supply default=str for datetime/UUID or an encoder; standard errors propagate.
text = Collection([{"id": 1}]).toJson(ensure_ascii=False, default=str)groupBy
Section titled “groupBy”def groupBy(self, key: str) -> CollectionSorts by key and returns a dict-backed Collection mapping value→list. Not SQL GROUP BY. Incomparable grouping values or unhashable keys may raise TypeError; use all() to consume groups.
groups = Collection([{"kind": "a", "id": 1}]).groupBy("kind").all()assert groups["a"][0]["id"] == 1transform
Section titled “transform”def transform(self, callback: Callable) -> CollectionReplaces every item with callback results and returns self, including None. map creates another collection; transform mutates this one.
assert Collection([1, 2]).transform(lambda n: n + 1).all() == [2, 3]unique
Section titled “unique”def unique(self, key: str | None=None) -> CollectionNew collection of unique values, optionally by key; preserves first occurrences and handles unhashable items. A dict-backed collection with key returns self unfiltered.
assert Collection([1, 1, 2]).unique().all() == [1, 2]def where(self, key: str, *args: object) -> CollectionIn-memory filter: (key,value) or (key,operator,value), operators <, <=, ==, !=, >, >=. Missing values raise ValueError; unsupported operators skip items as per-item errors are caught. SQL ”=” is not equivalent. Incompatible comparisons attempt text.
assert Collection([{"id": 1}, {"id": 2}]).where("id", ">", 1).count() == 1whereIn
Section titled “whereIn”def whereIn(self, key: str, values: list[Any] | Collection) -> CollectionNew collection by key membership in list/Collection. Also compares str representations, so 1 may match “1”. Not strict SQL comparison.
assert Collection([{"id": 1}]).whereIn("id", ["1"]).count() == 1whereNotIn
Section titled “whereNotIn”def whereNotIn(self, key: str, values: list[Any] | Collection) -> CollectionComplement of whereIn including its text comparison; returns a new collection.
assert Collection([{"id": 1}, {"id": 2}]).whereNotIn("id", [1]).count() == 1def zip(self, items: list[Any] | Collection) -> CollectionNew collection of pairs up to the shorter length. Only list/Collection is accepted; other types raise TypeError.
assert Collection([1, 2]).zip(["a"]).all() == [[1, "a"]]setAppends
Section titled “setAppends”def setAppends(self, appends: list[str]) -> CollectionAdds names to item serialization without changing class appends. Returns self; models use getAttribute and honor hidden. Does not calculate or query relationships.
rows = Collection([Article({"title": "Guide"})]).setAppends(["label"])assert rows.serialize()[0]["label"] == "GUIDE"addRelation
Section titled “addRelation”def addRelation(self, relation_data: dict[str, Any]) -> CollectionFor each item exposing setRelation, stores all supplied mapping pairs and returns self. Neither executes eager loading nor automatically adds those relations to toDict.
rows = Collection([Team({"name": "A"})])rows.addRelation({"players": Collection()})assert rows[0].relationLoaded("players")Python protocol
Section titled “Python protocol”Iterate with for, use len, compare collections/lists, read negative indices and slices (rows[:10] makes a new Collection), assign with rows[0] = value and delete with del rows[0]. Index/read/write/delete propagate underlying list/dict errors. Ordering comparisons use element lexicographic order, not PKs or SQL order. hash(rows) only works when every element is hashable; do not use a mutable collection as a key and subsequently change it.
groupBy and pluck(value, key) can create dict-backed storage despite list constructor/type annotations; iteration produces keys. Use .all() for that mapping rather than treating it as a model list or queryset serialize() result.