Colecciones y paginación
get() devuelve Collection, compartida por todo el framework. from orionis.orm import Collection, ModelCollection expone dos nombres del mismo tipo. Sus operaciones trabajan sobre memoria: collection.where(...) no añade WHERE SQL, y collection.count() no ejecuta COUNT. Filtra y pagina en el builder antes de cargar una tabla grande.
Ejemplo desde un controlador
Sección titulada «Ejemplo desde un controlador»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())Paginación de consultas
Sección titulada «Paginación de consultas»paginate
Sección titulada «paginate»async def paginate(self, page: int=1, per_page: int=_DEFAULT_PER_PAGE) -> PaginatorTerminal async con page=1, per_page=15 (_DEFAULT_PER_PAGE en fuente): exige enteros positivos sin bool, o InvalidQueryException. Aplica OFFSET/LIMIT al builder, ejecuta COUNT y recuperación y devuelve Paginator. Dentro de transacción ejecuta ambas consultas secuencialmente en la conexión transaccional; fuera las ejecuta con gather usando conexiones del pool. No garantiza snapshot consistente entre ambas fuera de una transacción.
async def article_page(page_number: int = 1): page = await Article.orderBy("id").paginate(page=page_number, per_page=20) return page.toDict()Ordena por una clave determinista, especialmente al cambiar de página. Paginación usa offset, no cursor/keyset. Una página posterior al final devuelve items vacíos y conserva el número solicitado. El COUNT y las filas pueden observar cambios concurrentes; para una vista consistente usa una transacción con el aislamiento adecuado de tu motor.
API de Paginator
Sección titulada «API de Paginator»Los ejemplos de propiedades suponen page = Paginator(Collection([{"id": 1}]), total=11, page=1, per_page=10). len(page) devuelve cantidad de items actuales, no total; repr muestra page/perPage/total. No es un builder ni un objeto Response.
__init__
Sección titulada «__init__»def __init__(self, items: Collection, total: int, page: int, per_page: int) -> NoneEnvuelve Collection sin consultar SQL. page y per_page deben ser int positivos, excluyendo bool (ValueError). total se convierte a int y se limita a cero mínimo. No ajusta una página superior a lastPage.
from orionis.orm import Collection, Paginator
page = Paginator(Collection([{"id": 1}]), total=11, page=1, per_page=10)def items(self) -> CollectionPropiedad de solo lectura: la misma Collection de la página; sus métodos mutadores sí pueden modificarla.
items = page.itemsdef total(self) -> intPropiedad int: filas totales del recuento, no longitud de la página.
assert page.total == 11def page(self) -> intPropiedad int: página actual, empezando por 1.
assert page.page == 1perPage
Sección titulada «perPage»def perPage(self) -> intPropiedad int: tamaño configurado, aunque la última página contenga menos filas.
assert page.perPage == 10lastPage
Sección titulada «lastPage»def lastPage(self) -> intPropiedad int: ceil(total/perPage), al menos 1 incluso con total=0.
assert page.lastPage == 2hasNext
Sección titulada «hasNext»def hasNext(self) -> boolPropiedad bool: page*perPage < total. No crea enlaces ni consulta otra página.
assert page.hasNext is TruehasPrevious
Sección titulada «hasPrevious»def hasPrevious(self) -> boolPropiedad bool: page > 1, incluso si esa página actual está fuera de rango.
assert page.hasPrevious is Falsedef toDict(self) -> dict[str, Any]Devuelve dict con items serializados y claves total, page, perPage, lastPage, hasNext, hasPrevious. No incluye URL ni cursor.
assert page.toDict()["items"] == [{"id": 1}]def toJson(self, **kwargs: Any) -> strSerializa toDict con json.dumps y default=str salvo override en kwargs. Retorna texto JSON.
text = page.toJson(indent=2, ensure_ascii=False)API completa de Collection utilizada por ORM
Sección titulada «API completa de Collection utilizada por ORM»Todos los ejemplos son operaciones en memoria. Article y Team se definen en las guías de modelos/relaciones. Los callbacks son síncronos: map/filter/each no esperan corutinas.
Operaciones mutadoras como merge, push, forget, put, sort, reverse, reject, transform, each y random(count>0) cambian la misma colección; si contiene modelos, estos también siguen siendo las mismas instancias. No guardan cambios SQL automáticamente. Las nuevas colecciones son copias superficiales de sus elementos.
__init__
Sección titulada «__init__»def __init__(self, items: list[Any] | None=None) -> NoneConstruye colección en memoria; items=None crea vacía. Con una lista no vacía conserva la referencia: pasa .copy() si necesitas aislarla. ModelCollection es el mismo tipo, no una subclase con persistencia.
values = Collection([1, 2, 3])def take(self, number: int) -> CollectionColección nueva con los primeros number elementos; si es negativo toma desde el final y si es cero devuelve vacía. No consulta ni limita SQL.
assert Collection([1, 2, 3]).take(-2).all() == [2, 3]def first(self, callback: Callable | None=None) -> objectPrimer elemento o None; con callback devuelve la primera coincidencia. Callback no callable produce ValueError; excepciones internas se propagan.
assert Collection([1, 2, 3]).first(lambda n: n > 1) == 2def last(self, callback: Callable | None=None) -> objectÚltimo elemento/coincidencia o None. Con callback recorre los elementos en orden para recordar el último que cumple.
assert Collection([1, 2, 3]).last(lambda n: n < 3) == 2def all(self) -> list[Any]Devuelve la lista subyacente, no una copia ni serialización. Si proviene de pluck(..., key)/groupBy, devuelve el mapping almacenado aunque la anotación indique list.
raw = Collection([1, 2]).all()assert raw == [1, 2]def avg(self, key: str | None=None) -> floatPromedio numérico en memoria, con clave opcional de dict/atributo; vacío o TypeError devuelve 0. No es un agregado SQL y no carga columnas ausentes.
assert Collection([{"score": 2}, {"score": 4}]).avg("score") == 3def max(self, key: str | None=None) -> objectMáximo en memoria, opcionalmente por clave; vacío o comparación incompatible devuelve 0.
assert Collection([2, 4]).max() == 4def min(self, key: str | None=None) -> objectMínimo en memoria, opcionalmente por clave; vacío o comparación incompatible devuelve 0.
assert Collection([2, 4]).min() == 2def chunk(self, size: int) -> CollectionNueva colección de subcolecciones de tamaño size; tamaño no positivo produce ValueError. Divide resultados ya cargados, no consulta por lotes SQL.
chunks = Collection([1, 2, 3]).chunk(2)assert chunks[0].all() == [1, 2]collapse
Sección titulada «collapse»def collapse(self) -> CollectionAplana un nivel de listas o colecciones; devuelve colección nueva. Entradas escalares no iterables pueden producir TypeError; para anidación profunda de listas/dicts usa flatten.
assert Collection([[1, 2], [3]]).collapse().all() == [1, 2, 3]contains
Sección titulada «contains»def contains(self, key: str | Callable, value: object=None) -> boolBool de pertenencia con valor directo, callback o clave/valor. value=None se interpreta como ausencia del segundo argumento; usa callback para buscar un atributo nulo.
assert Collection([{"id": 2}]).contains("id", 2)def count(self) -> intNúmero de elementos ya cargados. No ejecuta COUNT SQL.
assert Collection([1, 2]).count() == 2def diff(self, items: list[Any] | Collection) -> CollectionNueva colección de elementos que no están en lista/colección comparada. Usa igualdad; soporta elementos no hashables mediante comparación secuencial.
assert Collection([1, 2]).diff([2]).all() == [1]def each(self, callback: Callable) -> CollectionModifica cada posición con el retorno truthy del callback y se detiene en el primer retorno falsy, sin sustituir esa posición. Retorna self. No es un iterador de efectos secundarios con callbacks que devuelven None.
values = Collection([1, 2, 3])assert values.each(lambda n: n * 2).all() == [2, 4, 6]def every(self, callback: Callable) -> boolBool indicando si todos cumplen callback; en vacía devuelve True. Callback inválido: ValueError.
assert Collection([2, 4]).every(lambda n: n % 2 == 0)def filter(self, callback: Callable) -> CollectionColección nueva con elementos cuyo callback sea truthy. Callback obligatorio; inválido produce ValueError.
assert Collection([1, 2, 3]).filter(lambda n: n > 1).all() == [2, 3]flatten
Sección titulada «flatten»def flatten(self) -> CollectionColección nueva: aplana recursivamente listas y valores de diccionarios. Tuplas/Collection internas se conservan como elementos, no se expanden por esta función.
assert Collection([1, [2, {"x": 3}]]).flatten().all() == [1, 2, 3]def forget(self, *keys: int | str) -> CollectionBorra índices/claves en orden descendente y retorna self. Índices/claves inexistentes propagan IndexError/KeyError; una lista usa índices enteros.
assert Collection([1, 2, 3]).forget(0, 2).all() == [2]forPage
Sección titulada «forPage»def forPage(self, page: int, number: int) -> CollectionNueva colección por slice: normaliza page a al menos 1 y exige number>0 (ValueError). No produce metadatos ni consulta filas adicionales.
assert Collection([1, 2, 3]).forPage(2, 2).all() == [3]def get(self, key: int | str, default: object=None) -> objectLee índice/clave o default; si default es callable lo evalúa sin argumentos solo al faltar. Captura IndexError/KeyError, no un tipo de índice inválido.
assert Collection([1]).get(7, lambda: "missing") == "missing"implode
Sección titulada «implode»def implode(self, glue: str=',', key: str | None=None) -> strUne representaciones str con glue=","; key=None usa elementos completos. Con objetos/dicts y clave extrae esa columna. Vacía devuelve "".
assert Collection([{"name": "A"}, {"name": "B"}]).implode(" / ", "name") == "A / B"isEmpty
Sección titulada «isEmpty»def isEmpty(self) -> boolBool basado en longitud; no consulta existencia SQL.
assert Collection().isEmpty()def map(self, callback: Callable) -> CollectionColección nueva con cada retorno del callback, incluyendo None y valores falsos; no modifica la lista original.
assert Collection([1, 2]).map(lambda n: n * 3).all() == [3, 6]mapInto
Sección titulada «mapInto»def mapInto(self, cls: type, method: str | None=None, **kwargs: object) -> CollectionMapea con cls(item) o cls.method(item, **kwargs); kwargs solo se pasan cuando hay método. cls debe ser tipo (TypeError); método ausente o TypeError/AttributeError/ValueError por elemento se convierten en None. No persiste modelos.
assert Collection(["1", "bad"]).mapInto(int).all() == [1, None]def merge(self, items: list[Any] | Collection) -> CollectionAñade lista/Collection al mismo objeto y devuelve self; no hace unión por PK ni deduplica. Tipo no admitido: TypeError.
assert Collection([1]).merge([1, 2]).all() == [1, 1, 2]def pluck(self, value: str, key: str | None=None) -> CollectionExtrae por ruta de dict/atributo; sin key devuelve lista omitiendo None. Con key devuelve Collection respaldada por dict, sobreescribe claves repetidas y conserva valores nulos. Para leer ese mapping usa .all()/.get().
assert Collection([{"id": 1, "name": "A"}]).pluck("name", "id").all() == {1: "A"}def pop(self) -> objectExtrae y elimina el último elemento; vacía devuelve None.
values = Collection([1, 2])assert values.pop() == 2prepend
Sección titulada «prepend»def prepend(self, value: object) -> CollectionInserta al principio del mismo objeto y retorna self.
assert Collection([2]).prepend(1).all() == [1, 2]def pull(self, key: int | str) -> objectObtiene y elimina un índice/clave. Aunque primero usa get, el borrado posterior de una clave ausente lanza IndexError/KeyError; comprueba existencia antes.
values = Collection([1, 2])assert values.pull(0) == 1def push(self, value: object) -> CollectionAñade al final del mismo objeto y retorna self. No guarda entidades.
assert Collection([1]).push(2).all() == [1, 2]def put(self, key: int | str, value: object) -> CollectionSustituye índice/clave del mismo objeto y retorna self. En lista no amplía un índice fuera de rango: produce IndexError.
assert Collection([1]).put(0, 2).all() == [2]def random(self, count: int | None=None) -> object | Collection | NoneVacía devuelve None. Sin count o con 0, devuelve un elemento usando secrets.choice. Count positivo reemplaza la lista por una muestra y retorna self; negativo o mayor que longitud produce ValueError. No lo utilices como fuente de identidades persistidas únicas.
sample = Collection([1, 2, 3]).random(2)assert len(sample) == 2def reduce(self, callback: Callable, initial: object=0) -> objectReduce con callback(acumulado, elemento) e initial=0. Devuelve valor escalar/objeto; errores del callback se propagan.
assert Collection([1, 2, 3]).reduce(lambda total, n: total + n, 0) == 6def reject(self, callback: Callable) -> CollectionElimina del mismo objeto elementos cuyo callback es truthy y retorna self. A diferencia de filter, modifica la colección original.
assert Collection([1, 2, 3]).reject(lambda n: n == 2).all() == [1, 3]reverse
Sección titulada «reverse»def reverse(self) -> CollectionInvierte en el mismo objeto y retorna self.
assert Collection([1, 2]).reverse().all() == [2, 1]serialize
Sección titulada «serialize»def serialize(self) -> list[Any]Devuelve lista de salidas de item.serialize(), o to_dict(), o el propio elemento. En modelos respeta hidden y appends; no incluye automáticamente relaciones. Para colecciones respaldadas por dict itera claves: usa all() para obtener el mapping.
assert Collection([{"id": 1}]).serialize() == [{"id": 1}]def shift(self) -> objectExtrae índice 0 usando pull. En colección vacía puede lanzar IndexError; comprueba isEmpty primero.
values = Collection([1, 2])assert values.shift() == 1def sort(self, key: str | None=None) -> CollectionOrdena ascendentemente el mismo objeto, por clave opcional o elementos completos. Retorna self; valores no comparables propagan TypeError. No hay argumento descending.
assert Collection([3, 1, 2]).sort().all() == [1, 2, 3]def sum(self, key: str | None=None) -> floatSuma en memoria, opcionalmente por clave; ante TypeError devuelve 0. Si la extracción de una clave no produce valores usa los elementos completos como fallback, por eso evita claves ausentes.
assert Collection([{"points": 2}, {"points": 4}]).sum("points") == 6def toJson(self, **kwargs: object) -> strjson.dumps(serialize(), **kwargs); no añade default=str automáticamente. Pasa default=str para datetime/UUID o provee encoder; errores estándar se propagan.
text = Collection([{"id": 1}]).toJson(ensure_ascii=False, default=str)groupBy
Sección titulada «groupBy»def groupBy(self, key: str) -> CollectionOrdena por clave y devuelve Collection respaldada por mapping valor→lista. No es GROUP BY SQL. Valores no comparables durante ordenación o no hashables como clave pueden producir TypeError; usa all() para consumir los grupos.
groups = Collection([{"kind": "a", "id": 1}]).groupBy("kind").all()assert groups["a"][0]["id"] == 1transform
Sección titulada «transform»def transform(self, callback: Callable) -> CollectionReemplaza todos los elementos por resultados del callback y retorna self, conservando también None. map crea otra colección; transform modifica esta.
assert Collection([1, 2]).transform(lambda n: n + 1).all() == [2, 3]def unique(self, key: str | None=None) -> CollectionColección nueva con valores únicos, opcionalmente por clave; conserva primeras coincidencias y admite no hashables. Una colección respaldada por dict con key retorna self sin filtrar.
assert Collection([1, 1, 2]).unique().all() == [1, 2]def where(self, key: str, *args: object) -> CollectionFiltro en memoria: (key,value) o (key,operator,value), operadores <, <=, ==, !=, >, >=. Sin valor produce ValueError; operadores no admitidos se omiten al capturar el error por elemento. No equivale al operador SQL ”=”. Comparaciones incompatibles intentan texto.
assert Collection([{"id": 1}, {"id": 2}]).where("id", ">", 1).count() == 1whereIn
Sección titulada «whereIn»def whereIn(self, key: str, values: list[Any] | Collection) -> CollectionColección nueva por pertenencia de clave en lista/Collection. Además de igualdad compara representación str, por lo que 1 puede coincidir con “1”. No es comparación SQL estricta.
assert Collection([{"id": 1}]).whereIn("id", ["1"]).count() == 1whereNotIn
Sección titulada «whereNotIn»def whereNotIn(self, key: str, values: list[Any] | Collection) -> CollectionComplemento de whereIn, incluyendo su comparación textual; devuelve colección nueva.
assert Collection([{"id": 1}, {"id": 2}]).whereNotIn("id", [1]).count() == 1def zip(self, items: list[Any] | Collection) -> CollectionColección nueva de pares hasta la longitud menor. Solo lista/Collection, otro tipo produce TypeError.
assert Collection([1, 2]).zip(["a"]).all() == [[1, "a"]]setAppends
Sección titulada «setAppends»def setAppends(self, appends: list[str]) -> CollectionAgrega nombres a la serialización de elementos sin modificar appends de la clase. Retorna self; en modelos usa getAttribute y respeta hidden. No calcula ni consulta relaciones.
rows = Collection([Article({"title": "Guide"})]).setAppends(["label"])assert rows.serialize()[0]["label"] == "GUIDE"addRelation
Sección titulada «addRelation»def addRelation(self, relation_data: dict[str, Any]) -> CollectionPor cada elemento que tenga setRelation, guarda todos los pares del dict y retorna self. No ejecuta eager loading ni incorpora esas relaciones a toDict automáticamente.
rows = Collection([Team({"name": "A"})])rows.addRelation({"players": Collection()})assert rows[0].relationLoaded("players")Protocolo Python
Sección titulada «Protocolo Python»Puedes iterar con for, usar len, comparar colecciones/listas, leer índices negativos y slices (rows[:10] crea Collection nueva), asignar con rows[0] = value y borrar con del rows[0]. Indexación/asignación/borrado propagan errores de la lista/dict subyacente. Las comparaciones de orden usan orden lexicográfico de elementos, no PK ni orden SQL. hash(rows) solo funciona cuando todos los elementos son hashables; no uses una colección mutable como clave que después vas a modificar.
groupBy y pluck(value, key) pueden producir un almacén dict pese a la anotación de constructor/lista; sus iteraciones son claves. Usa .all() para ese mapping y no lo trates como una lista de modelos ni como el resultado de serialize() de un queryset.