Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-hishel for openSUSE:Factory checked in at 2026-06-28 21:07:17 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-hishel (Old) and /work/SRC/openSUSE:Factory/.python-hishel.new.11887 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-hishel" Sun Jun 28 21:07:17 2026 rev:12 rq:1362047 version:1.3.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-hishel/python-hishel.changes 2026-05-04 12:58:22.929493368 +0200 +++ /work/SRC/openSUSE:Factory/.python-hishel.new.11887/python-hishel.changes 2026-06-28 21:07:57.396950012 +0200 @@ -1,0 +2,8 @@ +Sat Jun 27 20:59:34 UTC 2026 - Dirk Müller <[email protected]> + +- update to 1.3.0: + * remove egg folder from source by @karpetrosyan + * add readme in pyproject.toml by @karpetrosyan + * use weak ETag comparison when freshening stored responses + +------------------------------------------------------------------- Old: ---- hishel-1.2.1.tar.gz New: ---- hishel-1.3.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-hishel.spec ++++++ --- /var/tmp/diff_new_pack.ATaVOT/_old 2026-06-28 21:07:58.380983169 +0200 +++ /var/tmp/diff_new_pack.ATaVOT/_new 2026-06-28 21:07:58.380983169 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-hishel -Version: 1.2.1 +Version: 1.3.0 Release: 0 Summary: Persistent cache implementation for popular HTTP clients License: BSD-3-Clause ++++++ hishel-1.2.1.tar.gz -> hishel-1.3.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/CHANGELOG.md new/hishel-1.3.0/CHANGELOG.md --- old/hishel-1.2.1/CHANGELOG.md 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/CHANGELOG.md 2026-06-11 10:39:31.000000000 +0200 @@ -1,3 +1,17 @@ +## What's Changed in 1.3.0 +### ⚙️ Miscellaneous Tasks + +* remove egg folder from source by @karpetrosyan +* add readme in pyproject.toml by @karpetrosyan +### 🐛 Bug Fixes + +* use weak ETag comparison when freshening stored responses by @karpetrosyan + +### Contributors +* @karpetrosyan + +**Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.2.1...1.3.0 + ## What's Changed in 1.2.1 ### ⚙️ Miscellaneous Tasks diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/docs/index.md new/hishel-1.3.0/docs/index.md --- old/hishel-1.2.1/docs/index.md 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/docs/index.md 2026-06-11 10:39:31.000000000 +0200 @@ -4,7 +4,7 @@ hero: name: "Hishel" text: An elegant HTTP caching library for Python - tagline: Clean HTTP semantics, bring your own transport + tagline: "HTTP caching for Python — RFC-compliant, transport-agnostic, streaming-friendly" actions: - theme: brand text: Get Started diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/pyproject.toml new/hishel-1.3.0/pyproject.toml --- old/hishel-1.2.1/pyproject.toml 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/pyproject.toml 2026-06-11 10:39:31.000000000 +0200 @@ -4,8 +4,9 @@ [project] name = "hishel" -version = "1.2.1" +version = "1.3.0" description = " Elegant HTTP Caching for Python" +readme = "README.md" requires-python = ">=3.10" authors = [{ name = "Kar Petrosyan", email = "[email protected]" }] classifiers = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/src/hishel/_core/_spec.py new/hishel-1.3.0/src/hishel/_core/_spec.py --- old/hishel-1.2.1/src/hishel/_core/_spec.py 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/src/hishel/_core/_spec.py 2026-06-11 10:39:31.000000000 +0200 @@ -1839,9 +1839,26 @@ after_revalidation=True, ).next(revalidation_response) + def _opaque_tag(self, etag: str) -> str: + """ + Return the opaque-tag portion of an entity-tag, dropping the weakness flag. + + RFC 9110 Section 8.8.3: + entity-tag = [ weak ] opaque-tag + weak = %s"W/" + + Weak comparison (RFC 9110 Section 8.8.3.2): two entity-tags are equivalent + if their opaque-tags match character-by-character, regardless of either or + both being tagged as "weak". This is the comparison If-None-Match uses. + """ + etag = etag.strip() + if etag.startswith(("W/", "w/")): + return etag[2:] + return etag + def freshening_stored_responses( self, revalidation_response: Response - ) -> "NeedToBeUpdated" | "InvalidateEntries" | "CacheMiss": + ) -> "NeedToBeUpdated | InvalidateEntries | CacheMiss": """ Freshens cached responses after receiving a 304 Not Modified response. @@ -1852,82 +1869,25 @@ 3. Invalidates any cached responses that don't match Matching is done using validators in this priority order: - 1. Strong ETag (if present and not weak) + 1. ETag (weak comparison, see note below) 2. Last-Modified (if present) 3. Single response assumption (if only one cached response exists) - Parameters: - ---------- - revalidation_response : Response - The 304 Not Modified response from the server, containing updated - metadata (Date, Cache-Control, ETag, etc.) - - Returns: - ------- - Union[NeedToBeUpdated, InvalidateEntries, CacheMiss] - - NeedToBeUpdated: When matching responses are found and updated - - InvalidateEntries: Wraps NeedToBeUpdated if non-matching responses exist - - CacheMiss: When no matching responses are found - RFC 9111 Compliance: ------------------- - From RFC 9111 Section 4.3.4: - "When a cache receives a 304 (Not Modified) response, it needs to identify - stored responses that are suitable for updating with the new information - provided, and then do so. - - The initial set of stored responses to update are those that could have - been chosen for that request... - - Then, that initial set of stored responses is further filtered by the - first match of: - - If the 304 response contains a strong entity tag: the stored responses - with the same strong entity tag. - - If the 304 response contains a Last-Modified value: the stored responses - with the same Last-Modified value. - - If there is only a single stored response: that response." - - Implementation Notes: - -------------------- - - Weak ETags (starting with "W/") are not used for matching - - Only strong ETags provide reliable validation - - If no validators match, all responses are invalidated - - Multiple responses can be freshened if they share the same validator - - Examples: - -------- - >>> # Matching by strong ETag - >>> cached_response = Response(headers=Headers({"etag": '"abc123"'})) - >>> revalidation_response = Response( - ... status_code=304, - ... headers=Headers({"etag": '"abc123"', "cache-control": "max-age=3600"}) - ... ) - >>> # Cached response will be freshened with new Cache-Control - - >>> # Non-matching ETag - >>> cached_response = Response(headers=Headers({"etag": '"old123"'})) - >>> revalidation_response = Response( - ... status_code=304, - ... headers=Headers({"etag": '"new456"'}) - ... ) - >>> # Cached response will be invalidated (doesn't match) + RFC 9111 Section 4.3.4 filters the stored responses by the first match of: + - 304 contains a strong entity tag -> stored responses with the same + strong entity tag + - 304 contains a Last-Modified value -> stored responses with the same + Last-Modified value + - only a single stored response -> that response """ # ============================================================================ # STEP 1: Identify Matching Responses Using Validators # ============================================================================ - # RFC 9111 Section 4.3.4: Freshening Stored Responses - # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.4 - # - # The 304 response tells us "the resource is unchanged", but we need to - # figure out WHICH of our cached responses match this confirmation. - # - # We use validators in priority order: - # Priority 1: Strong ETag (most reliable) - # Priority 2: Last-Modified timestamp - # Priority 3: Single response assumption - identified_for_revalidation: list[Entry] + need_to_be_invalidated: list[Entry] # MATCHING STRATEGY 1: Strong ETag # RFC 9110 Section 8.8.3: ETag @@ -1946,24 +1906,21 @@ if "etag" in revalidation_response.headers and (not revalidation_response.headers["etag"].startswith("W/")): # Found a strong ETag in the 304 response # Partition cached responses: matching vs non-matching ETags + # Use opaque-tag comparison so a stored weak ETag (W/"abc") matches + # a strong 304 ETag ("abc") with the same opaque-tag value. identified_for_revalidation, need_to_be_invalidated = partition( self.revalidating_entries, - lambda pair: pair.response.headers.get("etag") == revalidation_response.headers.get("etag"), # type: ignore[no-untyped-call] + lambda pair: ( + (stored_etag := pair.response.headers.get("etag")) is not None + and self._opaque_tag(stored_etag) == revalidation_response.headers["etag"] + ), # type: ignore[no-untyped-call] ) # MATCHING STRATEGY 2: Last-Modified - # RFC 9110 Section 8.8.2: Last-Modified - # https://www.rfc-editor.org/rfc/rfc9110#section-8.8.2 # - # "If the 304 response contains a Last-Modified value: the stored responses - # with the same Last-Modified value." - # - # Last-Modified is a timestamp indicating when the resource was last changed. - # It's less precise than ETags (1-second granularity) but widely supported. - # If the 304 has a Last-Modified, we can match it against cached responses. + # RFC 9111 Section 4.3.4: "If the 304 response contains a Last-Modified + # value: the stored responses with the same Last-Modified value." elif revalidation_response.headers.get("last-modified"): - # Found Last-Modified in the 304 response - # Partition cached responses: matching vs non-matching timestamps identified_for_revalidation, need_to_be_invalidated = partition( self.revalidating_entries, lambda pair: ( @@ -1972,24 +1929,18 @@ ) # MATCHING STRATEGY 3: Single Response Assumption - # RFC 9111 Section 4.3.4: - # - # "If there is only a single stored response: that response." # - # If we only have one cached response and the server says "not modified", - # we can safely assume that single response is the one being confirmed. - # This handles cases where the server doesn't return validators in the 304. + # RFC 9111 Section 4.3.4: "If there is only a single stored response: + # that response." else: if len(self.revalidating_entries) == 1: - # Only one cached response - it must be the matching one identified_for_revalidation, need_to_be_invalidated = ( [self.revalidating_entries[0]], [], ) else: - # Multiple cached responses but no validators to match them - # We cannot determine which (if any) are valid - # Conservative approach: invalidate all of them + # Multiple cached responses but no validators to match them. + # Conservative approach: invalidate all of them. identified_for_revalidation, need_to_be_invalidated = ( [], self.revalidating_entries, @@ -1998,25 +1949,13 @@ # ============================================================================ # STEP 2: Update Matching Responses or Create Cache Miss # ============================================================================ - # If we found matching responses, freshen them with new metadata. - # If we found no matches, treat it as a cache miss. - next_state: "NeedToBeUpdated" | "CacheMiss" if identified_for_revalidation: - # We found responses that match the 304 confirmation - # Update their headers with fresh metadata from the 304 response - # - # RFC 9111 Section 3.2: Updating Stored Header Fields - # https://www.rfc-editor.org/rfc/rfc9111.html#section-3.2 - # - # "When doing so, the cache MUST add each header field in the provided - # response to the stored response, replacing field values that are - # already present" - # - # The refresh_response_headers function handles this header merging - # while excluding certain headers that shouldn't be updated - # (Content-Encoding, Content-Type, Content-Range). + # RFC 9111 Section 3.2: add each header field in the 304 to the + # stored response, replacing values already present (handled by + # refresh_response_headers, which also excludes Content-Encoding, + # Content-Type, Content-Range). next_state = NeedToBeUpdated( updating_entries=[ replace( @@ -2029,10 +1968,8 @@ options=self.options, ) else: - # No matching responses found - # This is unusual - the server said "not modified" but we can't figure - # out which cached response it's referring to. - # Treat this as a cache miss and let the normal flow handle it. + # The server said "not modified" but no stored response could be + # identified. Treat as a cache miss and let the normal flow handle it. next_state = CacheMiss( options=self.options, request=self.original_request, @@ -2042,23 +1979,13 @@ # ============================================================================ # STEP 3: Invalidate Non-Matching Responses (if any) # ============================================================================ - # If we had multiple cached responses and only some matched, we need to - # invalidate the non-matching ones. They're outdated or incorrect. - # - # For example: - # - Cached: Two responses with different ETags - # - 304 response: Matches only one ETag - # - Action: Update the matching one, invalidate the other - if need_to_be_invalidated: - # Wrap the next state in an invalidation operation return InvalidateEntries( options=self.options, entry_ids=[entry.id for entry in need_to_be_invalidated], next_state=next_state, ) - # No invalidations needed, return the next state directly return next_state diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/src/hishel.egg-info/PKG-INFO new/hishel-1.3.0/src/hishel.egg-info/PKG-INFO --- old/hishel-1.2.1/src/hishel.egg-info/PKG-INFO 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/src/hishel.egg-info/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,38 +0,0 @@ -Metadata-Version: 2.4 -Name: hishel -Version: 1.1.10 -Summary: Elegant HTTP Caching for Python -Author-email: Kar Petrosyan <[email protected]> -Project-URL: Homepage, https://hishel.com -Project-URL: Source, https://github.com/karpetrosyan/hishel -Classifier: Development Status :: 3 - Alpha -Classifier: Environment :: Web Environment -Classifier: Framework :: AsyncIO -Classifier: Framework :: Trio -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: 3.14 -Classifier: Topic :: Internet :: WWW/HTTP -Requires-Python: >=3.10 -License-File: LICENSE -Requires-Dist: msgpack>=1.1.2 -Requires-Dist: typing-extensions>=4.14.1 -Provides-Extra: async -Requires-Dist: anyio>=4.9.0; extra == "async" -Requires-Dist: anysqlite>=0.0.5; extra == "async" -Provides-Extra: requests -Requires-Dist: requests>=2.32.5; extra == "requests" -Provides-Extra: httpx -Requires-Dist: httpx>=0.28.1; extra == "httpx" -Requires-Dist: anyio>=4.9.0; extra == "httpx" -Requires-Dist: anysqlite>=0.0.5; extra == "httpx" -Provides-Extra: fastapi -Requires-Dist: fastapi>=0.119.1; extra == "fastapi" -Dynamic: license-file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/src/hishel.egg-info/SOURCES.txt new/hishel-1.3.0/src/hishel.egg-info/SOURCES.txt --- old/hishel-1.2.1/src/hishel.egg-info/SOURCES.txt 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/src/hishel.egg-info/SOURCES.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1,32 +0,0 @@ -LICENSE -README.md -pyproject.toml -src/hishel/__init__.py -src/hishel/_async_cache.py -src/hishel/_async_httpx.py -src/hishel/_policies.py -src/hishel/_sync_cache.py -src/hishel/_sync_httpx.py -src/hishel/_utils.py -src/hishel/asgi.py -src/hishel/fastapi.py -src/hishel/httpx.py -src/hishel/py.typed -src/hishel/requests.py -src/hishel.egg-info/PKG-INFO -src/hishel.egg-info/SOURCES.txt -src/hishel.egg-info/dependency_links.txt -src/hishel.egg-info/requires.txt -src/hishel.egg-info/top_level.txt -src/hishel/_core/_headers.py -src/hishel/_core/_spec.py -src/hishel/_core/models.py -src/hishel/_core/_storages/_async_base.py -src/hishel/_core/_storages/_async_sqlite.py -src/hishel/_core/_storages/_packing.py -src/hishel/_core/_storages/_sync_base.py -src/hishel/_core/_storages/_sync_sqlite.py -tests/test_asgi.py -tests/test_async_httpx.py -tests/test_requests.py -tests/test_sync_httpx.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/src/hishel.egg-info/dependency_links.txt new/hishel-1.3.0/src/hishel.egg-info/dependency_links.txt --- old/hishel-1.2.1/src/hishel.egg-info/dependency_links.txt 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/src/hishel.egg-info/dependency_links.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/src/hishel.egg-info/requires.txt new/hishel-1.3.0/src/hishel.egg-info/requires.txt --- old/hishel-1.2.1/src/hishel.egg-info/requires.txt 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/src/hishel.egg-info/requires.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1,17 +0,0 @@ -msgpack>=1.1.2 -typing-extensions>=4.14.1 - -[async] -anyio>=4.9.0 -anysqlite>=0.0.5 - -[fastapi] -fastapi>=0.119.1 - -[httpx] -httpx>=0.28.1 -anyio>=4.9.0 -anysqlite>=0.0.5 - -[requests] -requests>=2.32.5 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/src/hishel.egg-info/top_level.txt new/hishel-1.3.0/src/hishel.egg-info/top_level.txt --- old/hishel-1.2.1/src/hishel.egg-info/top_level.txt 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/src/hishel.egg-info/top_level.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ -hishel diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hishel-1.2.1/tests/_core/spec/test_need_revalidation.py new/hishel-1.3.0/tests/_core/spec/test_need_revalidation.py --- old/hishel-1.2.1/tests/_core/spec/test_need_revalidation.py 2026-04-27 18:01:23.000000000 +0200 +++ new/hishel-1.3.0/tests/_core/spec/test_need_revalidation.py 2026-06-11 10:39:31.000000000 +0200 @@ -313,6 +313,53 @@ assert len(next_state.entry_ids) == 2 assert isinstance(next_state.next_state, CacheMiss) + def test_304_with_strong_etag_matches_stored_weak_etag(self, default_options: CacheOptions) -> None: + """ + Test: 304 response with strong ETag matches cached response with weak ETag + sharing the same opaque-tag, and identifies it for revalidation. + + RFC 9110 Section 8.8.3.2 (weak comparison): + Two entity-tags are equivalent if their opaque-tags match regardless of + the W/ prefix on either side. The implementation uses weak comparison + so W/"abc123" (stored) must match "abc123" (304 response). + """ + original_request = create_request() + conditional_request = create_request(headers={"if-none-match": 'W/"abc123"'}) + + # Cached response has a WEAK ETag + cached_response = create_response( + headers={ + "etag": 'W/"abc123"', + "cache-control": "max-age=3600", + "date": "Mon, 01 Jan 2024 00:00:00 GMT", + } + ) + cached_pair = create_pair(request=original_request, response=cached_response) + + need_revalidation = NeedRevalidation( + request=conditional_request, + original_request=original_request, + revalidating_entries=[cached_pair], + options=default_options, + ) + + # 304 response has the SAME tag but STRONG (no W/ prefix) + revalidation_response = create_response( + status_code=304, + headers={ + "etag": '"abc123"', + "cache-control": "max-age=7200", + "date": "Mon, 01 Jan 2024 12:00:00 GMT", + }, + ) + + next_state = need_revalidation.next(revalidation_response) + + assert isinstance(next_state, NeedToBeUpdated) + assert len(next_state.updating_entries) == 1 + updated_response = next_state.updating_entries[0].response + assert updated_response.headers["cache-control"] == "max-age=7200" + def test_304_with_non_matching_etag_invalidates_response(self, default_options: CacheOptions) -> None: """ Test: 304 with non-matching ETag invalidates cached response.
