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 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
+ ]
@@ -0,0 +1,8 @@
1
+ """
2
+ API modules for Corally calculator suite.
3
+ """
4
+
5
+ from .server import create_app, start_server
6
+ from .free_server import create_free_app, start_free_server
7
+
8
+ __all__ = ["create_app", "start_server", "create_free_app", "start_free_server"]
@@ -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()
@@ -0,0 +1,9 @@
1
+ """
2
+ Command-line interface modules for Corally calculator suite.
3
+ """
4
+
5
+ from .main import main_cli
6
+ from .calculator import calculator_cli
7
+ from .currency import currency_cli
8
+
9
+ __all__ = ["main_cli", "calculator_cli", "currency_cli"]
@@ -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()
@@ -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()