Coverage for dibbler / queries / transaction_log.py: 100%
83 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-12 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-12 18:53 +0000
1from datetime import datetime
3from sqlalchemy import BindParameter, select
4from sqlalchemy.orm import Session
6from dibbler.models import (
7 Product,
8 Transaction,
9 TransactionType,
10 User,
11)
14# TODO: should this include full joint transactions that involve a user?
15# TODO: should this involve throw-away transactions that affects a user?
16def transaction_log(
17 sql_session: Session,
18 user: User | None = None,
19 product: Product | None = None,
20 until_time: BindParameter[datetime] | datetime | None = None,
21 until_transaction: Transaction | None = None,
22 until_inclusive: bool = True,
23 after_time: BindParameter[datetime] | datetime | None = None,
24 after_transaction: Transaction | None = None,
25 after_inlcusive: bool = True,
26 transaction_type: list[TransactionType] | None = None,
27 negate_transaction_type_filter: bool = False,
28 limit: int | None = None,
29) -> list[Transaction]:
30 """
31 Retrieve the transaction log, optionally filtered.
33 Only one of `user` or `product` may be specified.
34 Only one of `until_time` or `until_transaction_id` may be specified.
35 Only one of `after_time` or `after_transaction_id` may be specified.
37 The after and after filters are inclusive by default.
38 """
40 if not (user is None or product is None):
41 raise ValueError("Cannot filter by both user and product.")
43 if isinstance(user, User):
44 if user.id is None:
45 raise ValueError("User must be persisted in the database.")
46 user_id = BindParameter("user_id", value=user.id)
47 else:
48 user_id = None
50 if isinstance(product, Product):
51 if product.id is None:
52 raise ValueError("Product must be persisted in the database.")
53 product_id = BindParameter("product_id", value=product.id)
54 else:
55 product_id = None
57 if not (until_time is None or until_transaction is None):
58 raise ValueError("Cannot filter by both after_time and after_transaction_id.")
60 if isinstance(until_time, datetime):
61 until_time = BindParameter("until_time", value=until_time)
63 if isinstance(until_transaction, Transaction):
64 if until_transaction.id is None:
65 raise ValueError("until_transaction must be persisted in the database.")
66 until_transaction_id = BindParameter("until_transaction_id", value=until_transaction.id)
67 else:
68 until_transaction_id = None
70 if not (after_time is None or after_transaction is None):
71 raise ValueError("Cannot filter by both after_time and after_transaction_id.")
73 if isinstance(after_time, datetime):
74 after_time = BindParameter("after_time", value=after_time)
76 if isinstance(after_transaction, Transaction):
77 if after_transaction.id is None:
78 raise ValueError("after_transaction must be persisted in the database.")
79 after_transaction_id = BindParameter("after_transaction_id", value=after_transaction.id)
80 else:
81 after_transaction_id = None
83 if after_time is not None and until_time is not None:
84 assert isinstance(after_time.value, datetime)
85 assert isinstance(until_time.value, datetime)
87 if after_time.value > until_time.value:
88 raise ValueError("after_time cannot be after until_time.")
90 if after_transaction is not None and until_transaction is not None:
91 assert after_transaction.time is not None
92 assert until_transaction.time is not None
94 if after_transaction.time > until_transaction.time:
95 raise ValueError("after_transaction cannot be after until_transaction.")
97 if limit is not None and limit <= 0:
98 raise ValueError("Limit must be positive.")
100 query = select(Transaction)
101 if user is not None:
102 query = query.where(Transaction.user_id == user_id)
103 if product is not None:
104 query = query.where(Transaction.product_id == product_id)
106 match (until_time, until_transaction_id, until_inclusive):
107 case (BindParameter(), None, True):
108 query = query.where(Transaction.time <= until_time)
109 case (BindParameter(), None, False):
110 query = query.where(Transaction.time < until_time)
111 case (None, BindParameter(), True):
112 query = query.where(Transaction.id <= until_transaction_id)
113 case (None, BindParameter(), False):
114 query = query.where(Transaction.id < until_transaction_id)
115 case _:
116 pass
118 match (after_time, after_transaction_id, after_inlcusive):
119 case (BindParameter(), None, True):
120 query = query.where(Transaction.time >= after_time)
121 case (BindParameter(), None, False):
122 query = query.where(Transaction.time > after_time)
123 case (None, BindParameter(), True):
124 query = query.where(Transaction.id >= after_transaction_id)
125 case (None, BindParameter(), False):
126 query = query.where(Transaction.id > after_transaction_id)
127 case _:
128 pass
130 if transaction_type is not None:
131 if negate_transaction_type_filter:
132 query = query.where(~Transaction.type_.in_(transaction_type))
133 else:
134 query = query.where(Transaction.type_.in_(transaction_type))
136 if limit is not None:
137 query = query.limit(limit)
139 query = query.order_by(Transaction.time.asc(), Transaction.id.asc())
140 result = sql_session.scalars(query).all()
142 return list(result)