From 64c03bd66e6dff5ca57ec28a21d7b4d0036b06ba Mon Sep 17 00:00:00 2001 From: bot50 Date: Fri, 26 Apr 2024 05:57:52 +0000 Subject: [PATCH] kukemuna-cs50/problems/2024/x/finance@20240426T055752.549633071Z --- app.py | 277 ++++++++++++++++++ finance.db | Bin 0 -> 20480 bytes .../16fa13c5890feb947b5d5c907cfedf5b | Bin 0 -> 46 bytes .../2029240f6d1128be89ddc32729463129 | Bin 0 -> 9 bytes .../39b31c290f4b506fd545e82536ad3484 | Bin 0 -> 46 bytes helpers.py | 88 ++++++ requirements.txt | 5 + static/I_heart_validator.png | Bin 0 -> 345 bytes static/favicon.ico | Bin 0 -> 15406 bytes static/styles.css | 23 ++ templates/apology.html | 11 + templates/buy.html | 17 ++ templates/history.html | 30 ++ templates/home.html | 40 +++ templates/layout.html | 91 ++++++ templates/login.html | 17 ++ templates/quote.html | 23 ++ templates/register.html | 20 ++ templates/sell.html | 22 ++ 19 files changed, 664 insertions(+) create mode 100644 app.py create mode 100644 finance.db create mode 100644 flask_session/16fa13c5890feb947b5d5c907cfedf5b create mode 100644 flask_session/2029240f6d1128be89ddc32729463129 create mode 100644 flask_session/39b31c290f4b506fd545e82536ad3484 create mode 100644 helpers.py create mode 100644 requirements.txt create mode 100644 static/I_heart_validator.png create mode 100644 static/favicon.ico create mode 100644 static/styles.css create mode 100644 templates/apology.html create mode 100644 templates/buy.html create mode 100644 templates/history.html create mode 100644 templates/home.html create mode 100644 templates/layout.html create mode 100644 templates/login.html create mode 100644 templates/quote.html create mode 100644 templates/register.html create mode 100644 templates/sell.html diff --git a/app.py b/app.py new file mode 100644 index 0000000..97f6a52 --- /dev/null +++ b/app.py @@ -0,0 +1,277 @@ +import os +import datetime + +from cs50 import SQL +from flask import Flask, flash, redirect, render_template, request, session +from flask_session import Session +from werkzeug.security import check_password_hash, generate_password_hash + +from helpers import apology, login_required, lookup, usd + +# Configure application +app = Flask(__name__) + +# Custom filter +app.jinja_env.filters["usd"] = usd + +# Configure session to use filesystem (instead of signed cookies) +app.config["SESSION_PERMANENT"] = False +app.config["SESSION_TYPE"] = "filesystem" +Session(app) + +# Configure CS50 Library to use SQLite database +db = SQL("sqlite:///finance.db") + + +@app.after_request +def after_request(response): + """Ensure responses aren't cached""" + response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + response.headers["Expires"] = 0 + response.headers["Pragma"] = "no-cache" + return response + + +@app.route("/") +@login_required +def index(): + """Show portfolio of stocks""" + user_id = session["user_id"] + + cash = db.execute("SELECT * FROM users where id = ?", user_id) + + stocks = db.execute( + "SELECT symbol, sum(shares) FROM transactions WHERE user_id = ? GROUP BY symbol HAVING sum(shares) > 0", user_id) + + stocks_total = 0 + + for stock in stocks: + + symbol = stock["symbol"] + # Update share price + stock_price = lookup(symbol.upper()) + stock["shares"] = stock["sum(shares)"] + stock["price"] = stock_price["price"] + # Get total value of owned stock + stock["total"] = stock_price["price"] * stock["sum(shares)"] + # Get value of all owned stocks + stocks_total = stocks_total + (stock_price["price"] * stock["sum(shares)"]) + + # Get total value of stocks and cash + total = cash[0]["cash"] + stocks_total + + return render_template("home.html", cash=cash[0]["cash"], stocks=stocks, total=total) + # return apology("MOFO") + + +@app.route("/buy", methods=["GET", "POST"]) +@login_required +def buy():# + """Buy shares of stock""" + if request.method == "GET": + return render_template("buy.html") + else: + symbol = request.form.get("symbol") + shares = request.form.get("shares") + + if not symbol: + return apology("Not Symbol") + + stock = lookup(symbol.upper()) + + if stock == None: + return apology("Symbol not found") + + if not shares or shares.isalpha() or not float(shares).is_integer(): + return apology("invalid shares") + + else: + if float(shares) > 0: + transaction_value = float(shares) * stock["price"] + + user_id = session["user_id"] + user_cash_db = db.execute("SELECT cash FROM users WHERE id = ?", user_id) + user_cash = user_cash_db[0]["cash"] + + if user_cash < transaction_value: + return apology("U broke, m8!") + + free_cash = user_cash - transaction_value + + db.execute("UPDATE users SET cash = ? WHERE id = ?", free_cash, user_id) + + date = datetime.datetime.now() + + db.execute("INSERT INTO transactions (user_id, symbol, shares, price, date) VALUES (?, ?, ?, ?, ?)", + user_id, stock["symbol"], shares, stock["price"], date) + + flash("Bought!") + else: + return apology("Missing shares", 400) + + return redirect("/") + # return apology("TODO") + + +@app.route("/history") +@login_required +def history(): + """Show history of transactions""" + stocks = db.execute("SELECT * FROM transactions") + + return render_template("history.html", stocks=stocks) + # return apology("TODO") + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + """Log user in""" + + # Forget any user_id + session.clear() + + # User reached route via POST (as by submitting a form via POST) + if request.method == "POST": + # Ensure username was submitted + if not request.form.get("username"): + return apology("must provide username", 403) + + # Ensure password was submitted + elif not request.form.get("password"): + return apology("must provide password", 403) + + # Query database for username + rows = db.execute( + "SELECT * FROM users WHERE username = ?", request.form.get("username") + ) + + # Ensure username exists and password is correct + if len(rows) != 1 or not check_password_hash( + rows[0]["hash"], request.form.get("password") + ): + return apology("invalid username and/or password", 403) + + # Remember which user has logged in + session["user_id"] = rows[0]["id"] + + # Redirect user to home page + return redirect("/") + + # User reached route via GET (as by clicking a link or via redirect) + else: + return render_template("login.html") + + +@app.route("/logout") +def logout(): + """Log user out""" + + # Forget any user_id + session.clear() + + # Redirect user to login form + return redirect("/") + + +@app.route("/quote", methods=["GET", "POST"]) +@login_required +def quote(): + """Get stock quote.""" + if request.method == "POST": + + symbol = request.form.get("symbol") + if not symbol: + return apology("Not Symbol") + + stock = lookup(symbol.upper()) + + if stock == None: + return apology("Symbol not found") + + stock["price"] = usd(stock["price"]) + + return render_template("quote.html", stock=stock) + else: + return render_template("quote.html") + # return apology("TODO") + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + """Register user""" + if request.method == "POST": + # Ensure username was submitted + if not request.form.get("username"): + return apology("must provide username", 400) + + # Ensure password was submitted + elif not request.form.get("password"): + return apology("must provide password", 400) + + # Ensure password repeat matches + if not request.form.get("password") == request.form.get("confirmation"): + return apology("passwords don't match", 400) + + username = request.form.get("username") + password = request.form.get("password") + + username_exists = db.execute("SELECT * FROM users WHERE username = ?", username) + + if username_exists: + return apology("username already exists!", 400) + + db.execute("INSERT INTO users(username, hash) VALUES(?, ?)", + username, generate_password_hash(password)) + return redirect("/") + else: + return render_template("register.html") + # return apology("MOFO") + + +@app.route("/sell", methods=["GET", "POST"]) +@login_required +def sell(): + """Sell shares of stock""" + user_id = session["user_id"] + + if request.method == "POST": + symbol = request.form.get("symbol") + shares = int(request.form.get("shares")) + + if not symbol: + return apology("Not Symbol") + + stock = lookup(symbol.upper()) + + if stock == None: + return apology("Symbol not found") + + stocks = db.execute( + "SELECT symbol, sum(shares) FROM transactions WHERE user_id = ? AND symbol = ?", user_id, symbol) + + if stocks[0]["sum(shares)"] < shares: + return apology("Not enough shares") + + transaction_value = shares * stock["price"] + + user_cash_db = db.execute("SELECT cash FROM users WHERE id = ?", user_id) + user_cash = user_cash_db[0]["cash"] + + new_cash = transaction_value + user_cash + + db.execute("UPDATE users SET cash = ? WHERE id = ?", new_cash, user_id) + + date = datetime.datetime.now() + + db.execute("INSERT INTO transactions (user_id, symbol, shares, price, date) VALUES (?, ?, ?, ?, ?)", + user_id, stock["symbol"], -abs(shares), stock["price"], date) + + flash("Sold!") + + return redirect("/") + + else: + stocks = db.execute( + "SELECT symbol FROM transactions WHERE user_id = ? GROUP BY symbol HAVING sum(shares) > 0", user_id) + return render_template("sell.html", stocks=stocks) + # return apology("TODO") diff --git a/finance.db b/finance.db new file mode 100644 index 0000000000000000000000000000000000000000..b280d4559f6b5bd112249da37aafaccbd92a1733 GIT binary patch literal 20480 zcmeI3TWs4@7=WGhk|k-90xRLRVW6#3+LhyTi7(nj!j@GX={FV}Jhr z|DS(bvh0kFdh`pJBun&T9rUR$*8KLC@00#B&Lnw&o^%T5Z?i5zTWIfrT6_~ zF|oEWc3nvf#~;M6h#44Q01SWuFaQR?02lxRU;qq&0Wk153@m-r7)o|@1eQNtbV^fv zalx6XIPT))>`Xyhs0FO=X)ZjD}3REw9Ugcm9x{O*(tSTnKD1)?|H_V=Kgo3alNGCVE;%lC&C}e z9alF9b!f0UUr|#Vi@Jt`&B^ZWz<5s~WSKR`nyeW&7$PH6~cT5Mlo`D&`E_KQ- zFLTE!VQ!=mb6Cl9na4Qc#4GbOahSmj*W`#(US^DWj_&Bx!v=Oq*+2&JJj=tnPA#ug zaw+E?v1keDoal@rVj?eXGLA)L>bV}pCgGWtz|cf$b!F+TV6&239tq|-pPH@7+sTa= zNBWMrB4=F8j!m~?%c6#962y5Krx~MG8CxQCKZ7*&m~I-_6iIMQbPsuqc!;^gDwRuy z*oZnhBQB++WICov31g;3ncbF!;IxWNo9Z^wweEFrIA0k3 zBg1}Nxj2!!yA>pcO^n_*eJ~UvKKSEQ~cqFG|!S%NL+^!1urKWq|*d=e+EN$yn=j)HiqsocD%f7ao zxJ<=FBw-_>sXi}Sz&~Jr)`(pLbsHn?RS9ga10tIeZNCKmwKGUen^@XD32dwjVoQ5v z9n5Bj^Y=6S;JKT9m-lXVUo}S^*+kb~UI+bteo)iT8z5%dOA=Vy_*D=Q8ynh-QjT|b z&JpQ0)3q0*9GiB|QGh~EQUdD~BS4bYo|nM-5p_drlX7gTnkAl+O^2h z_I;n1g^QNA{6qMx1d1Hl{;MOmFKh*==uAU z)PSNX64I_8WH_mgZD86aREtXy#UcD!)N<}-4XCHJm=rNsHzGn>ixe@EVPA@W0bHEe z7LnPQYEdbo$noF~AfdMC$A}bB=((~(F+`lPNDE7#C}t;*x;VK^ZI1+sh!<*(jA%ye z=NOW}JsBo0?4laE7;*L!zaN_=P+Y1%zb3}dHJ~V_X&v-W*I=gn)7I|6)oTwC*EFqB ViWtezd!l#tU)_wj24b5U{{XFn*Z2Sc literal 0 HcmV?d00001 diff --git a/flask_session/16fa13c5890feb947b5d5c907cfedf5b b/flask_session/16fa13c5890feb947b5d5c907cfedf5b new file mode 100644 index 0000000000000000000000000000000000000000..4bd85b062161b3536bfe68b253486fa3e6f3b319 GIT binary patch literal 46 xcmYe24@qlaohr`&0ku;!dbr{XQj2mE^HTFlrgZkOmlmfM#b>5W@n$U50{}2M4vPQ) literal 0 HcmV?d00001 diff --git a/flask_session/2029240f6d1128be89ddc32729463129 b/flask_session/2029240f6d1128be89ddc32729463129 new file mode 100644 index 0000000000000000000000000000000000000000..7f5741f13017ee705ea34021d222a06a6ee2a6c9 GIT binary patch literal 9 QcmZQzU|?uq^=8ro00XcA0RR91 literal 0 HcmV?d00001 diff --git a/flask_session/39b31c290f4b506fd545e82536ad3484 b/flask_session/39b31c290f4b506fd545e82536ad3484 new file mode 100644 index 0000000000000000000000000000000000000000..2f7fa3ad5df5db38fa0fd157f36f2033d36ae944 GIT binary patch literal 46 xcmZ=>7@XF?I#r$l0&1sd^l-%&q!#5S=B4J9OzG@lFD*_jiqA}$;>}p92LM4-4($K{ literal 0 HcmV?d00001 diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..43349ca --- /dev/null +++ b/helpers.py @@ -0,0 +1,88 @@ +import csv +import datetime +import pytz +import requests +import urllib +import uuid + +from flask import redirect, render_template, request, session +from functools import wraps + + +def apology(message, code=400): + """Render message as an apology to user.""" + + def escape(s): + """ + Escape special characters. + + https://github.com/jacebrowning/memegen#special-characters + """ + for old, new in [ + ("-", "--"), + (" ", "-"), + ("_", "__"), + ("?", "~q"), + ("%", "~p"), + ("#", "~h"), + ("/", "~s"), + ('"', "''"), + ]: + s = s.replace(old, new) + return s + + return render_template("apology.html", top=code, bottom=escape(message)), code + + +def login_required(f): + """ + Decorate routes to require login. + + https://flask.palletsprojects.com/en/latest/patterns/viewdecorators/ + """ + + @wraps(f) + def decorated_function(*args, **kwargs): + if session.get("user_id") is None: + return redirect("/login") + return f(*args, **kwargs) + + return decorated_function + + +def lookup(symbol): + """Look up quote for symbol.""" + + # Prepare API request + symbol = symbol.upper() + end = datetime.datetime.now(pytz.timezone("US/Eastern")) + start = end - datetime.timedelta(days=7) + + # Yahoo Finance API + url = ( + f"https://query1.finance.yahoo.com/v7/finance/download/{urllib.parse.quote_plus(symbol)}" + f"?period1={int(start.timestamp())}" + f"&period2={int(end.timestamp())}" + f"&interval=1d&events=history&includeAdjustedClose=true" + ) + + # Query API + try: + response = requests.get( + url, + cookies={"session": str(uuid.uuid4())}, + headers={"Accept": "*/*", "User-Agent": request.headers.get("User-Agent")}, + ) + response.raise_for_status() + + # CSV header: Date,Open,High,Low,Close,Adj Close,Volume + quotes = list(csv.DictReader(response.content.decode("utf-8").splitlines())) + price = round(float(quotes[-1]["Adj Close"]), 2) + return {"price": price, "symbol": symbol} + except (KeyError, IndexError, requests.RequestException, ValueError): + return None + + +def usd(value): + """Format value as USD.""" + return f"${value:,.2f}" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..743fadd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +cs50 +Flask +Flask-Session +pytz +requests diff --git a/static/I_heart_validator.png b/static/I_heart_validator.png new file mode 100644 index 0000000000000000000000000000000000000000..c7db3d2bd567deed30dafc9f831d1e4d3aa56c10 GIT binary patch literal 345 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 + + {{ top }} +{% endblock %} diff --git a/templates/buy.html b/templates/buy.html new file mode 100644 index 0000000..b34ebb5 --- /dev/null +++ b/templates/buy.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} + +{% block title %} + Buy +{% endblock %} + +{% block main %} +
+
+ +
+
+ +
+ +
+{% endblock %} diff --git a/templates/history.html b/templates/history.html new file mode 100644 index 0000000..c829e7b --- /dev/null +++ b/templates/history.html @@ -0,0 +1,30 @@ +{% extends "layout.html" %} + +{% block title %} + History +{% endblock %} + +{% block main %} + + + + + + + + + + + + {% for stock in stocks%} + + + + + + + {% endfor%} + +
SymbolSharesPriceTransacted
{{ stock.symbol }}{{ stock.shares }}{{ stock.price | usd}}{{ stock.date }}
+ +{% endblock %} diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..d32f80e --- /dev/null +++ b/templates/home.html @@ -0,0 +1,40 @@ +{% extends "layout.html" %} + +{% block title %} + Home +{% endblock %} + +{% block main %} + + + + + + + + + + + + {% for stock in stocks%} + + + + + + + {% endfor%} + + + + + + + + + + + + +
SymbolSharesPriceTOTAL
{{ stock.symbol }}{{ stock.shares }}{{ stock.price | usd }}{{ stock.total | usd }}
Cash{{ cash | usd }}
TOTAL{{ total | usd }}
+{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..14d3053 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + C$50 Finance: {% block title %}{% endblock %} + + + + + + + + {% if get_flashed_messages() %} +
+ +
+ {% endif %} + +
+ {% block main %}{% endblock %} +
+ +
+ +

+ Data provided by Yahoo +

+ +
+ + + +
+ +
+ + + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..0fe2d8e --- /dev/null +++ b/templates/login.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} + +{% block title %} + Log In +{% endblock %} + +{% block main %} +
+
+ +
+
+ +
+ +
+{% endblock %} diff --git a/templates/quote.html b/templates/quote.html new file mode 100644 index 0000000..aebde0e --- /dev/null +++ b/templates/quote.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} + +{% block title %} + Quote +{% endblock %} + +{% block main %} + + {% if stock %} +

A share of {{ stock.symbol }} costs {{ stock.price }}

+ + {% else%} +
+
+ +
+ +
+ {% endif %} + + + +{% endblock %} diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..c0bddda --- /dev/null +++ b/templates/register.html @@ -0,0 +1,20 @@ +{% extends "layout.html" %} + +{% block title %} + Register +{% endblock %} + +{% block main %} +
+
+ +
+
+ +
+
+ +
+ +
+{% endblock %} diff --git a/templates/sell.html b/templates/sell.html new file mode 100644 index 0000000..68f5186 --- /dev/null +++ b/templates/sell.html @@ -0,0 +1,22 @@ +{% extends "layout.html" %} + +{% block title %} + Sell +{% endblock %} + +{% block main %} +
+
+ +
+
+ +
+ +
+{% endblock %}