simplebooks 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ from .models import (
2
+ Account,
3
+ AccountType,
4
+ Currency,
5
+ Customer,
6
+ Entry,
7
+ EntryType,
8
+ Identity,
9
+ Ledger,
10
+ Transaction,
11
+ Vendor,
12
+ )
13
+ import sqloquent.tools
14
+
15
+ __version__ = '0.1.0'
16
+
17
+ def set_connection_info(db_file_path: str):
18
+ """Set the connection info for all models to use the specified
19
+ sqlite3 database file path.
20
+ """
21
+ Account.connection_info = db_file_path
22
+ Currency.connection_info = db_file_path
23
+ Customer.connection_info = db_file_path
24
+ Entry.connection_info = db_file_path
25
+ Identity.connection_info = db_file_path
26
+ Ledger.connection_info = db_file_path
27
+ Transaction.connection_info = db_file_path
28
+ Vendor.connection_info = db_file_path
29
+
30
+ def publish_migrations(migration_folder_path: str):
31
+ """Writes migration files for the models."""
32
+ sqloquent.tools.publish_migrations(migration_folder_path)
33
+ models = [
34
+ Account,
35
+ Currency,
36
+ Customer,
37
+ Entry,
38
+ Identity,
39
+ Ledger,
40
+ Transaction,
41
+ Vendor,
42
+ ]
43
+ for model in models:
44
+ name = model.__name__
45
+ m = sqloquent.tools.make_migration_from_model(model)
46
+ with open(f'{migration_folder_path}/create_{name}.py', 'w') as f:
47
+ f.write(m)
48
+
49
+ def automigrate(migration_folder_path: str, db_file_path: str):
50
+ """Executes the sqloquent automigrate tool."""
51
+ sqloquent.tools.automigrate(migration_folder_path, db_file_path)
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+ from sqloquent.asyncql import (
3
+ AsyncSqlModel, AsyncRelatedModel, AsyncRelatedCollection,
4
+ )
5
+ from .AccountType import AccountType
6
+ from .Entry import Entry
7
+ from .EntryType import EntryType
8
+ import packify
9
+
10
+
11
+ class Account(AsyncSqlModel):
12
+ connection_info: str = ''
13
+ table: str = 'accounts'
14
+ id_column: str = 'id'
15
+ columns: tuple[str] = (
16
+ 'id', 'name', 'type', 'ledger_id', 'parent_id', 'code',
17
+ 'category', 'details'
18
+ )
19
+ id: str
20
+ name: str
21
+ type: str
22
+ ledger_id: str
23
+ parent_id: str
24
+ code: str|None
25
+ category: str|None
26
+ details: bytes|None
27
+ ledger: AsyncRelatedModel
28
+ parent: AsyncRelatedModel
29
+ children: AsyncRelatedCollection
30
+ entries: AsyncRelatedCollection
31
+
32
+ # override automatic property
33
+ @property
34
+ def type(self) -> AccountType:
35
+ """The AccountType of the Account."""
36
+ return AccountType(self.data['type'])
37
+ @type.setter
38
+ def type(self, val: AccountType):
39
+ if type(val) is AccountType:
40
+ self.data['type'] = val.value
41
+
42
+ # override automatic property
43
+ @property
44
+ def details(self) -> packify.SerializableType:
45
+ """A packify.SerializableType stored in the database as a blob."""
46
+ return packify.unpack(self.data.get('details', None) or b'n\x00\x00\x00\x00')
47
+ @details.setter
48
+ def details(self, val: packify.SerializableType):
49
+ if isinstance(val, packify.SerializableType):
50
+ self.data['details'] = packify.pack(val)
51
+
52
+ @staticmethod
53
+ def _encode(data: dict|None) -> dict|None:
54
+ if type(data) is not dict:
55
+ return data
56
+ if type(data.get('type', None)) is AccountType:
57
+ data['type'] = data['type'].value
58
+ return data
59
+
60
+ @classmethod
61
+ async def insert(cls, data: dict) -> Account | None:
62
+ """Ensure data is encoded before inserting."""
63
+ result = await super().insert(cls._encode(data))
64
+ return result
65
+
66
+ async def balance(self, include_sub_accounts: bool = True) -> int:
67
+ """Tally all entries for this account. Includes the balances of
68
+ all sub-accounts if include_sub_accounts is True.
69
+ """
70
+ totals = {
71
+ EntryType.CREDIT: 0,
72
+ EntryType.DEBIT: 0,
73
+ 'subaccounts': 0,
74
+ }
75
+ async for entries in self.entries().query().chunk(500):
76
+ entry: Entry
77
+ for entry in entries:
78
+ totals[entry.type] += entry.amount
79
+
80
+ if include_sub_accounts:
81
+ for acct in self.children:
82
+ acct: Account
83
+ totals['subaccounts'] += await acct.balance(include_sub_accounts=True)
84
+
85
+ if self.type in (
86
+ AccountType.ASSET, AccountType.DEBIT_BALANCE,
87
+ AccountType.CONTRA_LIABILITY, AccountType.CONTRA_EQUITY,
88
+ ):
89
+ return totals[EntryType.DEBIT] - totals[EntryType.CREDIT] + totals['subaccounts']
90
+
91
+ return totals[EntryType.CREDIT] - totals[EntryType.DEBIT] + totals['subaccounts']
@@ -0,0 +1,16 @@
1
+ from enum import Enum
2
+
3
+
4
+ class AccountType(Enum):
5
+ """Enum of valid Account types: DEBIT_BALANCE, ASSET, CONTRA_ASSET,
6
+ CREDIT_BALANCE, LIABILITY, EQUITY, CONTRA_LIABILITY,
7
+ CONTRA_EQUITY.
8
+ """
9
+ DEBIT_BALANCE = 'd'
10
+ ASSET = 'a'
11
+ CONTRA_ASSET = '-a'
12
+ CREDIT_BALANCE = 'c'
13
+ LIABILITY = 'l'
14
+ EQUITY = 'e'
15
+ CONTRA_LIABILITY = '-l'
16
+ CONTRA_EQUITY = '-e'
@@ -0,0 +1,67 @@
1
+ from decimal import Decimal
2
+ from sqloquent.asyncql import AsyncSqlModel
3
+
4
+
5
+ class Currency(AsyncSqlModel):
6
+ connection_info: str = ''
7
+ table: str = 'currencies'
8
+ id_column: str = 'id'
9
+ columns: tuple[str] = (
10
+ 'id', 'name', 'prefix_symbol', 'postfix_symbol',
11
+ 'fx_symbol', 'unit_divisions', 'base', 'details'
12
+ )
13
+ id: str
14
+ name: str
15
+ prefix_symbol: str|None
16
+ postfix_symbol: str|None
17
+ fx_symbol: str|None
18
+ unit_divisions: int
19
+ base: int|None
20
+ details: str|None
21
+
22
+ def to_decimal(self, amount: int) -> Decimal:
23
+ """Convert the amount into a Decimal representation."""
24
+ base = self.base or 10
25
+ return Decimal(amount) / Decimal(base**self.unit_divisions)
26
+
27
+ def get_units(self, amount: int) -> tuple[int,]:
28
+ """Get the full units and subunits. The number of subunit
29
+ figures will be equal to unit_divisions; e.g. if base=10
30
+ and unit_divisions=2, get_units(200) will return (2, 0, 0);
31
+ if base=60 and unit_divisions=2, get_units(200) will return
32
+ (0, 3, 20).
33
+ """
34
+ def get_subunits(amount, base, unit_divisions):
35
+ units_and_change = divmod(amount, base ** unit_divisions)
36
+ if unit_divisions > 1:
37
+ units_and_change = (units_and_change[0], *get_subunits(units_and_change[1], base, unit_divisions-1))
38
+ return units_and_change
39
+ base = self.base or 10
40
+ unit_divisions = self.unit_divisions
41
+ return get_subunits(amount, base, unit_divisions)
42
+
43
+ def format(self, amount: int, *, decimal_places: int = 2,
44
+ use_prefix: bool = True, use_postfix: bool = False,
45
+ use_fx_symbol: bool = False) -> str:
46
+ """Format an amount using the correct number of decimal_places."""
47
+ amount: str = str(self.to_decimal(amount))
48
+ if '.' not in amount:
49
+ amount += '.'
50
+ digits = amount.split('.')[1]
51
+
52
+ while len(digits) < decimal_places:
53
+ digits += '0'
54
+
55
+ digits = digits[:decimal_places]
56
+ amount = f"{amount.split('.')[0]}.{digits}"
57
+
58
+ if self.postfix_symbol and use_postfix:
59
+ return f"{amount}{self.postfix_symbol}"
60
+
61
+ if self.fx_symbol and use_fx_symbol:
62
+ return f"{amount} {self.fx_symbol}"
63
+
64
+ if self.prefix_symbol and use_prefix:
65
+ return f"{self.prefix_symbol}{amount}"
66
+
67
+ return amount
@@ -0,0 +1,23 @@
1
+ from sqloquent.asyncql import AsyncSqlModel
2
+ import packify
3
+
4
+
5
+ class Customer(AsyncSqlModel):
6
+ connection_info: str = ''
7
+ table: str = 'customers'
8
+ id_column: str = 'id'
9
+ columns: tuple[str] = ('id', 'name', 'code', 'details')
10
+ id: str
11
+ name: str
12
+ code: str|None
13
+ details: str|None
14
+
15
+ # override automatic property
16
+ @property
17
+ def details(self) -> packify.SerializableType:
18
+ """A packify.SerializableType stored in the database as a blob."""
19
+ return packify.unpack(self.data.get('details', None) or b'n\x00\x00\x00\x00')
20
+ @details.setter
21
+ def details(self, val: packify.SerializableType):
22
+ if isinstance(val, packify.SerializableType):
23
+ self.data['details'] = packify.pack(val)
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+ from sqloquent.asyncql import (
3
+ AsyncSqlModel, AsyncRelatedModel, AsyncRelatedCollection, AsyncQueryBuilderProtocol
4
+ )
5
+ from .EntryType import EntryType
6
+ import packify
7
+
8
+
9
+ class Entry(AsyncSqlModel):
10
+ connection_info: str = ''
11
+ table: str = 'entries'
12
+ id_column: str = 'id'
13
+ columns: tuple[str] = ('id', 'type', 'amount', 'nonce', 'account_id', 'details')
14
+ id: str
15
+ type: str
16
+ amount: int
17
+ nonce: bytes
18
+ account_id: str
19
+ details: bytes
20
+ account: AsyncRelatedModel
21
+ transactions: AsyncRelatedCollection
22
+
23
+ def __hash__(self) -> int:
24
+ data = self.encode_value(self._encode(self.data))
25
+ return hash(bytes(data, 'utf-8'))
26
+
27
+ # override automatic properties
28
+ @property
29
+ def type(self) -> EntryType:
30
+ """The EntryType of the Entry."""
31
+ return EntryType(self.data['type'])
32
+ @type.setter
33
+ def type(self, val: EntryType):
34
+ if type(val) is not EntryType:
35
+ return
36
+ self.data['type'] = val.value
37
+
38
+ @property
39
+ def details(self) -> packify.SerializableType:
40
+ """A packify.SerializableType stored in the database as a blob."""
41
+ return packify.unpack(self.data.get('details', b'n\x00\x00\x00\x00'))
42
+ @details.setter
43
+ def details(self, val: packify.SerializableType):
44
+ self.data['details'] = packify.pack(val)
45
+
46
+ @staticmethod
47
+ def _encode(data: dict|None) -> dict|None:
48
+ if type(data) is not dict:
49
+ return data
50
+ if type(data.get('type', None)) is EntryType:
51
+ data['type'] = data['type'].value
52
+ if type(data.get('details', {})) is not bytes:
53
+ data['details'] = packify.pack(data.get('details', None))
54
+ return data
55
+
56
+ @staticmethod
57
+ def _parse(data: dict|None) -> dict|None:
58
+ if type(data) is dict and type(data['amount']) is str:
59
+ data['amount'] = int(data['amount'])
60
+ return data
61
+
62
+ @staticmethod
63
+ def parse(models: Entry|list[Entry]) -> Entry|list[Entry]:
64
+ if type(models) is list:
65
+ for model in models:
66
+ model.data = Entry._parse(model.data)
67
+ else:
68
+ models.data = Entry._parse(models.data)
69
+ return models
70
+
71
+ @classmethod
72
+ async def insert(cls, data: dict) -> Entry | None:
73
+ """Ensure data is encoded before inserting."""
74
+ result = await super().insert(cls._encode(data))
75
+ return result
76
+
77
+ @classmethod
78
+ async def insert_many(cls, items: list[dict]) -> int:
79
+ """Ensure data is encoded before inserting."""
80
+ items = [Entry._encode(data) for data in list]
81
+ return await super().insert_many(items)
82
+
83
+ @classmethod
84
+ def query(cls, conditions: dict = None) -> AsyncQueryBuilderProtocol:
85
+ """Ensure conditions are encoded properly before querying."""
86
+ return super().query(cls._encode(conditions))
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class EntryType(Enum):
5
+ """Enum of valid Entry types: CREDIT and DEBIT."""
6
+ CREDIT = 'c'
7
+ DEBIT = 'd'
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+ from .Account import Account, AccountType
3
+ from .Ledger import Ledger
4
+ from sqloquent.asyncql import AsyncSqlModel, AsyncRelatedCollection
5
+
6
+
7
+ class Identity(AsyncSqlModel):
8
+ connection_info: str = ''
9
+ table: str = 'identities'
10
+ id_column: str = 'id'
11
+ columns: tuple[str] = ('id', 'name', 'details', 'pubkey', 'seed', 'secret_details')
12
+ columns_excluded_from_hash: tuple[str] = ('seed', 'secret_details')
13
+ id: str
14
+ name: str
15
+ details: bytes
16
+ pubkey: bytes|None
17
+ seed: bytes|None
18
+ secret_details: bytes|None
19
+ ledgers: AsyncRelatedCollection
20
+
21
+ def public(self) -> dict:
22
+ """Return the public data for cloning the Identity."""
23
+ return {
24
+ k:v for k,v in self.data.items()
25
+ if k not in self.columns_excluded_from_hash
26
+ }
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+ from .Account import Account, AccountType
3
+ from sqloquent.asyncql import AsyncSqlModel, AsyncRelatedModel, AsyncRelatedCollection
4
+
5
+
6
+ class Ledger(AsyncSqlModel):
7
+ connection_info: str = ''
8
+ table: str = 'ledgers'
9
+ id_column: str = 'id'
10
+ columns: tuple[str] = ('id', 'name', 'identity_id', 'currency_id')
11
+ id: str
12
+ name: str
13
+ identity_id: str
14
+ currency_id: str
15
+ owner: AsyncRelatedModel
16
+ currency: AsyncRelatedModel
17
+ accounts: AsyncRelatedCollection
18
+ transactions: AsyncRelatedCollection
19
+
20
+ async def balances(self, reload: bool = False) -> dict[str, tuple[int, AccountType]]:
21
+ """Return a dict mapping account ids to their balances. Accounts
22
+ with sub-accounts will not include the sub-account balances;
23
+ the sub-account balances will be returned separately.
24
+ """
25
+ balances = {}
26
+ if reload:
27
+ await self.accounts().reload()
28
+ for account in self.accounts:
29
+ balances[account.id] = (await account.balance(False), account.type)
30
+ return balances
31
+
32
+ def setup_basic_accounts(self) -> list[Account]:
33
+ """Creates and returns a list of 3 unsaved Accounts covering the
34
+ 3 basic categories: Asset, Liability, Equity.
35
+ """
36
+ asset = Account({
37
+ 'name': f'General Asset ({self.owner.name})',
38
+ 'type': AccountType.ASSET,
39
+ 'ledger_id': self.id,
40
+ 'code': '1xx'
41
+ })
42
+ liability = Account({
43
+ 'name': f'General Liability ({self.owner.name})',
44
+ 'type': AccountType.LIABILITY,
45
+ 'ledger_id': self.id,
46
+ 'code': '2xx'
47
+ })
48
+ equity = Account({
49
+ 'name': f'General Equity ({self.owner.name})',
50
+ 'type': AccountType.EQUITY,
51
+ 'ledger_id': self.id,
52
+ 'code': '28x'
53
+ })
54
+ return [asset, liability, equity]
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+ from hashlib import sha256
3
+ from sqloquent.asyncql import AsyncSqlModel, AsyncRelatedCollection
4
+ from sqloquent.errors import vert, tert
5
+ from .Account import Account, AccountType
6
+ from .Entry import Entry, EntryType
7
+ from .Identity import Identity
8
+ import packify
9
+
10
+
11
+ class Transaction(AsyncSqlModel):
12
+ connection_info: str = ''
13
+ table: str = 'transactions'
14
+ id_column: str = 'id'
15
+ columns: tuple[str] = ('id', 'entry_ids', 'ledger_ids', 'timestamp', 'details')
16
+ id: str
17
+ entry_ids: str
18
+ ledger_ids: str
19
+ timestamp: str
20
+ details: bytes
21
+ entries: AsyncRelatedCollection
22
+ ledgers: AsyncRelatedCollection
23
+
24
+ # override automatic properties
25
+ @property
26
+ def details(self) -> dict[str, bytes]:
27
+ """A packify.SerializableType stored in the database as a blob."""
28
+ return packify.unpack(self.data.get('details', b'd\x00\x00\x00\x00'))
29
+ @details.setter
30
+ def details(self, val: dict[str, bytes]):
31
+ if type(val) is not dict:
32
+ return
33
+ if not all([type(k) is str and type(v) is bytes for k, v in val.items()]):
34
+ return
35
+ self.data['details'] = packify.pack(val)
36
+
37
+ @classmethod
38
+ def _encode(cls, data: dict|None) -> dict|None:
39
+ """Encode values for saving."""
40
+ if type(data) is not dict:
41
+ return None
42
+ if type(data.get('details', {})) is dict:
43
+ data['details'] = packify.pack(data.get('details', {}))
44
+ return data
45
+
46
+ @classmethod
47
+ async def prepare(cls, entries: list[Entry], timestamp: str,
48
+ details: packify.SerializableType = None,
49
+ reload: bool = False) -> Transaction:
50
+ """Prepare a transaction. Raises TypeError for invalid arguments.
51
+ Raises ValueError if the entries do not balance for each
52
+ ledger; if a required auth script is missing; or if any of
53
+ the entries is contained within an existing Transaction.
54
+ Entries and Transaction will have IDs generated but will not
55
+ be persisted to the database and must be saved separately.
56
+ """
57
+ tert(type(entries) is list and all([type(e) is Entry for e in entries]),
58
+ 'entries must be list[Entry]')
59
+ tert(type(timestamp) is str, 'timestamp must be str')
60
+
61
+ ledgers = set()
62
+ for entry in entries:
63
+ entry.id = entry.id if entry.id else entry.generate_id()
64
+ if reload:
65
+ await entry.account().reload()
66
+ vert(await Transaction.query().contains('entry_ids', entry.id).count() == 0,
67
+ f"entry {entry.id} is already contained within a Transaction")
68
+ ledgers.add(entry.account.ledger_id)
69
+
70
+ txn = cls({
71
+ 'entry_ids': ",".join(sorted([
72
+ e.id if e.id else e.generate_id()
73
+ for e in entries
74
+ ])),
75
+ 'ledger_ids': ",".join(sorted(list(ledgers))),
76
+ 'timestamp': timestamp,
77
+ })
78
+ txn.details = details
79
+ txn.entries = entries
80
+ assert await txn.validate(reload), \
81
+ 'transaction validation failed'
82
+ txn.id = txn.generate_id()
83
+ return txn
84
+
85
+ async def validate(self, reload: bool = False) -> bool:
86
+ """Determines if a Transaction is valid using the rules of accounting.
87
+ Raises TypeError for invalid arguments. Raises ValueError if the
88
+ entries do not balance for each ledger; or if any of the entries is
89
+ contained within an existing Transaction. If reload is set to True,
90
+ entries and accounts will be reloaded from the database.
91
+ """
92
+ if reload:
93
+ await self.entries().reload()
94
+
95
+ # first check that all ledgers balance
96
+ ledgers = {}
97
+ entry: Entry
98
+ for entry in self.entries:
99
+ if reload:
100
+ await entry.account().reload()
101
+ if entry.account.ledger_id not in ledgers:
102
+ ledgers[entry.account.ledger_id] = {'Dr': 0, 'Cr': 0}
103
+ if entry.type in (EntryType.CREDIT, EntryType.CREDIT.value):
104
+ ledgers[entry.account.ledger_id]['Cr'] += entry.amount
105
+ else:
106
+ ledgers[entry.account.ledger_id]['Dr'] += entry.amount
107
+
108
+ for ledger_id, balances in ledgers.items():
109
+ vert(balances['Cr'] == balances['Dr'],
110
+ f"ledger {ledger_id} unbalanced: {balances['Cr']} Cr != {balances['Dr']} Dr")
111
+
112
+ return True
113
+
114
+ async def save(self, reload: bool = False) -> Transaction:
115
+ """Validate the transaction, save the entries, then save the
116
+ transaction.
117
+ """
118
+ assert await self.validate(reload), 'cannot save an invalid Transaction'
119
+ for e in self.entries:
120
+ await e.save()
121
+ return await super().save()
@@ -0,0 +1,23 @@
1
+ from sqloquent.asyncql import AsyncSqlModel
2
+ import packify
3
+
4
+
5
+ class Vendor(AsyncSqlModel):
6
+ connection_info: str = ''
7
+ table: str = 'vendors'
8
+ id_column: str = 'id'
9
+ columns: tuple[str] = ('id', 'name', 'code', 'details')
10
+ id: str
11
+ name: str
12
+ code: str|None
13
+ details: str|None
14
+
15
+ # override automatic property
16
+ @property
17
+ def details(self) -> packify.SerializableType:
18
+ """A packify.SerializableType stored in the database as a blob."""
19
+ return packify.unpack(self.data.get('details', None) or b'n\x00\x00\x00\x00')
20
+ @details.setter
21
+ def details(self, val: packify.SerializableType):
22
+ if isinstance(val, packify.SerializableType):
23
+ self.data['details'] = packify.pack(val)
@@ -0,0 +1,53 @@
1
+ from .Account import Account, AccountType
2
+ from .Currency import Currency
3
+ from .Customer import Customer
4
+ from .Entry import Entry, EntryType
5
+ from .Identity import Identity
6
+ from .Ledger import Ledger
7
+ from .Transaction import Transaction
8
+ from .Vendor import Vendor
9
+ from sqloquent.asyncql import (
10
+ async_contains, async_within, async_has_many, async_belongs_to,
11
+ )
12
+
13
+
14
+ Identity.ledgers = async_has_many(Identity, Ledger, 'identity_id')
15
+ Ledger.owner = async_belongs_to(Ledger, Identity, 'identity_id')
16
+
17
+ Ledger.currency = async_belongs_to(Ledger, Currency, 'currency_id')
18
+
19
+ Ledger.accounts = async_has_many(Ledger, Account, 'ledger_id')
20
+ Account.ledger = async_belongs_to(Account, Ledger, 'ledger_id')
21
+
22
+ Account.children = async_has_many(Account, Account, 'parent_id')
23
+ Account.parent = async_belongs_to(Account, Account, 'parent_id')
24
+
25
+ Account.entries = async_has_many(Account, Entry, 'account_id')
26
+ Entry.account = async_belongs_to(Entry, Account, 'account_id')
27
+
28
+ Entry.transactions = async_within(Entry, Transaction, 'entry_ids')
29
+ Transaction.entries = async_contains(Transaction, Entry, 'entry_ids')
30
+
31
+ Transaction.ledgers = async_contains(Transaction, Ledger, 'ledger_ids')
32
+ Ledger.transactions = async_within(Ledger, Transaction, 'ledger_ids')
33
+
34
+
35
+ def set_connection_info(db_file_path: str):
36
+ """Set the connection info for all models to use the specified
37
+ sqlite3 database file path.
38
+ """
39
+ Account.connection_info = db_file_path
40
+ Currency.connection_info = db_file_path
41
+ Customer.connection_info = db_file_path
42
+ Entry.connection_info = db_file_path
43
+ Identity.connection_info = db_file_path
44
+ Ledger.connection_info = db_file_path
45
+ Transaction.connection_info = db_file_path
46
+ Vendor.connection_info = db_file_path
47
+
48
+
49
+ # no longer needed
50
+ del async_contains
51
+ del async_belongs_to
52
+ del async_within
53
+ del async_has_many