From 2484a2e9a80f4743ded1d3163625478554ac6142 Mon Sep 17 00:00:00 2001 From: cursorMan Date: Mon, 5 Oct 2026 13:55:11 +0000 Subject: [PATCH 1/2] Add the Coffer store: prepaid credit and supplier invoices. Buyers pay Coffer in whole agent credits. Promotional lots are spent before funded lots. There is no cash-out. A shirt is 20 credits, and the printer is invoiced in dollars only when Coffer must procure it. --- README.md | 16 +- coffer/__init__.py | 14 ++ coffer/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 460 bytes coffer/__pycache__/ledger.cpython-312.pyc | Bin 0 -> 6190 bytes coffer/__pycache__/store.cpython-312.pyc | Bin 0 -> 8451 bytes coffer/ledger.py | 102 ++++++++++ coffer/store.py | 187 +++++++++++++++++++ design/coffer.md | 44 +++++ tests/__pycache__/test_store.cpython-312.pyc | Bin 0 -> 9849 bytes tests/test_store.py | 123 ++++++++++++ 10 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 coffer/__init__.py create mode 100644 coffer/__pycache__/__init__.cpython-312.pyc create mode 100644 coffer/__pycache__/ledger.cpython-312.pyc create mode 100644 coffer/__pycache__/store.cpython-312.pyc create mode 100644 coffer/ledger.py create mode 100644 coffer/store.py create mode 100644 design/coffer.md create mode 100644 tests/__pycache__/test_store.cpython-312.pyc create mode 100644 tests/test_store.py diff --git a/README.md b/README.md index ad3b86e..4006d9a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ -# coffer +# Coffer -Coffer, the store. Prepaid agent credit in. Supplier invoices out. \ No newline at end of file +Coffer is the store of the new internet. Buyers pay Coffer with prepaid agent credit. One credit is one US dollar. Suppliers are paid by dollar invoice from Coffer's own account. Credit is not cashed out. + +The design note is `design/coffer.md`. Quill's Design space reads `leora/design`, which this agent cannot push. The note is ready to merge there as `coffer.md`. + +## Catalog + +The opening good is the Coffer shirt at 20 credits. The printer's unit cost is 8 dollars, which is Coffer's payable, not the buyer's price. + +## Run the tests + +```bash +python3 -m unittest discover -s tests +``` diff --git a/coffer/__init__.py b/coffer/__init__.py new file mode 100644 index 0000000..74e3fb3 --- /dev/null +++ b/coffer/__init__.py @@ -0,0 +1,14 @@ +"""Coffer, the store of the new internet.""" + +from coffer.ledger import CreditKind, InsufficientCredit, Ledger +from coffer.store import CannotSupply, Store, UnknownGood, new_store + +__all__ = [ + "CannotSupply", + "CreditKind", + "InsufficientCredit", + "Ledger", + "Store", + "UnknownGood", + "new_store", +] diff --git a/coffer/__pycache__/__init__.cpython-312.pyc b/coffer/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..410f295bae3469f0df85fc45abfde2a6d64ec6d2 GIT binary patch literal 460 zcmZ9Jy-Ncz7{-&k>wWYC(beTZ5DscPxE3k4AfmVwmn@+)xq5m@jx?pz&A-CkP4K^P zbP^}CI5>!#*sYVvwGR3YKc4q}d7mecr$(a+Ozl5BULHaK-fi<+&RQ%wM%)4d5Fn5^ zIb;rUn2TKIArIP_n^%~Rd={X9g(zeZiXa#R;@v@1omUe7dbcOil*(qTE@(VaLef~I zWy9$#&bXqIQ`P#kk%_CTJxNKXPBKpP)-j(<(=;1pl&jTT`=^wQsZ2aw?+rN@YA`K| ze6A}4YprYN{ECYiKN5oI$Ryx0G6|$_{H1H*SyU_ni_ju6I7@0ez?g^;#=0>opWVvK zxPOPbzTTGRE9<{r)@YdL7~g}1u?EYXwqiwlb+#vIA=bK$amF)+ajTdw!wwTQD~fXTMOmkS|$18_Z!fGlH-X$R`yW#ndvyQ+ literal 0 HcmV?d00001 diff --git a/coffer/__pycache__/ledger.cpython-312.pyc b/coffer/__pycache__/ledger.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..877e8e203b2eca3664154088d4858a5600381371 GIT binary patch literal 6190 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 UxFSQ*4&x(FKNEULprb7LFH=M9&;S4c literal 0 HcmV?d00001 diff --git a/coffer/__pycache__/store.cpython-312.pyc b/coffer/__pycache__/store.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..879d77dc21e75dde3fc384dc0bc73a32edf58b8c GIT binary patch literal 8451 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 literal 0 HcmV?d00001 diff --git a/coffer/ledger.py b/coffer/ledger.py new file mode 100644 index 0000000..870139e --- /dev/null +++ b/coffer/ledger.py @@ -0,0 +1,102 @@ +"""Prepaid agent credit. Whole credits, one credit to one dollar. + +Funded credit is a handler top-up. Promotional credit is a grant. +Both spend only at Coffer. Neither can be withdrawn or transferred. +""" + +from dataclasses import dataclass, field +from enum import Enum + + +class CreditKind(str, Enum): + FUNDED = "funded" + PROMOTIONAL = "promotional" + + +class InsufficientCredit(Exception): + """The agent does not have enough spendable credit.""" + + +@dataclass +class Lot: + kind: CreditKind + remaining: int + reason: str = "" + + +@dataclass +class Account: + agent_id: str + lots: list[Lot] = field(default_factory=list) + + def balance(self) -> dict[str, int]: + promotional = sum( + lot.remaining for lot in self.lots if lot.kind is CreditKind.PROMOTIONAL + ) + funded = sum( + lot.remaining for lot in self.lots if lot.kind is CreditKind.FUNDED + ) + return { + "promotional": promotional, + "funded": funded, + "spendable": promotional + funded, + } + + +def _positive_int(amount: int, label: str) -> None: + if type(amount) is not int or amount <= 0: + raise ValueError(f"{label} must be a positive whole number of credits") + + +class Ledger: + def __init__(self) -> None: + self.accounts: dict[str, Account] = {} + + def account(self, agent_id: str) -> Account: + if agent_id not in self.accounts: + self.accounts[agent_id] = Account(agent_id) + return self.accounts[agent_id] + + def balance(self, agent_id: str) -> dict[str, int]: + if agent_id not in self.accounts: + return {"promotional": 0, "funded": 0, "spendable": 0} + return self.accounts[agent_id].balance() + + def top_up(self, agent_id: str, amount: int) -> Lot: + """Record a handler prepayment. The credit cannot be cashed back out.""" + _positive_int(amount, "top-up") + lot = Lot(CreditKind.FUNDED, amount, "top-up") + self.account(agent_id).lots.append(lot) + return lot + + def grant(self, agent_id: str, amount: int, reason: str = "promotional") -> Lot: + """Grant promotional credit. It spends at Coffer and cannot be cashed out.""" + _positive_int(amount, "grant") + if not reason: + raise ValueError("a grant needs a reason") + lot = Lot(CreditKind.PROMOTIONAL, amount, reason) + self.account(agent_id).lots.append(lot) + return lot + + def spend(self, agent_id: str, amount: int) -> list[dict[str, int | str]]: + """Spend at Coffer. Promotional lots are used before funded lots.""" + _positive_int(amount, "spend") + account = self.accounts.get(agent_id) + if account is None or account.balance()["spendable"] < amount: + raise InsufficientCredit(agent_id) + ordered = [ + lot + for kind in (CreditKind.PROMOTIONAL, CreditKind.FUNDED) + for lot in account.lots + if lot.kind is kind and lot.remaining + ] + left = amount + draws: list[dict[str, int | str]] = [] + for lot in ordered: + if left == 0: + break + take = min(lot.remaining, left) + lot.remaining -= take + left -= take + draws.append({"kind": lot.kind.value, "amount": take}) + return draws diff --git a/coffer/store.py b/coffer/store.py new file mode 100644 index 0000000..27a136e --- /dev/null +++ b/coffer/store.py @@ -0,0 +1,187 @@ +"""Coffer sells its own goods for prepaid credit and pays suppliers by invoice.""" + +from dataclasses import dataclass, field + +from coffer.ledger import Ledger + + +class UnknownGood(Exception): + """No good with that sku is sold by Coffer.""" + + +class CannotSupply(Exception): + """Coffer has no stock and no supplier it can order this good from.""" + + +class DuplicateOrder(Exception): + """This idempotency key was already used for a different order.""" + + +@dataclass +class Supplier: + supplier_id: str + name: str + + +@dataclass +class Good: + sku: str + name: str + credit_price: int + supplier_id: str | None = None + supplier_unit_cost: int = 0 + stock: int = 0 + + +@dataclass +class Invoice: + invoice_id: str + supplier_id: str + sku: str + quantity: int + dollars: int + status: str = "payable" + + +@dataclass +class Order: + order_id: str + buyer_id: str + sku: str + quantity: int + credits_charged: int + draws: list[dict[str, int | str]] + from_stock: int + procured: int + status: str + invoice_id: str | None = None + + +@dataclass +class Store: + ledger: Ledger = field(default_factory=Ledger) + goods: dict[str, Good] = field(default_factory=dict) + suppliers: dict[str, Supplier] = field(default_factory=dict) + orders: dict[str, Order] = field(default_factory=dict) + invoices: dict[str, Invoice] = field(default_factory=dict) + _seq: int = 0 + _idempotency: dict[str, str] = field(default_factory=dict) + + def add_supplier(self, supplier_id: str, name: str) -> Supplier: + if not supplier_id or not name: + raise ValueError("a supplier needs an id and a name") + supplier = Supplier(supplier_id, name) + self.suppliers[supplier_id] = supplier + return supplier + + def add_good( + self, + sku: str, + name: str, + credit_price: int, + supplier_id: str | None = None, + supplier_unit_cost: int = 0, + stock: int = 0, + ) -> Good: + if type(credit_price) is not int or credit_price <= 0: + raise ValueError("credit price must be a positive whole number of credits") + if type(supplier_unit_cost) is not int or supplier_unit_cost < 0: + raise ValueError("supplier cost must be a whole number of dollars") + if type(stock) is not int or stock < 0: + raise ValueError("stock must be a whole number") + if supplier_id is not None and supplier_id not in self.suppliers: + raise ValueError(f"unknown supplier {supplier_id}") + good = Good(sku, name, credit_price, supplier_id, supplier_unit_cost, stock) + self.goods[sku] = good + return good + + def receive_stock(self, sku: str, quantity: int) -> Invoice | None: + """Take goods into Coffer's own inventory. Pay the supplier by invoice, if there is one.""" + if type(quantity) is not int or quantity <= 0: + raise ValueError("quantity must be a positive whole number") + good = self.goods.get(sku) + if good is None: + raise UnknownGood(sku) + good.stock += quantity + if good.supplier_id and good.supplier_unit_cost: + return self._invoice(good.supplier_id, sku, quantity, good.supplier_unit_cost * quantity) + return None + + def place_order( + self, + buyer_id: str, + sku: str, + quantity: int = 1, + idempotency_key: str | None = None, + ) -> Order: + """Buyer pays Coffer in credit. Coffer owes the goods.""" + if idempotency_key: + existing = self._idempotency.get(idempotency_key) + if existing is not None: + order = self.orders[existing] + if order.buyer_id != buyer_id or order.sku != sku or order.quantity != quantity: + raise DuplicateOrder(idempotency_key) + return order + if type(quantity) is not int or quantity <= 0: + raise ValueError("quantity must be a positive whole number") + good = self.goods.get(sku) + if good is None: + raise UnknownGood(sku) + from_stock = min(good.stock, quantity) + procured = quantity - from_stock + if procured and (not good.supplier_id or good.supplier_id not in self.suppliers): + raise CannotSupply(sku) + draws = self.ledger.spend(buyer_id, good.credit_price * quantity) + good.stock -= from_stock + invoice_id = None + if procured: + invoice = self._invoice( + good.supplier_id, + sku, + procured, + good.supplier_unit_cost * procured, + ) + invoice_id = invoice.invoice_id + self._seq += 1 + order = Order( + order_id=f"ord-{self._seq}", + buyer_id=buyer_id, + sku=sku, + quantity=quantity, + credits_charged=good.credit_price * quantity, + draws=draws, + from_stock=from_stock, + procured=procured, + status="fulfilled" if procured == 0 else "procuring", + invoice_id=invoice_id, + ) + self.orders[order.order_id] = order + if idempotency_key: + self._idempotency[idempotency_key] = order.order_id + return order + + def _invoice(self, supplier_id: str, sku: str, quantity: int, dollars: int) -> Invoice: + self._seq += 1 + invoice = Invoice( + invoice_id=f"inv-{self._seq}", + supplier_id=supplier_id, + sku=sku, + quantity=quantity, + dollars=dollars, + ) + self.invoices[invoice.invoice_id] = invoice + return invoice + + +def new_store() -> Store: + """The opening catalog: a Coffer shirt at 20 credits.""" + store = Store() + store.add_supplier("printer", "Printer") + store.add_good( + sku="coffer-shirt", + name="Coffer shirt", + credit_price=20, + supplier_id="printer", + supplier_unit_cost=8, + ) + return store diff --git a/design/coffer.md b/design/coffer.md new file mode 100644 index 0000000..f8f8be3 --- /dev/null +++ b/design/coffer.md @@ -0,0 +1,44 @@ +--- +title: Coffer +date: 2026-10-05 +author: cursorMan +summary: Coffer is the store. Buyers pay with prepaid agent credit. Coffer buys from suppliers it knows and sells from its own inventory. Credit is not cashed out. +--- + +# Coffer + +Coffer is the store of the new internet. A purchase on the platform is a purchase from Coffer. + +Dated 5 Oct 2026. Quarry task 55798d80. The component repo is `cursorman/coffer`. This note is the Design page. Quill shows it when the file is on `main` of `leora/design`. + +## Credits + +One agent credit is one US dollar. Credits are whole numbers. + +A handler top-up creates **funded** credit. A grant creates **promotional** credit. Checkout spends promotional lots first, then funded lots. + +Credit is spent only at Coffer. It cannot be withdrawn, transferred to another agent, or redeemed by a supplier. When the spendable balance is zero, the purchase stops. + +The dollars a handler pays in stay a liability while the credit is unspent. When the credit is spent on Coffer's goods, the sale belongs to Coffer. + +## The store + +The buyer places an order with Coffer and pays the credit price Coffer set. Coffer owes the buyer the goods, including the refund if the goods are bad or never arrive. + +Coffer already knows who it can buy from. A source can be an agent or an outside seller. Coffer can fill an order from its own inventory, from one supplier, or from several. + +A supplier invoices Coffer in dollars and is paid from Coffer's own account, on Coffer's terms. The supplier never holds agent credit. The supplier's price is Coffer's cost. It is not the buyer's credit price. + +A listing is not an order. Demand can cause Coffer to send a purchase order to a supplier it has agreed to use. It does not turn every listing into one. + +## First good + +The Coffer shirt is 20 agent credits. The opening supplier cost recorded for the printer is 8 dollars. That cost is Coffer's payable, not the price the buyer pays. + +Promotional credit granted for the launch spends on the shirt the same way funded credit does. Coffer still pays the printer. + +## Component + +One repo, `cursorman/coffer`. Code and this note live there. Change goes branch, pull request, merge. Nothing is live unless it is on `main`. + +To show this page in the Design space, merge `design/coffer.md` into `leora/design` as `coffer.md`. cursorman cannot see that repo. diff --git a/tests/__pycache__/test_store.cpython-312.pyc b/tests/__pycache__/test_store.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..506c4c3e6ea075683fc52bd513a9ff44f5fb33d5 GIT binary patch literal 9849 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 L?TljsUBdqXJ?VwH literal 0 HcmV?d00001 diff --git a/tests/test_store.py b/tests/test_store.py new file mode 100644 index 0000000..22cc772 --- /dev/null +++ b/tests/test_store.py @@ -0,0 +1,123 @@ +import unittest + +from coffer import CannotSupply, InsufficientCredit, UnknownGood, new_store +from coffer.ledger import Ledger +from coffer.store import Store + + +class LedgerTests(unittest.TestCase): + def test_top_up_is_funded_and_stays_spendable(self): + ledger = Ledger() + ledger.top_up("cursorMan", 20) + self.assertEqual( + ledger.balance("cursorMan"), + {"promotional": 0, "funded": 20, "spendable": 20}, + ) + + def test_grant_is_promotional(self): + ledger = Ledger() + ledger.grant("cursorMan", 20, "launch shirt") + self.assertEqual(ledger.balance("cursorMan")["promotional"], 20) + self.assertEqual(ledger.balance("cursorMan")["funded"], 0) + + def test_spend_uses_promotional_credit_first(self): + ledger = Ledger() + ledger.grant("cursorMan", 12, "launch shirt") + ledger.top_up("cursorMan", 20) + draws = ledger.spend("cursorMan", 20) + self.assertEqual( + draws, + [ + {"kind": "promotional", "amount": 12}, + {"kind": "funded", "amount": 8}, + ], + ) + self.assertEqual(ledger.balance("cursorMan")["spendable"], 12) + self.assertEqual(ledger.balance("cursorMan")["funded"], 12) + + def test_spend_stops_at_zero(self): + ledger = Ledger() + ledger.top_up("cursorMan", 19) + with self.assertRaises(InsufficientCredit): + ledger.spend("cursorMan", 20) + self.assertEqual(ledger.balance("cursorMan")["spendable"], 19) + + def test_rejects_fractions_and_zero(self): + ledger = Ledger() + with self.assertRaises(ValueError): + ledger.top_up("cursorMan", 0) + with self.assertRaises(ValueError): + ledger.grant("cursorMan", 1.5, "launch") + + +class StoreTests(unittest.TestCase): + def test_shirt_from_catalog_procures_and_invoices_in_dollars(self): + store = new_store() + store.ledger.grant("cursorMan", 20, "launch shirt") + order = store.place_order("cursorMan", "coffer-shirt") + self.assertEqual(order.credits_charged, 20) + self.assertEqual(order.status, "procuring") + self.assertEqual(order.procured, 1) + invoice = store.invoices[order.invoice_id] + self.assertEqual(invoice.supplier_id, "printer") + self.assertEqual(invoice.dollars, 8) + self.assertEqual(invoice.status, "payable") + self.assertEqual(store.ledger.balance("cursorMan")["spendable"], 0) + self.assertNotIn("printer", store.ledger.accounts) + + def test_stock_fulfills_without_an_invoice(self): + store = new_store() + store.receive_stock("coffer-shirt", 2) + self.assertEqual(len(store.invoices), 1) + store.ledger.top_up("buyer", 40) + order = store.place_order("buyer", "coffer-shirt", 2) + self.assertEqual(order.status, "fulfilled") + self.assertEqual(order.from_stock, 2) + self.assertIsNone(order.invoice_id) + self.assertEqual(store.goods["coffer-shirt"].stock, 0) + + def test_mixed_stock_and_procurement(self): + store = new_store() + store.goods["coffer-shirt"].stock = 1 + store.ledger.top_up("buyer", 60) + order = store.place_order("buyer", "coffer-shirt", 3) + self.assertEqual(order.from_stock, 1) + self.assertEqual(order.procured, 2) + self.assertEqual(store.invoices[order.invoice_id].dollars, 16) + self.assertEqual(order.status, "procuring") + + def test_failed_spend_does_not_take_stock(self): + store = new_store() + store.goods["coffer-shirt"].stock = 1 + with self.assertRaises(InsufficientCredit): + store.place_order("buyer", "coffer-shirt") + self.assertEqual(store.goods["coffer-shirt"].stock, 1) + self.assertEqual(store.orders, {}) + + def test_unknown_good_and_no_supplier(self): + store = Store() + store.add_good("own-note", "Note", 5) + store.ledger.top_up("buyer", 5) + with self.assertRaises(UnknownGood): + store.place_order("buyer", "missing") + with self.assertRaises(CannotSupply): + store.place_order("buyer", "own-note", 1) + self.assertEqual(store.ledger.balance("buyer")["spendable"], 5) + + def test_idempotency_key_returns_the_same_order(self): + store = new_store() + store.ledger.top_up("buyer", 40) + first = store.place_order("buyer", "coffer-shirt", idempotency_key="k1") + second = store.place_order("buyer", "coffer-shirt", idempotency_key="k1") + self.assertEqual(first.order_id, second.order_id) + self.assertEqual(store.ledger.balance("buyer")["spendable"], 20) + + def test_coffer_price_is_not_the_supplier_cost(self): + store = new_store() + good = store.goods["coffer-shirt"] + self.assertEqual(good.credit_price, 20) + self.assertEqual(good.supplier_unit_cost, 8) + + +if __name__ == "__main__": + unittest.main() -- 2.47.3 From bbdac420059a325ac42645007cf4761ba760f6ff Mon Sep 17 00:00:00 2001 From: cursorMan Date: Mon, 5 Oct 2026 13:55:18 +0000 Subject: [PATCH 2/2] Drop bytecode from the branch and ignore it. --- .gitignore | 2 ++ coffer/__pycache__/__init__.cpython-312.pyc | Bin 460 -> 0 bytes coffer/__pycache__/ledger.cpython-312.pyc | Bin 6190 -> 0 bytes coffer/__pycache__/store.cpython-312.pyc | Bin 8451 -> 0 bytes tests/__pycache__/test_store.cpython-312.pyc | Bin 9849 -> 0 bytes 5 files changed, 2 insertions(+) create mode 100644 .gitignore delete mode 100644 coffer/__pycache__/__init__.cpython-312.pyc delete mode 100644 coffer/__pycache__/ledger.cpython-312.pyc delete mode 100644 coffer/__pycache__/store.cpython-312.pyc delete mode 100644 tests/__pycache__/test_store.cpython-312.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/coffer/__pycache__/__init__.cpython-312.pyc b/coffer/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 410f295bae3469f0df85fc45abfde2a6d64ec6d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 460 zcmZ9Jy-Ncz7{-&k>wWYC(beTZ5DscPxE3k4AfmVwmn@+)xq5m@jx?pz&A-CkP4K^P zbP^}CI5>!#*sYVvwGR3YKc4q}d7mecr$(a+Ozl5BULHaK-fi<+&RQ%wM%)4d5Fn5^ zIb;rUn2TKIArIP_n^%~Rd={X9g(zeZiXa#R;@v@1omUe7dbcOil*(qTE@(VaLef~I zWy9$#&bXqIQ`P#kk%_CTJxNKXPBKpP)-j(<(=;1pl&jTT`=^wQsZ2aw?+rN@YA`K| ze6A}4YprYN{ECYiKN5oI$Ryx0G6|$_{H1H*SyU_ni_ju6I7@0ez?g^;#=0>opWVvK zxPOPbzTTGRE9<{r)@YdL7~g}1u?EYXwqiwlb+#vIA=bK$amF)+ajTdw!wwTQD~fXTMOmkS|$18_Z!fGlH-X$R`yW#ndvyQ+ diff --git a/coffer/__pycache__/ledger.cpython-312.pyc b/coffer/__pycache__/ledger.cpython-312.pyc deleted file mode 100644 index 877e8e203b2eca3664154088d4858a5600381371..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6190 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 UxFSQ*4&x(FKNEULprb7LFH=M9&;S4c diff --git a/coffer/__pycache__/store.cpython-312.pyc b/coffer/__pycache__/store.cpython-312.pyc deleted file mode 100644 index 879d77dc21e75dde3fc384dc0bc73a32edf58b8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8451 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 diff --git a/tests/__pycache__/test_store.cpython-312.pyc b/tests/__pycache__/test_store.cpython-312.pyc deleted file mode 100644 index 506c4c3e6ea075683fc52bd513a9ff44f5fb33d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9849 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 L?TljsUBdqXJ?VwH -- 2.47.3