From 00b8c64bed61b5fcd65d4c671bbb4dd984aac4b2 Mon Sep 17 00:00:00 2001 From: Karl Waldman Date: Wed, 12 Aug 2026 05:34:49 -0400 Subject: [PATCH] Remove nonexistent telemetry quota bonus claim --- CHANGELOG.md | 12 +++- oilpriceapi/async_client.py | 2 +- oilpriceapi/client.py | 2 +- oilpriceapi/version.py | 2 +- pyproject.toml | 2 +- scripts/validate_storefront_claims.py | 83 +++++++++++++++++++++++++++ tests/test_release_readiness.py | 2 +- tests/test_storefront_claims.py | 66 +++++++++++++++++++++ 8 files changed, 165 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 295b694..69f4fb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to the OilPriceAPI Python SDK will be documented in this file. +## [1.12.7] - 2026-08-12 + +### Fixed + +- Removed a nonexistent request-limit bonus claim from sync and async + usage-attribution header comments. +- Added red-first recursive authored and installed-wheel claim coverage so + telemetry or application metadata cannot be presented as changing account + entitlements. + The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). @@ -151,7 +161,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Data Sources Resource**: `client.data_sources.list()`, `get()`, `create()`, `update()`, `delete()`, `test()`, `logs()`, `health()`, `rotate_credentials()` for data connector management - **Enhanced Alerts**: Added `test()`, `triggers()`, `analytics_history()` methods to existing alerts resource - **Data Connector Support**: `client.get_data_connector_prices()` for BYOS (Bring Your Own Subscription) prices -- **Telemetry Headers**: `app_url` and `app_name` parameters for API usage attribution (10% rate limit bonus for app_url) +- **Telemetry Headers**: `app_url` and `app_name` parameters for API usage attribution ### Fixed diff --git a/oilpriceapi/async_client.py b/oilpriceapi/async_client.py index 42d8567..58e6ee7 100644 --- a/oilpriceapi/async_client.py +++ b/oilpriceapi/async_client.py @@ -121,7 +121,7 @@ def __init__( "X-Client-Type": "sdk", } - # Add optional telemetry headers (10% bonus for app_url!) + # Add optional usage-attribution headers. if self.app_url: self.headers["X-App-URL"] = self.app_url if self.app_name: diff --git a/oilpriceapi/client.py b/oilpriceapi/client.py index ceb8991..e61b2ad 100644 --- a/oilpriceapi/client.py +++ b/oilpriceapi/client.py @@ -142,7 +142,7 @@ def __init__( "X-Client-Type": "sdk", } - # Add optional telemetry headers (10% bonus for app_url!) + # Add optional usage-attribution headers. if self.app_url: self.headers["X-App-URL"] = self.app_url if self.app_name: diff --git a/oilpriceapi/version.py b/oilpriceapi/version.py index 0868701..3daaed7 100644 --- a/oilpriceapi/version.py +++ b/oilpriceapi/version.py @@ -5,6 +5,6 @@ Used in __init__.py, client.py, and async_client.py. """ -__version__ = "1.12.6" +__version__ = "1.12.7" SDK_VERSION = __version__ SDK_NAME = "oilpriceapi-python" diff --git a/pyproject.toml b/pyproject.toml index a548709..f823ff5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "oilpriceapi" -version = "1.12.6" +version = "1.12.7" description = "Official Python SDK for source-timestamped OilPriceAPI energy data" authors = [ {name = "OilPriceAPI", email = "support@oilpriceapi.com"} diff --git a/scripts/validate_storefront_claims.py b/scripts/validate_storefront_claims.py index c3534ad..8c2025f 100644 --- a/scripts/validate_storefront_claims.py +++ b/scripts/validate_storefront_claims.py @@ -41,6 +41,39 @@ _HTML_TAG_PATTERN = re.compile(r"<[^>]{1,500}>") _MAX_ACTION_COUNT_GAP = 64 _MAX_RATE_SPAN = 200 +_MAX_TELEMETRY_REWARD_SPAN = 320 +_TELEMETRY_IDENTITY_PATTERN = re.compile( + r"\b(?:telemetry|app(?:lication)?[- ]+(?:metadata|url|name)|" + r"app[_ -]?url|app[_ -]?name|x-app-(?:url|name))\b", + re.IGNORECASE, +) +_TELEMETRY_STRONG_REWARD_PATTERN = re.compile( + r"\b(?:bonus|increase(?:s|d)?|unlock(?:s|ed)?|" + r"earn(?:s|ed)?|grant(?:s|ed)?|reward(?:s|ed)?|boost(?:s|ed)?)\b", + re.IGNORECASE, +) +_TELEMETRY_MODIFIER_REWARD_PATTERN = re.compile( + r"\b(?:more|extra|additional)\b", re.IGNORECASE +) +_TELEMETRY_QUOTA_SIGNAL_PATTERN = re.compile( + r"\b(?:api[- ]+)?(?:requests?|calls?|quota|limits?|allowances?|credits?)\b|" + r"(? List[str]: return claims +def _telemetry_reward_claims(text: str) -> List[str]: + """Find attribution identity + reward + quota signals in one bounded sentence.""" + claims: List[str] = [] + seen: Set[Tuple[int, str]] = set() + + for segment_offset, segment in _bounded_rate_segments(text): + searchable = _HTML_TAG_PATTERN.sub(" ", segment) + identities = list(_TELEMETRY_IDENTITY_PATTERN.finditer(searchable)) + quota_signals = list(_TELEMETRY_QUOTA_SIGNAL_PATTERN.finditer(searchable)) + strong_rewards = list(_TELEMETRY_STRONG_REWARD_PATTERN.finditer(searchable)) + modifier_rewards = list(_TELEMETRY_MODIFIER_REWARD_PATTERN.finditer(searchable)) + reward_pairs: List[Tuple[int, int]] = [] + for reward in strong_rewards: + for quota_signal in quota_signals: + start = min(reward.start(), quota_signal.start()) + end = max(reward.end(), quota_signal.end()) + if end - start <= _MAX_STRONG_REWARD_SPAN: + reward_pairs.append((start, end)) + for reward in modifier_rewards: + for quota_signal in quota_signals: + if reward.end() > quota_signal.start(): + continue + gap = searchable[reward.end() : quota_signal.start()] + gap_words = re.findall(r"[a-z]+", gap.lower()) + if len(gap) <= 48 and all( + word in _TELEMETRY_MODIFIER_GAP_WORDS for word in gap_words + ): + reward_pairs.append((reward.start(), quota_signal.end())) + for identity in identities: + candidates = [ + (min(identity.start(), start), max(identity.end(), end)) + for start, end in reward_pairs + if max(identity.end(), end) - min(identity.start(), start) + <= _MAX_TELEMETRY_REWARD_SPAN + ] + if not candidates: + continue + start, end = min(candidates, key=lambda span: span[1] - span[0]) + claim = re.sub(r"\s+", " ", searchable[start:end]).strip() + key = (segment_offset + start, claim) + if key not in seen: + seen.add(key) + claims.append(claim) + return claims + + def _claim_failures(root: Path, surfaces: Iterable[Path]) -> List[str]: failures: List[str] = [] for path in surfaces: @@ -243,6 +322,10 @@ def _claim_failures(root: Path, surfaces: Iterable[Path]) -> List[str]: failures.append( f"{path.relative_to(root)}: fixed demo rate matched {claim!r}" ) + for claim in _telemetry_reward_claims(text): + failures.append( + f"{path.relative_to(root)}: telemetry quota reward matched {claim!r}" + ) return failures diff --git a/tests/test_release_readiness.py b/tests/test_release_readiness.py index 40a4efb..9139c94 100644 --- a/tests/test_release_readiness.py +++ b/tests/test_release_readiness.py @@ -159,7 +159,7 @@ def test_package_version_helper_reads_the_project_version() -> None: capture_output=True, text=True, ) - assert result.stdout.strip() == "1.12.6" + assert result.stdout.strip() == "1.12.7" def test_every_workflow_pins_actions_and_hardens_each_checkout_step() -> None: diff --git a/tests/test_storefront_claims.py b/tests/test_storefront_claims.py index bdaf0fb..d2fda17 100644 --- a/tests/test_storefront_claims.py +++ b/tests/test_storefront_claims.py @@ -35,6 +35,24 @@ def _installed_text_failures(tmp_path: Path, text: str) -> List[str]: return validate_package(tmp_path) +def _authored_text_failures(tmp_path: Path, text: str) -> List[str]: + package = tmp_path / "oilpriceapi" / "future" + package.mkdir(parents=True) + (tmp_path / "README.md").write_text( + "https://api.oilpriceapi.com/product-facts.json\n" + ) + (tmp_path / "EXAMPLES.md").write_text("Reviewed examples.\n") + (tmp_path / "CHANGELOG.md").write_text("Reviewed history.\n") + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "oilpriceapi"\nversion = "9.9.9"\n' + ) + (tmp_path / "oilpriceapi" / "version.py").write_text( + '__version__ = "9.9.9"\n' + ) + (package / "types.pyi").write_text(text) + return validate(tmp_path) + + def test_storefront_claims_match_reviewed_contract() -> None: assert validate() == [] @@ -142,6 +160,54 @@ def test_rejects_claim_in_future_installed_package_data(tmp_path: Path) -> None: ) +def test_rejects_telemetry_quota_reward_in_future_nested_authored_source( + tmp_path: Path, +) -> None: + failures = _authored_text_failures( + tmp_path, + '"""Application telemetry unlocks additional API calls for your app."""\n', + ) + + assert any( + "oilpriceapi/future/types.pyi" in failure + and "telemetry quota reward" in failure + for failure in failures + ), failures + + +@pytest.mark.parametrize( + "claim", + [ + "Add optional telemetry headers (10% bonus for app_url!).", + "App telemetry may unlock a 10% bonus to your request limit.", + "X-App-URL earns extra request credits.", + "More requests are granted when application metadata is sent.", + "Sending app_url increases your quota allowance.", + ], +) +def test_rejects_telemetry_quota_rewards_in_future_wheel_text( + tmp_path: Path, claim: str +) -> None: + failures = _installed_text_failures(tmp_path, claim) + + assert any("telemetry quota reward" in failure for failure in failures), failures + + +@pytest.mark.parametrize( + "text", + [ + "Optional telemetry headers identify SDK usage.", + "Application metadata supports usage attribution; entitlements come from Product Facts.", + "X-App-URL and X-App-Name are optional attribution headers.", + "Telemetry sends extra application metadata with API requests.", + ], +) +def test_allows_telemetry_attribution_without_a_quota_reward( + tmp_path: Path, text: str +) -> None: + assert _installed_text_failures(tmp_path, text) == [] + + @pytest.mark.parametrize( "claim", [