From 59ecb4a25969b8e885833cc7223fa9a859321e26 Mon Sep 17 00:00:00 2001 From: Averon <77922089+TheAveron@users.noreply.github.com> Date: Sat, 31 Aug 2024 11:00:39 +0200 Subject: [PATCH] refactor: refacoring for cleaner code --- app.py | 58 ++++++++--------- modules/__init__.py | 2 +- modules/database.py | 55 ++++++++++------- modules/iss_info.py | 11 ++-- modules/iss_tracker.py | 99 +++++++++++++---------------- modules/reminder_service.py | 120 +++++++++++++++--------------------- modules/tle_fetcher.py | 87 +++++++++++++++++--------- modules/user_service.py | 45 ++++++++------ static/css/components.css | 2 +- tle_cache.txt | 3 + 10 files changed, 248 insertions(+), 234 deletions(-) create mode 100644 tle_cache.txt diff --git a/app.py b/app.py index fceeef0..ce0940a 100644 --- a/app.py +++ b/app.py @@ -1,33 +1,39 @@ import os -from datetime import UTC, datetime - +from datetime import datetime, timezone from dotenv import load_dotenv -from flask import (Flask, flash, jsonify, redirect, render_template, request, - session, url_for) +from flask import ( + Flask, flash, jsonify, redirect, render_template, request, + session, url_for +) from flask_socketio import SocketIO +from modules import ( + init_db, start_reminder_checker, fetch_tle_data, get_logged_in_user, + register_user, verify_user, login_user, logout_user, get_current_position, + get_iss_crew, get_future_positions, get_iss_info, get_next_passes, add_reminder +) -from modules import * - +# Load environment variables load_dotenv() +# Flask app and SocketIO initialization app = Flask(__name__) - -app.secret_key = os.environ["TOKEN"] +app.secret_key = os.getenv("TOKEN") socketio = SocketIO(app) +# Initialize database and start background processes init_db() start_reminder_checker() TLE = fetch_tle_data() - @app.route("/") def index(): + """Render the home page with the current user's ID.""" user_id = get_logged_in_user() return render_template("index.html", user_id=user_id) - @app.route("/register", methods=["GET", "POST"]) def register(): + """Handle user registration.""" if request.method == "POST": username = request.form["username"] password = request.form["password"] @@ -40,9 +46,9 @@ def register(): return render_template("register.html") - @app.route("/login", methods=["GET", "POST"]) def login(): + """Handle user login.""" if request.method == "POST": username = request.form["username"] password = request.form["password"] @@ -57,45 +63,43 @@ def login(): return render_template("login.html") - @app.route("/logout") def logout(): + """Log out the current user.""" logout_user() flash("You have been logged out.", "info") return redirect(url_for("index")) - @app.route("/iss-now") def iss_now(): + """Return the current position of the ISS.""" current_position = get_current_position(TLE) return jsonify(current_position) - @app.route("/iss-crew") def iss_crew(): - iss_crew = get_iss_crew() - return jsonify(iss_crew[0], iss_crew[1]) - + """Return the current crew members on the ISS.""" + iss_crew_data, status_code = get_iss_crew() + return jsonify(iss_crew_data), status_code @app.route("/future-trajectory") def future_trajectory(): - start_time = datetime.now(UTC) - duration = float( - request.args.get("duration", 3600) - ) # Default to 1 hour if no duration provided + """Return the future trajectory of the ISS.""" + start_time = datetime.now(timezone.utc) + duration = float(request.args.get("duration", 3600)) # Default to 1 hour interval = 60 # Calculate every 60 seconds future_positions = get_future_positions(TLE, start_time, duration, interval) return jsonify(future_positions) - @app.route("/iss-info") def iss_info(): + """Return detailed information about the ISS.""" info = get_iss_info(TLE) return jsonify(info) - @app.route("/next-passes", methods=["GET"]) def next_passes(): + """Return the next passes of the ISS over a given location.""" lat = request.args.get("lat", type=float) lon = request.args.get("lon", type=float) if lat is None or lon is None: @@ -104,18 +108,14 @@ def next_passes(): passes = get_next_passes(TLE, lat, lon, num_passes=3) return jsonify(passes) - @app.route("/add-reminder", methods=["POST"]) def add_reminder_route(): + """Add a reminder for a specific ISS pass time.""" user_id = request.form["user_id"] - pass_time = request.form[ - "pass_time" - ] # Expecting ISO format: '2024-08-30T10:15:00Z' + pass_time = request.form["pass_time"] # ISO format: '2024-08-30T10:15:00Z' add_reminder(user_id, pass_time) - return jsonify({"status": "success"}), 200 - if __name__ == "__main__": app.run(debug=False) socketio.run(app) diff --git a/modules/__init__.py b/modules/__init__.py index 5d2b824..c161638 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -1,5 +1,5 @@ from .database import * -from .iss_info import get_iss_crew +from .iss_info import * from .iss_tracker import * from .reminder_service import * from .tle_fetcher import * diff --git a/modules/database.py b/modules/database.py index aefb4fc..05a95b3 100644 --- a/modules/database.py +++ b/modules/database.py @@ -1,33 +1,42 @@ import sqlite3 +DATABASE_FILE = "database.db" def init_db(): - conn = sqlite3.connect("database.db") - cursor = conn.cursor() - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS iss_reminders ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - pass_time DATETIME, - notified BOOLEAN DEFAULT 0 - ) """ - ) + Initializes the database by creating the necessary tables if they do not exist. + """ + with sqlite3.connect(DATABASE_FILE) as conn: + cursor = conn.cursor() - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL + # Create iss_reminders table + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS iss_reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + pass_time DATETIME, + notified BOOLEAN DEFAULT 0 + ) + """ ) - """ - ) - conn.commit() - conn.close() + + # Create users table + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL + ) + """ + ) + + conn.commit() def get_db_connection(): - conn = sqlite3.connect("database.db") - return conn + """ + Returns a new connection to the database. + """ + return sqlite3.connect(DATABASE_FILE) diff --git a/modules/iss_info.py b/modules/iss_info.py index 240065a..e44efd5 100644 --- a/modules/iss_info.py +++ b/modules/iss_info.py @@ -3,15 +3,18 @@ def get_iss_crew(): - # Placeholder for ISS crew data logic + """ + Fetch and return the list of crew members currently aboard the ISS. + """ try: response = requests.get("http://api.open-notify.org/astros.json") response.raise_for_status() data = response.json() - # Filter the crew members aboard the ISS - iss_crew = [person for person in data["people"] if person["craft"] == "ISS"] + # Extract crew members aboard the ISS + iss_crew = [person for person in data.get("people", []) if person.get("craft") == "ISS"] return {"crew": iss_crew}, 200 - except requests.RequestException as e: + + except requests.RequestException: return {"error": "Error fetching ISS crew data"}, 500 diff --git a/modules/iss_tracker.py b/modules/iss_tracker.py index 6649435..e47a03b 100644 --- a/modules/iss_tracker.py +++ b/modules/iss_tracker.py @@ -1,25 +1,26 @@ -from datetime import UTC, datetime, timedelta - +from datetime import datetime, timedelta, timezone import ephem -def observer_setup(lat="40.7128", lon="-74.0060"): - # Create an Observer object +def observer_setup(lat="40.7128", lon="-74.0060", elevation=10): + """ + Set up an observer with given latitude, longitude, and elevation. + """ observer = ephem.Observer() - - # Set the observer's location observer.lat = lat observer.lon = lon - observer.elevation = 10 - - observer.date = datetime.now(UTC) - + observer.elevation = elevation + observer.date = datetime.now(timezone.utc) return observer def get_current_position(tle): - satellite = ephem.readtle(tle[0], tle[1], tle[2]) - satellite.compute(observer_setup()) + """ + Get the current latitude and longitude of the satellite. + """ + observer = observer_setup() + satellite = ephem.readtle(*tle) + satellite.compute(observer) return { "lat": satellite.sublat * 180.0 / ephem.pi, "lon": satellite.sublong * 180.0 / ephem.pi, @@ -27,71 +28,55 @@ def get_current_position(tle): def get_future_positions(tle, start_time, duration, interval): - satellite = ephem.readtle(tle[0], tle[1], tle[2]) + """ + Get satellite positions at regular intervals over a specified duration. + """ + satellite = ephem.readtle(*tle) positions = [] current_time = start_time while current_time <= start_time + timedelta(seconds=duration): satellite.compute(current_time) - positions.append( - { - "lat": satellite.sublat * 180.0 / ephem.pi, - "lon": satellite.sublong * 180.0 / ephem.pi, - } - ) + positions.append({ + "lat": satellite.sublat * 180.0 / ephem.pi, + "lon": satellite.sublong * 180.0 / ephem.pi, + }) current_time += timedelta(seconds=interval) return positions def get_iss_info(tle): - # Create a satellite object from the TLE data - satellite = ephem.readtle(tle[0], tle[1], tle[2]) - - # Compute the current position of the satellite - satellite.compute(observer_setup()) # Use the current UTC time - - # Get latitude and longitude of the ISS - latitude = satellite.sublat * 180.0 / ephem.pi # Convert from radians to degrees - longitude = satellite.sublong * 180.0 / ephem.pi # Convert from radians to degrees + """ + Get current information about the ISS including position, altitude, and speed. + """ + observer = observer_setup() + satellite = ephem.readtle(*tle) + satellite.compute(observer) - # Calculate altitude in kilometers - altitude_km = satellite.elevation / 1000.0 # ephem returns elevation in meters - - # Calculate speed in km/h - speed_kmh = abs( - satellite.range_velocity / 1000.0 * 3600.0 - ) # ephem returns range_velocity in m/s - - # Return the data as JSON return { - "latitude": latitude, - "longitude": longitude, - "altitude": round(altitude_km, 2), - "speed": round(speed_kmh, 2), + "latitude": satellite.sublat * 180.0 / ephem.pi, + "longitude": satellite.sublong * 180.0 / ephem.pi, + "altitude": round(satellite.elevation / 1000.0, 2), # Convert to km + "speed": round(abs(satellite.range_velocity) / 1000.0 * 3600.0, 2), # Convert to km/h "timestamp": datetime.utcnow().isoformat() + "Z", } def get_next_passes(tle, observer_lat, observer_lon, num_passes=3): - iss = ephem.readtle(tle[0], tle[1], tle[2]) - - Paris = { - "lat": "48.864716", - "Lon": "2.349014", - } - observer = observer_setup(Paris["lat"], Paris["Lon"]) - + """ + Get the next passes of the ISS over a specified observer location. + """ + observer = observer_setup(lat=observer_lat, lon=observer_lon) + satellite = ephem.readtle(*tle) passes = [] + for _ in range(num_passes): - next_pass = observer.next_pass(iss) - print(next_pass) - passes.append( - { - "rise_time": next_pass[0].datetime().strftime("%Y-%m-%d %H:%M:%S UTC"), - "set_time": next_pass[4].datetime().strftime("%Y-%m-%d %H:%M:%S UTC"), - } - ) + next_pass = observer.next_pass(satellite) + passes.append({ + "rise_time": next_pass[0].datetime().strftime("%Y-%m-%d %H:%M:%S UTC"), + "set_time": next_pass[4].datetime().strftime("%Y-%m-%d %H:%M:%S UTC"), + }) observer.date = next_pass[4] + ephem.minute # Move time forward return passes diff --git a/modules/reminder_service.py b/modules/reminder_service.py index 8155c67..cf34746 100644 --- a/modules/reminder_service.py +++ b/modules/reminder_service.py @@ -1,83 +1,61 @@ import sqlite3 -import time -from datetime import UTC, datetime, timedelta -from threading import Thread - +from datetime import datetime, timezone +from threading import Timer from flask_socketio import emit - from .database import get_db_connection - def add_reminder(user_id, pass_time): - conn = get_db_connection() - cursor = conn.cursor() - cursor.execute( - """ - WITH params AS ( - SELECT ? AS user_id, ? AS pass_time - ) - INSERT INTO iss_reminders (user_id, pass_time) - SELECT p.user_id, p.pass_time - FROM params p - WHERE NOT EXISTS ( - SELECT 1 - FROM iss_reminders - WHERE user_id = p.user_id AND pass_time = p.pass_time - ); - """, - (user_id, pass_time), - ) - conn.commit() - conn.close() - - -def reminder_checker(): - while True: - - now = datetime.now(UTC) - - conn = sqlite3.connect("database.db") - c = conn.cursor() - - # Find reminders that are within the next 10 minutes and haven't been notified - c.execute( - """ - SELECT id, user_id, pass_time FROM iss_reminders - WHERE notified = 0 AND pass_time <= ? - """, - (now,), - ) - - reminders = c.fetchall() - - for reminder in reminders: - reminder_id, user_id, pass_time = reminder - # Notify the user (this is a placeholder function) - notify_user(user_id, pass_time) - - # Mark the reminder as notified - c.execute( - """ - DELETE FROM iss_reminders WHERE id = ? - """, - (reminder_id,), - ) - + """ + Adds a reminder for a user at a specified pass time. + Ensures that duplicate reminders are not added. + """ + query = """ + INSERT INTO iss_reminders (user_id, pass_time) + SELECT ?, ? + WHERE NOT EXISTS ( + SELECT 1 FROM iss_reminders WHERE user_id = ? AND pass_time = ? + ); + """ + with get_db_connection() as conn: + conn.execute(query, (user_id, pass_time, user_id, pass_time)) conn.commit() - conn.close() - time.sleep(60) # Check every minute +def reminder_checker(): + """ + Checks for reminders that need to be triggered and notifies users. + Deletes the reminder after notifying the user. + """ + now = datetime.utcnow().replace(tzinfo=timezone.utc) + query = """ + SELECT id, user_id, pass_time FROM iss_reminders + WHERE notified = 0 AND pass_time <= ? + """ + + try: + with get_db_connection() as conn: + cursor = conn.execute(query, (now,)) + reminders = cursor.fetchall() + + for reminder_id, user_id, pass_time in reminders: + notify_user(user_id, pass_time) + conn.execute("DELETE FROM iss_reminders WHERE id = ?", (reminder_id,)) + conn.commit() + + except sqlite3.Error as e: + print(f"Database error: {e}") + + # Schedule the next check after 60 seconds + Timer(60, reminder_checker).start() def start_reminder_checker(): - # Run reminder checker in a separate thread - Thread(target=reminder_checker, daemon=True).start() - + """ + Initiates the reminder checking loop. + """ + reminder_checker() def notify_user(user_id, pass_time): - # Send a WebSocket event to the client - emit( - "reminder", - {"user_id": user_id, "pass_time": pass_time}, - namespace="/notifications", - ) + """ + Sends a WebSocket notification to the user about the ISS pass time. + """ + emit("reminder", {"user_id": user_id, "pass_time": pass_time}, namespace="/notifications") print(f"Notification sent for user {user_id} at {pass_time}") diff --git a/modules/tle_fetcher.py b/modules/tle_fetcher.py index b72c66f..34e82a6 100644 --- a/modules/tle_fetcher.py +++ b/modules/tle_fetcher.py @@ -1,51 +1,80 @@ import os import time from datetime import datetime, timedelta - import requests # Local backup TLE data -local_tle_name = "ISS" -local_line1 = "1 25544U 98067A 24241.03733169 .00022625 00000+0 40054-3 0 9997" -local_line2 = "2 25544 51.6393 319.3593 0006301 282.8570 136.4539 15.50177998469691" +LOCAL_TLE = { + "name": "ISS", + "line1": "1 25544U 98067A 24241.03733169 .00022625 00000+0 40054-3 0 9997", + "line2": "2 25544 51.6393 319.3593 0006301 282.8570 136.4539 15.50177998469691", +} CACHE_FILE = "tle_cache.txt" # Path to the cache file CACHE_EXPIRATION = timedelta(days=1) # Cache duration def fetch_tle_data( - url="https://www.celestrak.com/NORAD/elements/stations.txt", retries=1, delay=5 + url="https://www.celestrak.com/NORAD/elements/stations.txt", retries=3, delay=5 ): - # Check if the cache file exists and is valid - if os.path.exists(CACHE_FILE): - file_mod_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE)) - if datetime.now() - file_mod_time < CACHE_EXPIRATION: - # Read the cached TLE data from the file - with open(CACHE_FILE, "r") as file: - tle_data = file.readlines() - if len(tle_data) >= 3: - return tle_data[0].strip(), tle_data[1].strip(), tle_data[2].strip() - - # If cache is expired or doesn't exist, fetch new data + """ + Fetch the latest TLE data from the specified URL or use cached data if available and valid. + If all fetch attempts fail, fallback to local TLE data. + """ + if is_cache_valid(): + return read_cached_tle() + for attempt in range(retries): try: response = requests.get(url) - response.raise_for_status() # Check if the request was successful + response.raise_for_status() tle_data = response.text.splitlines() - # Cache the new TLE data - with open(CACHE_FILE, "w") as file: - file.write("\n".join(tle_data[:3])) + if len(tle_data) >= 3: + cache_tle_data(tle_data) + return tle_data[0].strip(), tle_data[1].strip(), tle_data[2].strip() - return tle_data[0].strip(), tle_data[1].strip(), tle_data[2].strip() - - except ( - requests.exceptions.HTTPError, - requests.exceptions.ConnectionError, - ) as e: + except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError) as e: print(f"Attempt {attempt + 1} failed: {e}") if attempt < retries - 1: time.sleep(delay) - else: - print("All attempts failed, using local TLE data.") - return local_tle_name, local_line1, local_line2 + + print("All attempts failed, using local TLE data.") + return LOCAL_TLE["name"], LOCAL_TLE["line1"], LOCAL_TLE["line2"] + + +def is_cache_valid(): + """ + Check if the cache file exists and is within the expiration period. + """ + if os.path.exists(CACHE_FILE): + file_mod_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE)) + return datetime.now() - file_mod_time < CACHE_EXPIRATION + return False + + +def read_cached_tle(): + """ + Read the TLE data from the cache file. + """ + with open(CACHE_FILE, "r") as file: + tle_data = file.readlines() + if len(tle_data) >= 3: + return tle_data[0].strip(), tle_data[1].strip(), tle_data[2].strip() + return LOCAL_TLE["name"], LOCAL_TLE["line1"], LOCAL_TLE["line2"] + + +def cache_tle_data(tle_data): + """ + Cache the TLE data to a file. + """ + with open(CACHE_FILE, "w") as file: + file.write("\n".join(tle_data[:3])) + + +if __name__ == "__main__": + # Example usage + tle_name, tle_line1, tle_line2 = fetch_tle_data() + print(tle_name) + print(tle_line1) + print(tle_line2) diff --git a/modules/user_service.py b/modules/user_service.py index 97e40c7..74cbca2 100644 --- a/modules/user_service.py +++ b/modules/user_service.py @@ -1,34 +1,32 @@ import sqlite3 - from flask import session from werkzeug.security import check_password_hash, generate_password_hash - from .database import get_db_connection - def register_user(username, password): - conn = get_db_connection() - cursor = conn.cursor() + """ + Registers a new user with a hashed password. Returns True if successful, False if the username already exists. + """ try: - hashed_password = generate_password_hash(password) - cursor.execute( - "INSERT INTO users (username, password) VALUES (?, ?)", - (username, hashed_password), - ) - conn.commit() + with get_db_connection() as conn: + hashed_password = generate_password_hash(password) + conn.execute( + "INSERT INTO users (username, password) VALUES (?, ?)", + (username, hashed_password) + ) + conn.commit() return True except sqlite3.IntegrityError: return False # Username already exists - finally: - conn.close() def verify_user(username, password): - conn = get_db_connection() - cursor = conn.cursor() - cursor.execute("SELECT id, password FROM users WHERE username = ?", (username,)) - user = cursor.fetchone() - conn.close() + """ + Verifies a user's credentials. Returns the user_id if successful, None otherwise. + """ + query = "SELECT id, password FROM users WHERE username = ?" + with get_db_connection() as conn: + user = conn.execute(query, (username,)).fetchone() if user and check_password_hash(user[1], password): return user[0] # Return user_id if authentication is successful @@ -36,12 +34,21 @@ def verify_user(username, password): def login_user(user_id): + """ + Logs in a user by setting the session user_id. + """ session["user_id"] = user_id def logout_user(): + """ + Logs out the current user by removing the user_id from the session. + """ session.pop("user_id", None) def get_logged_in_user(): - return session.get("user_id", None) + """ + Returns the currently logged-in user_id from the session, or None if no user is logged in. + """ + return session.get("user_id") diff --git a/static/css/components.css b/static/css/components.css index 4046ca5..abc0545 100644 --- a/static/css/components.css +++ b/static/css/components.css @@ -8,5 +8,5 @@ } .leaflet-top { - top: 20vh; + top: 17vh; } diff --git a/tle_cache.txt b/tle_cache.txt new file mode 100644 index 0000000..26c82e3 --- /dev/null +++ b/tle_cache.txt @@ -0,0 +1,3 @@ +ISS (ZARYA) +1 25544U 98067A 24243.50397176 .00023688 00000+0 43380-3 0 9990 +2 25544 51.6414 307.1531 0011247 297.8860 206.3689 15.49139580470075 \ No newline at end of file