Skip to content
This repository has been archived by the owner on Oct 3, 2023. It is now read-only.

Commit

Permalink
add support for upath
Browse files Browse the repository at this point in the history
  • Loading branch information
obendidi committed Sep 13, 2023
1 parent adbbeda commit e4bb95a
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 21 deletions.
43 changes: 40 additions & 3 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ For caching, `httpx_cache.Client` adds 3 new key-args to the table:
- `cacheable_status_codes`: tuple of int http status codes that supports caching (if response does not have one of these status codes, it will not be cached), defaults to: `(200, 203, 300, 301, 308)`
- `always_cache`: bool, if True, all **valid** responses will be cached, regardless of the `no-store` directive set in either the request or response, defaults to False.

_Note: When using the `httpx_cache` client or transport, a new property will be added to the response to specify whether it comes from cache or not: `response.from_cache: bool`_

Example usage:

```py
Expand All @@ -31,6 +33,9 @@ import httpx_cache
with httpx_cache.Client() as client:
response1 = client.get("https://httpbin.org/get") # will be cached
response2 = client.get("https://httpbin.org/get") # will get it from cache

assert response1.from_cache is False
assert response2.from_cache is True
```

### AsyncClient
Expand All @@ -43,6 +48,9 @@ import httpx_cache
async with httpx_cache.AsyncClient() as client:
response1 = await client.get("https://httpbin.org/get") # will be cached
response2 = await client.get("https://httpbin.org/get") # will get it from cache

assert response1.from_cache is False
assert response2.from_cache is True
```

### Response Stream
Expand Down Expand Up @@ -123,6 +131,8 @@ The `(Async-)CacheControlTransport` also accepts the 3 key-args:
- `cacheable_methods`: tuple of str http methods that support caching (if a request does not use one of these methods, it's corresponding response will not be cached), defaults to `('GET',)`
- `cacheable_status_codes`: tuple of int http status codes that supports caching (if response does not have one of these status codes, it will not be cached), defaults to: `(200, 203, 300, 301, 308)`

_Note: When using the `httpx_cache` client or transport, a new property will be added to the response to specify whether it comes from cache or not: `response.from_cache: bool`_

```py
import httpx
import httpx_cache
Expand Down Expand Up @@ -168,6 +178,33 @@ with httpx_cache.Client(cache=httpx_cache.FileCache(cache_dir="./my-custom-dir")
response = client.get("https://httpbin.org/get")
```

#### fsspec/universal_pathlib integration

Filecache also works out of the box with [fsspec/universal_pathlib](https://github.com/fsspec/universal_pathlib) so that you can use any filesystem supported by fsspec as a cachedir. Please check the [fsspec/universal_pathlib](https://github.com/fsspec/universal_pathlib) docs for the list of supported filesystems (and schemes)

Example with an s3 filesystem:

(don't forget to also install the `s3fs` package to use this backend: `pip install universal_pathlib s3fs`)


```py
import httpx_cache
from upath import UPath

cache_dir = UPath("s3://my-bucket/httpx-cache")
cache = httpx_cache.FileCache(cache_dir=cache_dir)

with httpx_cache.Client(cache=cache) as client:
response = client.get("https://httpbin.org/get")

# OR async client
# async with httpx_cache.AsyncClient(cache=cache) as client:
# response = await client.get("https://httpbin.org/get")

# should contain one file, with the cached response
print([f for f in cache_dir.iterdir()])
```

### RedisCache

You need to install `redis` package to use this cache type, or install `httpx-cache[redis]` to install it automatically.
Expand Down Expand Up @@ -202,10 +239,10 @@ with httpx_cache.Client(cache=cache) as client:

Before caching an httpx.Response it needs to be serialized to a cacheable format supported by the used cache type (Dict/File).

| Serializer | DictCache | FileCache | RedisCache |
| Serializer | DictCache | FileCache | RedisCache |
| -------------------- | ------------------ | ------------------ | ------------------ |
| DictSerializer | :white_check_mark: | :x: | :x: |
| StringJsonSerializer | :white_check_mark: | :x: | :x: |
| DictSerializer | :white_check_mark: | :x: | :x: |
| StringJsonSerializer | :white_check_mark: | :x: | :x: |
| BytesJsonSerializer | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| MsgPackSerializer | :white_check_mark: | :white_check_mark: | :white_check_mark: |

Expand Down
26 changes: 12 additions & 14 deletions httpx_cache/cache/file.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import typing as tp
from pathlib import Path

import anyio
from anyio import to_thread
from fasteners import ReaderWriterLock as RWLock
from aiorwlock import RWLock as AsyncRWLock
import httpx
Expand Down Expand Up @@ -65,13 +65,15 @@ def get(self, request: httpx.Request) -> tp.Optional[httpx.Response]:
return None

async def aget(self, request: httpx.Request) -> tp.Optional[httpx.Response]:
filepath = anyio.Path(
get_cache_filepath(self.cache_dir, request, extra=self._extra)
)
filepath = get_cache_filepath(self.cache_dir, request, extra=self._extra)

async with self.async_lock.reader:
if await filepath.is_file():
if await to_thread.run_sync(filepath.is_file, cancellable=True):
try:
cached = await filepath.read_bytes()
cached = await to_thread.run_sync(
filepath.read_bytes,
cancellable=True,
)
return self.serializer.loads(request=request, cached=cached)
except Exception:
return None
Expand All @@ -96,13 +98,11 @@ async def aset(
response: httpx.Response,
content: tp.Optional[bytes] = None,
) -> None:
filepath = anyio.Path(
get_cache_filepath(self.cache_dir, request, extra=self._extra)
)
filepath = get_cache_filepath(self.cache_dir, request, extra=self._extra)
async with self.async_lock.writer:
to_cache = self.serializer.dumps(response=response, content=content)
try:
await filepath.write_bytes(to_cache)
await to_thread.run_sync(filepath.write_bytes, to_cache)
except Exception:
return None

Expand All @@ -113,8 +113,6 @@ def delete(self, request: httpx.Request) -> None:
filepath.unlink()

async def adelete(self, request: httpx.Request) -> None:
filepath = anyio.Path(
get_cache_filepath(self.cache_dir, request, extra=self._extra)
)
filepath = get_cache_filepath(self.cache_dir, request, extra=self._extra)
async with self.async_lock.writer:
await filepath.unlink(missing_ok=True)
await to_thread.run_sync(filepath.unlink, True)
7 changes: 3 additions & 4 deletions tests/cache/test_cache_file.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from pathlib import Path

import anyio
import httpx
import mock
import pytest
Expand Down Expand Up @@ -62,14 +61,14 @@ def test_file_cache_get_not_found(


@mock.patch.object(Path, "mkdir", new=lambda *args, **kwargs: None)
@mock.patch.object(anyio.Path, "is_file", return_value=False)
@mock.patch.object(Path, "is_file", return_value=False)
async def test_file_cache_aget_not_found(
mock_is_file: mock.AsyncMock,
mock_is_file: mock.Mock,
file_cache: httpx_cache.FileCache,
httpx_request: httpx.Request,
):
cached = await file_cache.aget(httpx_request)
mock_is_file.assert_awaited_once_with()
mock_is_file.assert_called_once_with()
assert cached is None


Expand Down

0 comments on commit e4bb95a

Please sign in to comment.