Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-posthog for openSUSE:Factory checked in at 2026-03-17 19:04:32 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-posthog (Old) and /work/SRC/openSUSE:Factory/.python-posthog.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-posthog" Tue Mar 17 19:04:32 2026 rev:7 rq:1339442 version:7.9.12 Changes: -------- --- /work/SRC/openSUSE:Factory/python-posthog/python-posthog.changes 2026-03-09 16:12:09.998972895 +0100 +++ /work/SRC/openSUSE:Factory/.python-posthog.new.8177/python-posthog.changes 2026-03-17 19:06:14.633013978 +0100 @@ -1,0 +2,11 @@ +Mon Mar 16 21:34:34 UTC 2026 - Dirk Müller <[email protected]> + +- update to 7.9.12: + * chore(flags): expose flag_definition_cache_provider + * chore(ci): fix release attribution + * fix(ci): attribute release tag to GitHub App + * Update release.yml to support commit signing + * feat(llma): support prompt versions in prompts sdk + * chore(llma): apply prompt SDK review cleanups + +------------------------------------------------------------------- Old: ---- posthog-7.9.7.tar.gz New: ---- posthog-7.9.12.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-posthog.spec ++++++ --- /var/tmp/diff_new_pack.Z6H1DF/_old 2026-03-17 19:06:15.149035363 +0100 +++ /var/tmp/diff_new_pack.Z6H1DF/_new 2026-03-17 19:06:15.153035528 +0100 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-posthog -Version: 7.9.7 +Version: 7.9.12 Release: 0 Summary: PostHog is developer-friendly, self-hosted product analytics License: MIT ++++++ posthog-7.9.7.tar.gz -> posthog-7.9.12.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/posthog-7.9.7/PKG-INFO new/posthog-7.9.12/PKG-INFO --- old/posthog-7.9.7/PKG-INFO 2026-03-05 23:09:32.431455100 +0100 +++ new/posthog-7.9.12/PKG-INFO 2026-03-12 10:01:02.150703400 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: posthog -Version: 7.9.7 +Version: 7.9.12 Summary: Integrate PostHog into any python application. Home-page: https://github.com/posthog/posthog-python Author: Posthog @@ -97,6 +97,8 @@ ## Development +This repo requires all commits to be signed. To configure commit signing, see the [PostHog handbook](https://posthog.com/handbook/engineering/security#commit-signing). + ### Testing Locally We recommend using [uv](https://docs.astral.sh/uv/). It's super fast. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/posthog-7.9.7/README.md new/posthog-7.9.12/README.md --- old/posthog-7.9.7/README.md 2026-03-05 23:09:03.000000000 +0100 +++ new/posthog-7.9.12/README.md 2026-03-12 10:00:31.000000000 +0100 @@ -22,6 +22,8 @@ ## Development +This repo requires all commits to be signed. To configure commit signing, see the [PostHog handbook](https://posthog.com/handbook/engineering/security#commit-signing). + ### Testing Locally We recommend using [uv](https://docs.astral.sh/uv/). It's super fast. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/posthog-7.9.7/posthog/__init__.py new/posthog-7.9.12/posthog/__init__.py --- old/posthog-7.9.7/posthog/__init__.py 2026-03-05 23:09:03.000000000 +0100 +++ new/posthog-7.9.12/posthog/__init__.py 2026-03-12 10:00:31.000000000 +0100 @@ -253,6 +253,7 @@ # Whether to enable feature flag polling for local evaluation by default. Defaults to True. # We recommend setting this to False if you are only using the personalApiKey for evaluating remote config payloads via `get_remote_config_payload` and not using local evaluation. enable_local_evaluation = True # type: bool +flag_definition_cache_provider = None # type: Optional[FlagDefinitionCacheProvider] default_client = None # type: Optional[Client] @@ -867,6 +868,7 @@ enable_exception_autocapture=enable_exception_autocapture, log_captured_exceptions=log_captured_exceptions, enable_local_evaluation=enable_local_evaluation, + flag_definition_cache_provider=flag_definition_cache_provider, capture_exception_code_variables=capture_exception_code_variables, code_variables_mask_patterns=code_variables_mask_patterns, code_variables_ignore_patterns=code_variables_ignore_patterns, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/posthog-7.9.7/posthog/ai/prompts.py new/posthog-7.9.12/posthog/ai/prompts.py --- old/posthog-7.9.7/posthog/ai/prompts.py 2026-03-05 23:09:03.000000000 +0100 +++ new/posthog-7.9.12/posthog/ai/prompts.py 2026-03-12 10:00:31.000000000 +0100 @@ -19,6 +19,7 @@ DEFAULT_CACHE_TTL_SECONDS = 300 # 5 minutes PromptVariables = Dict[str, Union[str, int, float, bool]] +PromptCacheKey = tuple[str, Optional[int]] class CachedPrompt: @@ -29,6 +30,22 @@ self.fetched_at = fetched_at +def _cache_key(name: str, version: Optional[int]) -> PromptCacheKey: + """Build a cache key for latest or versioned prompt fetches.""" + return (name, version) + + +def _prompt_reference( + name: str, version: Optional[int], *, capitalize: bool = False +) -> str: + """Format a prompt reference for logs and errors.""" + prefix = "Prompt" if capitalize else "prompt" + label = f'{prefix} "{name}"' + if version is not None: + return f"{label} version {version}" + return label + + def _is_prompt_api_response(data: Any) -> bool: """Check if the response is a valid prompt API response.""" return ( @@ -63,6 +80,9 @@ # Fetch with caching and fallback template = prompts.get('support-system-prompt', fallback='You are a helpful assistant.') + # Fetch a specific published version + prompt_v1 = prompts.get('support-system-prompt', version=1) + # Compile with variables system_prompt = prompts.compile(template, { 'company': 'Acme Corp', @@ -93,7 +113,7 @@ self._default_cache_ttl_seconds = ( default_cache_ttl_seconds or DEFAULT_CACHE_TTL_SECONDS ) - self._cache: Dict[str, CachedPrompt] = {} + self._cache: Dict[PromptCacheKey, CachedPrompt] = {} if posthog is not None: self._personal_api_key = getattr(posthog, "personal_api_key", None) or "" @@ -112,6 +132,7 @@ *, cache_ttl_seconds: Optional[int] = None, fallback: Optional[str] = None, + version: Optional[int] = None, ) -> str: """ Fetch a prompt by name from the PostHog API. @@ -126,6 +147,8 @@ name: The name of the prompt to fetch cache_ttl_seconds: Cache TTL in seconds (defaults to instance default) fallback: Fallback prompt to use if fetch fails and no cache available + version: Specific prompt version to fetch. If None, fetches the latest + version Returns: The prompt string @@ -138,9 +161,10 @@ if cache_ttl_seconds is not None else self._default_cache_ttl_seconds ) + cache_key = _cache_key(name, version) # Check cache first - cached = self._cache.get(name) + cached = self._cache.get(cache_key) now = time.time() if cached is not None: @@ -151,21 +175,22 @@ # Try to fetch from API try: - prompt = self._fetch_prompt_from_api(name) + prompt = self._fetch_prompt_from_api(name, version) fetched_at = time.time() # Update cache - self._cache[name] = CachedPrompt(prompt=prompt, fetched_at=fetched_at) + self._cache[cache_key] = CachedPrompt(prompt=prompt, fetched_at=fetched_at) return prompt except Exception as error: + prompt_reference = _prompt_reference(name, version) # Fallback order: # 1. Return stale cache (with warning) if cached is not None: log.warning( - '[PostHog Prompts] Failed to fetch prompt "%s", using stale cache: %s', - name, + "[PostHog Prompts] Failed to fetch %s, using stale cache: %s", + prompt_reference, error, ) return cached.prompt @@ -173,8 +198,8 @@ # 2. Return fallback (with warning) if fallback is not None: log.warning( - '[PostHog Prompts] Failed to fetch prompt "%s", using fallback: %s', - name, + "[PostHog Prompts] Failed to fetch %s, using fallback: %s", + prompt_reference, error, ) return fallback @@ -207,27 +232,43 @@ return re.sub(r"\{\{([\w.-]+)\}\}", replace_variable, prompt) - def clear_cache(self, name: Optional[str] = None) -> None: + def clear_cache( + self, name: Optional[str] = None, *, version: Optional[int] = None + ) -> None: """ Clear cached prompts. Args: - name: Specific prompt to clear. If None, clears all cached prompts. + name: Specific prompt name to clear. If None, clears all cached prompts. + version: Specific prompt version to clear. Requires name. """ - if name is not None: - self._cache.pop(name, None) - else: + if version is not None and name is None: + raise ValueError("'version' requires 'name' to be provided") + + if name is None: self._cache.clear() + return + + if version is not None: + self._cache.pop(_cache_key(name, version), None) + return + + keys_to_clear = [key for key in self._cache if key[0] == name] + for key in keys_to_clear: + self._cache.pop(key, None) - def _fetch_prompt_from_api(self, name: str) -> str: + def _fetch_prompt_from_api(self, name: str, version: Optional[int] = None) -> str: """ Fetch prompt from PostHog API. - Endpoint: {host}/api/environments/@current/llm_prompts/name/{encoded_name}/?token={encoded_project_api_key} + Endpoint: + {host}/api/environments/@current/llm_prompts/name/{encoded_name}/ + ?token={encoded_project_api_key}[&version={version}] Auth: Bearer {personal_api_key} Args: name: The name of the prompt to fetch + version: Specific prompt version to fetch. If None, fetches the latest Returns: The prompt string @@ -247,8 +288,13 @@ ) encoded_name = urllib.parse.quote(name, safe="") - encoded_project_api_key = urllib.parse.quote(self._project_api_key, safe="") - url = f"{self._host}/api/environments/@current/llm_prompts/name/{encoded_name}/?token={encoded_project_api_key}" + query_params: Dict[str, Union[str, int]] = {"token": self._project_api_key} + if version is not None: + query_params["version"] = version + encoded_query = urllib.parse.urlencode(query_params) + url = f"{self._host}/api/environments/@current/llm_prompts/name/{encoded_name}/?{encoded_query}" + prompt_reference = _prompt_reference(name, version) + prompt_label = _prompt_reference(name, version, capitalize=True) headers = { "Authorization": f"Bearer {self._personal_api_key}", @@ -259,28 +305,28 @@ if not response.ok: if response.status_code == 404: - raise Exception(f'[PostHog Prompts] Prompt "{name}" not found') + raise Exception(f"[PostHog Prompts] {prompt_label} not found") if response.status_code == 403: raise Exception( - f'[PostHog Prompts] Access denied for prompt "{name}". ' + f"[PostHog Prompts] Access denied for {prompt_reference}. " "Check that your personal_api_key has the correct permissions and the LLM prompts feature is enabled." ) raise Exception( - f'[PostHog Prompts] Failed to fetch prompt "{name}": HTTP {response.status_code}' + f"[PostHog Prompts] Failed to fetch {prompt_label}: HTTP {response.status_code}" ) try: data = response.json() except Exception: raise Exception( - f'[PostHog Prompts] Invalid response format for prompt "{name}"' + f"[PostHog Prompts] Invalid response format for {prompt_label}" ) if not _is_prompt_api_response(data): raise Exception( - f'[PostHog Prompts] Invalid response format for prompt "{name}"' + f"[PostHog Prompts] Invalid response format for {prompt_label}" ) return data["prompt"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/posthog-7.9.7/posthog/test/ai/test_prompts.py new/posthog-7.9.12/posthog/test/ai/test_prompts.py --- old/posthog-7.9.7/posthog/test/ai/test_prompts.py 2026-03-05 23:09:03.000000000 +0100 +++ new/posthog-7.9.12/posthog/test/ai/test_prompts.py 2026-03-12 10:00:31.000000000 +0100 @@ -1,6 +1,8 @@ import unittest from unittest.mock import MagicMock, patch +from parameterized import parameterized + from posthog.ai.prompts import Prompts @@ -73,6 +75,30 @@ ) @patch("posthog.ai.prompts._get_session") + def test_successfully_fetch_a_specific_prompt_version(self, mock_get_session): + """Should successfully fetch a specific prompt version.""" + mock_get = mock_get_session.return_value.get + versioned_prompt_response = { + **self.mock_prompt_response, + "prompt": "Prompt version 1", + "version": 1, + } + mock_get.return_value = MockResponse(json_data=versioned_prompt_response) + + posthog = self.create_mock_posthog() + prompts = Prompts(posthog) + + result = prompts.get("test-prompt", version=1) + + self.assertEqual(result, versioned_prompt_response["prompt"]) + mock_get.assert_called_once() + call_args = mock_get.call_args + self.assertEqual( + call_args[0][0], + "https://us.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key&version=1", + ) + + @patch("posthog.ai.prompts._get_session") @patch("posthog.ai.prompts.time.time") def test_return_cached_prompt_when_fresh(self, mock_time, mock_get_session): """Should return cached prompt when fresh (no API call).""" @@ -97,6 +123,41 @@ self.assertEqual(mock_get.call_count, 1) # No additional fetch @patch("posthog.ai.prompts._get_session") + def test_cache_latest_and_versioned_prompts_separately(self, mock_get_session): + """Should cache latest and historical prompt versions separately.""" + mock_get = mock_get_session.return_value.get + latest_prompt_response = { + **self.mock_prompt_response, + "prompt": "Latest prompt", + "version": 2, + } + versioned_prompt_response = { + **self.mock_prompt_response, + "prompt": "Prompt version 1", + "version": 1, + } + + mock_get.side_effect = [ + MockResponse(json_data=latest_prompt_response), + MockResponse(json_data=versioned_prompt_response), + ] + + posthog = self.create_mock_posthog() + prompts = Prompts(posthog) + + self.assertEqual(prompts.get("test-prompt"), latest_prompt_response["prompt"]) + self.assertEqual( + prompts.get("test-prompt", version=1), + versioned_prompt_response["prompt"], + ) + self.assertEqual(prompts.get("test-prompt"), latest_prompt_response["prompt"]) + self.assertEqual( + prompts.get("test-prompt", version=1), + versioned_prompt_response["prompt"], + ) + self.assertEqual(mock_get.call_count, 2) + + @patch("posthog.ai.prompts._get_session") @patch("posthog.ai.prompts.time.time") def test_refetch_when_cache_is_stale(self, mock_time, mock_get_session): """Should refetch when cache is stale.""" @@ -197,9 +258,21 @@ self.assertIn("Network error", str(context.exception)) + @parameterized.expand( + [ + ("latest", {}, 'Prompt "nonexistent-prompt" not found'), + ( + "versioned", + {"version": 3}, + 'Prompt "nonexistent-prompt" version 3 not found', + ), + ] + ) @patch("posthog.ai.prompts._get_session") - def test_handle_404_response(self, mock_get_session): - """Should handle 404 response.""" + def test_handle_404_response( + self, _scenario, get_kwargs, expected_message, mock_get_session + ): + """Should handle 404 responses for latest and versioned prompts.""" mock_get = mock_get_session.return_value.get mock_get.return_value = MockResponse(status_code=404, ok=False) @@ -207,9 +280,9 @@ prompts = Prompts(posthog) with self.assertRaises(Exception) as context: - prompts.get("nonexistent-prompt") + prompts.get("nonexistent-prompt", **get_kwargs) - self.assertIn('Prompt "nonexistent-prompt" not found', str(context.exception)) + self.assertIn(expected_message, str(context.exception)) @patch("posthog.ai.prompts._get_session") def test_handle_403_response(self, mock_get_session): @@ -542,6 +615,38 @@ class TestPromptsClearCache(TestPrompts): """Tests for the Prompts.clear_cache() method.""" + def _populate_versioned_cache(self, prompts, mock_get): + """Populate cache with latest and versioned entries for the same prompt.""" + latest_prompt_response = { + **self.mock_prompt_response, + "prompt": "Latest prompt", + "version": 2, + } + versioned_prompt_response = { + **self.mock_prompt_response, + "prompt": "Prompt version 1", + "version": 1, + } + mock_get.side_effect = [ + MockResponse(json_data=latest_prompt_response), + MockResponse(json_data=versioned_prompt_response), + ] + + prompts.get("test-prompt") + prompts.get("test-prompt", version=1) + + return latest_prompt_response, versioned_prompt_response + + def test_clear_cache_with_version_and_no_name_raises_value_error(self): + """Should enforce that versioned cache clearing requires a prompt name.""" + posthog = self.create_mock_posthog() + prompts = Prompts(posthog) + + with self.assertRaises(ValueError) as context: + prompts.clear_cache(version=1) + + self.assertIn("requires 'name'", str(context.exception)) + @patch("posthog.ai.prompts._get_session") def test_clear_a_specific_prompt_from_cache(self, mock_get_session): """Should clear a specific prompt from cache.""" @@ -574,6 +679,49 @@ self.assertEqual(mock_get.call_count, 3) @patch("posthog.ai.prompts._get_session") + def test_clear_a_specific_prompt_version_from_cache(self, mock_get_session): + """Should clear only the requested prompt version from cache.""" + mock_get = mock_get_session.return_value.get + + posthog = self.create_mock_posthog() + prompts = Prompts(posthog) + + _, versioned_prompt_response = self._populate_versioned_cache(prompts, mock_get) + self.assertEqual(mock_get.call_count, 2) + + mock_get.side_effect = [MockResponse(json_data=versioned_prompt_response)] + prompts.clear_cache("test-prompt", version=1) + + prompts.get("test-prompt") + self.assertEqual(mock_get.call_count, 2) + + prompts.get("test-prompt", version=1) + self.assertEqual(mock_get.call_count, 3) + + @patch("posthog.ai.prompts._get_session") + def test_clear_a_prompt_name_clears_all_cached_versions(self, mock_get_session): + """Should clear latest and versioned cache entries for the same prompt name.""" + mock_get = mock_get_session.return_value.get + + posthog = self.create_mock_posthog() + prompts = Prompts(posthog) + + latest_prompt_response, versioned_prompt_response = ( + self._populate_versioned_cache(prompts, mock_get) + ) + self.assertEqual(mock_get.call_count, 2) + + mock_get.side_effect = [ + MockResponse(json_data=latest_prompt_response), + MockResponse(json_data=versioned_prompt_response), + ] + prompts.clear_cache("test-prompt") + + prompts.get("test-prompt") + prompts.get("test-prompt", version=1) + self.assertEqual(mock_get.call_count, 4) + + @patch("posthog.ai.prompts._get_session") def test_clear_all_prompts_from_cache(self, mock_get_session): """Should clear all prompts from cache when no name is provided.""" mock_get = mock_get_session.return_value.get diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/posthog-7.9.7/posthog/version.py new/posthog-7.9.12/posthog/version.py --- old/posthog-7.9.7/posthog/version.py 2026-03-05 23:09:29.000000000 +0100 +++ new/posthog-7.9.12/posthog/version.py 2026-03-12 10:00:55.000000000 +0100 @@ -1 +1 @@ -VERSION = "7.9.7" +VERSION = "7.9.12" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/posthog-7.9.7/posthog.egg-info/PKG-INFO new/posthog-7.9.12/posthog.egg-info/PKG-INFO --- old/posthog-7.9.7/posthog.egg-info/PKG-INFO 2026-03-05 23:09:32.000000000 +0100 +++ new/posthog-7.9.12/posthog.egg-info/PKG-INFO 2026-03-12 10:01:02.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: posthog -Version: 7.9.7 +Version: 7.9.12 Summary: Integrate PostHog into any python application. Home-page: https://github.com/posthog/posthog-python Author: Posthog @@ -97,6 +97,8 @@ ## Development +This repo requires all commits to be signed. To configure commit signing, see the [PostHog handbook](https://posthog.com/handbook/engineering/security#commit-signing). + ### Testing Locally We recommend using [uv](https://docs.astral.sh/uv/). It's super fast. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/posthog-7.9.7/pyproject.toml new/posthog-7.9.12/pyproject.toml --- old/posthog-7.9.7/pyproject.toml 2026-03-05 23:09:28.000000000 +0100 +++ new/posthog-7.9.12/pyproject.toml 2026-03-12 10:00:55.000000000 +0100 @@ -4,7 +4,7 @@ [project] name = "posthog" -version = "7.9.7" +version = "7.9.12" description = "Integrate PostHog into any python application." authors = [{ name = "PostHog", email = "[email protected]" }] maintainers = [{ name = "PostHog", email = "[email protected]" }]
