Skip to content

Rules

RuleSet

A RuleSet is a collection of Conditions and Actions that will evaluate for one or more transactions.

The Conditions are list of logical comparisons that will compare fields from the transaction to stored values. If all conditions from a Transaction are met (or any, if the Rule has an or operation), then the actions will be applied.

The actions are a list of changes that will be applied to the transaction.

Full example ruleset: "If all of these conditions match 'notes' contains 'foo' then set 'notes' to 'bar'"

To create that rule set, you can do:

RuleSet(rules=[
    Rule(
        # all of these conditions match
        operation="and",
        # 'notes' contains 'foo'
        conditions=[Condition(field="notes", op=ConditionType.CONTAINS, value="foo")],
        # set 'notes' to 'bar'
        actions=[Action(field="notes", value="bar")],
    )
])

Parameters:

Methods:

  • __str__

    Returns a readable string representation of the ruleset.

  • __iter__

    Returns an iterator over the rules in the ruleset.

  • _run
  • run

    Runs the rules of the Ruleset for every transaction on the list.

  • add

    Adds a new rule to the ruleset.

Attributes:

rules instance-attribute

rules: list[Rule]

__str__

__str__()

Returns a readable string representation of the ruleset.

Source code in actual/rules.py
def __str__(self):
    """Returns a readable string representation of the ruleset."""
    return "\n".join([str(r) for r in self.rules])

__iter__

__iter__() -> Iterator[Rule]

Returns an iterator over the rules in the ruleset.

Source code in actual/rules.py
def __iter__(self) -> typing.Iterator[Rule]:
    """Returns an iterator over the rules in the ruleset."""
    return iter(self.rules)

_run

_run(
    transaction: Transactions | Sequence[Transactions],
    stage: Literal["pre", "post", None],
)
Source code in actual/rules.py
def _run(
    self,
    transaction: Transactions | typing.Sequence[Transactions],
    stage: typing.Literal["pre", "post", None],
):
    for rule in [r for r in self.rules if r.stage == stage]:
        if isinstance(transaction, Transactions):
            rule.run(transaction)
        else:
            for t in transaction:
                rule.run(t)

run

run(
    transaction: Transactions | Sequence[Transactions],
    stage: Literal["all", "pre", "post", None] = "all",
)

Runs the rules of the Ruleset for every transaction on the list.

If stage is set to 'all' (default), all rules are run in the order pre -> None -> post. You can provide a value to run only a certain stage of rules.

If necessary, you can also individually select the rules you want to run by initializing a new ruleset from the original one, or select individual rules from, by using the list of rules RuleSet.rules.

Source code in actual/rules.py
def run(
    self,
    transaction: Transactions | typing.Sequence[Transactions],
    stage: typing.Literal["all", "pre", "post", None] = "all",
):
    """
    Runs the rules of the Ruleset for every transaction on the list.

    If `stage` is set to 'all' (default), all rules are run in the order `pre` -> `None` -> `post`.
    You can provide a value to run only a certain stage of rules.

    If necessary, you can also individually select the rules you want to run by initializing a new ruleset from
    the original one, or select individual rules from, by using the list of rules `RuleSet.rules`.
    """
    if stage == "all":
        self._run(transaction, "pre")
        self._run(transaction, None)
        self._run(transaction, "post")
    else:
        self._run(transaction, stage)  # noqa

add

add(rule: Rule)

Adds a new rule to the ruleset.

Source code in actual/rules.py
def add(self, rule: Rule):
    """Adds a new rule to the ruleset."""
    self.rules.append(rule)

Rule

Bases: BaseModel

Contains an individual rule, with multiple conditions and multiple actions. Can only be used on a single transaction at a time. You can evaluate if the rule would activate without doing any changes on the transaction by calling the evaluate method.

Note that the frontend refers to the operation as either 'all' or 'any', but stores them as 'and' and 'or' respectively on the database. If you provide the frontend values, they will be converted on the background automatically.

Parameters:

  • conditions

    (list[Condition]) –

    List of conditions that need to be met (one or all) in order for the actions to be applied.

  • operation

    (Literal['and', 'or', 'any', 'all'], default: 'and' ) –

    Operation to apply for the rule evaluation. If 'all' or 'any' need to be evaluated.

  • actions

    (list[Action]) –

    List of actions to apply to the transaction.

  • stage

    (Literal['pre', 'post', None], default: None ) –

    Stage in which the rule will be evaluated (default None)

Methods:

  • correct_operation

    If the user provides the same all or any that the frontend provides, we fix it silently to and and

  • __str__

    Returns a readable string representation of the rule.

  • set_split_amount

    Run the rules from setting split amounts.

  • evaluate

    Evaluates the rule on the transaction, without applying any action.

  • run

    Runs the rule on the transaction, calling evaluate, and if the return is True then running each of

Attributes:

conditions class-attribute instance-attribute

conditions: list[Condition] = Field(
    ...,
    description="List of conditions that need to be met (one or all) in order for the actions to be applied.",
)

operation class-attribute instance-attribute

operation: Literal["and", "or", "any", "all"] = Field(
    "and",
    description="Operation to apply for the rule evaluation. If 'all' or 'any' need to be evaluated.",
)

actions class-attribute instance-attribute

actions: list[Action] = Field(
    ...,
    description="List of actions to apply to the transaction.",
)

stage class-attribute instance-attribute

stage: Literal["pre", "post", None] = Field(
    None,
    description="Stage in which the rule will be evaluated (default None)",
)

correct_operation

correct_operation(value)

If the user provides the same all or any that the frontend provides, we fix it silently to and and or respectively.

Source code in actual/rules.py
@pydantic.model_validator(mode="before")
def correct_operation(cls, value):
    """If the user provides the same `all` or `any` that the frontend provides, we fix it silently to `and` and
    `or` respectively."""
    if value.get("operation") == "all":
        value["operation"] = "and"
    elif value.get("operation") == "any":
        value["operation"] = "or"
    return value

__str__

__str__()

Returns a readable string representation of the rule.

Source code in actual/rules.py
def __str__(self):
    """Returns a readable string representation of the rule."""
    operation = "all" if self.operation == "and" else "any"
    conditions = f" {self.operation} ".join([str(c) for c in self.conditions])
    actions = ", ".join([str(a) for a in self.actions])
    return f"If {operation} of these conditions match {conditions} then {actions}"

set_split_amount

set_split_amount(
    transaction: Transactions,
) -> list[Transactions]

Run the rules from setting split amounts.

Source code in actual/rules.py
def set_split_amount(self, transaction: Transactions) -> list[Transactions]:
    """Run the rules from setting split amounts."""
    from actual.queries import create_split  # lazy import to prevert circular issues

    # get actions that split the transaction
    split_amount_actions = [action for action in self.actions if action.op == ActionType.SET_SPLIT_AMOUNT]
    if not split_amount_actions or len(transaction.splits) or transaction.is_child:
        return []  # nothing to create
    # get inner session from object
    session = transaction._object_session()
    # first, do all entries that have fixed values
    split_by_index: list[Transactions] = [None for _ in range(len(split_amount_actions))]  # type: ignore[misc]
    fixed_split_amount_actions = [a for a in split_amount_actions if a.options["method"] == "fixed-amount"]  # type: ignore[index]
    remainder = transaction.amount
    for action in fixed_split_amount_actions:
        remainder -= action.value  # type: ignore[operator]
        split = create_split(session, transaction, decimal.Decimal(action.value) / 100)  # type: ignore[arg-type]
        split_by_index[action.get_split_index() - 1] = split
    # now do the ones with a percentage amount
    percent_split_amount_actions = [a for a in split_amount_actions if a.options["method"] == "fixed-percent"]  # type: ignore[index]
    amount_to_distribute = remainder
    for action in percent_split_amount_actions:
        value = round(amount_to_distribute * action.value / 100, 0)  # type: ignore[operator]
        remainder -= value  # type: ignore[operator, assignment]
        split = create_split(session, transaction, decimal.Decimal(value) / 100)
        split_by_index[action.get_split_index() - 1] = split
    # now, divide the remainder equally between the entries
    remainder_split_amount_actions = [a for a in split_amount_actions if a.options["method"] == "remainder"]  # type: ignore[index]
    if not len(remainder_split_amount_actions) and remainder:
        # create a virtual split that contains the leftover remainders
        split = create_split(session, transaction, decimal.Decimal(remainder) / 100)
        split_by_index.append(split)
    elif len(remainder_split_amount_actions):
        amount_per_remainder_split = round(remainder / len(remainder_split_amount_actions), 0)  # type: ignore[operator]
        for action in remainder_split_amount_actions:
            split = create_split(session, transaction, decimal.Decimal(amount_per_remainder_split) / 100)
            remainder -= amount_per_remainder_split  # type: ignore[operator, assignment]
            split_by_index[action.get_split_index() - 1] = split
        # The last non-fixed split will be adjusted for the remainder
        split_by_index[remainder_split_amount_actions[-1].get_split_index() - 1].amount += remainder  # type: ignore[operator]
    # make sure the splits are still valid and the sum equals the parent
    if sum(s.amount for s in split_by_index) != transaction.amount:  # type: ignore[misc]
        raise ActualSplitTransactionError("Splits do not match amount of parent transaction.")
    transaction.is_parent, transaction.is_child = 1, 0
    # make sure the splits are ordered correctly
    for idx, split in enumerate(split_by_index):
        split.sort_order = -idx - 2
    return split_by_index

evaluate

evaluate(transaction: Transactions) -> bool

Evaluates the rule on the transaction, without applying any action.

Source code in actual/rules.py
def evaluate(self, transaction: Transactions) -> bool:
    """Evaluates the rule on the transaction, without applying any action."""
    op = any if self.operation == "or" else all
    return op(c.run(transaction) for c in self.conditions)

run

run(transaction: Transactions) -> bool

Runs the rule on the transaction, calling evaluate, and if the return is True then running each of the actions.

Source code in actual/rules.py
def run(self, transaction: Transactions) -> bool:
    """Runs the rule on the transaction, calling evaluate, and if the return is `True` then running each of
    the actions."""
    if condition_met := self.evaluate(transaction):
        splits = self.set_split_amount(transaction)
        if splits:
            transaction.splits = splits
        for action in self.actions:
            if action.op == ActionType.SET_SPLIT_AMOUNT:
                continue  # handle in the create_splits
            action.run(transaction)
    return condition_met

Condition

Bases: BaseModel

A condition does a single comparison check for a transaction. The op indicates the action type, usually being set to IS or CONTAINS, and the comparison is applied to a field with certain value. If the transaction value matches the condition, the run method returns True, otherwise it returns False. The individual condition cannot change the value of the transaction, as only the Action can.

Important: Actual shows the amount on frontend as decimal but handles it internally as cents. Make sure that, if you provide the amount rule manually, you either provide number of cents, as an integers, or a float that get automatically converted to cents. As an example, 50 will be interpreted as 50 cents, but 50.0 will be interpreted as 50 of the currency (or 5000 cents).

The 'field' can be one of the following ('type' will be set automatically):

  • imported_description: type must be string and value any string
  • acct: type must be id and value a valid uuid
  • category: type must be id and value a valid uuid
  • date: type must be date and value a string in the date format '2024-04-11'
  • description: type must be id and value a valid uuid (means payee_id)
  • notes: type must be 'string' and value any string
  • amount: type must be number and format in cents
  • amount_inflow: type must be number and format in cents, will set "options":{"inflow":true}
  • amount_outflow: type must be number and format in cents, will set "options":{"outflow":true}

Parameters:

  • field

    (Literal['imported_description', 'acct', 'category', 'date', 'description', 'notes', 'amount', 'amount_inflow', 'amount_outflow']) –
  • op

    (ConditionType) –
  • value

    (int | str | list[str] | Schedule | BetweenValue | date | None) –
  • type

    (ValueType, default: <ValueType.ID: 'id'> ) –
  • options

    (dict | None, default: None ) –

Methods:

Attributes:

field instance-attribute

field: Literal[
    "imported_description",
    "acct",
    "category",
    "date",
    "description",
    "notes",
    "amount",
    "amount_inflow",
    "amount_outflow",
]

op instance-attribute

value instance-attribute

value: Annotated[
    int
    | str
    | list[str]
    | Schedule
    | BetweenValue
    | date
    | None,
    BeforeValidator(_coerce_value),
]

type class-attribute instance-attribute

type: ValueType = ID

options class-attribute instance-attribute

options: dict | None = None

__str__

__str__() -> str
Source code in actual/rules.py
def __str__(self) -> str:
    v = f"'{self.value}'" if isinstance(self.value, str) or isinstance(self.value, Schedule) else str(self.value)
    return f"'{self.field}' {self.op.value} {v}"

serialize_model

serialize_model(handler) -> dict

Returns valid dict for database insertion.

Source code in actual/rules.py
@model_serializer(mode="wrap")
def serialize_model(self, handler) -> dict:
    """Returns valid dict for database insertion."""
    ret = handler(self)
    if not self.options:
        ret.pop("options", None)
    return ret

get_value

get_value() -> (
    int
    | date
    | list[str]
    | str
    | BetweenValue
    | Schedule
    | None
)
Source code in actual/rules.py
def get_value(self) -> int | datetime.date | list[str] | str | BetweenValue | Schedule | None:
    return get_value(self.value, self.type)

convert_value

convert_value()
Source code in actual/rules.py
@pydantic.model_validator(mode="after")
def convert_value(self):
    if self.field in ("amount_inflow", "amount_outflow") and self.options is None:
        self.options = {self.field.split("_")[1]: True}
        self.value = abs(self.value)
        self.field = "amount"
    return self

check_operation_type

check_operation_type()
Source code in actual/rules.py
@pydantic.model_validator(mode="after")
def check_operation_type(self):
    if "type" not in self.model_fields_set:
        self.type = ValueType.from_field(self.field)
    # check if types are fine
    if not self.type.is_valid(self.op):
        raise ValueError(f"Operation {self.op} not supported for type {self.type}")
    # make sure the data matches the value type
    if not self.type.validate(self.value, self.op):
        raise ValueError(f"Value {self.value} is not valid for type {self.type.name} and operation {self.op.name}")
    return self

run

run(transaction: Transactions) -> bool
Source code in actual/rules.py
def run(self, transaction: Transactions) -> bool:
    attr = get_attribute_by_table_name(str(Transactions.__tablename__), self.field)
    if attr is None:
        raise ActualError(f"Unknown condition field '{self.field}'.")
    true_value = get_value(getattr(transaction, attr), self.type)
    self_value = self.get_value()
    session = transaction._object_session()
    return condition_evaluation(self.op, true_value, self_value, self.options, session=session)

Action

Bases: BaseModel

An Action does a single column change for a transaction. The op indicates the ActionType, usually being to SET a field with certain value.

For the op LINKED_SCHEDULE, the operation will link the transaction to a certain schedule id that generated it. This is often done automatically when you create schedules, as they are also rules internally.

For the op SET_SPLIT_AMOUNT, the transaction will be split into multiple different splits depending on the method defined by the user, being it on fixed-amount, fixed-percent or remainder. The method will be stored under options with the format {"method": "remainder", "splitIndex": 1}, indicating both the method of splitting but also which index that split will take.

The field can be one of the following (type will be set automatically based on the database):

  • category: type must be id and value a valid uuid
  • description: type must be id value a valid uuid (additional "options":{"splitIndex":0})
  • notes: type must be string and value any string
  • cleared: type must be boolean and value is a literal True/False (additional "options":{"splitIndex":0})
  • acct: type must be id and value an uuid
  • date: type must be date and value a string in the date format "2024-04-11"
  • amount: type must be number and format in cents

Parameters:

  • field

    (Literal['category', 'description', 'notes', 'cleared', 'acct', 'date', 'amount'] | None, default: None ) –
  • op

    (ActionType, default: <ActionType.SET: 'set'> ) –

    Action type to apply (default changes a column).

  • value

    (str | bool | int | None) –
  • type

    (ValueType, default: <ValueType.ID: 'id'> ) –
  • options

    (dict[str, Union[str, int]] | None, default: None ) –

Methods:

Attributes:

field class-attribute instance-attribute

field: (
    Literal[
        "category",
        "description",
        "notes",
        "cleared",
        "acct",
        "date",
        "amount",
    ]
    | None
) = None

op class-attribute instance-attribute

op: ActionType = Field(
    SET,
    description="Action type to apply (default changes a column).",
)

value instance-attribute

value: Annotated[
    str | bool | int | None, BeforeValidator(_coerce_value)
]

type class-attribute instance-attribute

type: ValueType = ID

options class-attribute instance-attribute

options: dict[str, str | int] | None = None

__str__

__str__() -> str
Source code in actual/rules.py
def __str__(self) -> str:
    if self.op in (ActionType.SET, ActionType.LINK_SCHEDULE):
        split_info = ""
        split_index = self.get_split_index()
        if split_index > 0:
            split_info = f" at Split {split_index}"
        field_str = f" '{self.field}'" if self.field else ""
        return f"{self.op.value}{field_str}{split_info} to '{self.value}'"
    elif self.op == ActionType.SET_SPLIT_AMOUNT:
        method = self.options.get("method", "") if self.options else ""
        split_index_str = str(self.options.get("splitIndex", "")) if self.options else ""
        return f"allocate a {method} at Split {split_index_str}: {self.value}"
    elif self.op in (ActionType.APPEND_NOTES, ActionType.PREPEND_NOTES):
        return (
            f"append to notes '{self.value}'"
            if self.op == ActionType.APPEND_NOTES
            else f"prepend to notes '{self.value}'"
        )
    elif self.op == ActionType.DELETE_TRANSACTIONS:
        return "delete transaction"
    # We don't want to fail __str__ whenever a new action has been included, so we use a sensible default.
    return f"{self.op.value} '{self.value}'"

serialize_model

serialize_model(handler) -> dict

Returns valid dict for database insertion.

Source code in actual/rules.py
@model_serializer(mode="wrap")
def serialize_model(self, handler) -> dict:
    """Returns valid dict for database insertion."""
    ret = handler(self)
    if not self.options:
        ret.pop("options", None)
    return ret

convert_value classmethod

convert_value(value, info)
Source code in actual/rules.py
@pydantic.field_validator("value", mode="before")
@classmethod
def convert_value(cls, value, info):
    if info.data.get("field") == "cleared" and value in (0, 1):
        return bool(value)
    return value

check_operation_type

check_operation_type()
Source code in actual/rules.py
@pydantic.model_validator(mode="after")
def check_operation_type(self):
    if "type" not in self.model_fields_set:
        if self.field is not None:
            self.type = ValueType.from_field(self.field)
        elif self.op == ActionType.LINK_SCHEDULE:
            self.type = ValueType.ID
        elif self.op == ActionType.SET_SPLIT_AMOUNT:
            self.type = ValueType.NUMBER
        elif self.op == ActionType.DELETE_TRANSACTIONS:
            # actual seems to store both types as equivalents
            # [{"op":"delete-transaction","value":null,"options":{}}]
            # [{"op":"delete-transaction","field":"category","value":null,"type":"id","options":{}}]
            self.type = ValueType.ID
    # questionable choice from the developers to set it to ID, I hope they fix it at some point, but we change it
    if self.op in (ActionType.APPEND_NOTES, ActionType.PREPEND_NOTES):
        self.type = ValueType.STRING
    # make sure the data matches the value type
    if not self.type.validate(self.value):
        raise ValueError(f"Value {self.value} is not valid for type {self.type.name}")
    return self

get_split_index

get_split_index() -> int

Extract splitIndex from options as an int.

Source code in actual/rules.py
def get_split_index(self) -> int:
    """Extract splitIndex from options as an int."""
    if self.options is None:
        return 0
    return int(self.options.get("splitIndex", 0))

run

run(transaction: Transactions) -> None

Runs the action on the transaction, regardless of the condition. For the condition based rule, see Rule.run.

Source code in actual/rules.py
def run(self, transaction: Transactions) -> None:
    """Runs the action on the transaction, regardless of the condition. For the condition based rule, see
    [Rule.run][actual.rules.Rule.run]."""
    if self.op == ActionType.SET:
        attr = get_attribute_by_table_name(str(Transactions.__tablename__), str(self.field))
        value = get_value(self.value, self.type)
        # if the split index is existing, modify instead the split transaction
        split_index = self.get_split_index()
        if split_index and len(transaction.splits) >= split_index:
            transaction = transaction.splits[split_index - 1]
        # set the value
        if self.type == ValueType.DATE:
            transaction.set_date(value)  # type: ignore[arg-type]
        elif attr == "payee_id":
            # this has to be handled separately since, when setting a transfer payee, a transfer transaction needs
            # to be created
            from actual.queries import set_transaction_payee

            # get inner session from object
            session = transaction._object_session()
            set_transaction_payee(session, transaction, value)  # type: ignore[arg-type]
        else:
            setattr(transaction, attr, value)  # type: ignore[arg-type]
    elif self.op == ActionType.LINK_SCHEDULE:
        transaction.schedule_id = self.value  # type: ignore[assignment]
    # for the notes rule, check if the rule was already applied since actual does not do that.
    # this should ensure the prefix or suffix is not applied multiple times
    elif self.op == ActionType.APPEND_NOTES:
        notes = transaction.notes or ""
        if not notes.endswith(self.value):  # type: ignore[arg-type]
            transaction.notes = f"{notes}{self.value}"
    elif self.op == ActionType.PREPEND_NOTES:
        notes = transaction.notes or ""
        if not notes.startswith(self.value):  # type: ignore[arg-type]
            transaction.notes = f"{self.value}{notes}"
    elif self.op == ActionType.DELETE_TRANSACTIONS:
        transaction.tombstone = 1
    else:
        raise ActualError(f"Operation {self.op} not supported")

ValueType

Bases: Enum

Methods:

  • is_valid

    Returns if a conditional operation for a certain type is valid. For example, if the value is of type string,

  • validate
  • from_field

Attributes:

DATE class-attribute instance-attribute

DATE = 'date'

ID class-attribute instance-attribute

ID = 'id'

STRING class-attribute instance-attribute

STRING = 'string'

NUMBER class-attribute instance-attribute

NUMBER = 'number'

BOOLEAN class-attribute instance-attribute

BOOLEAN = 'boolean'

IMPORTED_PAYEE class-attribute instance-attribute

IMPORTED_PAYEE = 'imported_payee'

is_valid

is_valid(operation: ConditionType) -> bool

Returns if a conditional operation for a certain type is valid. For example, if the value is of type string, then RuleValueType.STRING.is_valid(ConditionType.GT) will return false, because there is no logical greater than defined for strings.

Source code in actual/rules.py
def is_valid(self, operation: ConditionType) -> bool:
    """Returns if a conditional operation for a certain type is valid. For example, if the value is of type string,
    then `RuleValueType.STRING.is_valid(ConditionType.GT)` will return false, because there is no logical
    greater than defined for strings."""
    if self == ValueType.DATE:
        return operation.value in ("is", "isapprox", "gt", "gte", "lt", "lte")
    elif self in (ValueType.STRING, ValueType.IMPORTED_PAYEE):
        return operation.value in (
            "is",
            "contains",
            "oneOf",
            "isNot",
            "doesNotContain",
            "notOneOf",
            "matches",
            "hasTags",
        )
    elif self == ValueType.ID:
        return operation.value in ("is", "isNot", "oneOf", "notOneOf", "onBudget", "offBudget")
    elif self == ValueType.NUMBER:
        return operation.value in ("is", "isapprox", "isbetween", "gt", "gte", "lt", "lte")
    else:
        # must be BOOLEAN
        return operation.value in ("is",)

validate

validate(
    value: int | list[str] | str | None,
    operation: ConditionType | None = None,
) -> bool
Source code in actual/rules.py
def validate(self, value: int | list[str] | str | None, operation: ConditionType | None = None) -> bool:
    if isinstance(value, list) and operation in (ConditionType.ONE_OF, ConditionType.NOT_ONE_OF):
        return all(self.validate(v, None) for v in value)
    if value is None:
        return True
    if self == ValueType.ID:
        # make sure it's an uuid
        return isinstance(value, str) and is_uuid(value)
    elif self in (ValueType.STRING, ValueType.IMPORTED_PAYEE):
        return isinstance(value, str)
    elif self == ValueType.DATE:
        try:
            res = bool(get_value(value, self))
        except ValueError:
            res = False
        return res
    elif self == ValueType.NUMBER:
        if operation == ConditionType.IS_BETWEEN:
            return isinstance(value, BetweenValue)
        else:
            return isinstance(value, int)
    else:
        # must be BOOLEAN
        return isinstance(value, bool)

from_field classmethod

from_field(field: str | None) -> ValueType
Source code in actual/rules.py
@classmethod
def from_field(cls, field: str | None) -> ValueType:
    if field in ("acct", "category", "description"):
        return ValueType.ID
    elif field in ("notes",):
        return ValueType.STRING
    elif field in ("imported_description",):
        return ValueType.IMPORTED_PAYEE
    elif field in ("date",):
        return ValueType.DATE
    elif field in ("cleared", "reconciled"):
        return ValueType.BOOLEAN
    elif field in ("amount",):
        return ValueType.NUMBER
    else:
        raise ValueError(f"Field '{field}' does not have a matching ValueType.")

ActionType

Bases: str, Enum

Attributes:

SET class-attribute instance-attribute

SET = 'set'

SET_SPLIT_AMOUNT class-attribute instance-attribute

SET_SPLIT_AMOUNT = 'set-split-amount'
LINK_SCHEDULE = 'link-schedule'

PREPEND_NOTES class-attribute instance-attribute

PREPEND_NOTES = 'prepend-notes'

APPEND_NOTES class-attribute instance-attribute

APPEND_NOTES = 'append-notes'

DELETE_TRANSACTIONS class-attribute instance-attribute

DELETE_TRANSACTIONS = 'delete-transaction'

ConditionType

Bases: str, Enum

Attributes:

IS class-attribute instance-attribute

IS = 'is'

IS_APPROX class-attribute instance-attribute

IS_APPROX = 'isapprox'

GT class-attribute instance-attribute

GT = 'gt'

GTE class-attribute instance-attribute

GTE = 'gte'

LT class-attribute instance-attribute

LT = 'lt'

LTE class-attribute instance-attribute

LTE = 'lte'

CONTAINS class-attribute instance-attribute

CONTAINS = 'contains'

ONE_OF class-attribute instance-attribute

ONE_OF = 'oneOf'

IS_NOT class-attribute instance-attribute

IS_NOT = 'isNot'

DOES_NOT_CONTAIN class-attribute instance-attribute

DOES_NOT_CONTAIN = 'doesNotContain'

NOT_ONE_OF class-attribute instance-attribute

NOT_ONE_OF = 'notOneOf'

IS_BETWEEN class-attribute instance-attribute

IS_BETWEEN = 'isbetween'

MATCHES class-attribute instance-attribute

MATCHES = 'matches'

HAS_TAGS class-attribute instance-attribute

HAS_TAGS = 'hasTags'

ON_BUDGET class-attribute instance-attribute

ON_BUDGET = 'onBudget'

OFF_BUDGET class-attribute instance-attribute

OFF_BUDGET = 'offBudget'