Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

#!/usr/bin/env python3 

 

"""\ 

Maintain a budget that updates every day. 

 

Usage: 

    two_cents [-d] [-D] [-I] [-h] [-v] 

    two_cents add-bank <name> [-u <command>] [-p <command>] 

    two_cents add-budget <name> [-b <dollars>] [-a <dollars-per-time>] 

    two_cents describe-budgets [-e] 

    two_cents download-payments [-I] 

    two_cents reassign-payment <payment-id> <budget> 

    two_cents show-allowance [<budgets>...] 

    two_cents show-payments [<budget>] 

    two_cents suggest-allowance [<budgets>...] 

    two_cents transfer-allowance <dollars-per-time> <budget-from> <budget-to> 

    two_cents transfer-money <dollars> <budget-from> <budget-to> 

 

Options: 

  -d, --download 

        Force new transactions to be downloaded from the bank.  Downloading new  

        transactions is the default behavior, so the purpose of this flag is to  

        allow the --no-download flag to be overridden. 

 

  -D, --no-download 

        Don't download new transactions from the bank.  This can be a slow  

        step, so you may want to skip it if you know nothing new has happened. 

 

  -I, --no-interaction 

        Don't use stdin to prompt for passwords.  If a password command is  

        found in the database, use that.  Otherwise print an error message  

        and exit. 

 

  -u, --username-cmd <command> 

        When adding a new bank, use this option to specify a command that can  

        be used to get your username.  You will be prompted for one if this  

        option isn't specified. 

 

  -p, --password-cmd <command> 

        When adding a new bank, use this option to specify a command that can  

        be used to get your password.  You will be prompted for one if this  

        option isn't specified. 

 

  -b, --initial-balance <dollars> 

        When adding a new budget, specify how much money should start off in  

        the budget. 

 

  -a, --initial-allowance <dollars-per-time> 

        When adding a new budget, specify how quickly money should accumulate  

        in that budget. 

 

  -e, --edit 

        Indicate that you want to create or update a description of your  

        budgeting scheme. 

 

  -h, --help 

        Print out this message. 

         

  -v, --version 

        Print the version number of the installed two_cents executable. 

""" 

 

import two_cents 

import appdirs; dirs = appdirs.AppDirs('two_cents', 'username') 

from contextlib import contextmanager 

 

def main(argv=None, db_path=None): 

    try: 

        import docopt 

        args = docopt.docopt(__doc__, argv) 

        from pprint import pprint 

        pprint(args) 

 

        if args['--version']: 

            print('two_cents 0.0') 

            raise SystemExit 

 

        if db_path is None: 

            import os 

            db_path = os.path.join(dirs.user_config_dir, 'budgets.db') 

 

        with two_cents.open_db(db_path) as session: 

            if args['add-bank']: 

                add_bank( 

                        session, 

                        scraper_key=args['<name>'], 

                        username_cmd=args['--username-cmd'], 

                        password_cmd=args['--password-cmd'], 

                ) 

            elif args['add-budget']: 

                add_budget( 

                        session, 

                        name=args['<name>'], 

                        initial_balance=args['--initial-balance'], 

                        initial_allowance=args['--initial-allowance'], 

                ) 

            elif args['describe-budgets']: 

                describe_budgets( 

                        edit=args['--edit'], 

                ) 

            elif args['download-payments']: 

                download_payments( 

                        session, 

                        interactive=not args['--no-interaction'], 

                ) 

            elif args['reassign-payment']: 

                reassign_payment( 

                        session, 

                        args['<payment-id>'], 

                        args['<budget>'], 

                ) 

            elif args['show-allowance']: 

                show_allowance( 

                        session, 

                        *args['<budgets>'] 

                ) 

            elif args['show-payments']: 

                show_payments( 

                        session, 

                        args['<budget>'], 

                ) 

            elif args['suggest-allowance']: 

                suggest_allowance( 

                        session, 

                        *args['<budgets>'] 

                ) 

            elif args['transfer-money']: 

                transfer_money( 

                        session, 

                        args['<dollars>'], 

                        args['<budget-from>'], 

                        args['<budget-to>'], 

                ) 

            else: 

                update_budgets( 

                        session, 

                        download=args['--download'] or not args['--no-download'], 

                        interactive=not args['--no-interaction'], 

                ) 

    except two_cents.UserError as error: 

        print(error) 

    except KeyboardInterrupt: 

        print() 

 

def add_bank(session, scraper_key, username_cmd=None, password_cmd=None): 

    bank = two_cents.Bank(session, scraper_key) 

 

    if not username_cmd and not password_cmd: 

        print("""\ 

Enter username and password commands.  You don't need to provide either  

command, but if you don't you'll have to provide the missing fields every time  

you download financial data from this bank.""") 

        print() 

        username_cmd = prompt("Username: ") 

        password_cmd = prompt("Password: ") 

 

    elif not username_cmd: 

        print("""\ 

Enter a username command.  If no command is given, you'll be prompted for a  

username every time you download financial data from this bank.""") 

        print() 

        username_cmd = prompt("Username: ") 

 

    elif not password_cmd: 

        print("""\ 

Enter a password command.  If no command is given, you'll be prompted for a  

password every time you download financial data from this bank.""") 

        print() 

        password_cmd = prompt("Password: ") 

 

    bank.username_command = username_cmd 

    bank.password_command = password_cmd 

 

    session.add(bank) 

 

def add_budget(session, name, initial_balance, initial_allowance): 

    budget = two_cents.Budget(name, initial_balance, initial_allowance) 

    session.add(budget) 

 

def describe_budgets(edit=False): 

    import os 

    import subprocess 

 

    description_path = os.path.join(dirs.user_config_dir, 'description.txt') 

 

    if edit or not os.path.exists(description_path): 

        editor = os.environ.get('EDITOR', 'vi') 

        subprocess.call((editor, description_path)) 

    else: 

        with open(description_path) as file: 

            print(file.read().strip()) 

 

def download_payments(session, interactive=True): 

    two_cents.download_payments( 

            session, 

            get_username_prompter(interactive), 

            get_password_prompter(interactive), 

    ) 

 

def reassign_payment(session, payment_id, budget): 

    payment = two_cents.get_payment(session, payment_id) 

    payment.assign(budget) 

 

def show_allowance(session, *budgets): 

    with print_table('lr') as table: 

        for budget in two_cents.get_budgets(session, *budgets): 

            table.add_row([ 

                    budget.name.title(), 

                    budget.pretty_allowance, 

            ]) 

 

def show_payments(session, budget=None): 

    for payment in two_cents.get_payments(session, budget): 

        show_payment(payment) 

        print() 

 

def suggest_allowance(session, *budgets): 

    # Populate a table with suggested allowances for each budget, then display  

    # that table. 

 

    with print_table('lr') as table: 

        for budget in two_cents.get_budgets(session, *budgets): 

            table.add_row([ 

                    budget.name.title(), 

                    "{}/mo".format(two_cents.format_dollars( 

                            two_cents.suggest_allowance(session, budget))), 

            ]) 

 

def transfer_money(session, dollars, budget_from, budget_to): 

    two_cents.transfer_money( 

            two_cents.parse_dollars(dollars), 

            two_cents.get_budget(session, budget_from), 

            two_cents.get_budget(session, budget_to)) 

 

def update_budgets(session, download=True, interactive=True): 

    if two_cents.get_num_budgets(session) == 0: 

        raise two_cents.UserError("No budgets to display.  Use 'two_cents add-budget' to create some.") 

 

    if download: 

        print("Downloading recent transactions...") 

        download_payments(session, interactive) 

 

    assign_payments(session) 

    two_cents.update_allowances(session) 

    show_budgets(session) 

 

 

def print(*args, **kwargs): 

    import builtins 

    return builtins.print(*args, **kwargs) 

 

def prompt(message, password=False): 

    if password: 

        import getpass 

        return getpass.getpass(message) 

    else: 

        return input(message) 

 

def assign_payments(session): 

    import readline 

 

    # Handle the payments using a simple state machine.  This architecture  

    # facilitates commands like 'skip all' and 'ignore all'. 

 

    class ReadEvalPrintLoop: 

 

        def __init__(self): 

            self.handle = self.default_handler 

 

        def go(self, session): 

            payments = two_cents.get_unassigned_payments(session) 

 

            if not payments: 

                return 

 

            elif len(payments) == 1: 

                print("Please assign the following payment to an budget:") 

                print() 

 

            else: 

                print("Please assign the following payments to budgets:") 

                print() 

 

            for payment in payments: 

                self.handle(payment) 

 

        def default_handler(self, payment): 

            show_payment(payment, indent='  ') 

            print() 

 

            while True: 

 

                # Prompt the user for an assignment. 

 

                command = prompt("Account: ") 

 

                # See if the user wants to skip assigning one or more payments  

                # and come back to them later. 

 

                if not command or command == 'skip': 

                    break 

 

                if command == 'skip all': 

                    self.handle = self.null_handler 

                    break 

 

                # See if the user wants to ignore one or more payments.  These  

                # payments will be permanently excluded from the budget. 

 

                if command == 'ignore': 

                    payment.ignore() 

                    break 

 

                if command == 'ignore all': 

                    payment.ignore() 

                    self.handle = self.ignore_handler 

                    break 

 

                # Attempt to assign the payment to the specified budgets.  If  

                # the input can't be parsed, print an error and ask again. 

 

                try: 

                    payment.assign(command) 

                    break 

 

                except two_cents.UserError as error: 

                    print(error.message) 

 

            print() 

 

        def ignore_handler(self, payment): 

            payment.ignore() 

 

        def null_handler(self, payment): 

            pass 

 

    class TabCompleter: 

 

        def __init__(self, session): 

            self.budgets = two_cents.get_budgets(session) 

            self.commands = [x.name for x in self.budgets] 

            self.commands += ['skip', 'ignore', 'all'] 

            self.commands.sort() 

 

        def __call__(self, prefix, index): 

            results = [x for x in self.commands if x.startswith(prefix)] 

            try: return results[index] 

            except IndexError: return None 

 

 

    readline.parse_and_bind('tab: complete') 

    readline.set_completer(TabCompleter(session)) 

 

    loop = ReadEvalPrintLoop() 

    loop.go(session) 

 

def show_budgets(session): 

    """ 

    Print a line briefly summarizing each budget. 

    """ 

 

    # I had to hack table.right_padding_width a little to format the recovery  

    # time the way I wanted.  Basically, I use table.right_padding_width to add  

    # manual padding, then I remove the padding once the table is complete. 

 

    with print_table('lr') as table: 

        for budget in two_cents.get_budgets(session): 

            table.add_row([ 

                budget.name.title() + ' ' * table.right_padding_width, 

                budget.pretty_balance + ' ', 

                '' if budget.recovery_time <= 0 else 

                    '({} {})'.format(budget.recovery_time, 

                        'day' if budget.recovery_time == 1 else 'days') 

            ]) 

        table.right_padding_width = 0 

 

@contextmanager 

def print_table(alignments=None): 

    import prettytable 

 

    table = prettytable.PrettyTable() 

    table.set_style(prettytable.PLAIN_COLUMNS) 

    table.header = False 

 

    yield table 

 

    if alignments is not None: 

        for column, alignment in zip(table.field_names, alignments): 

            table.align[column] = alignment 

 

    print(table) 

 

def show_payment(payment, indent=''): 

    import textwrap 

 

    print("{}Id: {}".format(indent, payment.id)) 

    print("{}Bank: {}".format(indent, payment.bank.title)) 

    print("{}Date: {}".format(indent, two_cents.format_date(payment.date))) 

    print("{}Value: {}".format(indent, two_cents.format_dollars(payment.value))) 

 

    if payment.assignment is not None: 

        print("{}Assignment: {}".format(indent, payment.assignment)) 

 

    if len(payment.description) < (79 - len(indent) - 13): 

        print("{}Description: {}".format(indent, payment.description)) 

    else: 

        description = textwrap.wrap( 

                payment.description, width=79, 

                initial_indent=indent+'  ', subsequent_indent=indent+'  ') 

 

        print("{}Description:".format(indent)) 

        print('\n'.join(description)) 

 

def get_username_prompter(interactive=True): 

    def username_prompter(bank, error_message): 

        if error_message: print(error_message) 

        if not interactive: raise SystemExit 

        return prompt("Username for {}: ".format(bank)) 

    return username_prompter 

 

def get_password_prompter(interactive=True): 

    def password_prompter(bank, error_message): 

        if error_message: print(error_message) 

        if not interactive: raise SystemExit 

        return prompt("Password for {}: ".format(bank), password=True) 

    return password_prompter