William Easton commited on
Commit
4a969a3
·
unverified ·
1 Parent(s): 6dcf299

Remove unused TimedCache (#1303)

Browse files
docs/python-sdk/fastmcp-utilities-cache.mdx DELETED
@@ -1,30 +0,0 @@
1
- ---
2
- title: cache
3
- sidebarTitle: cache
4
- ---
5
-
6
- # `fastmcp.utilities.cache`
7
-
8
- ## Classes
9
-
10
- ### `TimedCache` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cache.py#L7" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
11
-
12
- **Methods:**
13
-
14
- #### `set` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cache.py#L14" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
15
-
16
- ```python
17
- set(self, key: Any, value: Any) -> None
18
- ```
19
-
20
- #### `get` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cache.py#L18" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
21
-
22
- ```python
23
- get(self, key: Any) -> Any
24
- ```
25
-
26
- #### `clear` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cache.py#L25" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
27
-
28
- ```python
29
- clear(self) -> None
30
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/server/server.py CHANGED
@@ -2,7 +2,6 @@
2
 
3
  from __future__ import annotations
4
 
5
- import datetime
6
  import inspect
7
  import re
8
  import warnings
@@ -63,7 +62,6 @@ from fastmcp.settings import Settings
63
  from fastmcp.tools import ToolManager
64
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
65
  from fastmcp.tools.tool_transform import ToolTransformConfig
66
- from fastmcp.utilities.cache import TimedCache
67
  from fastmcp.utilities.cli import log_server_banner
68
  from fastmcp.utilities.components import FastMCPComponent
69
  from fastmcp.utilities.logging import get_logger
@@ -174,9 +172,6 @@ class FastMCP(Generic[LifespanResultT]):
174
  resource_prefix_format or fastmcp.settings.resource_prefix_format
175
  )
176
 
177
- self._cache = TimedCache(
178
- expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
179
- )
180
  self._additional_http_routes: list[BaseRoute] = []
181
  self._tool_manager = ToolManager(
182
  duplicate_behavior=on_duplicate_tools,
@@ -816,7 +811,6 @@ class FastMCP(Generic[LifespanResultT]):
816
  The tool instance that was added to the server.
817
  """
818
  self._tool_manager.add_tool(tool)
819
- self._cache.clear()
820
 
821
  # Send notification if we're in a request context
822
  try:
@@ -839,7 +833,6 @@ class FastMCP(Generic[LifespanResultT]):
839
  NotFoundError: If the tool is not found
840
  """
841
  self._tool_manager.remove_tool(name)
842
- self._cache.clear()
843
 
844
  # Send notification if we're in a request context
845
  try:
@@ -1033,7 +1026,6 @@ class FastMCP(Generic[LifespanResultT]):
1033
  The resource instance that was added to the server.
1034
  """
1035
  self._resource_manager.add_resource(resource)
1036
- self._cache.clear()
1037
 
1038
  # Send notification if we're in a request context
1039
  try:
@@ -1105,7 +1097,6 @@ class FastMCP(Generic[LifespanResultT]):
1105
  mime_type=mime_type,
1106
  tags=tags,
1107
  )
1108
- self._cache.clear()
1109
 
1110
  def resource(
1111
  self,
@@ -1254,7 +1245,6 @@ class FastMCP(Generic[LifespanResultT]):
1254
  The prompt instance that was added to the server.
1255
  """
1256
  self._prompt_manager.add_prompt(prompt)
1257
- self._cache.clear()
1258
 
1259
  # Send notification if we're in a request context
1260
  try:
@@ -1803,8 +1793,6 @@ class FastMCP(Generic[LifespanResultT]):
1803
  self._resource_manager.mount(mounted_server)
1804
  self._prompt_manager.mount(mounted_server)
1805
 
1806
- self._cache.clear()
1807
-
1808
  async def import_server(
1809
  self,
1810
  server: FastMCP[LifespanResultT],
@@ -1927,8 +1915,6 @@ class FastMCP(Generic[LifespanResultT]):
1927
  else:
1928
  logger.debug(f"Imported server {server.name}")
1929
 
1930
- self._cache.clear()
1931
-
1932
  @classmethod
1933
  def from_openapi(
1934
  cls,
 
2
 
3
  from __future__ import annotations
4
 
 
5
  import inspect
6
  import re
7
  import warnings
 
62
  from fastmcp.tools import ToolManager
63
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
64
  from fastmcp.tools.tool_transform import ToolTransformConfig
 
65
  from fastmcp.utilities.cli import log_server_banner
66
  from fastmcp.utilities.components import FastMCPComponent
67
  from fastmcp.utilities.logging import get_logger
 
172
  resource_prefix_format or fastmcp.settings.resource_prefix_format
173
  )
174
 
 
 
 
175
  self._additional_http_routes: list[BaseRoute] = []
176
  self._tool_manager = ToolManager(
177
  duplicate_behavior=on_duplicate_tools,
 
811
  The tool instance that was added to the server.
812
  """
813
  self._tool_manager.add_tool(tool)
 
814
 
815
  # Send notification if we're in a request context
816
  try:
 
833
  NotFoundError: If the tool is not found
834
  """
835
  self._tool_manager.remove_tool(name)
 
836
 
837
  # Send notification if we're in a request context
838
  try:
 
1026
  The resource instance that was added to the server.
1027
  """
1028
  self._resource_manager.add_resource(resource)
 
1029
 
1030
  # Send notification if we're in a request context
1031
  try:
 
1097
  mime_type=mime_type,
1098
  tags=tags,
1099
  )
 
1100
 
1101
  def resource(
1102
  self,
 
1245
  The prompt instance that was added to the server.
1246
  """
1247
  self._prompt_manager.add_prompt(prompt)
 
1248
 
1249
  # Send notification if we're in a request context
1250
  try:
 
1793
  self._resource_manager.mount(mounted_server)
1794
  self._prompt_manager.mount(mounted_server)
1795
 
 
 
1796
  async def import_server(
1797
  self,
1798
  server: FastMCP[LifespanResultT],
 
1915
  else:
1916
  logger.debug(f"Imported server {server.name}")
1917
 
 
 
1918
  @classmethod
1919
  def from_openapi(
1920
  cls,
src/fastmcp/utilities/cache.py DELETED
@@ -1,26 +0,0 @@
1
- import datetime
2
- from typing import Any
3
-
4
- UTC = datetime.timezone.utc
5
-
6
-
7
- class TimedCache:
8
- NOT_FOUND = object()
9
-
10
- def __init__(self, expiration: datetime.timedelta):
11
- self.expiration = expiration
12
- self.cache: dict[Any, tuple[Any, datetime.datetime]] = {}
13
-
14
- def set(self, key: Any, value: Any) -> None:
15
- expires = datetime.datetime.now(UTC) + self.expiration
16
- self.cache[key] = (value, expires)
17
-
18
- def get(self, key: Any) -> Any:
19
- value = self.cache.get(key)
20
- if value is not None and value[1] > datetime.datetime.now(UTC):
21
- return value[0]
22
- else:
23
- return self.NOT_FOUND
24
-
25
- def clear(self) -> None:
26
- self.cache.clear()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/server/test_mount.py CHANGED
@@ -629,7 +629,6 @@ class TestDynamicChanges:
629
 
630
  # The tool should no longer be accessible
631
  # Refresh the cache by clearing it
632
- main_app._cache.cache.clear()
633
  tools = await main_app.get_tools()
634
  assert "sub_temp_tool" not in tools
635
 
 
629
 
630
  # The tool should no longer be accessible
631
  # Refresh the cache by clearing it
 
632
  tools = await main_app.get_tools()
633
  assert "sub_temp_tool" not in tools
634
 
tests/utilities/test_cache.py DELETED
@@ -1,233 +0,0 @@
1
- """Tests for the cache.py module."""
2
-
3
- import datetime
4
- import time
5
- from unittest.mock import patch
6
-
7
- from fastmcp.utilities.cache import TimedCache
8
-
9
-
10
- class TestTimedCache:
11
- """Tests for the TimedCache class."""
12
-
13
- def test_init(self):
14
- """Test that a TimedCache can be initialized with an expiration."""
15
- expiration = datetime.timedelta(seconds=10)
16
- cache = TimedCache(expiration)
17
- assert cache.expiration == expiration
18
- assert isinstance(cache.cache, dict)
19
- assert len(cache.cache) == 0
20
-
21
- def test_set(self):
22
- """Test that values can be set in the cache."""
23
- cache = TimedCache(datetime.timedelta(seconds=10))
24
- key, value = "test_key", "test_value"
25
-
26
- with patch("datetime.datetime") as mock_datetime:
27
- now = datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc)
28
- mock_datetime.now.return_value = now
29
-
30
- cache.set(key, value)
31
-
32
- # Check that the value is stored with the correct expiration
33
- assert key in cache.cache
34
- stored_value, expiration = cache.cache[key]
35
- assert stored_value == value
36
- assert expiration == now + datetime.timedelta(seconds=10)
37
-
38
- def test_get_found(self):
39
- """Test retrieving a value that exists and has not expired."""
40
- cache = TimedCache(datetime.timedelta(seconds=10))
41
- key, value = "test_key", "test_value"
42
-
43
- # Set a future expiration time
44
- future = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
45
- seconds=30
46
- )
47
- cache.cache[key] = (value, future)
48
-
49
- # The value should be returned
50
- assert cache.get(key) == value
51
-
52
- def test_get_expired(self):
53
- """Test retrieving a value that exists but has expired."""
54
- cache = TimedCache(datetime.timedelta(seconds=10))
55
- key, value = "test_key", "test_value"
56
-
57
- # Set a past expiration time
58
- past = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
59
- seconds=1
60
- )
61
- cache.cache[key] = (value, past)
62
-
63
- # Should return NOT_FOUND
64
- assert cache.get(key) is TimedCache.NOT_FOUND
65
-
66
- def test_get_not_found(self):
67
- """Test retrieving a value that doesn't exist in the cache."""
68
- cache = TimedCache(datetime.timedelta(seconds=10))
69
-
70
- # Key doesn't exist
71
- assert cache.get("nonexistent_key") is TimedCache.NOT_FOUND
72
-
73
- def test_clear(self):
74
- """Test that the cache can be cleared."""
75
- cache = TimedCache(datetime.timedelta(seconds=10))
76
-
77
- # Add some items
78
- cache.set("key1", "value1")
79
- cache.set("key2", "value2")
80
- assert len(cache.cache) == 2
81
-
82
- # Clear the cache
83
- cache.clear()
84
- assert len(cache.cache) == 0
85
-
86
- def test_real_expiration(self):
87
- """Test that values actually expire after the specified time."""
88
- # Use a very short expiration for the test
89
- cache = TimedCache(datetime.timedelta(milliseconds=50))
90
- key, value = "test_key", "test_value"
91
-
92
- cache.set(key, value)
93
- # Value should be available immediately
94
- assert cache.get(key) == value
95
-
96
- # Wait for expiration
97
- time.sleep(0.06) # 60 milliseconds, slightly longer than expiration
98
-
99
- # Value should now be expired
100
- assert cache.get(key) is TimedCache.NOT_FOUND
101
-
102
- def test_overwrite_value(self):
103
- """Test that setting a key that already exists overwrites the old value."""
104
- cache = TimedCache(datetime.timedelta(seconds=10))
105
- key = "test_key"
106
-
107
- # Set initial value
108
- cache.set(key, "initial_value")
109
- assert cache.get(key) == "initial_value"
110
-
111
- # Overwrite with new value
112
- cache.set(key, "new_value")
113
- assert cache.get(key) == "new_value"
114
-
115
- def test_extends_expiration_on_overwrite(self):
116
- """Test that overwriting a key extends its expiration time."""
117
- cache = TimedCache(datetime.timedelta(seconds=10))
118
- key = "test_key"
119
-
120
- with patch("datetime.datetime") as mock_datetime:
121
- # Set initial value at t=0
122
- initial_time = datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc)
123
- mock_datetime.now.return_value = initial_time
124
- cache.set(key, "initial_value")
125
-
126
- initial_expiration = cache.cache[key][1]
127
- assert initial_expiration == initial_time + datetime.timedelta(seconds=10)
128
-
129
- # Overwrite at t=5
130
- later_time = initial_time + datetime.timedelta(seconds=5)
131
- mock_datetime.now.return_value = later_time
132
- cache.set(key, "new_value")
133
-
134
- # Expiration should be extended
135
- new_expiration = cache.cache[key][1]
136
- assert new_expiration == later_time + datetime.timedelta(seconds=10)
137
-
138
- def test_different_key_types(self):
139
- """Test that different types of keys can be used."""
140
- cache = TimedCache(datetime.timedelta(seconds=10))
141
-
142
- # Test various key types
143
- keys_and_values = [
144
- (42, "int_value"),
145
- (3.14, "float_value"),
146
- ((1, 2), "tuple_value"),
147
- (frozenset({1, 2, 3}), "frozenset_value"),
148
- ]
149
-
150
- for key, value in keys_and_values:
151
- cache.set(key, value)
152
- assert cache.get(key) == value
153
-
154
- def test_none_value(self):
155
- """Test that None can be stored as a value."""
156
- cache = TimedCache(datetime.timedelta(seconds=10))
157
- key = "none_key"
158
-
159
- cache.set(key, None)
160
- # The stored value is None, but get() should return None, not NOT_FOUND
161
- assert cache.get(key) is None
162
-
163
- def test_edge_case_zero_expiration(self):
164
- """Test with a zero expiration time."""
165
- cache = TimedCache(datetime.timedelta(seconds=0))
166
- key, value = "test_key", "test_value"
167
-
168
- cache.set(key, value)
169
- # The value might already be expired by the time we call get()
170
- # We can't make strong assertions here due to timing variability
171
- retrieved = cache.get(key)
172
- assert retrieved in (value, TimedCache.NOT_FOUND)
173
-
174
- def test_negative_expiration(self):
175
- """Test with a negative expiration time."""
176
- cache = TimedCache(datetime.timedelta(seconds=-1))
177
- key, value = "test_key", "test_value"
178
-
179
- cache.set(key, value)
180
- # Value should be immediately expired
181
- assert cache.get(key) is TimedCache.NOT_FOUND
182
-
183
- def test_cache_consistency(self):
184
- """Test cache consistency with multiple operations."""
185
- cache = TimedCache(datetime.timedelta(seconds=10))
186
-
187
- # Add multiple items
188
- cache.set("key1", "value1")
189
- cache.set("key2", "value2")
190
- cache.set("key3", "value3")
191
-
192
- # Check all items
193
- assert cache.get("key1") == "value1"
194
- assert cache.get("key2") == "value2"
195
- assert cache.get("key3") == "value3"
196
-
197
- # Overwrite one item
198
- cache.set("key2", "updated_value")
199
-
200
- # Check again
201
- assert cache.get("key1") == "value1"
202
- assert cache.get("key2") == "updated_value"
203
- assert cache.get("key3") == "value3"
204
-
205
- # Clear and verify all items are gone
206
- cache.clear()
207
- assert cache.get("key1") is TimedCache.NOT_FOUND
208
- assert cache.get("key2") is TimedCache.NOT_FOUND
209
- assert cache.get("key3") is TimedCache.NOT_FOUND
210
-
211
- def test_large_expiration(self):
212
- """Test with a very large expiration time."""
213
- # One year expiration
214
- cache = TimedCache(datetime.timedelta(days=365))
215
- key, value = "test_key", "test_value"
216
-
217
- cache.set(key, value)
218
- assert cache.get(key) == value
219
-
220
- def test_many_items(self):
221
- """Test cache with many items."""
222
- cache = TimedCache(datetime.timedelta(seconds=10))
223
-
224
- # Add 1000 items
225
- for i in range(1000):
226
- cache.set(f"key{i}", f"value{i}")
227
-
228
- # Check size
229
- assert len(cache.cache) == 1000
230
-
231
- # Check some random items
232
- for i in [0, 123, 456, 789, 999]:
233
- assert cache.get(f"key{i}") == f"value{i}"