|
1 | | -from flask import Flask, render_template, request |
2 | | -import requests |
| 1 | +from flask import Flask, render_template, request, jsonify, make_response |
| 2 | +import requests, os, time |
3 | 3 | import pycountry |
4 | | -import os |
| 4 | +from collections import defaultdict |
5 | 5 | from dotenv import load_dotenv |
6 | 6 |
|
7 | 7 | load_dotenv() |
8 | | - |
9 | 8 | app = Flask(__name__) |
10 | 9 |
|
11 | | -# ========================= |
12 | | -# API CONFIG |
13 | | -# ========================= |
14 | 10 | API_KEY = os.getenv("API_KEY") |
15 | | -if not API_KEY: |
16 | | - raise ValueError("API key not found") |
17 | | - |
18 | 11 | API_URL = f"https://v6.exchangerate-api.com/v6/{API_KEY}/pair" |
19 | 12 |
|
20 | | -# ========================= |
21 | | -# CURRENCIES LIST |
22 | | -# ========================= |
| 13 | +# ====================== |
| 14 | +# DATA |
| 15 | +# ====================== |
23 | 16 | CURRENCIES = sorted([ |
24 | | - (c.alpha_3, c.name) for c in pycountry.currencies |
25 | | - if hasattr(c, 'alpha_3') and hasattr(c, 'name') |
| 17 | + (c.alpha_3, c.name) |
| 18 | + for c in pycountry.currencies |
| 19 | + if hasattr(c, "alpha_3") |
26 | 20 | ]) |
27 | 21 |
|
28 | | -# ========================= |
29 | | -# FLAG + COUNTRY MAPPINGS |
30 | | -# ========================= |
31 | 22 | CURRENCY_TO_FLAG = { |
32 | | - "USD": "us", |
33 | | - "EUR": "eu", |
34 | | - "GBP": "gb", |
35 | | - "JPY": "jp", |
36 | | - "CHF": "ch", |
37 | | - "CAD": "ca", |
38 | | - "AUD": "au", |
39 | | - "CNY": "cn", |
40 | | - "INR": "in", |
| 23 | + "USD": "us", "EUR": "eu", "GBP": "gb", "JPY": "jp", |
| 24 | + "CHF": "ch", "CAD": "ca", "AUD": "au", "CNY": "cn", "INR": "in" |
41 | 25 | } |
42 | 26 |
|
43 | | -COUNTRY_TO_CURRENCY = { |
44 | | - "US": "USD", |
45 | | - "GB": "GBP", |
46 | | - "JP": "JPY", |
47 | | - "CH": "CHF", |
48 | | - "CA": "CAD", |
49 | | - "AU": "AUD", |
50 | | - "CN": "CNY", |
51 | | - "IN": "INR", |
52 | | -} |
53 | | - |
54 | | -EU_COUNTRIES = { |
| 27 | +EU = { |
55 | 28 | "GR","DE","FR","IT","ES","NL","BE","PT","AT","FI","IE", |
56 | 29 | "CY","EE","LV","LT","LU","MT","SI","SK","HR" |
57 | 30 | } |
58 | 31 |
|
59 | | -# ========================= |
60 | | -# HELPERS |
61 | | -# ========================= |
62 | | -def detect_user_currency(): |
63 | | - try: |
64 | | - response = requests.get( |
65 | | - "https://ipapi.co/json/", |
66 | | - timeout=3, |
67 | | - headers={"User-Agent": "CurrencyConverter"} |
68 | | - ) |
69 | | - data = response.json() |
| 32 | +# ====================== |
| 33 | +# CACHE & RATE LIMIT |
| 34 | +# ====================== |
| 35 | +RATE_CACHE = {} |
| 36 | +CACHE_TTL = 600 # 10 minutes |
70 | 37 |
|
71 | | - country = data.get("country_code") |
| 38 | +REQUESTS = defaultdict(list) |
| 39 | +LIMIT = 30 # per IP / minute |
72 | 40 |
|
73 | | - if country in EU_COUNTRIES: |
| 41 | +# ====================== |
| 42 | +# HELPERS |
| 43 | +# ====================== |
| 44 | +def rate_limited(ip): |
| 45 | + now = time.time() |
| 46 | + REQUESTS[ip] = [t for t in REQUESTS[ip] if now - t < 60] |
| 47 | + if len(REQUESTS[ip]) >= LIMIT: |
| 48 | + return True |
| 49 | + REQUESTS[ip].append(now) |
| 50 | + return False |
| 51 | + |
| 52 | +def detect_currency(): |
| 53 | + try: |
| 54 | + data = requests.get("https://ipapi.co/json/", timeout=3).json() |
| 55 | + cc = data.get("country_code") |
| 56 | + if cc in EU: |
74 | 57 | return "EUR" |
| 58 | + return data.get("currency", "USD") |
| 59 | + except: |
| 60 | + return "USD" |
75 | 61 |
|
76 | | - return COUNTRY_TO_CURRENCY.get(country, "USD") |
| 62 | +def get_rate(frm, to): |
| 63 | + key = (frm, to) |
| 64 | + now = time.time() |
77 | 65 |
|
78 | | - except Exception: |
79 | | - return "USD" |
| 66 | + if key in RATE_CACHE and now - RATE_CACHE[key]["time"] < CACHE_TTL: |
| 67 | + return RATE_CACHE[key]["rate"] |
80 | 68 |
|
81 | | -def currency_to_flag(currency): |
82 | | - return CURRENCY_TO_FLAG.get(currency, "generic") |
| 69 | + r = requests.get(f"{API_URL}/{frm}/{to}", timeout=5).json() |
| 70 | + if r.get("result") != "success": |
| 71 | + raise ValueError("API error") |
83 | 72 |
|
84 | | -# ========================= |
| 73 | + RATE_CACHE[key] = { |
| 74 | + "rate": r["conversion_rate"], |
| 75 | + "time": now |
| 76 | + } |
| 77 | + return r["conversion_rate"] |
| 78 | + |
| 79 | +# ====================== |
85 | 80 | # ROUTES |
86 | | -# ========================= |
87 | | -@app.route('/') |
| 81 | +# ====================== |
| 82 | +@app.route("/") |
88 | 83 | def index(): |
89 | | - user_currency = detect_user_currency() |
| 84 | + from_cur = request.cookies.get("from") or detect_currency() |
| 85 | + to_cur = request.cookies.get("to") or ("USD" if from_cur == "EUR" else "EUR") |
90 | 86 |
|
91 | 87 | return render_template( |
92 | 88 | "index.html", |
93 | 89 | currencies=CURRENCIES, |
94 | | - from_currency=user_currency, |
95 | | - to_currency="EUR" if user_currency != "EUR" else "USD", |
96 | | - from_currency_flag=currency_to_flag(user_currency), |
97 | | - to_currency_flag=currency_to_flag( |
98 | | - "EUR" if user_currency != "EUR" else "USD" |
99 | | - ) |
| 90 | + from_currency=from_cur, |
| 91 | + to_currency=to_cur, |
| 92 | + from_flag=CURRENCY_TO_FLAG.get(from_cur, "generic"), |
| 93 | + to_flag=CURRENCY_TO_FLAG.get(to_cur, "generic") |
100 | 94 | ) |
101 | 95 |
|
102 | | -@app.route('/convert', methods=['POST']) |
| 96 | +@app.route("/convert", methods=["POST"]) |
103 | 97 | def convert(): |
104 | | - try: |
105 | | - amount = float(request.form['amount']) |
106 | | - from_currency = request.form['from_currency'] |
107 | | - to_currency = request.form['to_currency'] |
108 | | - |
109 | | - response = requests.get( |
110 | | - f"{API_URL}/{from_currency}/{to_currency}", |
111 | | - timeout=5 |
112 | | - ) |
113 | | - data = response.json() |
114 | | - |
115 | | - if data.get('result') == 'success': |
116 | | - converted = round(amount * data['conversion_rate'], 2) |
117 | | - result = f"{amount} {from_currency} = {converted} {to_currency}" |
118 | | - else: |
119 | | - result = "Conversion failed." |
120 | | - |
121 | | - except Exception as e: |
122 | | - result = f"Error: {e}" |
123 | | - |
124 | | - return render_template( |
125 | | - "index.html", |
126 | | - currencies=CURRENCIES, |
127 | | - result=result, |
128 | | - from_currency=from_currency, |
129 | | - to_currency=to_currency, |
130 | | - from_currency_flag=currency_to_flag(from_currency), |
131 | | - to_currency_flag=currency_to_flag(to_currency) |
132 | | - ) |
| 98 | + ip = request.remote_addr |
| 99 | + if rate_limited(ip): |
| 100 | + return jsonify({"error": "Too many requests"}), 429 |
| 101 | + |
| 102 | + data = request.json |
| 103 | + amount = float(data["amount"]) |
| 104 | + frm = data["from"] |
| 105 | + to = data["to"] |
| 106 | + |
| 107 | + rate = get_rate(frm, to) |
| 108 | + result = round(amount * rate, 2) |
| 109 | + |
| 110 | + resp = make_response(jsonify({ |
| 111 | + "result": result, |
| 112 | + "rate": rate, |
| 113 | + "from_flag": CURRENCY_TO_FLAG.get(frm, "generic"), |
| 114 | + "to_flag": CURRENCY_TO_FLAG.get(to, "generic") |
| 115 | + })) |
| 116 | + |
| 117 | + resp.set_cookie("from", frm, max_age=86400 * 30) |
| 118 | + resp.set_cookie("to", to, max_age=86400 * 30) |
| 119 | + return resp |
| 120 | + |
| 121 | +@app.route("/health") |
| 122 | +def health(): |
| 123 | + return jsonify({"status": "ok"}) |
0 commit comments