Compare commits
12 Commits
event-sour
...
event-sour
| Author | SHA1 | Date | |
|---|---|---|---|
|
b7ba526224
|
|||
|
40744e44ba
|
|||
|
dace2e9f4a
|
|||
|
f229c5c4d0
|
|||
|
969be02a91
|
|||
|
7055f64c5e
|
|||
|
29db336b0d
|
|||
|
57bd83a651
|
|||
|
2a4c40c385
|
|||
|
f627a9738d
|
|||
|
bb8e9bb123
|
|||
|
9c46ec6072
|
@@ -1,3 +1,4 @@
|
||||
from dibbler.lib.render_tree import render_tree
|
||||
from dibbler.models import Transaction, TransactionType
|
||||
from dibbler.models.Transaction import EXPECTED_FIELDS
|
||||
|
||||
@@ -10,23 +11,19 @@ def render_transaction_log(transaction_log: list[Transaction]) -> str:
|
||||
aggregated_log = _aggregate_joint_transactions(transaction_log)
|
||||
|
||||
lines = []
|
||||
|
||||
for i, transaction in enumerate(aggregated_log):
|
||||
for transaction in aggregated_log:
|
||||
if isinstance(transaction, list):
|
||||
inner_lines = []
|
||||
is_last = i == len(aggregated_log) - 1
|
||||
lines.append(_render_transaction(transaction[0], is_last))
|
||||
for j, sub_transaction in enumerate(transaction[1:]):
|
||||
is_last_inner = j == len(transaction) - 2
|
||||
line = _render_transaction(sub_transaction, is_last_inner)
|
||||
lines.append(_render_transaction(transaction[0]))
|
||||
for sub_transaction in transaction[1:]:
|
||||
line = _render_transaction(sub_transaction)
|
||||
inner_lines.append(line)
|
||||
indented_inner_lines = _indent_lines(inner_lines, is_last=is_last)
|
||||
lines.extend(indented_inner_lines)
|
||||
lines.append(inner_lines)
|
||||
else:
|
||||
is_last = i == len(aggregated_log) - 1
|
||||
line = _render_transaction(transaction, is_last)
|
||||
line = _render_transaction(transaction)
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
return render_tree(lines)
|
||||
|
||||
|
||||
def _aggregate_joint_transactions(
|
||||
@@ -61,17 +58,7 @@ def _aggregate_joint_transactions(
|
||||
return aggregated
|
||||
|
||||
|
||||
def _indent_lines(lines: list[str], is_last: bool = False) -> list[str]:
|
||||
indented_lines = []
|
||||
for line in lines:
|
||||
if is_last:
|
||||
indented_lines.append(" " + line)
|
||||
else:
|
||||
indented_lines.append("│ " + line)
|
||||
return indented_lines
|
||||
|
||||
|
||||
def _render_transaction(transaction: Transaction, is_last: bool) -> str:
|
||||
def _render_transaction(transaction: Transaction) -> str:
|
||||
match transaction.type_:
|
||||
case TransactionType.ADD_PRODUCT:
|
||||
line = f"ADD_PRODUCT({transaction.id}, {transaction.user.name}"
|
||||
@@ -125,5 +112,4 @@ def _render_transaction(transaction: Transaction, is_last: bool) -> str:
|
||||
line = (
|
||||
f"UNKNOWN[{transaction.type_}](id={transaction.id}, user_id={transaction.user_id})"
|
||||
)
|
||||
|
||||
return "└─ " + line if is_last else "├─ " + line
|
||||
return line
|
||||
|
||||
115
dibbler/lib/render_tree.py
Normal file
115
dibbler/lib/render_tree.py
Normal file
@@ -0,0 +1,115 @@
|
||||
_TREE_CHARS = {
|
||||
"normal": {
|
||||
"vertical": "│ ",
|
||||
"branch": "├─ ",
|
||||
"last": "└─ ",
|
||||
"empty": " ",
|
||||
},
|
||||
"ascii": {
|
||||
"vertical": "| ",
|
||||
"branch": "|-- ",
|
||||
"last": "`-- ",
|
||||
"empty": " ",
|
||||
},
|
||||
}
|
||||
|
||||
assert set(_TREE_CHARS["normal"].keys()) == set(_TREE_CHARS["ascii"].keys())
|
||||
assert all(len(v) == 3 for v in _TREE_CHARS["normal"].values())
|
||||
assert all(len(v) == 4 for v in _TREE_CHARS["ascii"].values())
|
||||
|
||||
|
||||
def render_tree(
|
||||
tree: list[str | list],
|
||||
ascii_only: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Render a tree structure as a string.
|
||||
|
||||
Each item in the `tree` list can be either a string (a leaf node)
|
||||
or another list (a subtree).
|
||||
|
||||
When `ascii_only` is `True`, only ASCII characters are used for drawing the tree.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
tree = [
|
||||
"root",
|
||||
[
|
||||
"child1",
|
||||
[
|
||||
"grandchild1",
|
||||
"grandchild2",
|
||||
],
|
||||
"child2",
|
||||
],
|
||||
"root2",
|
||||
]
|
||||
print(render_tree(tree, ascii_only=False))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
├─ root
|
||||
│ ├─ child1
|
||||
│ │ ├─ grandchild1
|
||||
│ │ └─ grandchild2
|
||||
│ └─ child2
|
||||
└─ root2
|
||||
```
|
||||
|
||||
Example with ASCII only:
|
||||
|
||||
```python
|
||||
print(render_tree(tree, ascii_only=True))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
|-- root
|
||||
| |-- child1
|
||||
| | |-- grandchild1
|
||||
| | `-- grandchild2
|
||||
| `-- child2
|
||||
`-- root2
|
||||
```
|
||||
"""
|
||||
|
||||
result: list[str] = []
|
||||
for index, item in enumerate(tree):
|
||||
is_last = index == len(tree) - 1
|
||||
item_lines = _render_tree_line(item, is_last, ascii_only)
|
||||
result.extend(item_lines)
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def _render_tree_line(
|
||||
item: str | list,
|
||||
is_last: bool,
|
||||
ascii_only: bool,
|
||||
prefix: str = "",
|
||||
) -> list[str]:
|
||||
chars = _TREE_CHARS["ascii"] if ascii_only else _TREE_CHARS["normal"]
|
||||
lines: list[str] = []
|
||||
|
||||
if isinstance(item, str):
|
||||
line_prefix = chars["last"] if is_last else chars["branch"]
|
||||
item_lines = item.splitlines()
|
||||
for line_index, line in enumerate(item_lines):
|
||||
if line_index == 0:
|
||||
lines.append(f"{prefix}{line_prefix}{line}")
|
||||
else:
|
||||
lines.append(f"{prefix}{chars['vertical']}{line}")
|
||||
|
||||
elif isinstance(item, list):
|
||||
new_prefix = prefix + (chars["empty"] if is_last else chars["vertical"])
|
||||
for sub_index, sub_item in enumerate(item):
|
||||
sub_is_last = sub_index == len(item) - 1
|
||||
sub_lines = _render_tree_line(sub_item, sub_is_last, ascii_only, new_prefix)
|
||||
lines.extend(sub_lines)
|
||||
else:
|
||||
raise ValueError("Item must be either a string or a list.")
|
||||
|
||||
return lines
|
||||
@@ -8,6 +8,7 @@ from dibbler.models.Transaction import DEFAULT_INTEREST_RATE_PERCENTAGE
|
||||
# TODO: add until transaction parameter
|
||||
# TODO: add until datetime parameter
|
||||
|
||||
|
||||
def current_interest(sql_session: Session) -> int:
|
||||
result = sql_session.scalars(
|
||||
select(Transaction)
|
||||
|
||||
@@ -11,6 +11,7 @@ from dibbler.models.Transaction import (
|
||||
# TODO: add until transaction parameter
|
||||
# TODO: add until datetime parameter
|
||||
|
||||
|
||||
def current_penalty(sql_session: Session) -> tuple[int, int]:
|
||||
result = sql_session.scalars(
|
||||
select(Transaction)
|
||||
|
||||
@@ -17,7 +17,7 @@ def joint_buy_product(
|
||||
users: list[User],
|
||||
time: datetime | None = None,
|
||||
message: str | None = None,
|
||||
) -> None:
|
||||
) -> list[Transaction]:
|
||||
"""
|
||||
Create buy product transactions for multiple users at once.
|
||||
"""
|
||||
@@ -25,12 +25,18 @@ def joint_buy_product(
|
||||
if product.id is None:
|
||||
raise ValueError("Product must be persisted in the database.")
|
||||
|
||||
if instigator not in users:
|
||||
raise ValueError("Instigator must be in the list of users buying the product.")
|
||||
if instigator.id is None:
|
||||
raise ValueError("Instigator must be persisted in the database.")
|
||||
|
||||
if len(users) == 0:
|
||||
raise ValueError("At least bying one user must be specified.")
|
||||
|
||||
if any(user.id is None for user in users):
|
||||
raise ValueError("All users must be persisted in the database.")
|
||||
|
||||
if instigator not in users:
|
||||
raise ValueError("Instigator must be in the list of users buying the product.")
|
||||
|
||||
if product_count <= 0:
|
||||
raise ValueError("Product count must be positive.")
|
||||
|
||||
@@ -44,6 +50,8 @@ def joint_buy_product(
|
||||
sql_session.add(joint_transaction)
|
||||
sql_session.flush() # Ensure joint_transaction gets an ID
|
||||
|
||||
transactions = [joint_transaction]
|
||||
|
||||
for user in users:
|
||||
buy_transaction = Transaction.joint_buy_product(
|
||||
user_id=user.id,
|
||||
@@ -52,5 +60,7 @@ def joint_buy_product(
|
||||
message=message,
|
||||
)
|
||||
sql_session.add(buy_transaction)
|
||||
transactions.append(buy_transaction)
|
||||
|
||||
sql_session.commit()
|
||||
return transactions
|
||||
|
||||
@@ -8,6 +8,7 @@ from sqlalchemy import (
|
||||
bindparam,
|
||||
case,
|
||||
func,
|
||||
or_,
|
||||
select,
|
||||
)
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -26,7 +27,9 @@ def _product_owners_query(
|
||||
product_id: BindParameter[int] | int,
|
||||
use_cache: bool = True,
|
||||
until: BindParameter[datetime] | datetime | None = None,
|
||||
until_inclusive: bool = True,
|
||||
cte_name: str = "rec_cte",
|
||||
trx_subset_name: str = "trx_subset",
|
||||
) -> CTE:
|
||||
"""
|
||||
The inner query for inferring the owners of a given product.
|
||||
@@ -45,6 +48,7 @@ def _product_owners_query(
|
||||
product_id=product_id,
|
||||
use_cache=use_cache,
|
||||
until=until,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
|
||||
# Subset of transactions that we'll want to iterate over.
|
||||
@@ -59,20 +63,20 @@ def _product_owners_query(
|
||||
)
|
||||
# TODO: maybe add value constraint on ADJUST_STOCK?
|
||||
.where(
|
||||
Transaction.type_.in_(
|
||||
[
|
||||
TransactionType.ADD_PRODUCT.as_literal_column(),
|
||||
# TransactionType.BUY_PRODUCT,
|
||||
TransactionType.ADJUST_STOCK.as_literal_column(),
|
||||
# TransactionType.JOINT,
|
||||
# TransactionType.THROW_PRODUCT,
|
||||
]
|
||||
or_(
|
||||
Transaction.type_ == TransactionType.ADD_PRODUCT.as_literal_column(),
|
||||
and_(
|
||||
Transaction.type_ == TransactionType.ADJUST_STOCK.as_literal_column(),
|
||||
Transaction.product_count > CONST_ZERO,
|
||||
),
|
||||
),
|
||||
Transaction.product_id == product_id,
|
||||
CONST_TRUE if until is None else Transaction.time <= until,
|
||||
(Transaction.time <= until if until_inclusive else Transaction.time < until)
|
||||
if until is not None
|
||||
else CONST_TRUE,
|
||||
)
|
||||
.order_by(Transaction.time.desc())
|
||||
.subquery()
|
||||
.subquery(trx_subset_name)
|
||||
)
|
||||
|
||||
initial_element = select(
|
||||
@@ -117,35 +121,19 @@ def _product_owners_query(
|
||||
).label("product_count"),
|
||||
# How many products left to account for
|
||||
case(
|
||||
# Someone adds the product -> increase the number of products left to account for
|
||||
# Someone adds the product -> known owner, decrease the number of products left to account for
|
||||
(
|
||||
trx_subset.c.type_ == TransactionType.ADD_PRODUCT.as_literal_column(),
|
||||
recursive_cte.c.products_left_to_account_for - trx_subset.c.product_count,
|
||||
),
|
||||
# Someone buys/joins/throws the product -> decrease the number of products left to account for
|
||||
# (
|
||||
# trx_subset.c.type_.in_(
|
||||
# [
|
||||
# TransactionType.BUY_PRODUCT,
|
||||
# TransactionType.JOINT,
|
||||
# TransactionType.THROW_PRODUCT,
|
||||
# ]
|
||||
# ),
|
||||
# recursive_cte.c.products_left_to_account_for - trx_subset.c.product_count,
|
||||
# ),
|
||||
# Someone adjusts the stock ->
|
||||
# If adjusted upwards -> products owned by nobody, decrease products left to account for
|
||||
# If adjusted downwards -> products taken away from owners, decrease products left to account for
|
||||
# Stock got adjusted upwards -> none owner, decrease the number of products left to account for
|
||||
(
|
||||
(trx_subset.c.type_ == TransactionType.ADJUST_STOCK.as_literal_column())
|
||||
and (trx_subset.c.product_count > CONST_ZERO),
|
||||
and_(
|
||||
trx_subset.c.type_ == TransactionType.ADJUST_STOCK.as_literal_column(),
|
||||
trx_subset.c.product_count > CONST_ZERO,
|
||||
),
|
||||
recursive_cte.c.products_left_to_account_for - trx_subset.c.product_count,
|
||||
),
|
||||
# (
|
||||
# (trx_subset.c.type_ == TransactionType.ADJUST_STOCK)
|
||||
# and (trx_subset.c.product_count < 0),
|
||||
# recursive_cte.c.products_left_to_account_for + trx_subset.c.product_count,
|
||||
# ),
|
||||
else_=recursive_cte.c.products_left_to_account_for,
|
||||
).label("products_left_to_account_for"),
|
||||
)
|
||||
@@ -153,6 +141,7 @@ def _product_owners_query(
|
||||
.where(
|
||||
and_(
|
||||
trx_subset.c.i == recursive_cte.c.i + CONST_ONE,
|
||||
# Base case: stop if we've accounted for all products
|
||||
recursive_cte.c.products_left_to_account_for > CONST_ZERO,
|
||||
)
|
||||
)
|
||||
@@ -167,13 +156,16 @@ class ProductOwnersLogEntry:
|
||||
user: User | None
|
||||
products_left_to_account_for: int
|
||||
|
||||
|
||||
# TODO: add until datetime parameter
|
||||
|
||||
|
||||
def product_owners_log(
|
||||
sql_session: Session,
|
||||
product: Product,
|
||||
use_cache: bool = True,
|
||||
until: Transaction | None = None,
|
||||
until_inclusive: bool = True,
|
||||
) -> list[ProductOwnersLogEntry]:
|
||||
"""
|
||||
Returns a log of the product ownership calculation for the given product.
|
||||
@@ -191,6 +183,7 @@ def product_owners_log(
|
||||
product_id=product.id,
|
||||
use_cache=use_cache,
|
||||
until=until.time if until else None,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
|
||||
result = sql_session.execute(
|
||||
@@ -230,11 +223,13 @@ def product_owners_log(
|
||||
|
||||
# TODO: add until transaction parameter
|
||||
|
||||
|
||||
def product_owners(
|
||||
sql_session: Session,
|
||||
product: Product,
|
||||
use_cache: bool = True,
|
||||
until: datetime | None = None,
|
||||
until_inclusive: bool = True,
|
||||
) -> list[User | None]:
|
||||
"""
|
||||
Returns an ordered list of users owning the given product.
|
||||
@@ -249,6 +244,7 @@ def product_owners(
|
||||
product_id=product.id,
|
||||
use_cache=use_cache,
|
||||
until=until,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
|
||||
db_result = sql_session.execute(
|
||||
|
||||
@@ -27,8 +27,9 @@ def _product_price_query(
|
||||
product_id: int | ColumnElement[int],
|
||||
use_cache: bool = True,
|
||||
until: BindParameter[datetime] | datetime | None = None,
|
||||
until_including: BindParameter[bool] | bool = True,
|
||||
until_inclusive: bool = True,
|
||||
cte_name: str = "rec_cte",
|
||||
trx_subset_name: str = "trx_subset",
|
||||
):
|
||||
"""
|
||||
The inner query for calculating the product price.
|
||||
@@ -43,9 +44,6 @@ def _product_price_query(
|
||||
if isinstance(until, datetime):
|
||||
until = BindParameter("until", value=until)
|
||||
|
||||
if isinstance(until_including, bool):
|
||||
until_including = BindParameter("until_including", value=until_including)
|
||||
|
||||
initial_element = select(
|
||||
CONST_ZERO.label("i"),
|
||||
CONST_ZERO.label("time"),
|
||||
@@ -76,15 +74,12 @@ def _product_price_query(
|
||||
]
|
||||
),
|
||||
Transaction.product_id == product_id,
|
||||
case(
|
||||
(until_including, Transaction.time <= until),
|
||||
else_=Transaction.time < until,
|
||||
)
|
||||
(Transaction.time <= until if until_inclusive else Transaction.time < until)
|
||||
if until is not None
|
||||
else CONST_TRUE,
|
||||
)
|
||||
.order_by(Transaction.time.asc())
|
||||
.alias("trx_subset")
|
||||
.subquery(trx_subset_name)
|
||||
)
|
||||
|
||||
recursive_elements = (
|
||||
@@ -172,11 +167,13 @@ class ProductPriceLogEntry:
|
||||
|
||||
# TODO: add until datetime parameter
|
||||
|
||||
|
||||
def product_price_log(
|
||||
sql_session: Session,
|
||||
product: Product,
|
||||
use_cache: bool = True,
|
||||
until: Transaction | None = None,
|
||||
until_inclusive: bool = True,
|
||||
) -> list[ProductPriceLogEntry]:
|
||||
"""
|
||||
Calculates the price of a product and returns a log of the price changes.
|
||||
@@ -192,6 +189,7 @@ def product_price_log(
|
||||
product.id,
|
||||
use_cache=use_cache,
|
||||
until=until.time if until else None,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
|
||||
result = sql_session.execute(
|
||||
@@ -226,11 +224,13 @@ def product_price_log(
|
||||
|
||||
# TODO: add until datetime parameter
|
||||
|
||||
|
||||
def product_price(
|
||||
sql_session: Session,
|
||||
product: Product,
|
||||
use_cache: bool = True,
|
||||
until: Transaction | None = None,
|
||||
until_inclusive: bool = True,
|
||||
include_interest: bool = False,
|
||||
) -> int:
|
||||
"""
|
||||
@@ -247,6 +247,7 @@ def product_price(
|
||||
product.id,
|
||||
use_cache=use_cache,
|
||||
until=until.time if until else None,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
|
||||
# TODO: optionally verify subresults:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import Tuple
|
||||
|
||||
from sqlalchemy import (
|
||||
BindParameter,
|
||||
@@ -21,7 +22,8 @@ def _product_stock_query(
|
||||
product_id: BindParameter[int] | int,
|
||||
use_cache: bool = True,
|
||||
until: BindParameter[datetime] | datetime | None = None,
|
||||
) -> Select:
|
||||
until_inclusive: bool = True,
|
||||
) -> Select[Tuple[int]]:
|
||||
"""
|
||||
The inner query for calculating the product stock.
|
||||
"""
|
||||
@@ -60,7 +62,7 @@ def _product_stock_query(
|
||||
),
|
||||
else_=0,
|
||||
)
|
||||
)
|
||||
).label("stock")
|
||||
).where(
|
||||
Transaction.type_.in_(
|
||||
[
|
||||
@@ -72,7 +74,9 @@ def _product_stock_query(
|
||||
]
|
||||
),
|
||||
Transaction.product_id == product_id,
|
||||
Transaction.time <= until if until is not None else CONST_TRUE,
|
||||
(Transaction.time <= until if until_inclusive else Transaction.time < until)
|
||||
if until is not None
|
||||
else CONST_TRUE,
|
||||
)
|
||||
|
||||
return query
|
||||
@@ -80,11 +84,13 @@ def _product_stock_query(
|
||||
|
||||
# TODO: add until transaction parameter
|
||||
|
||||
|
||||
def product_stock(
|
||||
sql_session: Session,
|
||||
product: Product,
|
||||
use_cache: bool = True,
|
||||
until: datetime | None = None,
|
||||
until_inclusive: bool = True,
|
||||
) -> int:
|
||||
"""
|
||||
Returns the number of products in stock.
|
||||
@@ -99,6 +105,7 @@ def product_stock(
|
||||
product_id=product.id,
|
||||
use_cache=use_cache,
|
||||
until=until,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
|
||||
result = sql_session.scalars(query).one_or_none()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -16,10 +18,10 @@ def transaction_log(
|
||||
user: User | None = None,
|
||||
product: Product | None = None,
|
||||
exclusive_after: bool = False,
|
||||
after_time=None,
|
||||
after_time: datetime | None = None,
|
||||
after_transaction_id: int | None = None,
|
||||
exclusive_before: bool = False,
|
||||
before_time=None,
|
||||
before_time: datetime | None = None,
|
||||
before_transaction_id: int | None = None,
|
||||
transaction_type: list[TransactionType] | None = None,
|
||||
negate_transaction_type_filter: bool = False,
|
||||
@@ -45,7 +47,10 @@ def transaction_log(
|
||||
raise ValueError("Product must be persisted in the database.")
|
||||
|
||||
if not (after_time is None or after_transaction_id is None):
|
||||
raise ValueError("Cannot filter by both from_time and from_transaction_id.")
|
||||
raise ValueError("Cannot filter by both after_time and after_transaction_id.")
|
||||
|
||||
if not (before_time is None or before_transaction_id is None):
|
||||
raise ValueError("Cannot filter by both before_time and before_transaction_id.")
|
||||
|
||||
query = select(Transaction)
|
||||
if user is not None:
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Tuple
|
||||
|
||||
from sqlalchemy import (
|
||||
CTE,
|
||||
BindParameter,
|
||||
Float,
|
||||
Integer,
|
||||
Select,
|
||||
and_,
|
||||
case,
|
||||
cast,
|
||||
@@ -14,7 +16,7 @@ from sqlalchemy import (
|
||||
or_,
|
||||
select,
|
||||
)
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, aliased
|
||||
|
||||
from dibbler.lib.query_helpers import CONST_NONE, CONST_ONE, CONST_TRUE, CONST_ZERO, const
|
||||
from dibbler.models import (
|
||||
@@ -30,12 +32,186 @@ from dibbler.models.Transaction import (
|
||||
from dibbler.queries.product_price import _product_price_query
|
||||
|
||||
|
||||
def _joint_transaction_query(
|
||||
user_id: BindParameter[int] | int,
|
||||
use_cache: bool = True,
|
||||
until: BindParameter[datetime] | None = None,
|
||||
until_inclusive: bool = True,
|
||||
) -> Select[Tuple[int, int, int]]:
|
||||
"""
|
||||
The inner query for getting joint transactions relevant to a user.
|
||||
|
||||
This scans for JOINT_BUY_PRODUCT transactions made by the user,
|
||||
then finds the corresponding JOINT transactions, and counts how many "shares"
|
||||
of the joint transaction the user has, as well as the total number of shares.
|
||||
"""
|
||||
|
||||
# First, select all joint buy product transactions for the given user
|
||||
# sub_joint_transaction = aliased(Transaction, name="right_trx")
|
||||
sub_joint_transaction = (
|
||||
select(Transaction.joint_transaction_id.distinct().label("joint_transaction_id"))
|
||||
.where(
|
||||
Transaction.type_ == TransactionType.JOINT_BUY_PRODUCT.as_literal_column(),
|
||||
Transaction.user_id == user_id,
|
||||
)
|
||||
.subquery("sub_joint_transaction")
|
||||
)
|
||||
|
||||
# Join those with their main joint transaction
|
||||
# (just use Transaction)
|
||||
|
||||
# Then, count how many users are involved in each joint transaction
|
||||
joint_transaction_count = aliased(Transaction, name="count_trx")
|
||||
|
||||
joint_transaction = (
|
||||
select(
|
||||
Transaction.id,
|
||||
# Shares the user has in the transaction,
|
||||
func.sum(
|
||||
case(
|
||||
(joint_transaction_count.user_id == user_id, CONST_ONE),
|
||||
else_=CONST_ZERO,
|
||||
)
|
||||
).label("user_shares"),
|
||||
# The total number of shares in the transaction,
|
||||
func.count(joint_transaction_count.id).label("user_count"),
|
||||
)
|
||||
.select_from(sub_joint_transaction)
|
||||
.join(
|
||||
Transaction,
|
||||
onclause=Transaction.id == sub_joint_transaction.c.joint_transaction_id,
|
||||
)
|
||||
.join(
|
||||
joint_transaction_count,
|
||||
onclause=joint_transaction_count.joint_transaction_id == Transaction.id,
|
||||
)
|
||||
.where(
|
||||
(
|
||||
sub_joint_transaction.c.time <= until
|
||||
if until_inclusive
|
||||
else sub_joint_transaction.c.time < until
|
||||
)
|
||||
if until is not None
|
||||
else CONST_TRUE,
|
||||
)
|
||||
.group_by(joint_transaction_count.joint_transaction_id)
|
||||
)
|
||||
|
||||
return joint_transaction
|
||||
|
||||
|
||||
def _non_joint_transaction_query(
|
||||
user_id: BindParameter[int] | int,
|
||||
use_cache: bool = True,
|
||||
until: BindParameter[datetime] | None = None,
|
||||
until_inclusive: bool = True,
|
||||
) -> Select[Tuple[int, None, None]]:
|
||||
"""
|
||||
The inner query for getting non-joint transactions relevant to a user.
|
||||
"""
|
||||
|
||||
query = select(
|
||||
Transaction.id,
|
||||
CONST_NONE.label("user_shares"),
|
||||
CONST_NONE.label("user_count"),
|
||||
).where(
|
||||
or_(
|
||||
and_(
|
||||
Transaction.user_id == user_id,
|
||||
Transaction.type_.in_(
|
||||
[
|
||||
TransactionType.ADD_PRODUCT.as_literal_column(),
|
||||
TransactionType.ADJUST_BALANCE.as_literal_column(),
|
||||
TransactionType.BUY_PRODUCT.as_literal_column(),
|
||||
TransactionType.TRANSFER.as_literal_column(),
|
||||
]
|
||||
),
|
||||
),
|
||||
and_(
|
||||
Transaction.type_ == TransactionType.TRANSFER.as_literal_column(),
|
||||
Transaction.transfer_user_id == user_id,
|
||||
),
|
||||
Transaction.type_.in_(
|
||||
[
|
||||
TransactionType.THROW_PRODUCT.as_literal_column(),
|
||||
TransactionType.ADJUST_INTEREST.as_literal_column(),
|
||||
TransactionType.ADJUST_PENALTY.as_literal_column(),
|
||||
]
|
||||
),
|
||||
),
|
||||
(Transaction.time <= until if until_inclusive else Transaction.time < until)
|
||||
if until is not None
|
||||
else CONST_TRUE,
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def _product_cost_expression(
|
||||
product_count_column,
|
||||
product_id_column,
|
||||
interest_rate_percent_column,
|
||||
user_balance_column,
|
||||
penalty_threshold_column,
|
||||
penalty_multiplier_percent_column,
|
||||
use_cache: bool = True,
|
||||
until: BindParameter[datetime] | None = None,
|
||||
until_inclusive: bool = True,
|
||||
cte_name: str = "product_price_cte",
|
||||
trx_subset_name: str = "product_price_trx_subset",
|
||||
):
|
||||
expression = (
|
||||
product_count_column
|
||||
# Price of a single product, accounted for penalties and interest.
|
||||
* cast(
|
||||
func.ceil(
|
||||
# TODO: This can get quite expensive real quick, so we should do some caching of the
|
||||
# product prices somehow.
|
||||
# Base price
|
||||
(
|
||||
# FIXME: this always returns 0 for some reason...
|
||||
select(cast(column("price"), Float))
|
||||
.select_from(
|
||||
_product_price_query(
|
||||
product_id_column,
|
||||
use_cache=use_cache,
|
||||
until=until,
|
||||
until_inclusive=until_inclusive,
|
||||
cte_name=cte_name,
|
||||
trx_subset_name=trx_subset_name,
|
||||
)
|
||||
)
|
||||
.order_by(column("i").desc())
|
||||
.limit(CONST_ONE)
|
||||
).scalar_subquery()
|
||||
# TODO: should interest be applied before or after the penalty multiplier?
|
||||
# at the moment of writing, after sound right, but maybe ask someone?
|
||||
# Interest
|
||||
* (cast(interest_rate_percent_column, Float) / const(100))
|
||||
# TODO: these should be added together, not multiplied, see specification
|
||||
# Penalty
|
||||
* case(
|
||||
(
|
||||
user_balance_column < penalty_threshold_column,
|
||||
(cast(penalty_multiplier_percent_column, Float) / const(100)),
|
||||
),
|
||||
else_=const(1.0),
|
||||
)
|
||||
),
|
||||
Integer,
|
||||
)
|
||||
)
|
||||
|
||||
return expression
|
||||
|
||||
|
||||
def _user_balance_query(
|
||||
user_id: BindParameter[int] | int,
|
||||
use_cache: bool = True,
|
||||
until: BindParameter[datetime] | BindParameter[None] | datetime | None = None,
|
||||
until_including: BindParameter[bool] | bool = True,
|
||||
until: BindParameter[datetime] | datetime | None = None,
|
||||
until_inclusive: bool = True,
|
||||
cte_name: str = "rec_cte",
|
||||
trx_subset_name: str = "trx_subset",
|
||||
) -> CTE:
|
||||
"""
|
||||
The inner query for calculating the user's balance.
|
||||
@@ -48,10 +224,7 @@ def _user_balance_query(
|
||||
user_id = BindParameter("user_id", value=user_id)
|
||||
|
||||
if isinstance(until, datetime):
|
||||
until = BindParameter("until", value=until, type_=datetime)
|
||||
|
||||
if isinstance(until_including, bool):
|
||||
until_including = BindParameter("until_including", value=until_including, type_=bool)
|
||||
until = BindParameter("until", value=until)
|
||||
|
||||
initial_element = select(
|
||||
CONST_ZERO.label("i"),
|
||||
@@ -65,12 +238,30 @@ def _user_balance_query(
|
||||
|
||||
recursive_cte = initial_element.cte(name=cte_name, recursive=True)
|
||||
|
||||
trx_subset_subset = (
|
||||
_non_joint_transaction_query(
|
||||
user_id=user_id,
|
||||
use_cache=use_cache,
|
||||
until=until,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
.union_all(
|
||||
_joint_transaction_query(
|
||||
user_id=user_id,
|
||||
use_cache=use_cache,
|
||||
until=until,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
)
|
||||
.subquery(f"{trx_subset_name}_subset")
|
||||
)
|
||||
|
||||
# Subset of transactions that we'll want to iterate over.
|
||||
trx_subset = (
|
||||
select(
|
||||
func.row_number().over(order_by=Transaction.time.asc()).label("i"),
|
||||
Transaction.amount,
|
||||
Transaction.id,
|
||||
Transaction.amount,
|
||||
Transaction.interest_rate_percent,
|
||||
Transaction.penalty_multiplier_percent,
|
||||
Transaction.penalty_threshold,
|
||||
@@ -79,44 +270,16 @@ def _user_balance_query(
|
||||
Transaction.time,
|
||||
Transaction.transfer_user_id,
|
||||
Transaction.type_,
|
||||
trx_subset_subset.c.user_shares,
|
||||
trx_subset_subset.c.user_count,
|
||||
)
|
||||
.where(
|
||||
or_(
|
||||
and_(
|
||||
Transaction.user_id == user_id,
|
||||
Transaction.type_.in_(
|
||||
[
|
||||
TransactionType.ADD_PRODUCT.as_literal_column(),
|
||||
TransactionType.ADJUST_BALANCE.as_literal_column(),
|
||||
TransactionType.BUY_PRODUCT.as_literal_column(),
|
||||
TransactionType.TRANSFER.as_literal_column(),
|
||||
# TODO: join this with the JOINT transactions, and determine
|
||||
# how much the current user paid for the product.
|
||||
TransactionType.JOINT_BUY_PRODUCT.as_literal_column(),
|
||||
]
|
||||
),
|
||||
),
|
||||
and_(
|
||||
Transaction.type_ == TransactionType.TRANSFER.as_literal_column(),
|
||||
Transaction.transfer_user_id == user_id,
|
||||
),
|
||||
Transaction.type_.in_(
|
||||
[
|
||||
TransactionType.THROW_PRODUCT.as_literal_column(),
|
||||
TransactionType.ADJUST_INTEREST.as_literal_column(),
|
||||
TransactionType.ADJUST_PENALTY.as_literal_column(),
|
||||
]
|
||||
),
|
||||
),
|
||||
case(
|
||||
(until_including, Transaction.time <= until),
|
||||
else_=Transaction.time < until,
|
||||
)
|
||||
if until is not None
|
||||
else CONST_TRUE,
|
||||
.select_from(trx_subset_subset)
|
||||
.join(
|
||||
Transaction,
|
||||
onclause=Transaction.id == trx_subset_subset.c.id,
|
||||
)
|
||||
.order_by(Transaction.time.asc())
|
||||
.alias("trx_subset")
|
||||
.subquery(trx_subset_name)
|
||||
)
|
||||
|
||||
recursive_elements = (
|
||||
@@ -139,48 +302,41 @@ def _user_balance_query(
|
||||
(
|
||||
trx_subset.c.type_ == TransactionType.BUY_PRODUCT.as_literal_column(),
|
||||
recursive_cte.c.balance
|
||||
- (
|
||||
trx_subset.c.product_count
|
||||
# Price of a single product, accounted for penalties and interest.
|
||||
* cast(
|
||||
func.ceil(
|
||||
# TODO: This can get quite expensive real quick, so we should do some caching of the
|
||||
# product prices somehow.
|
||||
# Base price
|
||||
(
|
||||
# FIXME: this always returns 0 for some reason...
|
||||
select(cast(column("price"), Float))
|
||||
.select_from(
|
||||
_product_price_query(
|
||||
trx_subset.c.product_id,
|
||||
use_cache=use_cache,
|
||||
until=trx_subset.c.time,
|
||||
until_including=False,
|
||||
cte_name="product_price_cte",
|
||||
)
|
||||
)
|
||||
.order_by(column("i").desc())
|
||||
.limit(CONST_ONE)
|
||||
).scalar_subquery()
|
||||
# TODO: should interest be applied before or after the penalty multiplier?
|
||||
# at the moment of writing, after sound right, but maybe ask someone?
|
||||
# Interest
|
||||
* (cast(recursive_cte.c.interest_rate_percent, Float) / const(100))
|
||||
# TODO: these should be added together, not multiplied, see specification
|
||||
# Penalty
|
||||
* case(
|
||||
(
|
||||
recursive_cte.c.balance < recursive_cte.c.penalty_threshold,
|
||||
(
|
||||
cast(recursive_cte.c.penalty_multiplier_percent, Float)
|
||||
/ const(100)
|
||||
),
|
||||
),
|
||||
else_=const(1.0),
|
||||
)
|
||||
),
|
||||
Integer,
|
||||
- _product_cost_expression(
|
||||
product_count_column=trx_subset.c.product_count,
|
||||
product_id_column=trx_subset.c.product_id,
|
||||
interest_rate_percent_column=recursive_cte.c.interest_rate_percent,
|
||||
user_balance_column=recursive_cte.c.balance,
|
||||
penalty_threshold_column=recursive_cte.c.penalty_threshold,
|
||||
penalty_multiplier_percent_column=recursive_cte.c.penalty_multiplier_percent,
|
||||
use_cache=use_cache,
|
||||
until=until,
|
||||
until_inclusive=until_inclusive,
|
||||
cte_name=f"{cte_name}_price",
|
||||
trx_subset_name=f"{trx_subset_name}_price",
|
||||
),
|
||||
),
|
||||
# Joint transaction -> balance decreases proportionally
|
||||
(
|
||||
trx_subset.c.type_ == TransactionType.JOINT.as_literal_column(),
|
||||
recursive_cte.c.balance
|
||||
- func.ceil(
|
||||
_product_cost_expression(
|
||||
product_count_column=trx_subset.c.product_count,
|
||||
product_id_column=trx_subset.c.product_id,
|
||||
interest_rate_percent_column=recursive_cte.c.interest_rate_percent,
|
||||
user_balance_column=recursive_cte.c.balance,
|
||||
penalty_threshold_column=recursive_cte.c.penalty_threshold,
|
||||
penalty_multiplier_percent_column=recursive_cte.c.penalty_multiplier_percent,
|
||||
use_cache=use_cache,
|
||||
until=until,
|
||||
until_inclusive=until_inclusive,
|
||||
cte_name=f"{cte_name}_joint_price",
|
||||
trx_subset_name=f"{trx_subset_name}_joint_price",
|
||||
)
|
||||
# TODO: move this inside of the product cost expression
|
||||
* trx_subset.c.user_shares
|
||||
/ trx_subset.c.user_count
|
||||
),
|
||||
),
|
||||
# Transfers money to self -> balance increases
|
||||
@@ -200,8 +356,7 @@ def _user_balance_query(
|
||||
recursive_cte.c.balance - trx_subset.c.amount,
|
||||
),
|
||||
# Throws a product -> if the user is considered to have bought it, balance increases
|
||||
# TODO:
|
||||
# (
|
||||
# TODO: # (
|
||||
# trx_subset.c.type_ == TransactionType.THROW_PRODUCT,
|
||||
# recursive_cte.c.balance + trx_subset.c.amount,
|
||||
# ),
|
||||
@@ -259,13 +414,16 @@ class UserBalanceLogEntry:
|
||||
|
||||
# return self.transaction.type_ == TransactionType.BUY_PRODUCT and prev?
|
||||
|
||||
|
||||
# TODO: add until datetime parameter
|
||||
|
||||
|
||||
def user_balance_log(
|
||||
sql_session: Session,
|
||||
user: User,
|
||||
use_cache: bool = True,
|
||||
until: Transaction | None = None,
|
||||
until_inclusive: bool = True,
|
||||
) -> list[UserBalanceLogEntry]:
|
||||
"""
|
||||
Returns a log of the user's balance over time, including interest and penalty adjustments.
|
||||
@@ -283,6 +441,7 @@ def user_balance_log(
|
||||
user.id,
|
||||
use_cache=use_cache,
|
||||
until=until.time if until else None,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
|
||||
result = sql_session.execute(
|
||||
@@ -321,11 +480,13 @@ def user_balance_log(
|
||||
|
||||
# TODO: add until datetime parameter
|
||||
|
||||
|
||||
def user_balance(
|
||||
sql_session: Session,
|
||||
user: User,
|
||||
use_cache: bool = True,
|
||||
until: Transaction | None = None,
|
||||
until_inclusive: bool = True,
|
||||
) -> int:
|
||||
"""
|
||||
Calculates the balance of a user.
|
||||
@@ -340,6 +501,7 @@ def user_balance(
|
||||
user.id,
|
||||
use_cache=use_cache,
|
||||
until=until.time if until else None,
|
||||
until_inclusive=until_inclusive,
|
||||
)
|
||||
|
||||
result = sql_session.scalar(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# TODO: implement me
|
||||
@@ -1,4 +1,4 @@
|
||||
# Economics
|
||||
# Dibbler economy spec v1
|
||||
|
||||
This document provides an overview of how dibbler counts and calculates its running event log.
|
||||
|
||||
@@ -161,27 +161,52 @@ new_user_balance = user_balance - (products_bought * (product_price + penalty +
|
||||
|
||||
### When your balance is above the penalty threshold before buying, but the purchase pushes you below the threshold
|
||||
|
||||
TODO:
|
||||
When your balance is above the penalty threshold before buying, but the purchase pushes you below the threshold, the system not apply any penalty for the purchase. The entire purchase is done at the normal product price plus any interest.
|
||||
|
||||
```python
|
||||
new_user_balance = user_balance - (products_bought * product_price * (1 + interest_rate))
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> In the case where you are performing multiple transactions at once, the system should try its best to order the purchases in a way that minimizes the amount of penalties you need to pay.
|
||||
|
||||
### Joint purchases, when all users are above the penalty threshold and stays above the threshold
|
||||
|
||||
TODO: how does rounding work here, does one user pay more than the other?
|
||||
When making joint purchases (multiple users buying products together), and all users are above the penalty threshold before and after the purchase, the total cost (including interest) will be split equally between all users. The price will be rounded up for each user after splitting the bill.
|
||||
|
||||
TODO: ordering the purchases in favor of the user.
|
||||
```python
|
||||
total_cost = product_price * products_bought * (1 + interest_rate)
|
||||
cost_per_user = math.ceil(total_cost / number_of_users)
|
||||
new_user_balance = user_balance - cost_per_user
|
||||
```
|
||||
|
||||
When performing joint purchases (multiple users
|
||||
### Joint purchases where a user appears more than one time
|
||||
|
||||
When a user appears more than once in a joint purchase (e.g. two people buying together, but one of them is buying twice as much as the other), the system will treat these subtransactions as a single transaction for the purpose of calculating penalties and interest. You can think of it as if the user is having shares in the joint purchase.
|
||||
|
||||
### Joint purchases when one or more users are below the penalty threshold
|
||||
|
||||
TODO
|
||||
The cost for each user will be calculated as usual, but for the users who are below the penalty threshold, the penalty will also be calculated and added to this user's cost. The penalty is calculated based on the share of the total purchase that this user is responsible for.
|
||||
|
||||
```python
|
||||
total_cost = product_price * products_bought * (1 + interest_rate)
|
||||
cost_per_user = math.ceil(total_cost / number_of_users)
|
||||
penalty_for_user = math.ceil((product_price * products_bought / number_of_users) * (penalty_multiplier - 1))
|
||||
new_user_balance = user_balance - (cost_per_user * penalty_multiplier) - penalty_for_user
|
||||
```
|
||||
|
||||
### Joint purchases when one or more users will end up below the penalty threshold after the purchase
|
||||
|
||||
TODO
|
||||
Just as the single-user case, if a user who is part of a joint purchase is above the penalty threshold before the purchase, but will end up below the threshold after the purchase, no penalty will be applied to that user for this purchase. The entire cost (including interest) will be split equally between all users.
|
||||
|
||||
```python
|
||||
total_cost = product_price * products_bought * (1 + interest_rate)
|
||||
cost_per_user = math.ceil(total_cost / number_of_users)
|
||||
new_user_balance = user_balance - cost_per_user
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> In the case where you (and others) are performing multiple transactions at once, the system should try its best to order the purchases in a way that minimizes the amount of penalties you need to pay.
|
||||
|
||||
## Who owns a product
|
||||
|
||||
@@ -197,12 +222,26 @@ Upon throwing away products (not manual adjustment), the system will pull money
|
||||
|
||||
## Other actions
|
||||
|
||||
Transfers
|
||||
### Transfers
|
||||
|
||||
Note about self-transfers
|
||||
You can transfer money from one user to another. The amount transferred will be deducted from the sender's balance and added to the receiver's balance without any interest or penalty applied.
|
||||
|
||||
Balance adjustments
|
||||
```python
|
||||
new_sender_balance = sender_balance - amount_transferred
|
||||
new_receiver_balance = receiver_balance + amount_transferred
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Transfers from one user to itself are not allowed.
|
||||
|
||||
### Balance adjustments
|
||||
|
||||
You can manually adjust a user's balance. This action will not have any multipliers of any kind applied, and will simply add or subtract the specified amount from the user's balance.
|
||||
|
||||
```python
|
||||
new_user_balance = user_balance + adjustment_amount
|
||||
```
|
||||
|
||||
## Updating the economy specification
|
||||
|
||||
Keep old logic, database rows tagged with spec version.
|
||||
All transactions in the database are tagged with the economy specification version they were created under. If you are to update this document with changes to how the economy works, and change the software accordingly, you will want to keep the old logic around and bump the version number. This way, the old event log is still valid, and will be aggregated using the old logic, while new transactions will user the logic applicable to the version they were created under.
|
||||
|
||||
@@ -123,24 +123,6 @@ def test_transaction_buy_product_more_than_stock(sql_session: Session) -> None:
|
||||
assert product_stock(sql_session, product) == 1 - 10
|
||||
|
||||
|
||||
def test_transaction_buy_product_dont_allow_no_add_product_transactions(
|
||||
sql_session: Session,
|
||||
) -> None:
|
||||
user, product = insert_test_data(sql_session)
|
||||
|
||||
transaction = Transaction.buy_product(
|
||||
time=datetime(2023, 10, 1, 12, 0, 0),
|
||||
product_count=1,
|
||||
user_id=user.id,
|
||||
product_id=product.id,
|
||||
)
|
||||
|
||||
sql_session.add(transaction)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
sql_session.commit()
|
||||
|
||||
|
||||
def test_transaction_add_product_deny_amount_over_per_product_times_product_count(
|
||||
sql_session: Session,
|
||||
) -> None:
|
||||
|
||||
@@ -15,6 +15,18 @@ def insert_test_data(sql_session: Session) -> User:
|
||||
return user
|
||||
|
||||
|
||||
def test_adjust_interest_unitialized_user(sql_session: Session) -> None:
|
||||
user = User("Uninitialized User")
|
||||
|
||||
with pytest.raises(ValueError, match="User must be persisted in the database."):
|
||||
adjust_interest(
|
||||
sql_session,
|
||||
user=user,
|
||||
new_interest=4,
|
||||
message="Attempting to adjust interest for uninitialized user",
|
||||
)
|
||||
|
||||
|
||||
def test_adjust_interest_no_history(sql_session: Session) -> None:
|
||||
user = insert_test_data(sql_session)
|
||||
|
||||
|
||||
@@ -19,6 +19,19 @@ def insert_test_data(sql_session: Session) -> User:
|
||||
return user
|
||||
|
||||
|
||||
def test_adjust_penalty_unitialized_user(sql_session: Session) -> None:
|
||||
user = User("Uninitialized User")
|
||||
|
||||
with pytest.raises(ValueError, match="User must be persisted in the database."):
|
||||
adjust_penalty(
|
||||
sql_session,
|
||||
user=user,
|
||||
new_penalty=-100,
|
||||
new_penalty_multiplier=110,
|
||||
message="Attempting to adjust penalty for uninitialized user",
|
||||
)
|
||||
|
||||
|
||||
def test_adjust_penalty_no_history(sql_session: Session) -> None:
|
||||
user = insert_test_data(sql_session)
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ def insert_test_data(sql_session: Session) -> tuple[User, User, User, Product]:
|
||||
return user1, user2, user3, product
|
||||
|
||||
|
||||
def test_joint_buy_product_missing_product(sql_session: Session) -> None:
|
||||
def test_joint_buy_product_uninitialized_product(sql_session: Session) -> None:
|
||||
user = User("Test User 1")
|
||||
sql_session.add(user)
|
||||
sql_session.commit()
|
||||
|
||||
product = Product("1234567890123", "Test Product")
|
||||
product = Product("1234567890123", "Uninitialized Product")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
joint_buy_product(
|
||||
@@ -50,18 +50,42 @@ def test_joint_buy_product_missing_product(sql_session: Session) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_joint_buy_product_missing_user(sql_session: Session) -> None:
|
||||
user = User("Test User 1")
|
||||
|
||||
product = Product("1234567890123", "Test Product")
|
||||
sql_session.add(product)
|
||||
sql_session.commit()
|
||||
def test_joint_buy_product_no_users(sql_session: Session) -> None:
|
||||
user, _, _, product = insert_test_data(sql_session)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
joint_buy_product(
|
||||
sql_session,
|
||||
instigator=user,
|
||||
users=[user],
|
||||
users=[],
|
||||
product=product,
|
||||
product_count=1,
|
||||
)
|
||||
|
||||
|
||||
def test_joint_buy_product_uninitialized_instigator(sql_session: Session) -> None:
|
||||
user, user2, _, product = insert_test_data(sql_session)
|
||||
|
||||
uninitialized_user = User("Uninitialized User")
|
||||
with pytest.raises(ValueError):
|
||||
joint_buy_product(
|
||||
sql_session,
|
||||
instigator=uninitialized_user,
|
||||
users=[user, user2],
|
||||
product=product,
|
||||
product_count=1,
|
||||
)
|
||||
|
||||
|
||||
def test_joint_buy_product_uninitialized_user_in_list(sql_session: Session) -> None:
|
||||
user, _, _, product = insert_test_data(sql_session)
|
||||
|
||||
uninitialized_user = User("Uninitialized User")
|
||||
with pytest.raises(ValueError):
|
||||
joint_buy_product(
|
||||
sql_session,
|
||||
instigator=user,
|
||||
users=[user, uninitialized_user],
|
||||
product=product,
|
||||
product_count=1,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
from pprint import pprint
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import Product, User
|
||||
@@ -20,6 +21,17 @@ def insert_test_data(sql_session: Session) -> tuple[Product, User]:
|
||||
return product, user
|
||||
|
||||
|
||||
def test_product_owners_unitilialized_product(sql_session: Session) -> None:
|
||||
user = User("testuser")
|
||||
sql_session.add(user)
|
||||
sql_session.commit()
|
||||
|
||||
product = Product("1234567890123", "Uninitialized Product")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
product_owners(sql_session, product)
|
||||
|
||||
|
||||
def test_product_owners_no_transactions(sql_session: Session) -> None:
|
||||
product, _ = insert_test_data(sql_session)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import math
|
||||
from datetime import datetime
|
||||
from pprint import pprint
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import Product, Transaction, User
|
||||
@@ -22,6 +23,17 @@ def insert_test_data(sql_session: Session) -> tuple[User, Product]:
|
||||
return user, product
|
||||
|
||||
|
||||
def test_product_price_uninitialized_product(sql_session: Session) -> None:
|
||||
user = User("Test User")
|
||||
sql_session.add(user)
|
||||
sql_session.commit()
|
||||
|
||||
product = Product("1234567890123", "Uninitialized Product")
|
||||
|
||||
with pytest.raises(ValueError, match="Product must be persisted in the database."):
|
||||
product_price(sql_session, product)
|
||||
|
||||
|
||||
def test_product_price_no_transactions(sql_session: Session) -> None:
|
||||
_, product = insert_test_data(sql_session)
|
||||
|
||||
@@ -295,7 +307,6 @@ def test_product_price_with_negative_stock_single_addition(sql_session: Session)
|
||||
assert product1_price == 22
|
||||
|
||||
|
||||
# TODO: what happens when stock is still negative and yet new products are added?
|
||||
def test_product_price_with_negative_stock_multiple_additions(sql_session: Session) -> None:
|
||||
user, product = insert_test_data(sql_session)
|
||||
|
||||
@@ -337,9 +348,9 @@ def test_product_price_with_negative_stock_multiple_additions(sql_session: Sessi
|
||||
|
||||
pprint(product_price_log(sql_session, product))
|
||||
|
||||
# Stock went subzero, price should be the ceiled average of the last added products
|
||||
# Stock went subzero, price should be the last added product price
|
||||
product1_price = product_price(sql_session, product)
|
||||
assert product1_price == math.ceil((22 + 29 * 2) / (1 + 2))
|
||||
assert product1_price == math.ceil(29)
|
||||
|
||||
|
||||
def test_product_price_joint_transactions(sql_session: Session) -> None:
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import Product, Transaction, User
|
||||
from dibbler.models.TransactionType import TransactionTypeSQL
|
||||
from dibbler.queries import joint_buy_product, product_stock
|
||||
|
||||
|
||||
@@ -18,6 +16,17 @@ def insert_test_data(sql_session: Session) -> tuple[User, Product]:
|
||||
return user, product
|
||||
|
||||
|
||||
def test_product_stock_uninitialized_product(sql_session: Session) -> None:
|
||||
user = User("Test User 1")
|
||||
sql_session.add(user)
|
||||
sql_session.commit()
|
||||
|
||||
product = Product("1234567890123", "Uninitialized Product")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
product_stock(sql_session, product)
|
||||
|
||||
|
||||
def test_product_stock_basic_history(sql_session: Session) -> None:
|
||||
user, product = insert_test_data(sql_session)
|
||||
|
||||
|
||||
@@ -83,6 +83,76 @@ def insert_default_test_transactions(
|
||||
return transactions
|
||||
|
||||
|
||||
def test_transaction_log_uninitialized_user(sql_session: Session) -> None:
|
||||
user = User("Uninitialized User")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
transaction_log(sql_session, user=user)
|
||||
|
||||
|
||||
def test_transaction_log_uninitialized_product(sql_session: Session) -> None:
|
||||
product = Product("1234567890123", "Uninitialized Product")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
transaction_log(sql_session, product=product)
|
||||
|
||||
|
||||
def test_transaction_log_after_product_and_user_not_allowed(sql_session: Session) -> None:
|
||||
user, user2, product, product2 = insert_test_data(sql_session)
|
||||
insert_default_test_transactions(sql_session, user, user2, product, product2)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
transaction_log(
|
||||
sql_session,
|
||||
user=user,
|
||||
product=product,
|
||||
after_time=datetime(2023, 10, 1, 11, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def test_transaction_log_after_datetime_and_transaction_id_not_allowed(
|
||||
sql_session: Session,
|
||||
) -> None:
|
||||
user, user2, product, product2 = insert_test_data(sql_session)
|
||||
insert_default_test_transactions(sql_session, user, user2, product, product2)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
transaction_log(
|
||||
sql_session,
|
||||
user=user,
|
||||
after_time=datetime(2023, 10, 1, 11, 0, 0),
|
||||
after_transaction_id=1,
|
||||
)
|
||||
|
||||
|
||||
def test_user_transactions_before_product_and_user_not_allowed(sql_session: Session) -> None:
|
||||
user, user2, product, product2 = insert_test_data(sql_session)
|
||||
insert_default_test_transactions(sql_session, user, user2, product, product2)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
transaction_log(
|
||||
sql_session,
|
||||
user=user,
|
||||
product=product,
|
||||
before_time=datetime(2023, 10, 1, 15, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def test_transaction_log_before_datetime_and_transaction_id_not_allowed(
|
||||
sql_session: Session,
|
||||
) -> None:
|
||||
user, user2, product, product2 = insert_test_data(sql_session)
|
||||
insert_default_test_transactions(sql_session, user, user2, product, product2)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
transaction_log(
|
||||
sql_session,
|
||||
user=user,
|
||||
before_time=datetime(2023, 10, 1, 15, 0, 0),
|
||||
before_transaction_id=1,
|
||||
)
|
||||
|
||||
|
||||
def test_user_transactions_no_transactions(sql_session: Session) -> None:
|
||||
insert_test_data(sql_session)
|
||||
|
||||
@@ -91,6 +161,13 @@ def test_user_transactions_no_transactions(sql_session: Session) -> None:
|
||||
assert len(transactions) == 0
|
||||
|
||||
|
||||
def test_transaction_log_basic(sql_session: Session) -> None:
|
||||
user, user2, product, product2 = insert_test_data(sql_session)
|
||||
insert_default_test_transactions(sql_session, user, user2, product, product2)
|
||||
|
||||
assert len(transaction_log(sql_session)) == 7
|
||||
|
||||
|
||||
def test_transaction_log_filtered_by_user(sql_session: Session) -> None:
|
||||
user, user2, product, product2 = insert_test_data(sql_session)
|
||||
insert_default_test_transactions(sql_session, user, user2, product, product2)
|
||||
@@ -375,34 +452,6 @@ def test_transaction_log_before_transaction_id_after_date(sql_session: Session)
|
||||
)
|
||||
|
||||
|
||||
def test_transaction_log_after_product_and_user_not_allowed(sql_session: Session) -> None:
|
||||
user, user2, product, product2 = insert_test_data(sql_session)
|
||||
insert_default_test_transactions(sql_session, user, user2, product, product2)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
transaction_log(
|
||||
sql_session,
|
||||
user=user,
|
||||
product=product,
|
||||
after_time=datetime(2023, 10, 1, 11, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def test_transaction_log_after_datetime_and_transaction_id_not_allowed(
|
||||
sql_session: Session,
|
||||
) -> None:
|
||||
user, user2, product, product2 = insert_test_data(sql_session)
|
||||
insert_default_test_transactions(sql_session, user, user2, product, product2)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
transaction_log(
|
||||
sql_session,
|
||||
user=user,
|
||||
after_time=datetime(2023, 10, 1, 11, 0, 0),
|
||||
after_transaction_id=1,
|
||||
)
|
||||
|
||||
|
||||
def test_transaction_log_limit(sql_session: Session) -> None:
|
||||
user, user2, product, product2 = insert_test_data(sql_session)
|
||||
transactions = insert_default_test_transactions(sql_session, user, user2, product, product2)
|
||||
|
||||
@@ -8,6 +8,7 @@ from sqlalchemy.orm import Session
|
||||
from dibbler.models import Product, Transaction, User
|
||||
from dibbler.models.Transaction import DEFAULT_PENALTY_MULTIPLIER_PERCENTAGE
|
||||
from dibbler.queries import joint_buy_product, user_balance, user_balance_log
|
||||
from dibbler.queries.user_balance import _joint_transaction_query, _non_joint_transaction_query
|
||||
|
||||
# TODO: see if we can use pytest_runtest_makereport to print the "user_balance_log"s
|
||||
# only on failures instead of inlining it in every test function
|
||||
@@ -25,6 +26,174 @@ def insert_test_data(sql_session: Session) -> tuple[User, User, User, Product]:
|
||||
return user, user2, user3, product
|
||||
|
||||
|
||||
def _product_cost(
|
||||
per_product: int,
|
||||
product_count: int,
|
||||
interest_rate_percent: int,
|
||||
apply_penalty: bool,
|
||||
penalty_multiplier_percent: int,
|
||||
) -> int:
|
||||
base_cost = per_product * product_count
|
||||
cost_with_interest = math.ceil(base_cost * (interest_rate_percent / 100))
|
||||
if apply_penalty:
|
||||
total_cost = math.ceil(cost_with_interest * (penalty_multiplier_percent / 100))
|
||||
else:
|
||||
total_cost = cost_with_interest
|
||||
return total_cost
|
||||
|
||||
|
||||
def test_non_joint_transaction_query(sql_session) -> None:
|
||||
user1, user2, user3, product = insert_test_data(sql_session)
|
||||
|
||||
transactions = [
|
||||
Transaction.adjust_balance(
|
||||
time=datetime(2023, 10, 1, 10, 0, 0),
|
||||
user_id=user1.id,
|
||||
amount=100,
|
||||
),
|
||||
Transaction.adjust_balance(
|
||||
time=datetime(2023, 10, 1, 10, 0, 0),
|
||||
user_id=user2.id,
|
||||
amount=50,
|
||||
),
|
||||
Transaction.add_product(
|
||||
time=datetime(2023, 10, 1, 10, 0, 1),
|
||||
user_id=user2.id,
|
||||
amount=70,
|
||||
product_id=product.id,
|
||||
product_count=3,
|
||||
per_product=30,
|
||||
),
|
||||
Transaction.transfer(
|
||||
time=datetime(2023, 10, 1, 10, 0, 2),
|
||||
user_id=user1.id,
|
||||
transfer_user_id=user2.id,
|
||||
amount=50,
|
||||
),
|
||||
Transaction.transfer(
|
||||
time=datetime(2023, 10, 1, 10, 0, 3),
|
||||
user_id=user2.id,
|
||||
transfer_user_id=user3.id,
|
||||
amount=30,
|
||||
),
|
||||
]
|
||||
|
||||
sql_session.add_all(transactions)
|
||||
sql_session.commit()
|
||||
|
||||
t = transactions
|
||||
|
||||
result = [
|
||||
row[0]
|
||||
for row in sql_session.execute(
|
||||
_non_joint_transaction_query(
|
||||
user_id=user1.id,
|
||||
use_cache=False,
|
||||
),
|
||||
).all()
|
||||
]
|
||||
assert result == [t[0].id, t[3].id]
|
||||
|
||||
result = [
|
||||
row[0]
|
||||
for row in sql_session.execute(
|
||||
_non_joint_transaction_query(
|
||||
user_id=user2.id,
|
||||
use_cache=False,
|
||||
),
|
||||
).all()
|
||||
]
|
||||
assert result == [t[1].id, t[2].id, t[3].id, t[4].id]
|
||||
|
||||
result = [
|
||||
row[0]
|
||||
for row in sql_session.execute(
|
||||
_non_joint_transaction_query(
|
||||
user_id=user3.id,
|
||||
use_cache=False,
|
||||
),
|
||||
).all()
|
||||
]
|
||||
assert result == [t[4].id]
|
||||
|
||||
|
||||
def test_joint_transaction_query(sql_session: Session) -> None:
|
||||
user1, user2, user3, product = insert_test_data(sql_session)
|
||||
|
||||
j1 = joint_buy_product(
|
||||
sql_session,
|
||||
product=product,
|
||||
product_count=3,
|
||||
instigator=user1,
|
||||
users=[user1, user2],
|
||||
)
|
||||
|
||||
j2 = joint_buy_product(
|
||||
sql_session,
|
||||
product=product,
|
||||
product_count=2,
|
||||
instigator=user1,
|
||||
users=[user1, user1, user2],
|
||||
)
|
||||
|
||||
j3 = joint_buy_product(
|
||||
sql_session,
|
||||
product=product,
|
||||
product_count=2,
|
||||
instigator=user1,
|
||||
users=[user1, user3, user3],
|
||||
)
|
||||
|
||||
j4 = joint_buy_product(
|
||||
sql_session,
|
||||
product=product,
|
||||
product_count=2,
|
||||
instigator=user2,
|
||||
users=[user2, user3, user3],
|
||||
)
|
||||
|
||||
result = list(
|
||||
sql_session.execute(
|
||||
_joint_transaction_query(
|
||||
user_id=user1.id,
|
||||
use_cache=False,
|
||||
),
|
||||
).all(),
|
||||
)
|
||||
assert list(result) == [
|
||||
(j1[0].id, 1, 2),
|
||||
(j2[0].id, 2, 3),
|
||||
(j3[0].id, 1, 3),
|
||||
]
|
||||
|
||||
result = list(
|
||||
sql_session.execute(
|
||||
_joint_transaction_query(
|
||||
user_id=user2.id,
|
||||
use_cache=False,
|
||||
),
|
||||
).all(),
|
||||
)
|
||||
assert list(result) == [
|
||||
(j1[0].id, 1, 2),
|
||||
(j2[0].id, 1, 3),
|
||||
(j4[0].id, 1, 3),
|
||||
]
|
||||
|
||||
result = list(
|
||||
sql_session.execute(
|
||||
_joint_transaction_query(
|
||||
user_id=user3.id,
|
||||
use_cache=False,
|
||||
),
|
||||
).all(),
|
||||
)
|
||||
assert list(result) == [
|
||||
(j3[0].id, 2, 3),
|
||||
(j4[0].id, 2, 3),
|
||||
]
|
||||
|
||||
|
||||
def test_user_balance_no_transactions(sql_session: Session) -> None:
|
||||
user, *_ = insert_test_data(sql_session)
|
||||
|
||||
|
||||
220
uv.lock
generated
220
uv.lock
generated
@@ -133,89 +133,89 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.12.0"
|
||||
version = "7.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -1005,39 +1005,43 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.44"
|
||||
version = "2.0.45"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/81/15d7c161c9ddf0900b076b55345872ed04ff1ed6a0666e5e94ab44b0163c/sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd", size = 2140517, upload-time = "2025-10-10T15:36:15.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/d5/4abd13b245c7d91bdf131d4916fd9e96a584dac74215f8b5bc945206a974/sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa", size = 2130738, upload-time = "2025-10-10T15:36:16.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/3c/8418969879c26522019c1025171cefbb2a8586b6789ea13254ac602986c0/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e", size = 3304145, upload-time = "2025-10-10T15:34:19.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/2d/fdb9246d9d32518bda5d90f4b65030b9bf403a935cfe4c36a474846517cb/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e", size = 3304511, upload-time = "2025-10-10T15:47:05.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/fb/40f2ad1da97d5c83f6c1269664678293d3fe28e90ad17a1093b735420549/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399", size = 3235161, upload-time = "2025-10-10T15:34:21.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/cb/7cf4078b46752dca917d18cf31910d4eff6076e5b513c2d66100c4293d83/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b", size = 3261426, upload-time = "2025-10-10T15:47:07.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/3b/55c09b285cb2d55bdfa711e778bdffdd0dc3ffa052b0af41f1c5d6e582fa/sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3", size = 2105392, upload-time = "2025-10-10T15:38:20.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/23/907193c2f4d680aedbfbdf7bf24c13925e3c7c292e813326c1b84a0b878e/sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5", size = 2130293, upload-time = "2025-10-10T15:38:21.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/c4/59c7c9b068e6813c898b771204aad36683c96318ed12d4233e1b18762164/sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", size = 2139675, upload-time = "2025-10-10T16:03:31.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/ae/eeb0920537a6f9c5a3708e4a5fc55af25900216bdb4847ec29cfddf3bf3a/sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", size = 2127726, upload-time = "2025-10-10T16:03:35.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/d5/2ebbabe0379418eda8041c06b0b551f213576bfe4c2f09d77c06c07c8cc5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", size = 3327603, upload-time = "2025-10-10T15:35:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/e5/5aa65852dadc24b7d8ae75b7efb8d19303ed6ac93482e60c44a585930ea5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", size = 3337842, upload-time = "2025-10-10T15:43:45.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/92/648f1afd3f20b71e880ca797a960f638d39d243e233a7082c93093c22378/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", size = 3264558, upload-time = "2025-10-10T15:35:29.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/cf/e27d7ee61a10f74b17740918e23cbc5bc62011b48282170dc4c66da8ec0f/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", size = 3301570, upload-time = "2025-10-10T15:43:48.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/3d/3116a9a7b63e780fb402799b6da227435be878b6846b192f076d2f838654/sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", size = 2103447, upload-time = "2025-10-10T15:03:21.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/83/24690e9dfc241e6ab062df82cc0df7f4231c79ba98b273fa496fb3dd78ed/sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", size = 2130912, upload-time = "2025-10-10T15:03:24.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user