-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathpybudget
299 lines (219 loc) · 9.8 KB
/
pybudget
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
#!/bin/python3
# Program to account expenses and determine a budget
# Syntax: ./pybudget -p <value of your paycheck>
import decimal as d # Allows me to accurately round up
from os import path # Allows me to determine if file exists
from sys import exit as terminate # Allows me to end with codes
from rich import print # Allows me to color and format text output
from rich.console import Console # Allows me to have clean word wrapping
import click # Allows me to have better argument acquisition and cleaner CLI
# ============ FUNCTIONS ============
def info(content, kind='info'):
"""
This prints info to the terminal in a fancy way
:param content: This is the string you want to display
:param kind: bad, info, question; changes color
:return: None
"""
if kind == 'info':
print(f'[bold blue]\[i][/bold blue] [white]{content}[/white]')
elif kind == 'bad':
print(f'[bold red][X][/bold red] [white]{content}[/white]')
elif kind == 'question':
print(f'[bold yellow]\[?][/bold yellow] [white]{content}[/white]')
def paragraph(content: str, style='white'):
"""
This prints properly wrapped text to the console.
:param content: a string that you want printed
:return: None
"""
console = Console(width=65)
console.print(f'{content}', style=style, justify='left')
def getDec(rawFloat: float):
"""
This function will convert any passed floats into a decimal to the hundredths place.
:param rawFloat: This is an unconverted float
:return: Float object of hundredths place
"""
dVal = d.Decimal # Create decimal object
cent = dVal('0.01') # Create placeholder
converted = float(dVal(str(rawFloat)).quantize(cent, rounding=d.ROUND_UP)) # Convert to quantized d then to float
return converted # Return final product
def getExpenses():
"""
This function will either get expenses from file in the format of "Expense,Cost"
:return: list of tuples, where every element has (string: item name,
float: item cost)
"""
# 2. Open file and get expenses
with open('expenses.acct', 'r') as file:
expenses = file.read().split('\n')
# 3. Convert to appropriate list
itemCosts = []
for expense in expenses:
try:
itemCost = expense.split(',')
if not itemCost[0] == '':
itemCosts.append((itemCost[0], float(itemCost[1])))
except Exception as e: # Stop if format is wrong
error = str(e)
info('Failure: Expenses File Format Wrong', kind='bad')
info(f'Error Cause: {error}') # This should only appear if the exception was triggered
info('Ensure that the cost section has no non-numeral characters other than a period.')
terminate(4)
return itemCosts
def getExpenseSum(itemCosts: list):
"""
This function calculates the sum of all recurring expenses and returns a decimal
:param itemCosts: list of (item, cost) tuple
:return: float decimal
"""
# 1. Set Up Display and Total
print('[bold red]========= EXPENSES =========[/bold red]')
totalExpense = 0
# 2. Add to total for each item in expenses
for pair in itemCosts: # For each item
totalExpense += pair[1] # [1] is the numerical value
info(f'Reserved ${pair[1]:.02f} for {pair[0]}')
return getDec(totalExpense)
def getBudget(paycheck: float, expenses: float, save: int, invest: int):
"""
This function divides your available cash into several portions,
depending on how you wish to allocate it.
:param paycheck: This is how much money you have to use
:param expenses: This is how much money you must spend
:param save: This represents a percentage of your budget to save
:param invest: This represents a percentage of your budget to invest
:return: None
"""
# 1. Determine if okay to continue onto the computation
if (save + invest) > 100: # If you have a bad percentage combination
info('Invalid Allocation of Available Funds', kind='bad')
info('You may have it so that the percent you save and invest is ' +
'greater than 100%\n')
terminate(5)
elif expenses > paycheck: # If you're spending more than you have
print('\n[bold red]========= BUDGET =========[/bold red]')
info('WARNING: BUDGET DEFICIT', kind='bad')
info('Your paycheck was insufficient to meet your expenses.\n' +
'As a consequence, you have a net loss this period.')
info(f'Current Deficit -------> [bold italic underline red]' +
f'${getDec(paycheck - expenses):.02f}[/bold italic underline red]\n')
terminate(0) # This represents a successful run of the script
# 2. Compute budget
free = paycheck - expenses
save = getDec((save / 100) * free)
invest = getDec((invest / 100) * free)
spend = getDec(paycheck - (save + invest + expenses))
# 3. Inform user
print('\n[bold green]========= BUDGET =========[/bold green]')
info(f'You will pay ${expenses:.02f} for your expenses')
info(f'You will invest ${invest:.02f}')
info(f'You will save ${save:.02f}')
info(f'Available Budget -------> [bold green]${spend:.02f}[/bold green]\n')
# 3.1 Easter eggs
easterEgg(spend)
# 4. END
terminate(0)
def easterEgg(budget):
"""
Prints informative information
:param budget: the amount that you can spend
:return: None
"""
if budget >= 69 and budget < 70:
print('\n[italic white]Nice[/italic white]')
elif budget >= 420 and budget < 421:
print('[italic bold green]https://www.youtube.com/watch?v=LT9lGggkWCU[/italic bold green]')
elif budget in [1.98, 19.87, 198.70, 1987]: # NTS: 118.68 for personal rickroll
paragraph('[bold red][!!!][/bold red] [bold blue]Important Information ' +
'Regarding Your Budget: ' +
'https://www.youtube.com/watch?v=dQw4w9WgXcQ')
elif budget >= 666 and budget < 667:
print('😈😈😈😈😈 [bold italic red]I\'m on the HIGHWAY TO HELL!!!!![/bold italic red]')
print('[bold italic red]https://www.youtube.com/watch?v=l482T0yNkeo[/bold italic red]')
print('')
def setupConfig():
"""
This determines if you set up the files needed
:return: None
"""
# Exit variable
end = False
# 1. Determine if expenses file exists
if not path.isfile('expenses.acct'):
info('Failed to Locate Expenses File', kind='bad')
with open('expenses.acct', 'w') as file: # This creates a file with example
file.write('example,0.00')
paragraph('Made expenses.acct file for you. Please enter expenses in the format "expense,x.xx" ' +
'without the quote, where "x.xx" represents a decimal. Create a new line for every expense.\n')
end = True
# 2. Determine if the config file exists
if not path.isfile('config.acct'):
info('Failed to Locate Config File', kind='bad')
# Create config file
with open('config.acct', 'w') as file:
file.write('Save:0\nInvest:0')
paragraph('Created config.acct file for you. The please replace the "0" ' +
'with a whole number representing a percentage. This value will tell ' +
'me how to allocate your budget.\n')
end = True
# End?
if end:
terminate(3)
def getConfig():
"""
Read from the config file. Terminate if bad format.
:return: tuple containing int, int for save, invest
"""
# 1. Open file and determine if settings are in there
with open('config.acct', 'r') as file:
content = file.read()
if not 'Invest' in content and not 'Save' in content:
info('Config file not formatted properly', kind='bad')
terminate(6)
else:
options = content.split('\n')
# 2. Find option values
gotInvest = False # Represents if invest value was found
gotSave = False # Both of these need to turn True to pass
for option in options: # For every located option in config
if 'Invest' in option: # If you find the invest option
invest = option.split(':')[1] # Get its numerical value
gotInvest = True # Flip its switch to True
elif 'Save' in option: # Do the same for save
save = option.split(':')[1]
gotSave = True
if gotInvest and gotSave: # If both switches were flipped
try:
return (int(save), int(invest)) # Return the information
except Exception as e: # Otherwise tell me what's up
info('Unknown Error:' + str(e), kind='bad')
terminate(9)
terminate(7) # End program: config file not formatted properly
# ============ CLICK & MAIN ============
"""
The following is the primary function for pybudget. It is formatted in this way
to allow it to get information from click. The first two @s are necesary for this.
:param @click.option --pay: This represents the amount of money
you've been paid this period.
"""
@click.command()
@click.option('-p', '--pay', 'pay', default=0.00, help='This is how much you\'ve been paid')
def main(pay):
# ============ START ============
# 0. Determine if properly configured
setupConfig()
# 1. Get paycheck
paycheck = pay
# 2. Convert paycheck to decimal
paycheck = getDec(paycheck)
# 3. Get expenses and calculate sum
itemCosts = getExpenses()
totalExpenses = getExpenseSum(itemCosts)
# 4. Determine how much to allocate
saveInvest = getConfig()
# 5. Get budget (take money from budget for investments and savings)
getBudget(paycheck, totalExpenses, saveInvest[0], saveInvest[1])
if __name__ == '__main__':
main()