import csv from io import StringIO from flask import Blueprint, render_template, redirect, request, url_for, flash, stream_with_context from app.models import Reading, db from flask_login import login_required, current_user from datetime import datetime from app.forms import DeleteForm from app.insights import parse_range, readings_query, user_timezone data = Blueprint('data', __name__) @data.route('/', methods=['GET', 'POST']) @login_required def manage_data(): form = DeleteForm() if request.method == 'POST': if not form.validate_on_submit(): flash('Your form expired. Please try again.', 'danger') return redirect(url_for('data.manage_data')) # Handle CSV file upload file = request.files.get('file') if file and file.filename.endswith('.csv'): try: payload = file.read(2_000_001) if len(payload) > 2_000_000: raise ValueError('CSV must be 2 MB or smaller.') csv_data = csv.reader(StringIO(payload.decode('utf-8-sig'))) if next(csv_data, None) != ['Timestamp', 'Systolic', 'Diastolic', 'Heart Rate']: raise ValueError('Use the column headings from a BP Tracker export.') readings_to_add = [] for line, row in enumerate(csv_data, 2): timestamp, systolic, diastolic, heart_rate = row if not (50 <= int(systolic) <= 250 and 30 <= int(diastolic) <= 150 and 30 <= int(heart_rate) <= 200): raise ValueError(f'Check the measurement values on row {line}.') readings_to_add.append(Reading( user_id=current_user.id, timestamp=datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S'), systolic=int(systolic), diastolic=int(diastolic), heart_rate=int(heart_rate), )) db.session.bulk_save_objects(readings_to_add) db.session.commit() flash('Data imported successfully!', 'success') except Exception as e: db.session.rollback() flash(f'Error importing data: {str(e)}', 'danger') else: flash('Please upload a valid CSV file.', 'danger') return redirect(url_for('data.manage_data')) return render_template('data.html', form=form) @data.route('/export', methods=['GET']) @login_required def export_data(): import io from flask import Response user_id = current_user.id selected = None if any(request.args.get(key) for key in ('start_date', 'end_date', 'days')): try: selected = parse_range(request.args, user_timezone(), 30) except ValueError as error: return render_template('error.html', message=str(error)), 400 def generate_csv(): """Stream CSV rows to avoid loading all readings into memory.""" # Write header output = io.StringIO() writer = csv.writer(output) writer.writerow(['Timestamp', 'Systolic', 'Diastolic', 'Heart Rate']) yield output.getvalue() output.seek(0) output.truncate(0) # Stream readings in chunks using yield_per readings = readings_query(user_id, selected).order_by( Reading.timestamp, Reading.id ).yield_per(500) for reading in readings: writer.writerow([ reading.timestamp.strftime('%Y-%m-%d %H:%M:%S'), reading.systolic, reading.diastolic, reading.heart_rate, ]) yield output.getvalue() output.seek(0) output.truncate(0) return Response( stream_with_context(generate_csv()), mimetype='text/csv', headers={'Content-Disposition': 'attachment; filename=readings.csv'} )