Facts and rows¶
Example data¶
The examples use this data and the names todo, today and tasks.
import datetime as dt
from enum import StrEnum
from string.templatelib import Template
from scriptt.world import Fact, Goal, Key, Log, Ref, Row, World
class TodoList(Fact):
name: Key[str]
class Status(StrEnum):
OPEN = "open"
DONE = "done"
class Task(Fact):
id: Key[str]
list: Ref[TodoList]
title: str
status: Status = Status.OPEN
due: dt.date | None = None
def __describe__(self) -> Template: # how say() names a task
return t"task {self.id}"
class Note(Fact):
task: Key[Ref[Task]]
text: str
todo = World.new(TodoList, Task, Note)
for name in ["home", "work"]:
todo.add(
TodoList(name=name),
source="me",
true_from="2 Nov 2026",
known="2 Nov 2026 09:00",
)
for key, home, title, due, true_from, known in [
("t1", "home", "Buy milk", dt.date(2026, 11, 3), "2 Nov 2026", "2 Nov 2026 09:00"),
(
"t2",
"work",
"Send the report",
dt.date(2026, 11, 4),
"2 Nov 2026",
"2 Nov 2026 09:00",
),
(
"t3",
"work",
"Book the room",
dt.date(2026, 11, 6),
"2 Nov 2026",
"2 Nov 2026 09:00",
),
("t4", "home", "Call the plumber", None, "2 Nov 2026", "2 Nov 2026 09:00"),
(
"t5",
"home",
"Water the plants",
dt.date(2026, 11, 4),
"2 Nov 2026",
"2 Nov 2026 09:00",
),
(
"t6",
"home",
"Pay the rent",
dt.date(2026, 11, 5),
"3 Nov 2026",
"3 Nov 2026 12:00",
),
]:
todo.add(
Task(id=key, list=home, title=title, due=due),
source="me",
true_from=true_from,
known=known,
)
for task, text in [("t1", "Oat milk"), ("t2", "Use the new template")]:
todo.add(
Note(task=task, text=text),
source="me",
true_from="2 Nov 2026",
known="2 Nov 2026 09:00",
)
milk = todo.on("2 Nov 2026").one(Task, id="t1")
todo.add(
milk.but(status=Status.DONE),
source="me",
true_from="3 Nov 2026",
known="3 Nov 2026 18:30",
)
today = todo.on("4 Nov 2026")
tasks = today.facts(Task)
Facts ¶
Facts(items: Iterable[T] = ())
An ordered collection of facts or of rows. It has dplyr-style verbs. Get one from
World.facts.
Each verb gives a new collection and leaves this one as it was. You can also do
these things with a Facts.
- Iterate it.
- Take its
len. - Index it with a number.
- Slice it.
- Compare two with
==.
Printing a Facts shows table().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
Iterable[T]
|
The elements in order. The default is an empty collection. |
()
|
Example
from scriptt.world import Facts
tasks.count() # -> 6
Facts([3, 1, 2]).arrange(lambda n: n).first() # -> 1
tasks[0].list in {"home", "work"} # -> True
__add__ ¶
Join two collections. The result has the elements of this one and then the elements of the other.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Facts[T]
|
The collection to add after this one. |
required |
Returns:
| Type | Description |
|---|---|
Facts[T]
|
A new collection. Neither input changes. |
Example
both = tasks.where(list="home") + tasks.where(list="work")
both.count() # -> 6
where ¶
where(
rule: Callable[[T], object] | None = None,
/,
**equal: Any,
) -> Facts[T]
Keep the elements for which rule is true and every field=value is true.
An element whose rule uses a missing value (None in t.due > cutoff) is not
kept. This is as in dplyr's filter.
Any other error in the rule is raised.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rule
|
Callable[[T], object] | None
|
A function that takes 1 element and gives a true or false value. The
default |
None
|
**equal
|
Any
|
Fields that must equal a value. An example is
|
{}
|
Returns:
| Type | Description |
|---|---|
Facts[T]
|
A new collection with the elements that agree. They keep their order. |
Example
open_tasks = tasks.where(status=Status.OPEN)
open_tasks.where(lambda t: t.list == "home").count() # -> 3
where_known ¶
where_known(
value: Callable[[T], V | None],
rule: Callable[[V], object] = lambda v: True,
) -> Facts[T]
Keep the elements whose value is not None and agrees with rule.
The rule gets the value with None taken out. So a type checker accepts it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Callable[[T], V | None]
|
A function that takes 1 element and gives a value or |
required |
rule
|
Callable[[V], object]
|
A function that takes that value and gives a true or false value. The default keeps every element that has a value. |
lambda v: True
|
Returns:
| Type | Description |
|---|---|
Facts[T]
|
A new collection with the elements that agree. They keep their order. |
Example
tasks.where_known(lambda t: t.due, lambda due: due.day > 4).count()
# -> 2
arrange ¶
arrange(
by: Callable[[T], Any], *, descending: bool = False
) -> Facts[T]
Put the elements in order of a value. Elements whose value is None come
last. This is as in dplyr.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
by
|
Callable[[T], Any]
|
A function that takes 1 element and gives the value to sort by. |
required |
descending
|
bool
|
Whether the largest value comes first. The default |
False
|
Returns:
| Type | Description |
|---|---|
Facts[T]
|
A new collection in the new order. |
Example
tasks.arrange(lambda t: t.id, descending=True)[0].title # -> 'Pay the rent'
first ¶
first() -> T | None
Give the first element. It is None if the collection is empty.
Returns:
| Type | Description |
|---|---|
T | None
|
The first element or |
Example
tasks.arrange(lambda t: t.title).first().title # -> 'Book the room'
tasks.where(list="school").first() # -> None
one ¶
one() -> T
Give the only element of the collection.
Returns:
| Type | Description |
|---|---|
T
|
The element. |
Raises:
| Type | Description |
|---|---|
LookupError
|
The collection has no element or has 2 or more. |
Example
tasks.where(title="Buy milk").one().list # -> 'home'
mutate ¶
Add fields that you compute from each element. Each element becomes a Row
with its old fields and the new ones.
A new field with the name of an old field replaces it. This is as in dplyr. A
value that you compute from a missing value
(None) is None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**new
|
Callable[[T], Any]
|
For each new field, a function that takes 1 element and gives the value. |
{}
|
Returns:
| Type | Description |
|---|---|
Facts[Row]
|
A new collection of rows. |
Example
milk = tasks.where(title="Buy milk")
milk.mutate(shout=lambda t: t.title.upper()).one().shout # -> 'BUY MILK'
map ¶
map(function: Callable[[T], U]) -> Facts[U]
Give a new collection of the results of a function on each element.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
function
|
Callable[[T], U]
|
A function that takes 1 element and gives any value. |
required |
Returns:
| Type | Description |
|---|---|
Facts[U]
|
A new collection of the results, in order. |
Example
tasks.where(title="Buy milk").map(lambda t: t.title.upper()).one()
# -> 'BUY MILK'
count ¶
count() -> int
Count the elements.
Returns:
| Type | Description |
|---|---|
int
|
The number of elements. |
Example
tasks.count() # -> 6
distinct ¶
distinct(value: Callable[[T], U]) -> Facts[U]
Give the different values of a function. They are in the order that you first see them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Callable[[T], U]
|
A function that takes 1 element and gives a value that can be a dictionary key. |
required |
Returns:
| Type | Description |
|---|---|
Facts[U]
|
A new collection of the different values. |
Example
tasks.distinct(lambda t: t.list).count() # -> 2
sum ¶
sum(value: Callable[[T], N | None]) -> N
Add the values of a function over the elements. An element whose value is
None adds nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Callable[[T], N | None]
|
A function that takes 1 element and gives a number or |
required |
Returns:
| Type | Description |
|---|---|
N
|
The total. It is 0 for an empty collection. |
Example
tasks.where(list="work").sum(lambda t: t.due.day) # -> 10
mean ¶
mean(value: Callable[[T], float | None]) -> float | None
Give the mean of the values of a function. An element whose value is None
is left out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Callable[[T], float | None]
|
A function that takes 1 element and gives a number or |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
The mean. It is |
Example
tasks.where(list="work").mean(lambda t: t.due.day) # -> 5.0
tasks.where(list="school").mean(lambda t: t.due.day) # -> None
max ¶
max(value: Callable[[T], Any]) -> Any
Give the largest value of a function. An element whose value is None is
left out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Callable[[T], Any]
|
A function that takes 1 element and gives a value that can be
compared or |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The largest value. It is |
Example
tasks.max(lambda t: len(t.title)) # -> 16
min ¶
min(value: Callable[[T], Any]) -> Any
Give the smallest value of a function. An element whose value is None is
left out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Callable[[T], Any]
|
A function that takes 1 element and gives a value that can be
compared or |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The smallest value. It is |
Example
tasks.min(lambda t: len(t.title)) # -> 8
top ¶
top(n: int, by: Callable[[T], Any]) -> Facts[T]
Keep the n elements with the largest value. Elements whose value is None
come last.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
How many elements to keep. |
required |
by
|
Callable[[T], Any]
|
A function that takes 1 element and gives the value to rank by. |
required |
Returns:
| Type | Description |
|---|---|
Facts[T]
|
A new collection of at most |
Example
[t.title for t in tasks.top(2, by=lambda t: len(t.title))]
# -> ['Call the plumber', 'Water the plants']
count_by ¶
Count the elements in each group. The largest group comes first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**by
|
Callable[[T], Any]
|
A name and a function for each grouping value. The function takes 1 element and gives the value. |
{}
|
Returns:
| Type | Description |
|---|---|
Facts[Row]
|
A collection of rows. Each row has the grouping values and |
Facts[Row]
|
count. |
Example
rows = tasks.count_by(status=lambda t: t.status)
[(row.status.value, row.n) for row in rows]
# -> [('open', 5), ('done', 1)]
group_by ¶
group_by(**by: Callable[[T], Any]) -> Groups[T]
Put the elements in groups by one value or more. Then call summarise to
make 1 row for each group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**by
|
Callable[[T], Any]
|
A name and a function for each grouping value. The function takes 1 element and gives the value. |
{}
|
Returns:
| Type | Description |
|---|---|
Groups[T]
|
A |
Example
groups = tasks.group_by(list=lambda t: t.list)
rows = groups.summarise(
n=lambda g: g.count(), last=lambda g: g.max(lambda t: t.id)
)
sorted((row.list, row.n, row.last) for row in rows)
# -> [('home', 4, 't6'), ('work', 2, 't3')]
join ¶
Match each element of this collection with each element of another collection when a rule is true.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Iterable[U]
|
The other collection. Any iterable works. |
required |
on
|
Callable[[T, U], bool]
|
A function that takes 1 element of this collection and 1 of the other. It gives a true or false value. |
required |
Returns:
| Type | Description |
|---|---|
Facts[Pair[T, U]]
|
A collection of |
Facts[Pair[T, U]]
|
with no partner gives no pair. |
Example
open_tasks = tasks.where(status=Status.OPEN)
pairs = today.facts(Note).join(
open_tasks, on=lambda note, task: note.task == task.id
)
pairs.count() # -> 1
left_join ¶
Match elements as join does. Keep an element that has no partner.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Iterable[U]
|
The other collection. Any iterable works. |
required |
on
|
Callable[[T, U], bool]
|
A function that takes 1 element of this collection and 1 of the other. It gives a true or false value. |
required |
Returns:
| Type | Description |
|---|---|
Facts[Pair[T, U | None]]
|
A collection of |
Facts[Pair[T, U | None]]
|
|
Example
open_tasks = tasks.where(status=Status.OPEN)
pairs = today.facts(Note).left_join(
open_tasks, on=lambda note, task: note.task == task.id
)
sorted((pair.left.task, pair.right is None) for pair in pairs)
# -> [('t1', True), ('t2', False)]
table ¶
table(limit: int = 20) -> str
Write the elements as an aligned text table. repr and print of a Facts
call this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int
|
The most rows to show. The default is 20. A last line tells how many more rows exist. |
20
|
Returns:
| Type | Description |
|---|---|
str
|
The table as text. An empty collection gives "(no items)". |
Example
print(tasks.table(limit=2))
Groups ¶
Groups(
names: tuple[str, ...], groups: dict[tuple, Facts[T]]
)
The groups that Facts.group_by makes. Do not call the constructor. Use
group_by.
Iterate it to get (values, facts) for each group. values is a tuple of the
grouping values.
summarise ¶
Make 1 row for each group. The row has its grouping values and one or more summary values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**values
|
Callable[[Facts[T]], Any]
|
A name and a function for each summary. The function takes the
|
{}
|
Returns:
| Type | Description |
|---|---|
Facts[Row]
|
A collection of rows in the order that the groups first appear. |
Example
by_list = tasks.group_by(list=lambda t: t.list)
rows = by_list.summarise(titles=lambda g: g.sum(lambda t: len(t.title)))
sorted((row.list, row.titles) for row in rows)
# -> [('home', 52), ('work', 28)]
Row ¶
Row(**values: Any)
A plain set of named fields. mutate, map and summarise make rows. You can
also make one.
Read a field as an attribute such as row.task. A row cannot change. Two rows with
the same fields are equal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**values
|
Any
|
The fields of the row as keyword arguments. |
{}
|
Raises:
| Type | Description |
|---|---|
AttributeError
|
You read a field that the row does not have or you try to set a field. |
Example
from scriptt.world import Row
row = Row(task="t1", hours=1.5)
row.task # -> 't1'
row.fields() # -> {'task': 't1', 'hours': 1.5}
fields ¶
fields() -> dict[str, Any]
Give the fields of the row as a dictionary.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A new dictionary from field name to value in the order of the row. |
Example
from scriptt.world import Row
list(Row(task="t1", hours=1.5).fields()) # -> ['task', 'hours']
Pair
dataclass
¶
Pair(left: L, right: R)
Two elements that join or left_join matched.
Attributes:
| Name | Type | Description |
|---|---|---|
left |
L
|
The element from the collection that you called |
right |
R
|
The element from the other collection. With |
Example
open_tasks = tasks.where(status=Status.OPEN)
pairs = today.facts(Note).join(
open_tasks, on=lambda note, task: note.task == task.id
)
sorted((pair.left.text, pair.right.title) for pair in pairs)
# -> [('Use the new template', 'Send the report')]