diff --git a/packet/context_processors.py b/packet/context_processors.py index 93ab115..c13adf6 100644 --- a/packet/context_processors.py +++ b/packet/context_processors.py @@ -62,9 +62,9 @@ def get_rit_image(username: str) -> str: for addr in addresses: url = 'https://gravatar.com/avatar/' + hashlib.md5(addr.encode('utf8')).hexdigest() + '.jpg?d=404&s=250' try: - gravatar = urllib.request.urlopen(url) - if gravatar.getcode() == 200: - return url + with urllib.request.urlopen(url) as gravatar: + if gravatar.getcode() == 200: + return url except: continue return 'https://www.gravatar.com/avatar/freshmen?d=mp&f=y' diff --git a/packet/ldap.py b/packet/ldap.py index a7008a0..005ac43 100644 --- a/packet/ldap.py +++ b/packet/ldap.py @@ -13,7 +13,11 @@ class MockMember: - def __init__(self, uid: str, groups: list = None, cn: str = None, room_number: int = None): + def __init__(self, + uid: str, + groups: Optional[list] = None, + cn: Optional[str] = None, + room_number: Optional[int] = None): self.uid = uid self.groups = groups if groups else list() if room_number: @@ -37,7 +41,7 @@ def __repr__(self) -> str: class LDAPWrapper: - def __init__(self, cshldap: CSHLDAP = None, mock_members: list[MockMember] = None): + def __init__(self, cshldap: Optional[CSHLDAP] = None, mock_members: Optional[list[MockMember]] = None): self.ldap = cshldap self.mock_members = cast(list[MockMember], mock_members) if self.ldap: diff --git a/packet/mail.py b/packet/mail.py index 5aa32f5..c0f9db6 100644 --- a/packet/mail.py +++ b/packet/mail.py @@ -1,4 +1,4 @@ -from typing import TypedDict +from typing import TypedDict, List, Union, cast from flask import render_template from flask_mail import Mail, Message @@ -15,10 +15,10 @@ class ReportForm(TypedDict): def send_start_packet_mail(packet: Packet) -> None: if app.config['MAIL_PROD']: - recipients = ['<' + packet.freshman.rit_username + '@rit.edu>'] + recipients = ['<' + str(packet.freshman.rit_username) + '@rit.edu>'] msg = Message(subject='CSH Packet Starts ' + packet.start.strftime('%A, %B %-d'), sender=app.config.get('MAIL_USERNAME'), - recipients=recipients) + recipients=cast(List[Union[str, tuple[str, str]]], recipients)) template = 'mail/packet_start' msg.body = render_template(template + '.txt', packet=packet) @@ -31,7 +31,7 @@ def send_report_mail(form_results: ReportForm, reporter: str) -> None: recipients = [''] msg = Message(subject='Packet Report', sender=app.config.get('MAIL_USERNAME'), - recipients=recipients) + recipients=cast(List[Union[str, tuple[str, str]]], recipients)) person = form_results['person'] report = form_results['report'] diff --git a/packet/routes/admin.py b/packet/routes/admin.py index 96a877b..0d13063 100644 --- a/packet/routes/admin.py +++ b/packet/routes/admin.py @@ -1,3 +1,4 @@ +from typing import Dict, Any from flask import render_template from packet import app @@ -13,7 +14,7 @@ @admin_auth @before_request @log_time -def admin_packets(info=None): +def admin_packets(info: Dict[str, Any]) -> str: open_packets = Packet.open_packets() # Pre-calculate and store the return values of did_sign(), signatures_received(), and signatures_required() @@ -35,7 +36,7 @@ def admin_packets(info=None): @admin_auth @before_request @log_time -def admin_freshmen(info=None): +def admin_freshmen(info: Dict[str, Any]) -> str: all_freshmen = Freshman.get_all() return render_template('admin_freshmen.html', diff --git a/packet/routes/api.py b/packet/routes/api.py index c364962..5e14eef 100644 --- a/packet/routes/api.py +++ b/packet/routes/api.py @@ -1,8 +1,9 @@ """ Shared API endpoints """ -from datetime import datetime +from datetime import datetime, date from json import dumps +from typing import Dict, Any, Union, Tuple from flask import session, request @@ -18,15 +19,15 @@ class POSTFreshman: - def __init__(self, freshman): - self.name = freshman['name'].strip() - self.rit_username = freshman['rit_username'].strip() - self.onfloor = freshman['onfloor'].strip() == 'TRUE' + def __init__(self, freshman: Dict[str, Any]) -> None: + self.name: str = freshman['name'].strip() + self.rit_username: str = freshman['rit_username'].strip() + self.onfloor: bool = freshman['onfloor'].strip() == 'TRUE' @app.route('/api/v1/freshmen', methods=['POST']) @packet_auth -def sync_freshman(): +def sync_freshman() -> Tuple[str, int]: """ Create or update freshmen entries from a list @@ -40,11 +41,13 @@ def sync_freshman(): """ # Only allow evals to create new frosh - username = str(session['userinfo'].get('preferred_username', '')) + username: str = str(session['userinfo'].get('preferred_username', '')) if not ldap.is_evals(ldap.get_member(username)): return 'Forbidden: not Evaluations Director', 403 - freshmen_in_post = {freshman.rit_username: freshman for freshman in map(POSTFreshman, request.json)} + freshmen_in_post: Dict[str, POSTFreshman] = { + freshman.rit_username: freshman for freshman in map(POSTFreshman, request.json) + } sync_freshman_list(freshmen_in_post) return dumps('Done'), 200 @@ -52,7 +55,7 @@ def sync_freshman(): @app.route('/api/v1/packets', methods=['POST']) @packet_auth @log_time -def create_packet(): +def create_packet() -> Tuple[str, int]: """ Create a new packet. @@ -69,13 +72,15 @@ def create_packet(): """ # Only allow evals to create new packets - username = str(session['userinfo'].get('preferred_username', '')) + username: str = str(session['userinfo'].get('preferred_username', '')) if not ldap.is_evals(ldap.get_member(username)): return 'Forbidden: not Evaluations Director', 403 - base_date = datetime.strptime(request.json['start_date'], '%m/%d/%Y').date() + base_date: date = datetime.strptime(request.json['start_date'], '%m/%d/%Y').date() - freshmen_in_post = {freshman.rit_username: freshman for freshman in map(POSTFreshman, request.json['freshmen'])} + freshmen_in_post: Dict[str, POSTFreshman] = { + freshman.rit_username: freshman for freshman in map(POSTFreshman, request.json['freshmen']) + } create_new_packets(base_date, freshmen_in_post) @@ -85,9 +90,9 @@ def create_packet(): @app.route('/api/v1/sync', methods=['POST']) @packet_auth @log_time -def sync_ldap(): +def sync_ldap() -> Tuple[str, int]: # Only allow evals to sync ldap - username = str(session['userinfo'].get('preferred_username', '')) + username: str = str(session['userinfo'].get('preferred_username', '')) if not ldap.is_evals(ldap.get_member(username)): return 'Forbidden: not Evaluations Director', 403 sync_with_ldap() @@ -97,14 +102,14 @@ def sync_ldap(): @app.route('/api/v1/packets/', methods=['GET']) @packet_auth @before_request -def get_packets_by_user(username: str, info=None) -> dict: +def get_packets_by_user(username: str, info: Dict[str, Any]) -> Union[Dict[int, Dict[str, Any]], Tuple[str, int]]: """ Return a dictionary of packets for a freshman by username, giving packet start and end date by packet id """ if info['ritdn'] != username: return 'Forbidden - not your packet', 403 - frosh = Freshman.by_username(username) + frosh: Freshman = Freshman.by_username(username) return {packet.id: { 'start': packet.start, @@ -115,7 +120,7 @@ def get_packets_by_user(username: str, info=None) -> dict: @app.route('/api/v1/packets//newest', methods=['GET']) @packet_auth @before_request -def get_newest_packet_by_user(username: str, info=None) -> dict: +def get_newest_packet_by_user(username: str, info: Dict[str, Any]) -> Union[Dict[int, Dict[str, Any]], Tuple[str, int]]: """ Return a user's newest packet """ @@ -123,9 +128,9 @@ def get_newest_packet_by_user(username: str, info=None) -> dict: if not info['is_upper'] and info['ritdn'] != username: return 'Forbidden - not your packet', 403 - frosh = Freshman.by_username(username) + frosh: Freshman = Freshman.by_username(username) - packet = frosh.packets[-1] + packet: Packet = frosh.packets[-1] return { packet.id: { @@ -137,15 +142,15 @@ def get_newest_packet_by_user(username: str, info=None) -> dict: } -@app.route('/api/v1/packet/', methods=['GET']) +@app.route('/api/v1/packet/', methods=['GET']) @packet_auth @before_request -def get_packet_by_id(packet_id: int, info=None) -> dict: +def get_packet_by_id(packet_id: int, info: Dict[str, Any]) -> Union[Dict[str, Dict[str, Any]], Tuple[str, int]]: """ Return the scores of the packet in question """ - packet = Packet.by_id(packet_id) + packet: Packet = Packet.by_id(packet_id) if not info['is_upper'] and info['ritdn'] != packet.freshman.rit_username: return 'Forbidden - not your packet', 403 @@ -156,14 +161,14 @@ def get_packet_by_id(packet_id: int, info=None) -> dict: } -@app.route('/api/v1/sign//', methods=['POST']) +@app.route('/api/v1/sign//', methods=['POST']) @packet_auth @before_request -def sign(packet_id, info): - packet = Packet.by_id(packet_id) +def sign(packet_id: int, info: Dict[str, Any]) -> str: + packet: Packet = Packet.by_id(packet_id) if packet is not None and packet.is_open(): - was_100 = packet.is_100() + was_100: bool = packet.is_100() if app.config['REALM'] == 'csh': # Check if the CSHer is an upperclassman and if so, sign that row for sig in filter(lambda sig: sig.member == info['uid'], packet.upper_signatures): @@ -189,8 +194,9 @@ def sign(packet_id, info): @app.route('/api/v1/subscribe/', methods=['POST']) @packet_auth @before_request -def subscribe(info): +def subscribe(info: Dict[str, Any]) -> str: data = request.form + subscription: NotificationSubscription if app.config['REALM'] == 'csh': subscription = NotificationSubscription(token=data['token'], member=info['uid']) else: @@ -203,16 +209,16 @@ def subscribe(info): @app.route('/api/v1/report/', methods=['POST']) @packet_auth @before_request -def report(info): +def report(info: Dict[str, Any]) -> str: form_results = request.form send_report_mail(form_results, get_rit_name(info['uid'])) return 'Success: ' + get_rit_name(info['uid']) + ' sent a report' -@app.route('/api/v1/stats/packet/') +@app.route('/api/v1/stats/packet/') @packet_auth @before_request -def packet_stats(packet_id, info=None): +def packet_stats(packet_id: int, info: Dict[str, Any]) -> Union[stats.PacketStats, Tuple[str, int]]: if not info['is_upper'] and info['ritdn'] != Packet.by_id(packet_id).freshman.rit_username: return 'Forbidden - not your packet', 403 return stats.packet_stats(packet_id) @@ -221,7 +227,7 @@ def packet_stats(packet_id, info=None): @app.route('/api/v1/stats/upperclassman/') @packet_auth @before_request -def upperclassman_stats(uid, info=None): +def upperclassman_stats(uid: str, info: Dict[str, Any]) -> Union[stats.UpperStats, Tuple[str, int]]: if not info['is_upper']: return 'Forbidden', 403 @@ -229,12 +235,12 @@ def upperclassman_stats(uid, info=None): @app.route('/readiness') -def readiness() -> tuple[str, int]: +def readiness() -> Tuple[str, int]: """A basic healthcheck. Returns 200 to indicate flask is running""" return 'ready', 200 -def commit_sig(packet, was_100, uid): +def commit_sig(packet: Packet, was_100: bool, uid: str) -> str: packet_signed_notification(packet, uid) db.session.commit() if not was_100 and packet.is_100(): diff --git a/packet/routes/freshmen.py b/packet/routes/freshmen.py index 85d3a28..1e09363 100644 --- a/packet/routes/freshmen.py +++ b/packet/routes/freshmen.py @@ -2,7 +2,8 @@ Routes available to freshmen only """ -from flask import redirect, url_for +from typing import Any +from flask import Response, redirect, url_for from packet import app from packet.models import Packet @@ -12,8 +13,11 @@ @app.route('/') @packet_auth @before_request -def index(info=None): - most_recent_packet = Packet.query.filter_by(freshman_username=info['uid']).order_by(Packet.id.desc()).first() +def index(info: dict[str, Any]) -> Response: + most_recent_packet = (Packet.query + .filter_by(freshman_username=info['uid']) + .order_by(Packet.id.desc()) # type: ignore + .first()) if most_recent_packet is not None: return redirect(url_for('freshman_packet', packet_id=most_recent_packet.id), 302) diff --git a/packet/routes/shared.py b/packet/routes/shared.py index bd671bf..13ebe3f 100644 --- a/packet/routes/shared.py +++ b/packet/routes/shared.py @@ -2,7 +2,8 @@ Routes available to both freshmen and CSH users """ -from flask import render_template, redirect +from typing import Optional, Dict, Any, Tuple, Union, List +from flask import render_template, redirect, Response from packet import auth, app from packet.utils import before_request, packet_auth @@ -12,16 +13,16 @@ @app.route('/logout/') @auth.oidc_logout -def logout(): +def logout() -> Response: return redirect('https://csh.rit.edu') -@app.route('/packet//') +@app.route('/packet//') @log_cache @packet_auth @before_request @log_time -def freshman_packet(packet_id, info=None): +def freshman_packet(packet_id: int, info: Dict[str, Any]) -> Union[str, Tuple[str, int]]: packet = Packet.by_id(packet_id) if packet is None: @@ -29,7 +30,7 @@ def freshman_packet(packet_id, info=None): else: # The current user's freshman signature on this packet - fresh_sig = list(filter( + fresh_sig: List[Any] = list(filter( lambda sig: sig.freshman_username == info['ritdn'] if info else '', packet.fresh_signatures )) @@ -44,7 +45,7 @@ def freshman_packet(packet_id, info=None): fresh_sig=fresh_sig) -def packet_sort_key(packet): +def packet_sort_key(packet: Packet) -> Tuple[str, int, bool]: """ Utility function for generating keys for sorting packets """ @@ -56,7 +57,7 @@ def packet_sort_key(packet): @packet_auth @before_request @log_time -def packets(info=None): +def packets(info: Dict[str, Any]) -> str: open_packets = Packet.open_packets() # Pre-calculate and store the return values of did_sign(), signatures_received(), and signatures_required() @@ -72,25 +73,25 @@ def packets(info=None): @app.route('/sw.js', methods=['GET']) @app.route('/OneSignalSDKWorker.js', methods=['GET']) -def service_worker(): +def service_worker() -> Response: return app.send_static_file('js/sw.js') @app.route('/update-sw.js', methods=['GET']) @app.route('/OneSignalSDKUpdaterWorker.js', methods=['GET']) -def update_service_worker(): +def update_service_worker() -> Response: return app.send_static_file('js/update-sw.js') @app.errorhandler(404) @packet_auth @before_request -def not_found(e, info=None): +def not_found(e: Exception, info: Optional[Dict[str, Any]] = None) -> Tuple[str, int]: return render_template('not_found.html', e=e, info=info), 404 @app.errorhandler(500) @packet_auth @before_request -def error(e, info=None): +def error(e: Exception, info: Optional[Dict[str, Any]] = None) -> Tuple[str, int]: return render_template('error.html', e=e, info=info), 500 diff --git a/packet/routes/upperclassmen.py b/packet/routes/upperclassmen.py index e01fc74..7c21916 100644 --- a/packet/routes/upperclassmen.py +++ b/packet/routes/upperclassmen.py @@ -4,7 +4,8 @@ import json from operator import itemgetter -from flask import redirect, render_template, url_for +from typing import Optional, Dict, Any, List +from flask import redirect, render_template, url_for, Response from packet import app from packet.models import Packet @@ -15,7 +16,7 @@ @app.route('/') @packet_auth -def index(): +def index() -> Response: return redirect(url_for('packets'), 302) @@ -24,14 +25,14 @@ def index(): @packet_auth @before_request @log_time -def upperclassman(uid, info=None): +def upperclassman(uid: str, info: Optional[Dict[str, Any]] = None) -> str: open_packets = Packet.open_packets() # Pre-calculate and store the return value of did_sign() for packet in open_packets: packet.did_sign_result = packet.did_sign(uid, True) - signatures = sum(map(lambda packet: 1 if packet.did_sign_result else 0, open_packets)) + signatures: int = sum(map(lambda packet: 1 if packet.did_sign_result else 0, open_packets)) open_packets.sort(key=lambda packet: packet.freshman_username) open_packets.sort(key=lambda packet: packet.did_sign_result, reverse=True) @@ -45,12 +46,12 @@ def upperclassman(uid, info=None): @packet_auth @before_request @log_time -def upperclassmen_total(info=None): +def upperclassmen_total(info: Optional[Dict[str, Any]] = None) -> str: open_packets = Packet.open_packets() # Sum up the signed packets per upperclassman - upperclassmen = dict() - misc = dict() + upperclassmen: Dict[str, int] = dict() + misc: Dict[str, int] = dict() for packet in open_packets: for sig in packet.upper_signatures: if sig.member not in upperclassmen: @@ -69,16 +70,17 @@ def upperclassmen_total(info=None): @app.route('/stats/packet/') @packet_auth @before_request -def packet_graphs(packet_id, info=None): +def packet_graphs(packet_id: int, info: Optional[Dict[str, Any]] = None) -> str: stats = packet_stats(packet_id) - fresh = [] - misc = [] - upper = [] - + fresh: List[int] = [] + misc: List[int] = [] + upper: List[int] = [] # Make a rolling sum of signatures over time - agg = lambda l, attr, date: l.append((l[-1] if l else 0) + len(stats['dates'][date][attr])) - dates = list(stats['dates'].keys()) + def agg(l: List[int], attr: str, date: str) -> None: + l.append((l[-1] if l else 0) + len(stats['dates'][date][attr])) + + dates: List[str] = list(stats['dates'].keys()) for date in dates: agg(fresh, 'fresh', date) agg(misc, 'misc', date) @@ -92,11 +94,11 @@ def packet_graphs(packet_id, info=None): return render_template('packet_stats.html', info=info, data=json.dumps({ - 'dates':dates, + 'dates': dates, 'accum': { - 'fresh':fresh, - 'misc':misc, - 'upper':upper, + 'fresh': fresh, + 'misc': misc, + 'upper': upper, }, 'daily': { diff --git a/requirements.in b/requirements.in index 0797579..910537c 100644 --- a/requirements.in +++ b/requirements.in @@ -3,7 +3,7 @@ Flask-Mail==0.10.0 Flask-Migrate~=2.7.0 Flask-pyoidc~=3.7.0 Flask~=1.1.4 -csh_ldap~=2.4.0 +csh-ldap @ git+https://github.com/costowell/csh_ldap@67dd183744746c758d6c13878f539437d2628b63 ddtrace==3.12.2 flask_sqlalchemy~=2.5.1 gunicorn~=20.0.4 diff --git a/requirements.txt b/requirements.txt index cc66006..986ffab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ click==7.1.2 # pip-tools cryptography==45.0.6 # via oic -csh-ldap==2.4.0 +csh-ldap @ git+https://github.com/costowell/csh_ldap@67dd183744746c758d6c13878f539437d2628b63 # via -r requirements.in ddtrace==3.12.2 # via -r requirements.in @@ -141,7 +141,7 @@ pylint-quotes==0.2.3 # via -r requirements.in python-dotenv==1.1.1 # via pydantic-settings -python-ldap==3.4.0 +python-ldap==3.4.4 # via csh-ldap requests==2.32.5 # via