notpixel 0.1.0__py3-none-any.whl

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.
notpixel/__init__.py ADDED
@@ -0,0 +1,71 @@
1
+ from .cache import AdCache, generate_cache_key
2
+ from .client import Ads, AsyncAds
3
+ from .compose import compose_text, format_ad_block
4
+ from .embedding import (
5
+ DEFAULT_MODEL,
6
+ LocalEmbeddingResult,
7
+ generate_local_embedding,
8
+ get_model_info,
9
+ is_model_loaded,
10
+ preload_embedding_model,
11
+ unload_model,
12
+ )
13
+ from .mock import MOCK_ADS
14
+ from .sanitize import sanitize_and_truncate, sha256
15
+ from .types import (
16
+ AD_EXPIRATION_MS,
17
+ Ad,
18
+ AdCTA,
19
+ AdEmptyEvent,
20
+ AdEmptyReason,
21
+ AdErrorEvent,
22
+ AdFetchedEvent,
23
+ AdsBatchResult,
24
+ AnalyticsHooks,
25
+ CacheConfig,
26
+ FeedbackResponse,
27
+ PrefetchResult,
28
+ PrivacyConfig,
29
+ RespondResult,
30
+ TrackClickResponse,
31
+ TrackConversionResponse,
32
+ is_ad_expired,
33
+ )
34
+
35
+ __version__ = "0.1.0"
36
+
37
+ __all__ = [
38
+ "AD_EXPIRATION_MS",
39
+ "DEFAULT_MODEL",
40
+ "MOCK_ADS",
41
+ "Ad",
42
+ "AdCTA",
43
+ "AdCache",
44
+ "AdEmptyEvent",
45
+ "AdEmptyReason",
46
+ "AdErrorEvent",
47
+ "AdFetchedEvent",
48
+ "Ads",
49
+ "AdsBatchResult",
50
+ "AnalyticsHooks",
51
+ "AsyncAds",
52
+ "CacheConfig",
53
+ "FeedbackResponse",
54
+ "LocalEmbeddingResult",
55
+ "PrefetchResult",
56
+ "PrivacyConfig",
57
+ "RespondResult",
58
+ "TrackClickResponse",
59
+ "TrackConversionResponse",
60
+ "compose_text",
61
+ "format_ad_block",
62
+ "generate_cache_key",
63
+ "generate_local_embedding",
64
+ "get_model_info",
65
+ "is_ad_expired",
66
+ "is_model_loaded",
67
+ "preload_embedding_model",
68
+ "sanitize_and_truncate",
69
+ "sha256",
70
+ "unload_model",
71
+ ]
notpixel/cache.py ADDED
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from dataclasses import dataclass
6
+ from typing import Any, Generic, TypeVar
7
+
8
+ from .sanitize import sha256
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ @dataclass
14
+ class CacheEntry(Generic[T]):
15
+ value: T
16
+ expires_at: float
17
+
18
+
19
+ class AdCache(Generic[T]):
20
+ def __init__(self, ttl_ms: int = 5 * 60 * 1000, max_size: int = 100) -> None:
21
+ self._cache: dict[str, CacheEntry[T]] = {}
22
+ self.ttl_ms = ttl_ms
23
+ self.max_size = max_size
24
+
25
+ def get(self, key: str) -> T | None:
26
+ entry = self._cache.get(key)
27
+ if entry is None:
28
+ return None
29
+ if time.time() * 1000 > entry.expires_at:
30
+ self._cache.pop(key, None)
31
+ return None
32
+ return entry.value
33
+
34
+ def set(self, key: str, value: T) -> None:
35
+ if len(self._cache) >= self.max_size:
36
+ self.cleanup()
37
+ if len(self._cache) >= self.max_size and self._cache:
38
+ oldest_key = next(iter(self._cache))
39
+ self._cache.pop(oldest_key, None)
40
+ self._cache[key] = CacheEntry(
41
+ value=value,
42
+ expires_at=time.time() * 1000 + self.ttl_ms,
43
+ )
44
+
45
+ def delete(self, key: str) -> bool:
46
+ return self._cache.pop(key, None) is not None
47
+
48
+ def clear(self) -> None:
49
+ self._cache.clear()
50
+
51
+ def cleanup(self) -> int:
52
+ now = time.time() * 1000
53
+ expired = [k for k, v in self._cache.items() if now > v.expires_at]
54
+ for k in expired:
55
+ self._cache.pop(k, None)
56
+ return len(expired)
57
+
58
+ def stats(self) -> dict[str, Any]:
59
+ return {
60
+ "size": len(self._cache),
61
+ "maxSize": self.max_size,
62
+ "ttlMs": self.ttl_ms,
63
+ }
64
+
65
+
66
+ def generate_cache_key(
67
+ publisher_id: str | None = None,
68
+ input_text: str | None = None,
69
+ mode: str | None = None,
70
+ embedding: list[float] | None = None,
71
+ context: dict[str, Any] | None = None,
72
+ request_id: str | None = None,
73
+ ) -> str:
74
+ embedding_key = (
75
+ sha256(",".join(f"{n:.6f}" for n in embedding))
76
+ if embedding
77
+ else ""
78
+ )
79
+ parts = [
80
+ publisher_id or "",
81
+ mode or "standard",
82
+ input_text or embedding_key or "",
83
+ ]
84
+ if context and len(context) > 0:
85
+ sorted_ctx = sorted(context.items(), key=lambda item: item[0])
86
+ parts.append(sha256(json.dumps(sorted_ctx, separators=(",", ":"))))
87
+ else:
88
+ parts.append("")
89
+ parts.append(sha256(request_id) if request_id else "")
90
+ # Fixed positions distinguish context, opportunity IDs and delimiters in input.
91
+ return json.dumps(parts, separators=(",", ":"))