Skip to content

Commit 9966954

Browse files
author
Karl Wooster
committed
test(settings): makes settings lazily loaded and then uses monkeypatch to "override" settings
1 parent 6538e00 commit 9966954

File tree

11 files changed

+240
-160
lines changed

11 files changed

+240
-160
lines changed

src/ynamazon/amazon/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, model_validator
1111

1212
from ynamazon.amazon_transactions import AmazonConfig
13-
from ynamazon.settings import settings
13+
from ynamazon.settings import get_settings
1414
from ynamazon.types_pydantic import AmazonSellerType
1515
from ynamazon.utilities import MISSING, Missing, getattr_path
1616
from ynamazon.utilities.bases import SimpleDict
@@ -35,7 +35,7 @@ class Item(Entity):
3535
quantity: Union[int, None] = None
3636

3737
def __str__(self) -> str:
38-
if settings.ynab_use_markdown:
38+
if get_settings().ynab_use_markdown:
3939
return f"[{self.title}]({self.link})"
4040
return self.title
4141

src/ynamazon/amazon_transactions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from rich import print as rprint
2323
from rich.table import Table
2424

25-
from .settings import settings
25+
from .settings import get_settings
2626
from .types_pydantic import AmazonItemType
2727

2828

@@ -73,8 +73,8 @@ class AmazonConfig(BaseModel):
7373

7474
model_config = ConfigDict(arbitrary_types_allowed=True)
7575

76-
username: EmailStr = Field(default_factory=lambda: settings.amazon_user)
77-
password: SecretStr = Field(default_factory=lambda: settings.amazon_password)
76+
username: EmailStr = Field(default_factory=lambda: get_settings().amazon_user)
77+
password: SecretStr = Field(default_factory=lambda: get_settings().amazon_password)
7878
config: AmazonOrdersConfig = Field(default_factory=lambda: AmazonOrdersConfig())
7979
debug: bool = False
8080

src/ynamazon/cli/cli.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ynamazon.amazon_transactions import AmazonConfig, get_amazon_transactions
1717
from ynamazon.exceptions import YnabSetupError
1818
from ynamazon.main import process_transactions
19-
from ynamazon.settings import ConfigFile, SecretApiKey, SecretBudgetId, settings
19+
from ynamazon.settings import ConfigFile, SecretApiKey, SecretBudgetId, get_settings
2020
from ynamazon.ynab_transactions import (
2121
TempYnabTransaction,
2222
get_ynab_transactions,
@@ -35,14 +35,14 @@ def print_ynab_transactions(
3535
str | None,
3636
Argument(
3737
help="YNAB API key",
38-
default_factory=lambda: settings.ynab_api_key.get_secret_value(),
38+
default_factory=lambda: get_settings().ynab_api_key.get_secret_value(),
3939
),
4040
],
4141
budget_id: Annotated[
4242
str | None,
4343
Argument(
4444
help="YNAB Budget ID",
45-
default_factory=lambda: settings.ynab_budget_id.get_secret_value(),
45+
default_factory=lambda: get_settings().ynab_budget_id.get_secret_value(),
4646
),
4747
],
4848
) -> None:
@@ -83,13 +83,15 @@ def print_ynab_transactions(
8383
def print_amazon_transactions(
8484
user_email: Annotated[
8585
str,
86-
Argument(help="Amazon username", default_factory=lambda: settings.amazon_user),
86+
Argument(
87+
help="Amazon username", default_factory=lambda: get_settings().amazon_user
88+
),
8789
],
8890
user_password: Annotated[
8991
str,
9092
Argument(
9193
help="Amazon password",
92-
default_factory=lambda: settings.amazon_password.get_secret_value(),
94+
default_factory=lambda: get_settings().amazon_password.get_secret_value(),
9395
),
9496
],
9597
order_years: Annotated[
@@ -148,28 +150,28 @@ def ynamazon(
148150
str | None,
149151
Argument(
150152
help="YNAB API key",
151-
default_factory=lambda: settings.ynab_api_key.get_secret_value(),
153+
default_factory=lambda: get_settings().ynab_api_key.get_secret_value(),
152154
),
153155
],
154156
ynab_budget_id: Annotated[
155157
str | None,
156158
Argument(
157159
help="YNAB Budget ID",
158-
default_factory=lambda: settings.ynab_budget_id.get_secret_value(),
160+
default_factory=lambda: get_settings().ynab_budget_id.get_secret_value(),
159161
),
160162
],
161163
amazon_user: Annotated[
162164
str,
163165
Argument(
164166
help="Amazon username",
165-
default_factory=lambda: settings.amazon_user,
167+
default_factory=lambda: get_settings().amazon_user,
166168
),
167169
],
168170
amazon_password: Annotated[
169171
str,
170172
Argument(
171173
help="Amazon password",
172-
default_factory=lambda: settings.amazon_password.get_secret_value(),
174+
default_factory=lambda: get_settings().amazon_password.get_secret_value(),
173175
),
174176
],
175177
) -> None:
@@ -210,13 +212,13 @@ def new_ynamazon( # noqa: C901
210212
config.amazon_user = amazon_user
211213
if amazon_password is not None:
212214
config.amazon_password = SecretStr(amazon_password)
213-
cli_settings = settings.model_copy(update=config.model_dump())
215+
cli_settings = get_settings().model_copy(update=config.model_dump())
214216
else:
215217
assert ynab_api_key is not None, "YNAB API key is required"
216218
assert ynab_budget_id is not None, "YNAB Budget ID is required"
217219
assert amazon_user is not None, "Amazon username is required"
218220
assert amazon_password is not None, "Amazon password is required"
219-
cli_settings = settings.model_copy(
221+
cli_settings = get_settings().model_copy(
220222
update={
221223
"ynab_api_key": SecretApiKey(ynab_api_key),
222224
"ynab_budget_id": SecretBudgetId(ynab_budget_id),
@@ -235,7 +237,7 @@ def new_ynamazon( # noqa: C901
235237
budget_id=cli_settings.ynab_budget_id.get_secret_value(),
236238
)
237239
except YnabSetupError as e:
238-
console.print(f"[bold red]Settings error: {e}[/]")
240+
console.print(f"[bold red]get_settings() error: {e}[/]")
239241
console.print(
240242
"[bold red]Please check your .env file or use the --config option to specify a config file.[/]"
241243
)
@@ -248,7 +250,7 @@ def new_ynamazon( # noqa: C901
248250
debug=debug,
249251
)
250252
except ValidationError as e:
251-
console.print(f"[bold red]Settings error: {e}[/]")
253+
console.print(f"[bold red]get_settings() error: {e}[/]")
252254
console.print(
253255
"[bold red]Please check your .env file or use the --config option to specify a config file.[/]"
254256
)

src/ynamazon/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ynamazon.ynab_memo import process_memo
1515

1616
from .exceptions import YnabSetupError
17-
from .settings import settings
17+
from .settings import get_settings
1818
from .ynab_transactions import default_configuration as ynab_configuration
1919
from .ynab_transactions import (
2020
get_ynab_transactions,
@@ -114,7 +114,7 @@ def process_transactions(
114114
"""Match YNAB transactions to Amazon Transactions and optionally update YNAB Memos."""
115115
amazon_config = amazon_config or AmazonConfig()
116116
ynab_config = ynab_config or ynab_configuration
117-
budget_id = budget_id or settings.ynab_budget_id.get_secret_value()
117+
budget_id = budget_id or get_settings().ynab_budget_id.get_secret_value()
118118

119119
console = Console()
120120

src/ynamazon/settings.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import lru_cache
12
from os import PathLike
23
from pathlib import Path
34
from typing import Union
@@ -63,8 +64,20 @@ def validate_settings(self) -> "Settings":
6364
)
6465
return self
6566

67+
def get_secret_value(self, key: str) -> Union[str, None]:
68+
"""Get secret API key or budget ID."""
69+
value = getattr(self, key)
70+
if value is None:
71+
return None
72+
if not isinstance(value, SecretStr):
73+
raise ValueError(f"{key} is not a SecretStr")
74+
return value.get_secret_value()
6675

67-
settings = Settings() # type: ignore[call-arg]
76+
77+
@lru_cache
78+
def get_settings() -> Settings:
79+
"""Get settings from environment variables."""
80+
return Settings() # type: ignore[call-arg]
6881

6982

7083
class ConfigFile(BaseModel):

src/ynamazon/ynab_memo.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
AMAZON_SUMMARY_PLAIN_PROMPT,
1212
AMAZON_SUMMARY_SYSTEM_PROMPT,
1313
)
14-
from ynamazon.settings import settings
14+
from ynamazon.settings import get_settings
1515

1616
from .exceptions import (
1717
InvalidOpenAIAPIKey,
@@ -49,14 +49,11 @@ def generate_ai_summary(
4949
Exception: For other OpenAI API errors
5050
"""
5151
# Check if OpenAI key is available
52-
if (
53-
settings.openai_api_key is None
54-
or not settings.openai_api_key.get_secret_value()
55-
):
52+
if get_settings().openai_api_key is None:
5653
raise MissingOpenAIAPIKey("OpenAI API key not found")
5754

5855
# Create client
59-
client = OpenAI(api_key=settings.openai_api_key.get_secret_value())
56+
client = OpenAI(api_key=get_settings().get_secret_value("openai_api_key"))
6057

6158
# Prepare content for summarization
6259
partial_order_note = ""
@@ -69,7 +66,7 @@ def generate_ai_summary(
6966
# Select the appropriate prompt based on markdown setting
7067
user_prompt = (
7168
AMAZON_SUMMARY_MARKDOWN_PROMPT
72-
if settings.ynab_use_markdown
69+
if get_settings().ynab_use_markdown
7370
else AMAZON_SUMMARY_PLAIN_PROMPT
7471
)
7572

@@ -308,12 +305,12 @@ def summarize_memo_with_ai(memo: str, order_url: str) -> str:
308305
def process_memo(memo: str) -> str:
309306
"""Process a memo using AI summarization if enabled, otherwise use truncation if needed.
310307
311-
This function handles both markdown and non-markdown memos based on the settings.ynab_use_markdown setting:
308+
This function handles both markdown and non-markdown memos based on the get_settings().ynab_use_markdown setting:
312309
- If markdown is enabled, it preserves markdown formatting in the output
313310
- If markdown is disabled, it strips all markdown formatting
314311
315312
The processing strategy is:
316-
1. If AI summarization is enabled (settings.use_ai_summarization):
313+
1. If AI summarization is enabled (get_settings().use_ai_summarization):
317314
- Uses OpenAI to generate a concise summary
318315
- Preserves markdown formatting if enabled
319316
- Ensures the summary fits within YNAB's character limit
@@ -335,7 +332,7 @@ def process_memo(memo: str) -> str:
335332
logger.warning("No Amazon order URL found in memo")
336333
return original_memo
337334

338-
if settings.use_ai_summarization:
335+
if get_settings().use_ai_summarization:
339336
logger.info("Using AI summarization")
340337
processed_memo = summarize_memo_with_ai(original_memo, order_url)
341338
if processed_memo:
@@ -367,7 +364,7 @@ def summarize_memo(memo: str) -> str:
367364
logger.warning("No Amazon order URL found in memo")
368365
return truncate_memo(original_memo)
369366

370-
if settings.use_ai_summarization:
367+
if get_settings().use_ai_summarization:
371368
processed_memo = summarize_memo_with_ai(original_memo, order_url)
372369
if processed_memo:
373370
return processed_memo

src/ynamazon/ynab_transactions.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818

1919
from ynamazon.amazon.models import Transaction
2020
from ynamazon.exceptions import YnabSetupError
21-
from ynamazon.settings import settings
21+
from ynamazon.settings import get_settings
2222

2323
default_configuration = Configuration(
24-
access_token=settings.ynab_api_key.get_secret_value()
24+
access_token=get_settings().ynab_api_key.get_secret_value()
2525
)
26-
my_budget_id = settings.ynab_budget_id
26+
my_budget_id = get_settings().ynab_budget_id
2727

2828
PARTIAL_ORDER_MEMO = "-This transaction doesn't represent the entire order. The order total is ${order_total:.2f}-"
2929
YNAB_MAX_MEMO_LENGTH = 500
@@ -236,20 +236,20 @@ def get_ynab_transactions(
236236
amazon_needs_memo_payee = find_item_by_attribute(
237237
items=payees,
238238
attribute="name",
239-
value=settings.ynab_payee_name_to_be_processed,
239+
value=get_settings().ynab_payee_name_to_be_processed,
240240
)
241241
amazon_with_memo_payee = find_item_by_attribute(
242242
items=payees,
243243
attribute="name",
244-
value=settings.ynab_payee_name_processing_completed,
244+
value=get_settings().ynab_payee_name_processing_completed,
245245
)
246246
if amazon_needs_memo_payee is None:
247247
raise YnabSetupError(
248-
f"Payee '{settings.ynab_payee_name_to_be_processed}' not found in YNAB."
248+
f"Payee '{get_settings().ynab_payee_name_to_be_processed}' not found in YNAB."
249249
)
250250
if amazon_with_memo_payee is None:
251251
raise YnabSetupError(
252-
f"Payee '{settings.ynab_payee_name_processing_completed}' not found in YNAB."
252+
f"Payee '{get_settings().ynab_payee_name_processing_completed}' not found in YNAB."
253253
)
254254

255255
ynab_transactions = get_transactions_by_payee(
@@ -376,7 +376,7 @@ def markdown_formatted_title(title: str, url: Union[str, AnyUrl]) -> str:
376376
Returns:
377377
str: A URL string suitable for injection into the memo
378378
"""
379-
if settings.ynab_use_markdown:
379+
if get_settings().ynab_use_markdown:
380380
return f"[{title}]({url})"
381381

382382
return title
@@ -392,7 +392,7 @@ def markdown_formatted_link(title: str, url: Union[str, AnyUrl]) -> str:
392392
Returns:
393393
str: A URL string suitable for injection into the memo
394394
"""
395-
if settings.ynab_use_markdown:
395+
if get_settings().ynab_use_markdown:
396396
return f"[{title}]({url})"
397397

398398
if isinstance(url, AnyUrl):

0 commit comments

Comments
 (0)