Repository: PrefectHQ/prefect

Issue:
Create a Redis filesystem Block
The results system and tasks submitted outside of flows use filesystem Blocks to persist data. Redis is an ideal data store for these purposes due to its speed, especially when running tasks outside of flows, AKA "background tasks" in many web applications. Currently, no Redis filesystem Block exists, so we should create one.

**Evaluate existing Redis block:**
A possibly usable [Redis filesystem block](https://github.com/PrefectHQ/prefect-redis/blob/master/prefect_redis/credentials.py) exists. We should evaluate this and see if it works! Then add a named alias if it does, such as `RedisFilesystem`. If it doesn't work, let's fix it and then alias.

Related: https://github.com/PrefectHQ/prefect/issues/5841


Discussion:

Resolving patch:
diff --git a/src/integrations/prefect-redis/prefect_redis/__init__.py b/src/integrations/prefect-redis/prefect_redis/__init__.py
--- a/src/integrations/prefect-redis/prefect_redis/__init__.py
+++ b/src/integrations/prefect-redis/prefect_redis/__init__.py
@@ -1,6 +1,6 @@
-from .filesystem import RedisFilesystem
+from .filesystem import RedisStorageContainer
 
 
 __all__ = [
-    "RedisFilesystem",
+    "RedisStorageContainer",
 ]
diff --git a/src/integrations/prefect-redis/prefect_redis/filesystem.py b/src/integrations/prefect-redis/prefect_redis/filesystem.py
--- a/src/integrations/prefect-redis/prefect_redis/filesystem.py
+++ b/src/integrations/prefect-redis/prefect_redis/filesystem.py
@@ -1,4 +1,5 @@
 from contextlib import asynccontextmanager
+from pathlib import Path
 from typing import AsyncGenerator, Optional, Union
 
 import redis.asyncio as redis
@@ -7,9 +8,10 @@
 from typing_extensions import Self
 
 from prefect.filesystems import WritableFileSystem
+from prefect.utilities.asyncutils import sync_compatible
 
 
-class RedisFilesystem(WritableFileSystem):
+class RedisStorageContainer(WritableFileSystem):
     """
     Block used to interact with Redis as a filesystem
 
@@ -24,17 +26,17 @@ class RedisFilesystem(WritableFileSystem):
     Example:
         Create a new block from hostname, username and password:
         ```python
-        from prefect_redis import RedisFilesystem
+        from prefect_redis import RedisStorageContainer
 
-        block = RedisFilesystem.from_host(
+        block = RedisStorageContainer.from_host(
             host="myredishost.com", username="redis", password="SuperSecret")
         block.save("BLOCK_NAME")
         ```
 
         Create a new block from a connection string
         ```python
-        from prefect_redis import RedisFilesystem
-        block = RedisFilesystem.from_url(""redis://redis:SuperSecret@myredishost.com:6379")
+        from prefect_redis import RedisStorageContainer
+        block = RedisStorageContainer.from_url(""redis://redis:SuperSecret@myredishost.com:6379")
         block.save("BLOCK_NAME")
         ```
     """
@@ -60,8 +62,9 @@ def block_initialization(self) -> None:
                 "Initialization error: 'username' is provided, but 'password' is missing. Both are required."
             )
 
-    async def read_path(self, path: str):
-        """Read a redis key
+    @sync_compatible
+    async def read_path(self, path: Union[Path, str]):
+        """Read the redis content at `path`
 
         Args:
             path: Redis key to read from
@@ -70,10 +73,11 @@ async def read_path(self, path: str):
             Contents at key as bytes
         """
         async with self._client() as client:
-            return await client.get(path)
+            return await client.get(str(path))
 
-    async def write_path(self, path: str, content: bytes):
-        """Write to a redis key
+    @sync_compatible
+    async def write_path(self, path: Union[Path, str], content: bytes):
+        """Write `content` to the redis at `path`
 
         Args:
             path: Redis key to write to
@@ -81,7 +85,7 @@ async def write_path(self, path: str, content: bytes):
         """
 
         async with self._client() as client:
-            return await client.set(path, content)
+            return await client.set(str(path), content)
 
     @asynccontextmanager
     async def _client(self) -> AsyncGenerator[redis.Redis, None]:
@@ -120,7 +124,7 @@ def from_host(
             port: Redis port
 
         Returns:
-            `RedisFilesystem` instance
+            `RedisStorageContainer` instance
         """
 
         username = SecretStr(username) if isinstance(username, str) else username
@@ -145,7 +149,7 @@ def from_connection_string(cls, connection_string: Union[str, SecretStr]) -> Sel
             connection_string: Redis connection string
 
         Returns:
-            `RedisFilesystem` instance
+            `RedisStorageContainer` instance
         """
 
         connection_string = (


Test patch:
diff --git a/src/integrations/prefect-redis/tests/test_filesystem.py b/src/integrations/prefect-redis/tests/test_filesystem.py
--- a/src/integrations/prefect-redis/tests/test_filesystem.py
+++ b/src/integrations/prefect-redis/tests/test_filesystem.py
@@ -1,8 +1,9 @@
 import os
+from pathlib import Path
 from typing import Union
 
 import pytest
-from prefect_redis.filesystem import RedisFilesystem
+from prefect_redis.filesystem import RedisStorageContainer
 from pydantic.types import SecretStr
 
 
@@ -18,20 +19,20 @@ async def redis_config() -> dict[str, Union[str, int, None]]:
 
 
 @pytest.fixture
-async def redis_from_host(redis_config: dict) -> RedisFilesystem:
-    return RedisFilesystem.from_host(**redis_config)
+async def redis_from_host(redis_config: dict) -> RedisStorageContainer:
+    return RedisStorageContainer.from_host(**redis_config)
 
 
 @pytest.fixture
-async def redis_from_connection_string(redis_config: dict) -> RedisFilesystem:
+async def redis_from_connection_string(redis_config: dict) -> RedisStorageContainer:
     connection_string = (
         f"redis://{redis_config['host']}:{redis_config['port']}/{redis_config['db']}"
     )
-    return RedisFilesystem.from_connection_string(connection_string)
+    return RedisStorageContainer.from_connection_string(connection_string)
 
 
 async def test_initialize_from_host(redis_config: dict):
-    redis = RedisFilesystem.from_host(**redis_config)
+    redis = RedisStorageContainer.from_host(**redis_config)
 
     assert redis.host == redis_config["host"]
     assert redis.port == redis_config["port"]
@@ -42,25 +43,31 @@ async def test_initialize_from_host(redis_config: dict):
 
 async def test_initialize_without_host_fails():
     with pytest.raises(ValueError, match="'host' is required"):
-        RedisFilesystem(host=None)
+        RedisStorageContainer(host=None)
 
 
 async def test_initialize_with_username_but_no_password_fails():
     with pytest.raises(
         ValueError, match="'username' is provided, but 'password' is missing"
     ):
-        RedisFilesystem(
+        RedisStorageContainer(
             host="localhost", username=SecretStr("test_user"), password=None
         )
 
 
-async def test_read_write_path_from_host(redis_from_host: RedisFilesystem):
+async def test_read_write_path_from_host(redis_from_host: RedisStorageContainer):
     await redis_from_host.write_path("test_key", b"test_value")
     assert await redis_from_host.read_path("test_key") == b"test_value"
 
 
 async def test_read_write_path_from_connection_string(
-    redis_from_connection_string: RedisFilesystem,
+    redis_from_connection_string: RedisStorageContainer,
 ):
     await redis_from_connection_string.write_path("test_key", b"test_value")
     assert await redis_from_connection_string.read_path("test_key") == b"test_value"
+
+
+async def test_read_write_path_pathlib(redis_from_host: RedisStorageContainer):
+    path = Path("hello/world.txt")
+    await redis_from_host.write_path(path, b"Hi there!")
+    assert await redis_from_host.read_path(path) == b"Hi there!"