Skip to content

Commit 96f298f

Browse files
author
lilydu
committed
add storage module, decr bump to 3.12 for python version, add tests for logging and storage
1 parent a07ebcc commit 96f298f

File tree

11 files changed

+555
-3
lines changed

11 files changed

+555
-3
lines changed

packages/cards/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ readme = "README.md"
66
authors = [
77
{ name = "lilydu", email = "[email protected]" }
88
]
9-
requires-python = ">=3.13"
9+
requires-python = ">=3.12"
1010
dependencies = [
1111
"coverage>=7.8.0",
1212
"pytest>=8.3.5",

packages/common/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ license = "MIT"
99
authors = [
1010
{ name = "Microsoft", email = "[email protected]" }
1111
]
12-
requires-python = ">=3.13"
12+
requires-python = ">=3.12"
1313
dependencies = [
1414
"coverage>=7.8.0",
1515
"pytest>=8.3.5",
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
7+
import logging
8+
from unittest.mock import MagicMock
9+
10+
from .filter import ConsoleFilter
11+
12+
def test_default_pattern():
13+
filter = ConsoleFilter()
14+
record = MagicMock(spec=logging.LogRecord)
15+
record.name = "test"
16+
17+
assert filter.filter(record) is True
18+
19+
def test_exact_match():
20+
filter = ConsoleFilter("test")
21+
record = MagicMock(spec=logging.LogRecord)
22+
record.name = "test"
23+
24+
assert filter.filter(record) is True
25+
26+
def test_wildcard_prefix():
27+
filter = ConsoleFilter("test*")
28+
29+
matching_record = MagicMock(spec=logging.LogRecord)
30+
matching_record.name = "testLogger"
31+
assert filter.filter(matching_record) is True
32+
33+
non_matching_record = MagicMock(spec=logging.LogRecord)
34+
non_matching_record.name = "logger"
35+
assert filter.filter(non_matching_record) is False
36+
37+
def test_wildcard_suffix():
38+
filter = ConsoleFilter("*test")
39+
40+
matching_record = MagicMock(spec=logging.LogRecord)
41+
matching_record.name = "mytest"
42+
assert filter.filter(matching_record) is True
43+
44+
non_matching_record = MagicMock(spec=logging.LogRecord)
45+
non_matching_record.name = "tester"
46+
assert filter.filter(non_matching_record) is False
47+
48+
def test_wildcard_middle():
49+
filter = ConsoleFilter("my*test")
50+
51+
matching_record = MagicMock(spec=logging.LogRecord)
52+
matching_record.name = "myloggertest"
53+
assert filter.filter(matching_record) is True
54+
55+
non_matching_record = MagicMock(spec=logging.LogRecord)
56+
non_matching_record.name = "mylogger"
57+
assert filter.filter(non_matching_record) is False
58+
59+
def test_multiple_wildcards():
60+
filter = ConsoleFilter("my*log*test")
61+
62+
matching_record = MagicMock(spec=logging.LogRecord)
63+
matching_record.name = "myapplicationloggertest"
64+
assert filter.filter(matching_record) is True
65+
66+
partial_match_record = MagicMock(spec=logging.LogRecord)
67+
partial_match_record.name = "mylogger"
68+
assert filter.filter(partial_match_record) is False
69+
70+
def test_case_sensitivity():
71+
filter = ConsoleFilter("Test")
72+
73+
upper_record = MagicMock(spec=logging.LogRecord)
74+
upper_record.name = "Test"
75+
assert filter.filter(upper_record) is True
76+
77+
lower_record = MagicMock(spec=logging.LogRecord)
78+
lower_record.name = "test"
79+
assert filter.filter(lower_record) is False
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
7+
import logging
8+
from typing import Union
9+
10+
from .formatter import ConsoleFormatter
11+
from .ansi import ANSI
12+
13+
14+
def create_record(name: str, level: int, msg: Union[str, dict, list]) -> logging.LogRecord:
15+
record = logging.LogRecord(
16+
name=name,
17+
level=level,
18+
pathname="test.py",
19+
lineno=1,
20+
msg=msg,
21+
args=(),
22+
exc_info=None
23+
)
24+
record.levelname = logging.getLevelName(level)
25+
return record
26+
27+
def test_error_formatting():
28+
formatter = ConsoleFormatter()
29+
record = create_record("test", logging.ERROR, "Error message")
30+
31+
result = formatter.format(record)
32+
expected = f"{ANSI.FOREGROUND_RED}{ANSI.BOLD}[ERROR] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET} Error message"
33+
assert result == expected
34+
35+
def test_warning_formatting():
36+
formatter = ConsoleFormatter()
37+
record = create_record("test", logging.WARNING, "Warning message")
38+
39+
result = formatter.format(record)
40+
expected = f"{ANSI.FOREGROUND_YELLOW}{ANSI.BOLD}[WARNING] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET} Warning message"
41+
assert result == expected
42+
43+
def test_info_formatting():
44+
formatter = ConsoleFormatter()
45+
record = create_record("test", logging.INFO, "Info message")
46+
47+
result = formatter.format(record)
48+
expected = f"{ANSI.FOREGROUND_CYAN}{ANSI.BOLD}[INFO] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET} Info message"
49+
assert result == expected
50+
51+
def test_debug_formatting():
52+
formatter = ConsoleFormatter()
53+
record = create_record("test", logging.DEBUG, "Debug message")
54+
55+
result = formatter.format(record)
56+
expected = f"{ANSI.FOREGROUND_MAGENTA}{ANSI.BOLD}[DEBUG] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET} Debug message"
57+
assert result == expected
58+
59+
def test_dict_message_formatting():
60+
formatter = ConsoleFormatter()
61+
dict_msg = {"key": "value", "nested": {"inner": "data"}}
62+
record = create_record("test", logging.INFO, dict_msg)
63+
64+
result = formatter.format(record)
65+
result_lines = result.split("\n")
66+
67+
prefix = f"{ANSI.FOREGROUND_CYAN}{ANSI.BOLD}[INFO] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET}"
68+
assert result_lines[0].startswith(prefix)
69+
assert '"key": "value"' in result
70+
assert '"nested": {' in result
71+
assert '"inner": "data"' in result
72+
73+
def test_list_message_formatting():
74+
formatter = ConsoleFormatter()
75+
list_msg = ["item1", "item2", {"key": "value"}]
76+
record = create_record("test", logging.INFO, list_msg)
77+
78+
result = formatter.format(record)
79+
result_lines = result.split("\n")
80+
81+
prefix = f"{ANSI.FOREGROUND_CYAN}{ANSI.BOLD}[INFO] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET}"
82+
assert result_lines[0].startswith(prefix)
83+
assert '"item1"' in result
84+
assert '"item2"' in result
85+
assert '"key": "value"' in result
86+
87+
def test_multiline_message_formatting():
88+
formatter = ConsoleFormatter()
89+
multiline_msg = "Line 1\nLine 2\nLine 3"
90+
record = create_record("test", logging.INFO, multiline_msg)
91+
92+
result = formatter.format(record)
93+
expected_lines = [
94+
f"{ANSI.FOREGROUND_CYAN}{ANSI.BOLD}[INFO] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET} Line 1",
95+
f"{ANSI.FOREGROUND_CYAN}{ANSI.BOLD}[INFO] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET} Line 2",
96+
f"{ANSI.FOREGROUND_CYAN}{ANSI.BOLD}[INFO] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET} Line 3"
97+
]
98+
expected = "\n".join(expected_lines)
99+
assert result == expected
100+
101+
def test_unknown_level_formatting():
102+
formatter = ConsoleFormatter()
103+
record = create_record("test", 123, "Custom level message")
104+
105+
result = formatter.format(record)
106+
expected = f"{ANSI.BOLD}[Level 123] test{ANSI.FOREGROUND_RESET}{ANSI.BOLD_RESET} Custom level message"
107+
assert result.endswith("Custom level message")
108+
assert "[LEVEL 123]" in result
109+
assert "test" in result
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
from .storage import Storage, ListStorage
7+
from .local_storage import LocalStorage
8+
from .list_local_storage import ListLocalStorage
9+
10+
__all__ = ["Storage", "ListStorage", "LocalStorage", "ListLocalStorage"]
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
7+
from __future__ import annotations
8+
9+
from .storage import ListStorage
10+
from typing import TypeVar, Optional, List, Callable
11+
from .storage import ListStorage
12+
13+
V = TypeVar('V')
14+
15+
16+
class ListLocalStorage(ListStorage[V]):
17+
18+
@property
19+
def list(self) -> List[V]:
20+
return self._list
21+
22+
def __init__(self, items: Optional[List[V]] = []):
23+
self._items = items or []
24+
25+
def get(self, index: int) -> Optional[V]:
26+
if index < 0 or index >= len(self._items):
27+
return None
28+
return self._items[index]
29+
30+
async def async_get(self, index: int) -> Optional[V]:
31+
return await self.get(index)
32+
33+
def set(self, index: int, value: V) -> None:
34+
self._items[index] = value
35+
36+
async def async_set(self, index: int, value: V) -> None:
37+
return await self.set(index, value)
38+
39+
def delete(self, index: int) -> None:
40+
del self._items[index]
41+
42+
async def async_delete(self, index: int) -> None:
43+
return await self.delete(index)
44+
45+
def append(self, value: V) -> None:
46+
return self._items.append(value)
47+
48+
async def async_append(self, value: V) -> None:
49+
return await self.append(value)
50+
51+
def pop(self) -> Optional[V]:
52+
return self._items.pop()
53+
54+
async def async_pop(self) -> Optional[V]:
55+
return await self.pop()
56+
57+
def items(self) -> List[V]:
58+
return self._items
59+
60+
async def async_items(self) -> List[V]:
61+
return await self.items()
62+
63+
def length(self) -> int:
64+
return len(self._items)
65+
66+
async def async_length(self) -> int:
67+
return self.length()
68+
69+
def filter(self, predicate: Callable[[V, int], bool]) -> List[V]:
70+
return [item for i, item in enumerate(self._items) if predicate(item, i)]
71+
72+
async def async_filter(self, predicate: Callable[[V, int], bool]) -> List[V]:
73+
return await self.filter(predicate)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
7+
from __future__ import annotations
8+
9+
from typing import Dict, List, TypeVar, Optional
10+
from collections import OrderedDict
11+
12+
from pydantic import BaseModel, Field
13+
14+
from .storage import Storage
15+
16+
V = TypeVar('V')
17+
18+
19+
class LocalStorageOptions(BaseModel):
20+
max: Optional[int] = Field(default=None, frozen=True)
21+
22+
class LocalStorage(Storage[str, V]):
23+
"""A key-value storage with optional size limit and LRU behavior.
24+
25+
When max is set, implements LRU (Least Recently Used) behavior.
26+
"""
27+
28+
@property
29+
def store(self) -> OrderedDict[str, V]:
30+
return self._store
31+
32+
@property
33+
def options(self) -> LocalStorageOptions:
34+
return self._options
35+
36+
@property
37+
def keys(self) -> List[str]:
38+
return list(self._store.keys())
39+
40+
@property
41+
def size(self) -> int:
42+
return len(self._store)
43+
44+
def __init__(
45+
self,
46+
data: Optional[Dict[str, V]] = None,
47+
options: Optional[LocalStorageOptions] = None,
48+
):
49+
self._store = OrderedDict(data or {})
50+
self._options = options or LocalStorageOptions()
51+
52+
def get(self, key: str) -> Optional[V]:
53+
if key not in self._store:
54+
return None
55+
56+
value = self._store.pop(key)
57+
self._store[key] = value
58+
return value
59+
60+
async def async_get(self, key: str) -> Optional[V]:
61+
return await self.get(key)
62+
63+
def set(self, key: str, value: V) -> None:
64+
if key in self._store:
65+
del self._store[key]
66+
elif self._options.max and len(self._store) >= self._options.max:
67+
self._store.popitem(last=False)
68+
69+
self._store[key] = value
70+
71+
async def async_set(self, key: str, value: V) -> None:
72+
return await self.set(key, value)
73+
74+
75+
def delete(self, key: str) -> None:
76+
if key in self._store:
77+
del self._store[key]
78+
79+
async def async_delete(self, key: str) -> None:
80+
return await self.delete(key)

0 commit comments

Comments
 (0)