hishel 1.1.5__tar.gz → 1.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {hishel-1.1.5 → hishel-1.1.7}/.gitignore +2 -1
  2. {hishel-1.1.5 → hishel-1.1.7}/CHANGELOG.md +39 -0
  3. {hishel-1.1.5 → hishel-1.1.7}/PKG-INFO +40 -1
  4. {hishel-1.1.5 → hishel-1.1.7}/hishel/_async_cache.py +5 -5
  5. {hishel-1.1.5 → hishel-1.1.7}/hishel/_core/_spec.py +38 -1
  6. {hishel-1.1.5 → hishel-1.1.7}/hishel/_core/_storages/_async_sqlite.py +9 -9
  7. {hishel-1.1.5 → hishel-1.1.7}/hishel/_core/_storages/_sync_sqlite.py +9 -9
  8. {hishel-1.1.5 → hishel-1.1.7}/hishel/_sync_cache.py +5 -5
  9. {hishel-1.1.5 → hishel-1.1.7}/pyproject.toml +6 -6
  10. {hishel-1.1.5 → hishel-1.1.7}/LICENSE +0 -0
  11. {hishel-1.1.5 → hishel-1.1.7}/README.md +0 -0
  12. {hishel-1.1.5 → hishel-1.1.7}/hishel/__init__.py +0 -0
  13. {hishel-1.1.5 → hishel-1.1.7}/hishel/_async_httpx.py +0 -0
  14. {hishel-1.1.5 → hishel-1.1.7}/hishel/_core/_headers.py +0 -0
  15. {hishel-1.1.5 → hishel-1.1.7}/hishel/_core/_storages/_async_base.py +0 -0
  16. {hishel-1.1.5 → hishel-1.1.7}/hishel/_core/_storages/_packing.py +0 -0
  17. {hishel-1.1.5 → hishel-1.1.7}/hishel/_core/_storages/_sync_base.py +0 -0
  18. {hishel-1.1.5 → hishel-1.1.7}/hishel/_core/models.py +0 -0
  19. {hishel-1.1.5 → hishel-1.1.7}/hishel/_policies.py +0 -0
  20. {hishel-1.1.5 → hishel-1.1.7}/hishel/_sync_httpx.py +0 -0
  21. {hishel-1.1.5 → hishel-1.1.7}/hishel/_utils.py +0 -0
  22. {hishel-1.1.5 → hishel-1.1.7}/hishel/asgi.py +0 -0
  23. {hishel-1.1.5 → hishel-1.1.7}/hishel/fastapi.py +0 -0
  24. {hishel-1.1.5 → hishel-1.1.7}/hishel/httpx.py +0 -0
  25. {hishel-1.1.5 → hishel-1.1.7}/hishel/py.typed +0 -0
  26. {hishel-1.1.5 → hishel-1.1.7}/hishel/requests.py +0 -0
@@ -5,4 +5,5 @@ __pycache__/
5
5
  .cache/
6
6
  .idea/
7
7
  coverage.xml
8
- site
8
+ site
9
+ .DS_Store
@@ -1,3 +1,42 @@
1
+ ## What's Changed in 1.1.7
2
+ ### ♻️ Refactoring
3
+
4
+ * refactor(storage): create sqlite database path only when creating connections by @jeefberkey in [#426](https://github.com/karpetrosyan/hishel/pull/426)
5
+ ### ⚙️ Miscellaneous Tasks
6
+
7
+ * chore(deps-dev): bump the python-packages group with 5 updates by @dependabot[bot] in [#424](https://github.com/karpetrosyan/hishel/pull/424)
8
+ ### 🐛 Bug Fixes
9
+
10
+ * fix(cache): Lambda parameter name clashes the loop variable being closed over by @dump247 in [#427](https://github.com/karpetrosyan/hishel/pull/427)
11
+ ### 📚 Documentation
12
+
13
+ * add release process guidelines for maintainers by @karpetrosyan
14
+ ### 🚀 Features
15
+
16
+ * Feature/accept pathlib path in SqliteStorage by @daudef in [#419](https://github.com/karpetrosyan/hishel/pull/419)
17
+
18
+ ### Contributors
19
+ * @daudef
20
+ * @dependabot[bot]
21
+ * @jeefberkey
22
+ * @dump247
23
+ * @karpetrosyan
24
+
25
+ **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.6...1.1.7
26
+
27
+ ## What's Changed in 1.1.6
28
+ ### 📚 Documentation
29
+
30
+ * remove some stale httpx configs by @karpetrosyan
31
+ ### 🚀 Features
32
+
33
+ * Add support for request no-cache directive by @karpetrosyan in [#416](https://github.com/karpetrosyan/hishel/pull/416)
34
+
35
+ ### Contributors
36
+ * @karpetrosyan
37
+
38
+ **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.5...1.1.6
39
+
1
40
  ## What's Changed in 1.1.5
2
41
  ### 🐛 Bug Fixes
3
42
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hishel
3
- Version: 1.1.5
3
+ Version: 1.1.7
4
4
  Summary: Elegant HTTP Caching for Python
5
5
  Project-URL: Homepage, https://hishel.com
6
6
  Project-URL: Source, https://github.com/karpetrosyan/hishel
@@ -406,6 +406,45 @@ Hishel is inspired by and builds upon the excellent work in the Python HTTP ecos
406
406
  <strong>Made with ❤️ by <a href="https://github.com/karpetrosyan">Kar Petrosyan</a></strong>
407
407
  </p>
408
408
 
409
+ ## What's Changed in 1.1.7
410
+ ### ♻️ Refactoring
411
+
412
+ * refactor(storage): create sqlite database path only when creating connections by @jeefberkey in [#426](https://github.com/karpetrosyan/hishel/pull/426)
413
+ ### ⚙️ Miscellaneous Tasks
414
+
415
+ * chore(deps-dev): bump the python-packages group with 5 updates by @dependabot[bot] in [#424](https://github.com/karpetrosyan/hishel/pull/424)
416
+ ### 🐛 Bug Fixes
417
+
418
+ * fix(cache): Lambda parameter name clashes the loop variable being closed over by @dump247 in [#427](https://github.com/karpetrosyan/hishel/pull/427)
419
+ ### 📚 Documentation
420
+
421
+ * add release process guidelines for maintainers by @karpetrosyan
422
+ ### 🚀 Features
423
+
424
+ * Feature/accept pathlib path in SqliteStorage by @daudef in [#419](https://github.com/karpetrosyan/hishel/pull/419)
425
+
426
+ ### Contributors
427
+ * @daudef
428
+ * @dependabot[bot]
429
+ * @jeefberkey
430
+ * @dump247
431
+ * @karpetrosyan
432
+
433
+ **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.6...1.1.7
434
+
435
+ ## What's Changed in 1.1.6
436
+ ### 📚 Documentation
437
+
438
+ * remove some stale httpx configs by @karpetrosyan
439
+ ### 🚀 Features
440
+
441
+ * Add support for request no-cache directive by @karpetrosyan in [#416](https://github.com/karpetrosyan/hishel/pull/416)
442
+
443
+ ### Contributors
444
+ * @karpetrosyan
445
+
446
+ **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.5...1.1.6
447
+
409
448
  ## What's Changed in 1.1.5
410
449
  ### 🐛 Bug Fixes
411
450
 
@@ -197,12 +197,12 @@ class AsyncCacheProxy:
197
197
  return state.next(revalidation_response)
198
198
 
199
199
  async def _handle_update(self, state: NeedToBeUpdated) -> AnyState:
200
- for entry in state.updating_entries:
200
+ for updating_entry in state.updating_entries:
201
201
  await self.storage.update_entry(
202
- entry.id,
203
- lambda entry: replace(
204
- entry,
205
- response=replace(entry.response, headers=entry.response.headers),
202
+ updating_entry.id,
203
+ lambda existing_entry: replace(
204
+ existing_entry,
205
+ response=replace(existing_entry.response, headers=updating_entry.response.headers),
206
206
  ),
207
207
  )
208
208
  return state.next()
@@ -1207,6 +1207,7 @@ class IdleClient(State):
1207
1207
  - Unsafe methods (POST, PUT, DELETE, etc.) are written through to origin
1208
1208
  - Multiple matching responses are sorted by Date header (most recent first)
1209
1209
  - Age header is updated when serving from cache
1210
+ - Request no-cache directive forces revalidation of cached responses
1210
1211
 
1211
1212
  Examples:
1212
1213
  --------
@@ -1229,6 +1230,18 @@ class IdleClient(State):
1229
1230
  >>> next_state = idle.next(get_request, [cached_pair])
1230
1231
  >>> isinstance(next_state, NeedRevalidation)
1231
1232
  True
1233
+
1234
+ >>> # Need revalidation - request no-cache forces validation of fresh response
1235
+ >>> idle = IdleClient(options=default_options)
1236
+ >>> no_cache_request = Request(
1237
+ ... method="GET",
1238
+ ... url="https://example.com",
1239
+ ... headers=Headers({"cache-control": "no-cache"})
1240
+ ... )
1241
+ >>> cached_pair = CompletePair(no_cache_request, fresh_response)
1242
+ >>> next_state = idle.next(no_cache_request, [cached_pair])
1243
+ >>> isinstance(next_state, NeedRevalidation)
1244
+ True
1232
1245
  """
1233
1246
 
1234
1247
  # ============================================================================
@@ -1388,7 +1401,31 @@ class IdleClient(State):
1388
1401
  ready_to_use, need_revalidation = partition(filtered_pairs, fresh_or_allowed_stale)
1389
1402
 
1390
1403
  # ============================================================================
1391
- # STEP 7: Determine Next State Based on Available Responses
1404
+ # STEP 7: Handle Request no-cache Directive
1405
+ # ============================================================================
1406
+ # RFC 9111 Section 5.2.1.4: no-cache Request Directive
1407
+ # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.4
1408
+ #
1409
+ # "The no-cache request directive indicates that a cache MUST NOT use a
1410
+ # stored response to satisfy the request without successful validation on
1411
+ # the origin server."
1412
+ #
1413
+ # When a client sends Cache-Control: no-cache in the request, it's explicitly
1414
+ # requesting that the cache not use any stored response without first validating
1415
+ # it with the origin server. This is different from the response no-cache directive,
1416
+ # which applies to how responses should be cached.
1417
+ request_cache_control = parse_cache_control(request.headers.get("cache-control"))
1418
+
1419
+ if request_cache_control.no_cache is True:
1420
+ # Move all fresh responses to the revalidation queue
1421
+ # This ensures that even fresh cached responses will be validated
1422
+ # with the origin server via conditional requests (If-None-Match,
1423
+ # If-Modified-Since) before being served to the client.
1424
+ need_revalidation.extend(ready_to_use)
1425
+ ready_to_use = []
1426
+
1427
+ # ============================================================================
1428
+ # STEP 8: Determine Next State Based on Available Responses
1392
1429
  # ============================================================================
1393
1430
 
1394
1431
  if ready_to_use:
@@ -43,16 +43,12 @@ try:
43
43
  self,
44
44
  *,
45
45
  connection: Optional[anysqlite.Connection] = None,
46
- database_path: str = "hishel_cache.db",
46
+ database_path: Union[str, Path] = "hishel_cache.db",
47
47
  default_ttl: Optional[float] = None,
48
48
  refresh_ttl_on_access: bool = True,
49
49
  ) -> None:
50
- db_path = Path(database_path)
51
-
52
50
  self.connection = connection
53
- self.database_path = (
54
- ensure_cache_dict(db_path.parent if db_path.parent != Path(".") else None) / db_path.name
55
- )
51
+ self.database_path: Path = database_path if isinstance(database_path, Path) else Path(database_path)
56
52
  self.default_ttl = default_ttl
57
53
  self.refresh_ttl_on_access = refresh_ttl_on_access
58
54
  self.last_cleanup = time.time() - BATCH_CLEANUP_INTERVAL + BATCH_CLEANUP_START_DELAY
@@ -63,7 +59,10 @@ try:
63
59
  async def _ensure_connection(self) -> anysqlite.Connection:
64
60
  """Ensure connection is established and database is initialized."""
65
61
  if self.connection is None:
66
- self.connection = await anysqlite.connect(str(self.database_path))
62
+ # Create cache directory and resolve full path on first connection
63
+ parent = self.database_path.parent if self.database_path.parent != Path(".") else None
64
+ full_path = ensure_cache_dict(parent) / self.database_path.name
65
+ self.connection = await anysqlite.connect(str(full_path))
67
66
  if not self._initialized:
68
67
  await self._initialize_database()
69
68
  self._initialized = True
@@ -340,8 +339,8 @@ try:
340
339
  await connection.commit()
341
340
 
342
341
  async def _is_corrupted(self, pair: Entry, cursor: anysqlite.Cursor) -> bool:
343
- # if entry was created more than 1 hour ago and still has no response (incomplete)
344
- if pair.meta.created_at + 3600 < time.time() and not self._is_stream_complete(pair.id, cursor):
342
+ # if entry was created more than 1 hour ago and still has no full response data
343
+ if pair.meta.created_at + 3600 < time.time() and not (await self._is_stream_complete(pair.id, cursor)):
345
344
  return True
346
345
  return False
347
346
 
@@ -421,6 +420,7 @@ try:
421
420
  break
422
421
  yield chunk
423
422
  chunk_number += 1
423
+
424
424
  except ImportError:
425
425
 
426
426
  class AsyncSqliteStorage: # type: ignore[no-redef]
@@ -43,16 +43,12 @@ try:
43
43
  self,
44
44
  *,
45
45
  connection: Optional[sqlite3.Connection] = None,
46
- database_path: str = "hishel_cache.db",
46
+ database_path: Union[str, Path] = "hishel_cache.db",
47
47
  default_ttl: Optional[float] = None,
48
48
  refresh_ttl_on_access: bool = True,
49
49
  ) -> None:
50
- db_path = Path(database_path)
51
-
52
50
  self.connection = connection
53
- self.database_path = (
54
- ensure_cache_dict(db_path.parent if db_path.parent != Path(".") else None) / db_path.name
55
- )
51
+ self.database_path: Path = database_path if isinstance(database_path, Path) else Path(database_path)
56
52
  self.default_ttl = default_ttl
57
53
  self.refresh_ttl_on_access = refresh_ttl_on_access
58
54
  self.last_cleanup = time.time() - BATCH_CLEANUP_INTERVAL + BATCH_CLEANUP_START_DELAY
@@ -63,7 +59,10 @@ try:
63
59
  def _ensure_connection(self) -> sqlite3.Connection:
64
60
  """Ensure connection is established and database is initialized."""
65
61
  if self.connection is None:
66
- self.connection = sqlite3.connect(str(self.database_path))
62
+ # Create cache directory and resolve full path on first connection
63
+ parent = self.database_path.parent if self.database_path.parent != Path(".") else None
64
+ full_path = ensure_cache_dict(parent) / self.database_path.name
65
+ self.connection = sqlite3.connect(str(full_path))
67
66
  if not self._initialized:
68
67
  self._initialize_database()
69
68
  self._initialized = True
@@ -340,8 +339,8 @@ try:
340
339
  connection.commit()
341
340
 
342
341
  def _is_corrupted(self, pair: Entry, cursor: sqlite3.Cursor) -> bool:
343
- # if entry was created more than 1 hour ago and still has no response (incomplete)
344
- if pair.meta.created_at + 3600 < time.time() and not self._is_stream_complete(pair.id, cursor):
342
+ # if entry was created more than 1 hour ago and still has no full response data
343
+ if pair.meta.created_at + 3600 < time.time() and not (self._is_stream_complete(pair.id, cursor)):
345
344
  return True
346
345
  return False
347
346
 
@@ -421,6 +420,7 @@ try:
421
420
  break
422
421
  yield chunk
423
422
  chunk_number += 1
423
+
424
424
  except ImportError:
425
425
 
426
426
  class SyncSqliteStorage: # type: ignore[no-redef]
@@ -197,12 +197,12 @@ class SyncCacheProxy:
197
197
  return state.next(revalidation_response)
198
198
 
199
199
  def _handle_update(self, state: NeedToBeUpdated) -> AnyState:
200
- for entry in state.updating_entries:
200
+ for updating_entry in state.updating_entries:
201
201
  self.storage.update_entry(
202
- entry.id,
203
- lambda entry: replace(
204
- entry,
205
- response=replace(entry.response, headers=entry.response.headers),
202
+ updating_entry.id,
203
+ lambda existing_entry: replace(
204
+ existing_entry,
205
+ response=replace(existing_entry.response, headers=updating_entry.response.headers),
206
206
  ),
207
207
  )
208
208
  return state.next()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hishel"
7
- version = "1.1.5"
7
+ version = "1.1.7"
8
8
  dynamic = ["readme"]
9
9
  description = " Elegant HTTP Caching for Python"
10
10
  license = "BSD-3-Clause"
@@ -118,7 +118,7 @@ combine-as-imports = true
118
118
 
119
119
  [dependency-groups]
120
120
  dev = [
121
- "anyio==4.11.0",
121
+ "anyio==4.12.0",
122
122
  "anysqlite>=0.0.5",
123
123
  "coverage==7.10.7",
124
124
  "fastapi[standard]>=0.119.1",
@@ -128,16 +128,16 @@ dev = [
128
128
  "mkdocs==1.6.1",
129
129
  "mkdocs-git-committers-plugin>=0.2.3",
130
130
  "mkdocs-git-revision-date-localized-plugin>=1.4.7",
131
- "mkdocs-material==9.6.22",
132
- "mypy==1.18.2",
131
+ "mkdocs-material==9.7.0",
132
+ "mypy==1.19.0",
133
133
  "pyright>=1.1.404",
134
134
  "pytest==8.4.2",
135
135
  "pytest-cov>=6.2.1",
136
136
  "pytest-icdiff>=0.9",
137
- "ruff==0.14.3",
137
+ "ruff==0.14.7",
138
138
  "time-machine>=2.19.0",
139
139
  "trio==0.31.0",
140
- "types-boto3==1.40.64",
140
+ "types-boto3==1.41.5",
141
141
  "types-pyyaml==6.0.12.20250915",
142
142
  "types-requests>=2.31.0.6",
143
143
  "zipp>=3.19.1",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes