Skip to content

Commit ea8f82b

Browse files
Merge pull request #79 from BytecodeBrewer/refactor-the-service-layer
Refactor the service layer
2 parents 8e563e7 + 17cfee1 commit ea8f82b

9 files changed

Lines changed: 156 additions & 39 deletions

File tree

File renamed without changes.

src/argus/clients/exchangerate_client.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import requests as req
1+
import requests as reqs
22
from argus.config import (
33
EXCHANGE_RATE_BASE_URL,
44
EXCHANGE_RATE_API_KEY,
55
REQUEST_TIMEOUT_SECONDS,
66
)
7+
from argus.domain.internal_models import MarketDataRequest
78

89

9-
def get_rates(curr1: str, curr2: str):
10+
def get_rates(req: MarketDataRequest) -> dict | None:
1011
"""
1112
Get the exchange rate between two currencies using the ExchangeRate-API.
1213
@@ -16,21 +17,23 @@ def get_rates(curr1: str, curr2: str):
1617
1718
Returns: A dictionary containing the result status, error type (if any), and conversion rate (if successful).
1819
"""
20+
curr1 = req.instrument.base_currency
21+
curr2 = req.instrument.quote_currency
1922
url = f"{EXCHANGE_RATE_BASE_URL}/{EXCHANGE_RATE_API_KEY}/pair/{curr1}/{curr2}"
2023
data = {"result": "", "error_type": "", "conversion_rate": None}
2124

2225
try:
23-
resp = req.get(url, timeout=REQUEST_TIMEOUT_SECONDS)
26+
resp = reqs.get(url, timeout=REQUEST_TIMEOUT_SECONDS)
2427
resp.raise_for_status()
2528
payload = resp.json()
2629

27-
except req.exceptions.Timeout:
30+
except reqs.exceptions.Timeout:
2831
print("API hat zu lange gebraucht.")
2932
return None
30-
except req.exceptions.ConnectionError:
33+
except reqs.exceptions.ConnectionError:
3134
print("Keine Verbindung zur API.")
3235
return None
33-
except req.exceptions.RequestException as error:
36+
except reqs.exceptions.RequestException as error:
3437
print(f"Request fehlgeschlagen: {error}")
3538
# Request fehlgeschlagen: 403 Client Error: Forbidden for url: https://v6.exchangerate-api.com/v6/None/pair/EUR/USD -> sollte nicht gezeigt werden!!!
3639
return None

src/argus/domain/validation.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,19 @@ def is_valid_op(op: str) -> bool:
211211
Return: bool - True if the operation is valid, otherwise False
212212
"""
213213
return op in VALID_OPS
214+
215+
216+
def check_currency(question: str) -> str | None:
217+
"""
218+
Checks if the input question contains a valid currency code.
219+
220+
Arg1: question: str - the question to be checked for a valid currency code
221+
222+
Return: str or None - the valid currency code if found, otherwise None
223+
"""
224+
resp = normalize_input_string(question)
225+
226+
if is_valid_curr_code(resp):
227+
return resp
228+
229+
return None

src/argus/gui/app.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import tkinter as tk
2-
import pandas as pd
2+
from datetime import date
33
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
4-
from argus.services.market_data_service import prepare_trend_analysis
5-
from argus.services.calculator_service import calc, check_op
6-
from argus.services.conversion_service import convert, check_currency
7-
from argus.domain.validation import parse_amount
4+
from matplotlib.figure import Figure
5+
from argus.services.trend_analysis_service import prepare_trend_analysis
6+
from legacy.services.calculator_service import calc, check_op
7+
from argus.services.market_data_service import convert
8+
from argus.domain.validation import parse_amount, check_currency
9+
from argus.domain.internal_models import DataSource, Instrument, MarketDataRequest
10+
11+
db = ""
812

913

1014
def on_close() -> None:
@@ -69,7 +73,7 @@ def show_conv() -> None:
6973
conv_frame.pack(fill=tk.BOTH, expand=True)
7074

7175

72-
def show_trend() -> None:
76+
def show_trend(db: str) -> None:
7377
"""
7478
Displays the trend chart in the application. It prepares the data for trend analysis,
7579
creates the trend chart, and updates the GUI to show the chart.
@@ -86,9 +90,17 @@ def show_trend() -> None:
8690
content.pack(side="top", fill=tk.BOTH, expand=True)
8791

8892
if trend_canvas is None:
89-
df = pd.DataFrame()
90-
fig = prepare_trend_analysis(df)
91-
if fig is None:
93+
source = DataSource(name="", provider_kind="")
94+
instrument = Instrument(symbol="EUR/USD", name="EUR - USD", asset_class="fx")
95+
req = MarketDataRequest(
96+
source=source,
97+
instrument=instrument,
98+
timeframe="1d",
99+
start=date(2026, 1, 1),
100+
end=date(2026, 1, 4),
101+
)
102+
fig = prepare_trend_analysis(db, req)
103+
if not isinstance(fig, Figure):
92104
return None
93105
fig.set_size_inches(7, 4)
94106

@@ -145,6 +157,21 @@ def act_convert() -> None:
145157
resp1 = check_currency(curr1.get())
146158
resp2 = check_currency(curr2.get())
147159
amount = parse_amount(amount_e.get())
160+
source = DataSource(name="Exchange API", provider_kind="ex-api")
161+
instrument = Instrument(
162+
symbol="EUR/USD",
163+
name="EUR - USD",
164+
asset_class="fx",
165+
base_currency=resp1,
166+
quote_currency=resp2,
167+
)
168+
req = MarketDataRequest(
169+
source=source,
170+
instrument=instrument,
171+
timeframe="1d",
172+
start=date(2026, 1, 1),
173+
end=date(2026, 1, 4),
174+
)
148175

149176
if resp1 is None:
150177
result_conv_label.config(text="Please enter a valid currency for 'Currency 1'")
@@ -160,7 +187,7 @@ def act_convert() -> None:
160187
)
161188
return
162189

163-
response = convert(amount, resp1, resp2)
190+
response = convert(amount, req)
164191

165192
if response is None:
166193
result_conv_label.config(text="Currency conversion error")
@@ -251,11 +278,15 @@ def app() -> None:
251278
# Buttons
252279
from_menu_calc = tk.Button(menu_frame, text="Calculator", command=show_calc)
253280
from_menu_conv = tk.Button(menu_frame, text="Converter", command=show_conv)
254-
from_menu_trend_chart = tk.Button(menu_frame, text="Trend Chart", command=show_trend)
281+
from_menu_trend_chart = tk.Button(
282+
menu_frame, text="Trend Chart", command=lambda: show_trend(db)
283+
)
255284

256285
from_sidebar_calc = tk.Button(sidebar, text="Calculator", command=show_calc)
257286
from_sidebar_conv = tk.Button(sidebar, text="Converter", command=show_conv)
258-
from_sidebar_trend_chart = tk.Button(sidebar, text="Trend Chart", command=show_trend)
287+
from_sidebar_trend_chart = tk.Button(
288+
sidebar, text="Trend Chart", command=lambda: show_trend(db)
289+
)
259290
return_menu = tk.Button(sidebar, text="Back to menu", command=show_menu)
260291

261292
click_calculate = tk.Button(calc_frame, text="Calculate", command=act_calculate)

src/argus/services/market_data_service.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from argus.domain.internal_models import MarketDataRequest, MarketDataResponse
22
from argus.clients.yfinance_client import get_timeseries
3+
from argus.clients.exchangerate_client import get_rates
34
from argus.storage.database import read_price_bars, insert_price_bar
45
import pandas as pd
56

@@ -45,3 +46,38 @@ def get_market_data(
4546
bars=pd.DataFrame(),
4647
message=str(e),
4748
)
49+
50+
51+
def get_conv_rate(req: MarketDataRequest) -> float | None:
52+
"""
53+
Gets the conversion rate between two currencies.
54+
55+
Arg1: resp1: str - the first currency code
56+
Arg2: resp2: str - the second currency code
57+
58+
Return: float or None - the conversion rate if found, otherwise None
59+
"""
60+
61+
data = get_rates(req)
62+
63+
if data is None:
64+
return None
65+
66+
return float(data["conversion_rate"])
67+
68+
69+
def convert(amount: float, req: MarketDataRequest) -> float | None:
70+
"""
71+
Converts an amount from one currency to another using the conversion rate.
72+
73+
Arg1: amount: float - the amount to be converted
74+
Arg2: resp1: str - the first currency code
75+
Arg3: resp2: str - the second currency code
76+
77+
Return: float or None - the converted amount if conversion rate is found, otherwise None
78+
"""
79+
80+
rate = get_conv_rate(req)
81+
if rate is not None:
82+
return amount * rate
83+
return None

src/legacy/cli/interface.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from argus.services.calculator_service import check_op, calc
2-
from argus.services.conversion_service import convert, check_currency
1+
from legacy.services.calculator_service import check_op, calc
2+
from legacy.services.conversion_service import convert, check_currency
33
from argus.domain.validation import parse_amount
44

55

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from argus.clients import exchangerate_client as ex_client
21
from argus.domain.validation import normalize_input_string, is_valid_curr_code
32

43

@@ -18,7 +17,7 @@ def check_currency(question: str) -> str | None:
1817
return None
1918

2019

21-
def get_conv_rate(resp1: str, resp2: str) -> float | None:
20+
def get_conv_rate(resp1, resp2) -> float | None:
2221
"""
2322
Gets the conversion rate between two currencies.
2423
@@ -28,12 +27,12 @@ def get_conv_rate(resp1: str, resp2: str) -> float | None:
2827
Return: float or None - the conversion rate if found, otherwise None
2928
"""
3029

31-
data = ex_client.get_rates(resp1, resp2)
30+
# data = ex_client.get_rates(req)
3231

33-
if data is None:
32+
if 0 == 0:
3433
return None
3534

36-
return float(data["conversion_rate"])
35+
return None # float(data["conversion_rate"])
3736

3837

3938
def convert(amount: float, resp1: str, resp2: str) -> float | None:

tests/test_exchangerate_client.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,71 @@
11
import requests as req
2+
import pytest
23
from unittest.mock import Mock
4+
from datetime import date
5+
from argus.domain.internal_models import DataSource, Instrument, MarketDataRequest
36
from argus.clients.exchangerate_client import get_rates, check_error
47

58

6-
def test_check_currency_timeout(monkeypatch):
9+
@pytest.fixture
10+
def sample_source():
11+
return DataSource(
12+
name="Exchange API", provider_kind="ex_client", requires_api_key=False
13+
)
14+
15+
16+
@pytest.fixture
17+
def sample_instrument():
18+
return Instrument(
19+
symbol="EUR/USD",
20+
name="EUR - USD Rate",
21+
asset_class="fx",
22+
base_currency="EUR",
23+
quote_currency="USD",
24+
)
25+
26+
27+
@pytest.fixture
28+
def sample_request(sample_source, sample_instrument):
29+
return MarketDataRequest(
30+
source=sample_source,
31+
instrument=sample_instrument,
32+
timeframe="",
33+
start=date(2026, 1, 1),
34+
end=date(2026, 1, 1),
35+
)
36+
37+
38+
def test_check_currency_timeout(monkeypatch, sample_request):
739
def test_get_resp(url, timeout):
840
raise req.exceptions.Timeout()
941

1042
monkeypatch.setattr("requests.get", test_get_resp)
1143

12-
data = get_rates("EUR", "USD")
44+
data = get_rates(sample_request)
1345
assert data is None
1446

1547

16-
def test_check_currency_connection_error(monkeypatch):
48+
def test_check_currency_connection_error(monkeypatch, sample_request):
1749
def test_get_resp(url, timeout):
1850
raise req.exceptions.ConnectionError()
1951

2052
monkeypatch.setattr("requests.get", test_get_resp)
2153

22-
data = get_rates("EUR", "USD")
54+
data = get_rates(sample_request)
2355
assert data is None
2456

2557

26-
def test_check_currency_request_exception(monkeypatch):
58+
def test_check_currency_request_exception(monkeypatch, sample_request):
2759
def test_get_resp(url, timeout):
2860
raise req.exceptions.RequestException("Testfehler")
2961

3062
monkeypatch.setattr("requests.get", test_get_resp)
3163

32-
data = get_rates("EUR", "USD")
64+
data = get_rates(sample_request)
3365
assert data is None
3466

3567

36-
def test_check_currency_value_error(monkeypatch):
68+
def test_check_currency_value_error(monkeypatch, sample_request):
3769
test_resp = Mock()
3870
test_resp.raise_for_status.return_value = None
3971
test_resp.json.side_effect = ValueError("Ungültige JSON-Antwort")
@@ -43,11 +75,11 @@ def test_get_resp(url, timeout):
4375

4476
monkeypatch.setattr("requests.get", test_get_resp)
4577

46-
data = get_rates("EUR", "USD")
78+
data = get_rates(sample_request)
4779
assert data is None
4880

4981

50-
def test_check_currency_key_error(monkeypatch):
82+
def test_check_currency_key_error(monkeypatch, sample_request):
5183
test_resp = Mock()
5284
test_resp.raise_for_status.return_value = None
5385
test_resp.json.return_value = {
@@ -61,11 +93,11 @@ def test_get_resp(url, timeout):
6193

6294
monkeypatch.setattr("requests.get", test_get_resp)
6395

64-
data = get_rates("EUR", "USD")
96+
data = get_rates(sample_request)
6597
assert data is None
6698

6799

68-
def test_check_currency_valid(monkeypatch):
100+
def test_check_currency_valid(monkeypatch, sample_request):
69101
test_resp = Mock()
70102
test_resp.raise_for_status.return_value = None
71103
test_resp.json.return_value = {
@@ -79,11 +111,11 @@ def test_get_resp(url, timeout):
79111

80112
monkeypatch.setattr("requests.get", test_get_resp)
81113

82-
data = get_rates("EUR", "USD")
114+
data = get_rates(sample_request)
83115
assert data == {"result": "success", "error_type": "", "conversion_rate": 1.2}
84116

85117

86-
def test_check_currency_invalid(monkeypatch):
118+
def test_check_currency_invalid(monkeypatch, sample_request):
87119
test_resp = Mock()
88120
test_resp.raise_for_status.return_value = None
89121
test_resp.json.return_value = {
@@ -97,7 +129,7 @@ def test_get_resp(url, timeout):
97129

98130
monkeypatch.setattr("requests.get", test_get_resp)
99131

100-
data = get_rates("EUR", "USD")
132+
data = get_rates(sample_request)
101133
assert data is None
102134

103135

0 commit comments

Comments
 (0)