corally 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- corally/__init__.py +36 -0
- corally/api/__init__.py +8 -0
- corally/api/free_server.py +259 -0
- corally/api/server.py +190 -0
- corally/cli/__init__.py +9 -0
- corally/cli/calculator.py +59 -0
- corally/cli/currency.py +65 -0
- corally/cli/main.py +208 -0
- corally/core/__init__.py +7 -0
- corally/core/calculator.py +392 -0
- corally/gui/__init__.py +8 -0
- corally/gui/launcher.py +138 -0
- corally/gui/main.py +886 -0
- corally-1.0.0.dist-info/METADATA +192 -0
- corally-1.0.0.dist-info/RECORD +19 -0
- corally-1.0.0.dist-info/WHEEL +5 -0
- corally-1.0.0.dist-info/entry_points.txt +5 -0
- corally-1.0.0.dist-info/licenses/LICENSE +21 -0
- corally-1.0.0.dist-info/top_level.txt +1 -0
corally/__init__.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
"""
|
2
|
+
Corally - A comprehensive calculator suite with GUI and API support.
|
3
|
+
|
4
|
+
This package provides:
|
5
|
+
- Basic arithmetic operations with logging
|
6
|
+
- Currency conversion (static and live rates)
|
7
|
+
- Interest calculations with multiple methods
|
8
|
+
- Modern GUI interface
|
9
|
+
- REST API for currency conversion
|
10
|
+
- Command-line interface
|
11
|
+
|
12
|
+
Example usage:
|
13
|
+
>>> from corally import CalculatorCore, CurrencyConverter
|
14
|
+
>>> calc = CalculatorCore()
|
15
|
+
>>> result = calc.add(5, 3)
|
16
|
+
>>> print(result) # 8.0
|
17
|
+
|
18
|
+
>>> converter = CurrencyConverter()
|
19
|
+
>>> usd = converter.eur_to_usd(100)
|
20
|
+
>>> print(usd) # 117.0
|
21
|
+
"""
|
22
|
+
|
23
|
+
from .core.calculator import CalculatorCore, CurrencyConverter, InterestCalculator
|
24
|
+
|
25
|
+
__version__ = "2.0.0"
|
26
|
+
__author__ = "Corally Team"
|
27
|
+
__email__ = "contact@corally.dev"
|
28
|
+
__description__ = "A comprehensive calculator suite with GUI and API support"
|
29
|
+
|
30
|
+
# Make main classes available at package level
|
31
|
+
__all__ = [
|
32
|
+
"CalculatorCore",
|
33
|
+
"CurrencyConverter",
|
34
|
+
"InterestCalculator",
|
35
|
+
"__version__",
|
36
|
+
]
|
corally/api/__init__.py
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
import csv
|
4
|
+
import httpx
|
5
|
+
import logging
|
6
|
+
from pathlib import Path
|
7
|
+
from fastapi import FastAPI, HTTPException, Query
|
8
|
+
|
9
|
+
# Create data directory if it doesn't exist
|
10
|
+
data_dir = Path("data")
|
11
|
+
data_dir.mkdir(exist_ok=True)
|
12
|
+
|
13
|
+
logging.basicConfig(
|
14
|
+
level=logging.INFO,
|
15
|
+
filename=str(data_dir / "api.log"),
|
16
|
+
filemode="w",
|
17
|
+
format="%(asctime)s %(levelname)s %(message)s"
|
18
|
+
)
|
19
|
+
|
20
|
+
# Free API endpoints that don't require API keys
|
21
|
+
FREE_API_ENDPOINTS = [
|
22
|
+
"https://api.exchangerate-api.com/v4/latest/", # Free, no key required
|
23
|
+
"https://api.fixer.io/latest?access_key=", # Backup (requires key)
|
24
|
+
]
|
25
|
+
|
26
|
+
CACHE_FILE = str(data_dir / "cache.csv")
|
27
|
+
CACHE_TTL = 3600 # 60 minutes
|
28
|
+
|
29
|
+
app = FastAPI(title="Free Currency Converter with CSV Cache")
|
30
|
+
|
31
|
+
# Cache: Memory + CSV
|
32
|
+
CACHE: dict = {}
|
33
|
+
|
34
|
+
def get_cache_key(from_currency: str, to_currency: str, amount: float) -> str:
|
35
|
+
return f"{from_currency.upper()}-{to_currency.upper()}-{amount}"
|
36
|
+
|
37
|
+
def load_cache():
|
38
|
+
"""Load existing cache entries from CSV"""
|
39
|
+
if not os.path.exists(CACHE_FILE):
|
40
|
+
return
|
41
|
+
try:
|
42
|
+
with open(CACHE_FILE, mode="r", newline="") as f:
|
43
|
+
reader = csv.DictReader(f)
|
44
|
+
for row in reader:
|
45
|
+
key = get_cache_key(row["from"], row["to"], float(row["amount"]))
|
46
|
+
CACHE[key] = {
|
47
|
+
"timestamp": float(row["timestamp"]),
|
48
|
+
"data": {
|
49
|
+
"from": row["from"],
|
50
|
+
"to": row["to"],
|
51
|
+
"amount": float(row["amount"]),
|
52
|
+
"result": float(row["result"]),
|
53
|
+
"info": {
|
54
|
+
"rate": float(row["rate"]) if row["rate"] not in (None, '', 'None') else None
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
except Exception as e:
|
59
|
+
logging.error(f"Error loading cache: {e}")
|
60
|
+
|
61
|
+
def save_cache():
|
62
|
+
"""Write cache to CSV file"""
|
63
|
+
try:
|
64
|
+
with open(CACHE_FILE, mode="w", newline="") as f:
|
65
|
+
fieldnames = ["from", "to", "amount", "result", "rate", "timestamp"]
|
66
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
67
|
+
writer.writeheader()
|
68
|
+
for entry in CACHE.values():
|
69
|
+
data = entry["data"]
|
70
|
+
writer.writerow({
|
71
|
+
"from": data["from"],
|
72
|
+
"to": data["to"],
|
73
|
+
"amount": data["amount"],
|
74
|
+
"result": data["result"],
|
75
|
+
"rate": data["info"].get("rate", None),
|
76
|
+
"timestamp": entry["timestamp"]
|
77
|
+
})
|
78
|
+
except Exception as e:
|
79
|
+
logging.error(f"Error saving cache: {e}")
|
80
|
+
|
81
|
+
def get_cache(key: str):
|
82
|
+
item = CACHE.get(key)
|
83
|
+
if item:
|
84
|
+
if time.time() - item["timestamp"] < CACHE_TTL:
|
85
|
+
item["timestamp"] = time.time()
|
86
|
+
save_cache()
|
87
|
+
return item["data"]
|
88
|
+
else:
|
89
|
+
del CACHE[key]
|
90
|
+
save_cache()
|
91
|
+
return None
|
92
|
+
|
93
|
+
def set_cache(key: str, value: dict):
|
94
|
+
CACHE[key] = {"timestamp": time.time(), "data": value}
|
95
|
+
save_cache()
|
96
|
+
|
97
|
+
def cleanup_cache():
|
98
|
+
"""Remove all expired cache entries"""
|
99
|
+
now = time.time()
|
100
|
+
keys_to_delete = [key for key, entry in CACHE.items() if now - entry["timestamp"] >= CACHE_TTL]
|
101
|
+
for key in keys_to_delete:
|
102
|
+
del CACHE[key]
|
103
|
+
if keys_to_delete:
|
104
|
+
save_cache()
|
105
|
+
|
106
|
+
async def get_exchange_rate(from_currency: str, to_currency: str):
|
107
|
+
"""Get exchange rate using free API"""
|
108
|
+
from_currency = from_currency.upper()
|
109
|
+
to_currency = to_currency.upper()
|
110
|
+
|
111
|
+
logging.info(f"Getting exchange rate: {from_currency} -> {to_currency}")
|
112
|
+
|
113
|
+
# Try the free API first
|
114
|
+
try:
|
115
|
+
async with httpx.AsyncClient() as client:
|
116
|
+
url = f"https://api.exchangerate-api.com/v4/latest/{from_currency}"
|
117
|
+
logging.info(f"Making request to: {url}")
|
118
|
+
|
119
|
+
response = await client.get(url, timeout=15)
|
120
|
+
logging.info(f"Response status: {response.status_code}")
|
121
|
+
|
122
|
+
if response.status_code == 200:
|
123
|
+
data = response.json()
|
124
|
+
rates = data.get("rates", {})
|
125
|
+
logging.info(f"Got {len(rates)} exchange rates")
|
126
|
+
|
127
|
+
if to_currency in rates:
|
128
|
+
rate = rates[to_currency]
|
129
|
+
logging.info(f"Exchange rate {from_currency}/{to_currency}: {rate}")
|
130
|
+
return rate
|
131
|
+
else:
|
132
|
+
available_currencies = list(rates.keys())[:10] # Show first 10
|
133
|
+
error_msg = f"Currency {to_currency} not supported. Available: {available_currencies}..."
|
134
|
+
logging.error(error_msg)
|
135
|
+
raise HTTPException(status_code=400, detail=error_msg)
|
136
|
+
else:
|
137
|
+
error_msg = f"External API returned status {response.status_code}: {response.text}"
|
138
|
+
logging.error(error_msg)
|
139
|
+
raise HTTPException(status_code=response.status_code, detail=error_msg)
|
140
|
+
|
141
|
+
except HTTPException:
|
142
|
+
# Re-raise HTTP exceptions as-is
|
143
|
+
raise
|
144
|
+
except httpx.TimeoutException as e:
|
145
|
+
error_msg = f"Request timeout: {str(e)}"
|
146
|
+
logging.error(error_msg)
|
147
|
+
raise HTTPException(status_code=504, detail=error_msg)
|
148
|
+
except httpx.RequestError as e:
|
149
|
+
error_msg = f"Network error: {str(e)}"
|
150
|
+
logging.error(error_msg)
|
151
|
+
raise HTTPException(status_code=503, detail=error_msg)
|
152
|
+
except Exception as e:
|
153
|
+
error_msg = f"Unexpected error: {str(e)}"
|
154
|
+
logging.error(error_msg)
|
155
|
+
raise HTTPException(status_code=500, detail=error_msg)
|
156
|
+
|
157
|
+
# Load cache on startup
|
158
|
+
load_cache()
|
159
|
+
cleanup_cache()
|
160
|
+
|
161
|
+
@app.get("/convert")
|
162
|
+
async def convert(
|
163
|
+
from_currency: str,
|
164
|
+
to_currency: str,
|
165
|
+
amount: str = Query(...)
|
166
|
+
):
|
167
|
+
logging.info(f"Convert request: {amount} {from_currency} -> {to_currency}")
|
168
|
+
|
169
|
+
# Validate and parse amount
|
170
|
+
try:
|
171
|
+
amount_float = float(amount.replace(",", "."))
|
172
|
+
if amount_float <= 0:
|
173
|
+
raise ValueError("Amount must be positive")
|
174
|
+
except ValueError as e:
|
175
|
+
error_msg = f"Invalid amount '{amount}': {str(e)}"
|
176
|
+
logging.error(error_msg)
|
177
|
+
raise HTTPException(status_code=400, detail=error_msg)
|
178
|
+
|
179
|
+
# Validate currencies
|
180
|
+
if not from_currency or not to_currency:
|
181
|
+
raise HTTPException(status_code=400, detail="Both from_currency and to_currency are required")
|
182
|
+
|
183
|
+
if len(from_currency) != 3 or len(to_currency) != 3:
|
184
|
+
raise HTTPException(status_code=400, detail="Currency codes must be 3 letters (e.g., EUR, USD)")
|
185
|
+
|
186
|
+
cache_key = get_cache_key(from_currency, to_currency, amount_float)
|
187
|
+
logging.info(f"Cache key: {cache_key}")
|
188
|
+
|
189
|
+
# 1. Check cache
|
190
|
+
try:
|
191
|
+
cached = get_cache(cache_key)
|
192
|
+
if cached:
|
193
|
+
logging.info("Returning cached result")
|
194
|
+
return {"cached": True, **cached}
|
195
|
+
except Exception as e:
|
196
|
+
logging.warning(f"Cache lookup failed: {e}")
|
197
|
+
|
198
|
+
# 2. Get exchange rate
|
199
|
+
try:
|
200
|
+
rate = await get_exchange_rate(from_currency, to_currency)
|
201
|
+
except HTTPException:
|
202
|
+
# Re-raise HTTP exceptions
|
203
|
+
raise
|
204
|
+
except Exception as e:
|
205
|
+
error_msg = f"Failed to get exchange rate: {str(e)}"
|
206
|
+
logging.error(error_msg)
|
207
|
+
raise HTTPException(status_code=500, detail=error_msg)
|
208
|
+
|
209
|
+
# 3. Calculate result
|
210
|
+
try:
|
211
|
+
result_amount = amount_float * rate
|
212
|
+
|
213
|
+
result = {
|
214
|
+
"from": from_currency.upper(),
|
215
|
+
"to": to_currency.upper(),
|
216
|
+
"amount": amount_float,
|
217
|
+
"result": round(result_amount, 2),
|
218
|
+
"info": {
|
219
|
+
"rate": rate
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
logging.info(f"Conversion result: {result}")
|
224
|
+
|
225
|
+
# 4. Save to cache
|
226
|
+
try:
|
227
|
+
set_cache(cache_key, result)
|
228
|
+
except Exception as e:
|
229
|
+
logging.warning(f"Failed to save to cache: {e}")
|
230
|
+
|
231
|
+
return {"cached": False, **result}
|
232
|
+
|
233
|
+
except Exception as e:
|
234
|
+
error_msg = f"Calculation failed: {str(e)}"
|
235
|
+
logging.error(error_msg)
|
236
|
+
raise HTTPException(status_code=500, detail=error_msg)
|
237
|
+
|
238
|
+
@app.get("/")
|
239
|
+
async def root():
|
240
|
+
return {"message": "Free Currency Converter API", "status": "running"}
|
241
|
+
|
242
|
+
@app.get("/health")
|
243
|
+
async def health():
|
244
|
+
return {"status": "healthy", "cache_entries": len(CACHE)}
|
245
|
+
|
246
|
+
|
247
|
+
def create_free_app() -> FastAPI:
|
248
|
+
"""Create and return the free FastAPI app instance."""
|
249
|
+
return app
|
250
|
+
|
251
|
+
|
252
|
+
def start_free_server(host: str = "127.0.0.1", port: int = 8000) -> None:
|
253
|
+
"""Start the free API server."""
|
254
|
+
import uvicorn
|
255
|
+
uvicorn.run(app, host=host, port=port)
|
256
|
+
|
257
|
+
|
258
|
+
if __name__ == "__main__":
|
259
|
+
start_free_server()
|
corally/api/server.py
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
import csv
|
4
|
+
import httpx
|
5
|
+
import logging
|
6
|
+
from pathlib import Path
|
7
|
+
from dotenv import load_dotenv
|
8
|
+
from fastapi import FastAPI, HTTPException, Query
|
9
|
+
|
10
|
+
# Create data directory if it doesn't exist
|
11
|
+
data_dir = Path("data")
|
12
|
+
data_dir.mkdir(exist_ok=True)
|
13
|
+
|
14
|
+
logging.basicConfig(
|
15
|
+
level=logging.INFO,
|
16
|
+
filename=str(data_dir / "api.log"), # Log file name
|
17
|
+
filemode="w", # Overwrite log file on each start
|
18
|
+
format="%(asctime)s %(levelname)s %(message)s"
|
19
|
+
)
|
20
|
+
|
21
|
+
# .env laden
|
22
|
+
load_dotenv()
|
23
|
+
API_KEY = os.getenv("API_KEY")
|
24
|
+
# Note: API_KEY will be checked when the server starts, not at import time
|
25
|
+
|
26
|
+
BASE_URL = "https://api.exchangerate.host/convert"
|
27
|
+
CACHE_FILE = str(data_dir / "cache.csv")
|
28
|
+
CACHE_TTL = 3600 # 60 Minuten
|
29
|
+
|
30
|
+
app = FastAPI(title="Wรคhrungsrechner mit CSV-Cache")
|
31
|
+
|
32
|
+
# -----------------------------
|
33
|
+
# Cache: Speicher im Speicher + CSV
|
34
|
+
# -----------------------------
|
35
|
+
CACHE: dict = {}
|
36
|
+
|
37
|
+
def get_cache_key(from_currency: str, to_currency: str, amount: float) -> str:
|
38
|
+
return f"{from_currency.upper()}-{to_currency.upper()}-{amount}"
|
39
|
+
|
40
|
+
def load_cache():
|
41
|
+
"""Lรคdt vorhandene Cache-Eintrรคge aus der CSV"""
|
42
|
+
if not os.path.exists(CACHE_FILE):
|
43
|
+
return
|
44
|
+
with open(CACHE_FILE, mode="r", newline="") as f:
|
45
|
+
reader = csv.DictReader(f)
|
46
|
+
for row in reader:
|
47
|
+
key = get_cache_key(row["from"], row["to"], float(row["amount"]))
|
48
|
+
CACHE[key] = {
|
49
|
+
"timestamp": float(row["timestamp"]),
|
50
|
+
"data": {
|
51
|
+
"from": row["from"],
|
52
|
+
"to": row["to"],
|
53
|
+
"amount": float(row["amount"]),
|
54
|
+
"result": float(row["result"]),
|
55
|
+
"info": {
|
56
|
+
"rate": float(row["rate"]) if row["rate"] not in (None, '', 'None') else None
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
def save_cache():
|
62
|
+
"""Schreibt den Cache in die CSV-Datei"""
|
63
|
+
with open(CACHE_FILE, mode="w", newline="") as f:
|
64
|
+
fieldnames = ["from", "to", "amount", "result", "rate", "timestamp"]
|
65
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
66
|
+
writer.writeheader()
|
67
|
+
for entry in CACHE.values():
|
68
|
+
data = entry["data"]
|
69
|
+
writer.writerow({
|
70
|
+
"from": data["from"],
|
71
|
+
"to": data["to"],
|
72
|
+
"amount": data["amount"],
|
73
|
+
"result": data["result"],
|
74
|
+
"rate": data["info"].get("rate", None), # <-- Fix here
|
75
|
+
"timestamp": entry["timestamp"]
|
76
|
+
})
|
77
|
+
|
78
|
+
def get_cache(key: str):
|
79
|
+
item = CACHE.get(key)
|
80
|
+
if item:
|
81
|
+
if time.time() - item["timestamp"] < CACHE_TTL:
|
82
|
+
# Update the timestamp to extend TTL
|
83
|
+
item["timestamp"] = time.time()
|
84
|
+
save_cache()
|
85
|
+
return item["data"]
|
86
|
+
else:
|
87
|
+
del CACHE[key]
|
88
|
+
save_cache()
|
89
|
+
return None
|
90
|
+
|
91
|
+
def set_cache(key: str, value: dict):
|
92
|
+
CACHE[key] = {"timestamp": time.time(), "data": value}
|
93
|
+
save_cache()
|
94
|
+
|
95
|
+
|
96
|
+
def cleanup_cache():
|
97
|
+
"""Entfernt alle abgelaufenen Cache-Eintrรคge."""
|
98
|
+
now = time.time()
|
99
|
+
keys_to_delete = [key for key, entry in CACHE.items() if now - entry["timestamp"] >= CACHE_TTL]
|
100
|
+
for key in keys_to_delete:
|
101
|
+
del CACHE[key]
|
102
|
+
if keys_to_delete:
|
103
|
+
save_cache()
|
104
|
+
|
105
|
+
# Lade Cache beim Start und bereinige abgelaufene Eintrรคge
|
106
|
+
load_cache()
|
107
|
+
cleanup_cache()
|
108
|
+
# -----------------------------
|
109
|
+
# API-Endpunkt
|
110
|
+
# -----------------------------
|
111
|
+
@app.get("/convert")
|
112
|
+
async def convert(
|
113
|
+
from_currency: str,
|
114
|
+
to_currency: str,
|
115
|
+
amount: str = Query(...)
|
116
|
+
):
|
117
|
+
# Allow comma as decimal separator
|
118
|
+
try:
|
119
|
+
amount_float = float(amount.replace(",", "."))
|
120
|
+
except ValueError:
|
121
|
+
raise HTTPException(status_code=400, detail="Ungรผltiger Betrag. Bitte Zahl mit Punkt oder Komma eingeben.")
|
122
|
+
|
123
|
+
cache_key = get_cache_key(from_currency, to_currency, amount_float)
|
124
|
+
|
125
|
+
# 1. Cache prรผfen
|
126
|
+
cached = get_cache(cache_key)
|
127
|
+
if cached:
|
128
|
+
return {"cached": True, **cached}
|
129
|
+
|
130
|
+
# 2. API-Aufruf
|
131
|
+
params = {
|
132
|
+
"access_key": API_KEY,
|
133
|
+
"from": from_currency.upper(),
|
134
|
+
"to": to_currency.upper(),
|
135
|
+
"amount": amount_float
|
136
|
+
}
|
137
|
+
|
138
|
+
async with httpx.AsyncClient() as client:
|
139
|
+
response = await client.get(BASE_URL, params=params)
|
140
|
+
|
141
|
+
logging.info("API response: %s", response.json())
|
142
|
+
|
143
|
+
if response.status_code != 200:
|
144
|
+
raise HTTPException(status_code=response.status_code, detail=f"API-Anfrage fehlgeschlagen: {response.status_code}")
|
145
|
+
|
146
|
+
data = response.json()
|
147
|
+
if not data.get("success", False):
|
148
|
+
err = data.get("error", {})
|
149
|
+
raise HTTPException(status_code=400, detail=f"API-Fehler: {err}")
|
150
|
+
|
151
|
+
# Extract rate safely
|
152
|
+
rate = None
|
153
|
+
if "info" in data and isinstance(data["info"], dict):
|
154
|
+
rate = data["info"].get("rate")
|
155
|
+
if rate is None:
|
156
|
+
rate = data["info"].get("quote")
|
157
|
+
elif "rate" in data:
|
158
|
+
rate = data.get("rate")
|
159
|
+
|
160
|
+
result = {
|
161
|
+
"from": from_currency.upper(),
|
162
|
+
"to": to_currency.upper(),
|
163
|
+
"amount": amount_float,
|
164
|
+
"result": data.get("result"),
|
165
|
+
"info": {
|
166
|
+
"rate": rate
|
167
|
+
}
|
168
|
+
}
|
169
|
+
|
170
|
+
# 3. Cache speichern
|
171
|
+
set_cache(cache_key, result)
|
172
|
+
|
173
|
+
return {"cached": False, **result}
|
174
|
+
|
175
|
+
|
176
|
+
def create_app() -> FastAPI:
|
177
|
+
"""Create and return the FastAPI app instance."""
|
178
|
+
return app
|
179
|
+
|
180
|
+
|
181
|
+
def start_server(host: str = "127.0.0.1", port: int = 8000) -> None:
|
182
|
+
"""Start the API server."""
|
183
|
+
if not API_KEY:
|
184
|
+
raise RuntimeError("Missing API_KEY in environment variables. Please set API_KEY in .env file.")
|
185
|
+
import uvicorn
|
186
|
+
uvicorn.run(app, host=host, port=port)
|
187
|
+
|
188
|
+
|
189
|
+
if __name__ == "__main__":
|
190
|
+
start_server()
|
corally/cli/__init__.py
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
"""
|
2
|
+
Calculator CLI module for Corally.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from ..core import CalculatorCore
|
6
|
+
|
7
|
+
|
8
|
+
def calculator_cli() -> None:
|
9
|
+
"""Standalone calculator CLI interface."""
|
10
|
+
calc = CalculatorCore()
|
11
|
+
print("๐งฎ Corally Calculator")
|
12
|
+
print("=" * 20)
|
13
|
+
|
14
|
+
while True:
|
15
|
+
print("\nOperations:")
|
16
|
+
print("1: Addition")
|
17
|
+
print("2: Subtraction")
|
18
|
+
print("3: Multiplication")
|
19
|
+
print("4: Division")
|
20
|
+
print("5: Exit")
|
21
|
+
|
22
|
+
try:
|
23
|
+
choice = int(input("\nSelect operation (1-5): "))
|
24
|
+
except ValueError:
|
25
|
+
print("โ Invalid input. Please enter a number.")
|
26
|
+
continue
|
27
|
+
|
28
|
+
if choice == 5:
|
29
|
+
print("๐ Calculator closed.")
|
30
|
+
break
|
31
|
+
elif choice not in [1, 2, 3, 4]:
|
32
|
+
print("โ Invalid choice. Please select 1-5.")
|
33
|
+
continue
|
34
|
+
|
35
|
+
try:
|
36
|
+
a = float(input("Enter first number: "))
|
37
|
+
b = float(input("Enter second number: "))
|
38
|
+
except ValueError:
|
39
|
+
print("โ Invalid number input.")
|
40
|
+
continue
|
41
|
+
|
42
|
+
result = None
|
43
|
+
if choice == 1:
|
44
|
+
result = calc.add(a, b)
|
45
|
+
print(f"โ
{a} + {b} = {result}")
|
46
|
+
elif choice == 2:
|
47
|
+
result = calc.subtract(a, b)
|
48
|
+
print(f"โ
{a} - {b} = {result}")
|
49
|
+
elif choice == 3:
|
50
|
+
result = calc.multiply(a, b)
|
51
|
+
print(f"โ
{a} ร {b} = {result}")
|
52
|
+
elif choice == 4:
|
53
|
+
result = calc.divide(a, b)
|
54
|
+
if result is not None:
|
55
|
+
print(f"โ
{a} รท {b} = {result}")
|
56
|
+
|
57
|
+
|
58
|
+
if __name__ == "__main__":
|
59
|
+
calculator_cli()
|
corally/cli/currency.py
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
"""
|
2
|
+
Currency converter CLI module for Corally.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from ..core import CurrencyConverter
|
6
|
+
|
7
|
+
|
8
|
+
def currency_cli() -> None:
|
9
|
+
"""Standalone currency converter CLI interface."""
|
10
|
+
converter = CurrencyConverter()
|
11
|
+
print("๐ฑ Corally Currency Converter")
|
12
|
+
print("=" * 30)
|
13
|
+
|
14
|
+
while True:
|
15
|
+
print("\nAvailable conversions:")
|
16
|
+
print("1: EUR to USD")
|
17
|
+
print("2: USD to EUR")
|
18
|
+
print("3: EUR to GBP")
|
19
|
+
print("4: GBP to EUR")
|
20
|
+
print("5: EUR to JPY")
|
21
|
+
print("6: JPY to EUR")
|
22
|
+
print("7: Exit")
|
23
|
+
|
24
|
+
try:
|
25
|
+
choice = int(input("\nSelect conversion (1-7): "))
|
26
|
+
except ValueError:
|
27
|
+
print("โ Invalid input. Please enter a number.")
|
28
|
+
continue
|
29
|
+
|
30
|
+
if choice == 7:
|
31
|
+
print("๐ Currency converter closed.")
|
32
|
+
break
|
33
|
+
elif choice not in range(1, 7):
|
34
|
+
print("โ Invalid choice. Please select 1-7.")
|
35
|
+
continue
|
36
|
+
|
37
|
+
try:
|
38
|
+
amount = float(input("Enter amount: "))
|
39
|
+
except ValueError:
|
40
|
+
print("โ Invalid amount.")
|
41
|
+
continue
|
42
|
+
|
43
|
+
result = None
|
44
|
+
if choice == 1:
|
45
|
+
result = converter.eur_to_usd(amount)
|
46
|
+
print(f"โ
{amount} EUR = {result} USD")
|
47
|
+
elif choice == 2:
|
48
|
+
result = converter.usd_to_eur(amount)
|
49
|
+
print(f"โ
{amount} USD = {result} EUR")
|
50
|
+
elif choice == 3:
|
51
|
+
result = converter.eur_to_gbp(amount)
|
52
|
+
print(f"โ
{amount} EUR = {result} GBP")
|
53
|
+
elif choice == 4:
|
54
|
+
result = converter.gbp_to_eur(amount)
|
55
|
+
print(f"โ
{amount} GBP = {result} EUR")
|
56
|
+
elif choice == 5:
|
57
|
+
result = converter.eur_to_jpy(amount)
|
58
|
+
print(f"โ
{amount} EUR = {result} JPY")
|
59
|
+
elif choice == 6:
|
60
|
+
result = converter.jpy_to_eur(amount)
|
61
|
+
print(f"โ
{amount} JPY = {result} EUR")
|
62
|
+
|
63
|
+
|
64
|
+
if __name__ == "__main__":
|
65
|
+
currency_cli()
|