Skip to content

Commit 2d58df9

Browse files
ran-isenbergRan Isenberg
andauthored
feature: allow to disable lru_caching when needed in testing context (#68)
--------- Co-authored-by: Ran Isenberg <ran.isenberg@ranthebuilder.cloud>
1 parent 8f52c5f commit 2d58df9

File tree

14 files changed

+753
-546
lines changed

14 files changed

+753
-546
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,4 @@ cdk.out
248248
.vscode
249249
lib_requirements.txt
250250
.dccache
251+
.ruff_cache

README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ class MyEnvVariables(BaseModel):
7373
API_URL: HttpUrl
7474
```
7575

76-
You must first use the `@init_environment_variables` decorator to automatically validate and initialize the environment variables before executing a function:
76+
Before executing a function, you must use the `@init_environment_variables` decorator to validate and initialize the environment variables automatically.
77+
78+
The decorator guarantees that the function will run with the correct variable configuration.
79+
80+
Then, you can fetch the environment variables using the global getter function, 'get_environment_variables,' and use them just like a data class. At this point, they are parsed and validated.
7781

7882
```python
7983
from aws_lambda_env_modeler import init_environment_variables
@@ -93,6 +97,51 @@ env_vars = get_environment_variables(MyEnvVariables)
9397
print(env_vars.DB_HOST)
9498
```
9599

100+
## Disabling Cache for Testing
101+
102+
By default, the modeler uses cache - the parsed model is cached for performance improvement for multiple 'get' calls.
103+
104+
In some cases, such as during testing, you may want to turn off the cache. You can do this by setting the `LAMBDA_ENV_MODELER_DISABLE_CACHE` environment variable to 'True.'
105+
106+
This is especially useful in tests where you want to run multiple tests concurrently, each with a different set of environment variables.
107+
108+
Here's an example of how you can use this in a pytest test:
109+
110+
```python
111+
import json
112+
from http import HTTPStatus
113+
from typing import Any, Dict
114+
from unittest.mock import patch
115+
116+
from pydantic import BaseModel
117+
from typing_extensions import Literal
118+
119+
from aws_lambda_env_modeler import LAMBDA_ENV_MODELER_DISABLE_CACHE, get_environment_variables, init_environment_variables
120+
121+
122+
class MyHandlerEnvVars(BaseModel):
123+
LOG_LEVEL: Literal['DEBUG', 'INFO', 'ERROR', 'CRITICAL', 'WARNING', 'EXCEPTION']
124+
125+
126+
@init_environment_variables(model=MyHandlerEnvVars)
127+
def my_handler(event: Dict[str, Any], context) -> Dict[str, Any]:
128+
env_vars = get_environment_variables(model=MyHandlerEnvVars) # noqa: F841
129+
# can access directly env_vars.LOG_LEVEL as dataclass
130+
return {
131+
'statusCode': HTTPStatus.OK,
132+
'headers': {'Content-Type': 'application/json'},
133+
'body': json.dumps({'message': 'success'}),
134+
}
135+
136+
137+
@patch.dict('os.environ', {LAMBDA_ENV_MODELER_DISABLE_CACHE: 'true', 'LOG_LEVEL': 'DEBUG'})
138+
def test_my_handler():
139+
response = my_handler({}, None)
140+
assert response['statusCode'] == HTTPStatus.OK
141+
assert response['headers'] == {'Content-Type': 'application/json'}
142+
assert json.loads(response['body']) == {'message': 'success'}
143+
```
144+
96145
## Code Contributions
97146
Code contributions are welcomed. Read this [guide.](https://github.com/ran-isenberg/aws-lambda-env-modeler/blob/main/CONTRIBUTING.md)
98147

aws_lambda_env_modeler/__init__.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
"""Advanced event_parser utility
2-
"""
31
from pydantic import BaseModel
42

5-
from .modeler import get_environment_variables, init_environment_variables
6-
from .types import Model
3+
from aws_lambda_env_modeler.modeler import get_environment_variables, init_environment_variables
4+
from aws_lambda_env_modeler.modeler_impl import LAMBDA_ENV_MODELER_DISABLE_CACHE
5+
from aws_lambda_env_modeler.types import Annotated, Model
76

8-
__all__ = [
9-
'Model',
10-
'BaseModel',
11-
'init_environment_variables',
12-
'get_environment_variables',
13-
]
7+
__all__ = ['Model', 'BaseModel', 'init_environment_variables', 'get_environment_variables', 'LAMBDA_ENV_MODELER_DISABLE_CACHE', 'Annotated']

aws_lambda_env_modeler/modeler.py

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,50 @@
1-
import os
2-
from functools import lru_cache, wraps
1+
from functools import wraps
32
from typing import Any, Callable, Dict, Type
43

4+
from aws_lambda_env_modeler.modeler_impl import __get_environment_variables_impl
55
from aws_lambda_env_modeler.types import Model
66

77

8-
def get_environment_variables(model: Type[Model]) -> Model:
9-
"""
10-
This function receives a model of type Model, uses it to validate the environment variables, and returns the
11-
validated model.
12-
13-
Args:
14-
model (Type[Model]): A Pydantic model that defines the structure and types of the expected environment variables.
15-
16-
Returns:
17-
Model: An instance of the provided model filled with the values of the validated environment variables.
18-
"""
19-
return __parse_model(model)
20-
21-
228
def init_environment_variables(model: Type[Model]):
239
"""
24-
A decorator function for AWS Lambda handler functions that initializes environment variables based on the given Pydantic model before executing
25-
the decorated function. The decorator validates the environment variables according to the model structure before
26-
running the handler.
10+
A decorator for AWS Lambda handler functions. It initializes and validates environment variables based on the provided Pydantic model before the execution of the decorated function.
11+
It uses LRU Cache by model class type to optimize parsing time. Cache can be disabled by setting the environment variable 'LAMBDA_ENV_MODELER_DISABLE_CACHE' to FALSE (default: cache is enabled)
2712
2813
Args:
29-
model (Type[Model]): A Pydantic model that defines the structure and types of the expected environment variables.
14+
model (Type[Model]): A Pydantic model that outlines the structure and types of the expected environment variables.
3015
3116
Returns:
32-
Callable: A decorated function that first initializes the environment variables and then runs the function.
17+
Callable: A decorated function that first initializes and validates the environment variables, then executes the original function.
18+
19+
Raises:
20+
ValueError: If the environment variables do not align with the model's structure or fail validation.
3321
"""
3422

3523
def decorator(lambda_handler_function: Callable):
3624
@wraps(lambda_handler_function)
3725
def wrapper(event: Dict[str, Any], context, **kwargs):
38-
__parse_model(model)
26+
# Initialize and validate environment variables before executing the lambda handler function
27+
__get_environment_variables_impl(model)
3928
return lambda_handler_function(event, context, **kwargs)
4029

4130
return wrapper
4231

4332
return decorator
4433

4534

46-
@lru_cache
47-
def __parse_model(model: Type[Model]) -> Model:
35+
def get_environment_variables(model: Type[Model]) -> Model:
4836
"""
49-
A helper function to validate and parse environment variables based on a given Pydantic model. This function is
50-
also cached to improve performance in successive calls.
37+
Retrieves and validates environment variables based on the provided Pydantic model.
38+
It uses LRU Cache by model class type to optimize parsing time. Cache can be disabled by setting the environment variable 'LAMBDA_ENV_MODELER_DISABLE_CACHE' to FALSE (default: cache is enabled)
39+
It's recommended to use anywhere in the function's after init_environment_variables decorator was used on the handler function.
5140
5241
Args:
53-
model (Type[Model]): A Pydantic model that defines the structure and types of the expected environment variables.
42+
model (Type[Model]): A Pydantic model that outlines the structure and types of the expected environment variables.
5443
5544
Returns:
56-
Model: An instance of the provided model filled with the values of the validated environment variables.
45+
Model: An instance of the provided model populated with the values of the validated environment variables.
5746
5847
Raises:
59-
ValueError: If the environment variables do not match the structure of the model or cannot be validated.
48+
ValueError: If the environment variables do not align with the model's structure or fail validation.
6049
"""
61-
try:
62-
return model.model_validate(os.environ)
63-
except Exception as exc:
64-
raise ValueError(f'failed to load environment variables, exception={str(exc)}') from exc
50+
return __get_environment_variables_impl(model)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import os
2+
from functools import lru_cache
3+
from typing import Type
4+
5+
from aws_lambda_env_modeler.types import Model
6+
7+
# Environment variable to control caching
8+
LAMBDA_ENV_MODELER_DISABLE_CACHE = 'LAMBDA_ENV_MODELER_DISABLE_CACHE'
9+
10+
11+
def __get_environment_variables_impl(model: Type[Model]) -> Model:
12+
# Check if the environment variable for disabling cache is set to true
13+
disable_cache = True if os.getenv(LAMBDA_ENV_MODELER_DISABLE_CACHE, 'false').lower() == 'true' else False
14+
if disable_cache:
15+
# If LAMBDA_ENV_MODELER_DISABLE_CACHE is true, parse the model without cache
16+
return __parse_model_impl(model)
17+
# If LAMBDA_ENV_MODELER_DISABLE_CACHE is not true, parse the model with cache
18+
return __parse_model_with_cache(model)
19+
20+
21+
@lru_cache
22+
def __parse_model_with_cache(model: Type[Model]) -> Model:
23+
# Parse the model with cache enabled
24+
return __parse_model_impl(model)
25+
26+
27+
def __parse_model_impl(model: Type[Model]) -> Model:
28+
try:
29+
# Validate the model with the environment variables
30+
return model.model_validate(os.environ)
31+
except Exception as exc:
32+
# If validation fails, raise an exception with the error message
33+
raise ValueError(f'failed to load environment variables, exception={str(exc)}') from exc

aws_lambda_env_modeler/types.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
import sys
12
from typing import TypeVar
23

34
from pydantic import BaseModel
45

56
Model = TypeVar('Model', bound=BaseModel)
67

7-
__all__ = ['Model', 'BaseModel']
8+
9+
if sys.version_info >= (3, 9):
10+
from typing import Annotated
11+
else:
12+
from typing_extensions import Annotated
13+
14+
__all__ = ['Model', 'BaseModel', 'Annotated']

docs/index.md

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,26 +44,42 @@ pip install aws-lambda-env-modeler
4444

4545
## Usage
4646

47-
First, define a Pydantic model for your environment variables:
47+
### Schema Definition
4848

49-
```python
50-
from pydantic import BaseModel
49+
First, define a Pydantic model for your environment variables:
5150

52-
class MyEnvVariables(BaseModel):
53-
DB_HOST: str
54-
DB_PORT: int
55-
DB_USER: str
56-
DB_PASS: str
51+
```python title="schema.py"
52+
--8<-- "docs/snippets/schema.py"
5753
```
5854

59-
You must first use the `@init_environment_variables` decorator to automatically validate and initialize the environment variables before executing a function:
55+
Notice how you can use advanced types and value assertions and not just plain strings.
56+
57+
### Decorator
6058

61-
Then, you can fetch and validate the environment variables with your model:
59+
Before executing a function, you must use the `@init_environment_variables` decorator to validate and initialize the environment variables automatically.
6260

63-
```python hl_lines="8 18 20" title="my_handler.py"
61+
The decorator guarantees that the function will run with the correct variable configuration.
62+
63+
Then, you can fetch the environment variables using the global getter function, 'get_environment_variables,' and use them just like a data class. At this point, they are parsed and validated.
64+
65+
```python hl_lines="7 18 20" title="my_handler.py"
6466
--8<-- "docs/snippets/my_handler.py"
6567
```
6668

69+
## Disabling Cache for Testing
70+
71+
By default, the modeler uses cache - the parsed model is cached for performance improvement for multiple 'get' calls.
72+
73+
In some cases, such as during testing, you may want to turn off the cache. You can do this by setting the `LAMBDA_ENV_MODELER_DISABLE_CACHE` environment variable to 'True.'
74+
75+
This is especially useful in tests where you want to run multiple tests concurrently, each with a different set of environment variables.
76+
77+
Here's an example of how you can use this in a pytest test:
78+
79+
```python hl_lines="8 26" title="pytest.py"
80+
--8<-- "docs/snippets/pytest.py"
81+
```
82+
6783
## License
6884

6985
This library is licensed under the MIT License. See the [LICENSE](https://github.com/ran-isenberg/aws-lambda-env-modeler/blob/main/LICENSE) file.

docs/snippets/my_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import json
22
from http import HTTPStatus
3-
from typing import Any, Dict
3+
from typing import Any, Dict, Literal
44

55
from pydantic import BaseModel, Field, HttpUrl
6-
from typing_extensions import Annotated, Literal
76

87
from aws_lambda_env_modeler import get_environment_variables, init_environment_variables
8+
from aws_lambda_env_modeler.types import Annotated
99

1010

1111
class MyHandlerEnvVars(BaseModel):

docs/snippets/pytest.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import json
2+
from http import HTTPStatus
3+
from typing import Any, Dict, Literal
4+
from unittest.mock import patch
5+
6+
from pydantic import BaseModel
7+
8+
from aws_lambda_env_modeler import LAMBDA_ENV_MODELER_DISABLE_CACHE, get_environment_variables, init_environment_variables
9+
10+
11+
class MyHandlerEnvVars(BaseModel):
12+
LOG_LEVEL: Literal['DEBUG', 'INFO', 'ERROR', 'CRITICAL', 'WARNING', 'EXCEPTION']
13+
14+
15+
@init_environment_variables(model=MyHandlerEnvVars)
16+
def my_handler(event: Dict[str, Any], context) -> Dict[str, Any]:
17+
env_vars = get_environment_variables(model=MyHandlerEnvVars) # noqa: F841
18+
# can access directly env_vars.LOG_LEVEL as dataclass
19+
return {
20+
'statusCode': HTTPStatus.OK,
21+
'headers': {'Content-Type': 'application/json'},
22+
'body': json.dumps({'message': 'success'}),
23+
}
24+
25+
26+
@patch.dict('os.environ', {LAMBDA_ENV_MODELER_DISABLE_CACHE: 'true', 'LOG_LEVEL': 'DEBUG'})
27+
def test_my_handler():
28+
response = my_handler({}, None)
29+
assert response['statusCode'] == HTTPStatus.OK
30+
assert response['headers'] == {'Content-Type': 'application/json'}
31+
assert json.loads(response['body']) == {'message': 'success'}

docs/snippets/schema.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import Literal
2+
3+
from pydantic import BaseModel, Field, HttpUrl
4+
5+
from aws_lambda_env_modeler.types import Annotated
6+
7+
8+
class MyEnvVariables(BaseModel):
9+
REST_API: HttpUrl
10+
ROLE_ARN: Annotated[str, Field(min_length=20, max_length=2048)]
11+
POWERTOOLS_SERVICE_NAME: Annotated[str, Field(min_length=1)]
12+
LOG_LEVEL: Literal['DEBUG', 'INFO', 'ERROR', 'CRITICAL', 'WARNING', 'EXCEPTION']

0 commit comments

Comments
 (0)