notpixel 0.1.0__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.
- notpixel-0.1.0/.gitignore +70 -0
- notpixel-0.1.0/LICENSE +21 -0
- notpixel-0.1.0/PKG-INFO +113 -0
- notpixel-0.1.0/README.md +87 -0
- notpixel-0.1.0/pyproject.toml +52 -0
- notpixel-0.1.0/scripts/check_distribution.py +45 -0
- notpixel-0.1.0/src/notpixel/__init__.py +71 -0
- notpixel-0.1.0/src/notpixel/cache.py +91 -0
- notpixel-0.1.0/src/notpixel/client.py +844 -0
- notpixel-0.1.0/src/notpixel/compose.py +22 -0
- notpixel-0.1.0/src/notpixel/embedding.py +123 -0
- notpixel-0.1.0/src/notpixel/mock.py +51 -0
- notpixel-0.1.0/src/notpixel/py.typed +0 -0
- notpixel-0.1.0/src/notpixel/sanitize.py +19 -0
- notpixel-0.1.0/src/notpixel/types.py +185 -0
- notpixel-0.1.0/tests/test_notpixel.py +250 -0
- notpixel-0.1.0/tests/test_opportunities.py +83 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist/
|
|
6
|
+
.next/
|
|
7
|
+
.turbo/
|
|
8
|
+
out/
|
|
9
|
+
|
|
10
|
+
# Environment variables
|
|
11
|
+
.env
|
|
12
|
+
.env.local
|
|
13
|
+
.env.*.local
|
|
14
|
+
.dev.vars
|
|
15
|
+
.dev.vars.*
|
|
16
|
+
!.env.example
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
*.swo
|
|
23
|
+
|
|
24
|
+
# OS
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
|
|
28
|
+
# Logs
|
|
29
|
+
*.log
|
|
30
|
+
npm-debug.log*
|
|
31
|
+
yarn-debug.log*
|
|
32
|
+
|
|
33
|
+
# TypeScript
|
|
34
|
+
*.tsbuildinfo
|
|
35
|
+
|
|
36
|
+
# Test coverage
|
|
37
|
+
coverage/
|
|
38
|
+
|
|
39
|
+
# Package manager locks (keep package-lock.json for now)
|
|
40
|
+
yarn.lock
|
|
41
|
+
pnpm-lock.yaml
|
|
42
|
+
|
|
43
|
+
# Claude Code
|
|
44
|
+
.claude/
|
|
45
|
+
|
|
46
|
+
# Temporary files
|
|
47
|
+
*.tmp
|
|
48
|
+
*.temp
|
|
49
|
+
|
|
50
|
+
# Generated tarballs
|
|
51
|
+
*.tgz
|
|
52
|
+
|
|
53
|
+
# Prisma
|
|
54
|
+
server/prisma/migrations/.applied
|
|
55
|
+
packages/landing/src/app/marketing/*
|
|
56
|
+
packages/landing/src/app/marketing/
|
|
57
|
+
!packages/landing/src/app/marketing/
|
|
58
|
+
!packages/landing/src/app/marketing/assets.yaml
|
|
59
|
+
|
|
60
|
+
.agent
|
|
61
|
+
.agents
|
|
62
|
+
.codex
|
|
63
|
+
.vercel
|
|
64
|
+
|
|
65
|
+
# Python
|
|
66
|
+
__pycache__/
|
|
67
|
+
*.py[cod]
|
|
68
|
+
*.egg-info/
|
|
69
|
+
.pytest_cache/
|
|
70
|
+
build/
|
notpixel-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 NotPixel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
notpixel-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notpixel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NotPixel Python SDK — Privacy-first contextual ads for LLM applications (FastAPI, Django, LangChain, CrewAI)
|
|
5
|
+
Project-URL: Homepage, https://notpixel.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.notpixel.ai/http-api
|
|
7
|
+
Project-URL: Repository, https://github.com/Cocco-Enterprises/notpixel
|
|
8
|
+
Author-email: NotPixel <dev@notpixel.ai>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: Contextual-ads,ads,ai,fastapi,langchain,llm,monetization
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: fastembed<1,>=0.7.0
|
|
22
|
+
Requires-Dist: httpx>=0.25.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# NotPixel Python SDK (`notpixel`)
|
|
28
|
+
|
|
29
|
+
Official Python SDK for **NotPixel** — privacy-first contextual monetization for LLM applications, agents, and backends (**FastAPI, Django, Flask, LangChain, LlamaIndex, CrewAI**).
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install notpixel
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start (Async — FastAPI / LangChain)
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from notpixel import AsyncAds
|
|
41
|
+
|
|
42
|
+
# Privacy=True by default: generates 384-d embeddings locally (BAAI/bge-small-en-v1.5)
|
|
43
|
+
# User prompts NEVER leave your server.
|
|
44
|
+
ads = AsyncAds(publisher_id="pub_10565")
|
|
45
|
+
|
|
46
|
+
# 1. Fetch a contextual ad + run your LLM in parallel (best-effort)
|
|
47
|
+
result = await ads.respond(
|
|
48
|
+
input="How do I optimize PostgreSQL queries with indexes?",
|
|
49
|
+
llm=lambda: call_my_llm("How do I optimize PostgreSQL queries with indexes?"),
|
|
50
|
+
)
|
|
51
|
+
print(result.text) # LLM response + sponsored ad block (if matched)
|
|
52
|
+
|
|
53
|
+
# 2. Track events
|
|
54
|
+
if result.ad:
|
|
55
|
+
await ads.impression(result.ad)
|
|
56
|
+
await ads.track_click(result.ad, metadata={"source": "fastapi_chat"})
|
|
57
|
+
await ads.track_conversion(
|
|
58
|
+
result.ad,
|
|
59
|
+
type="signup",
|
|
60
|
+
value_cents=2900,
|
|
61
|
+
external_id="order_12345",
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Synchronous Usage (Django WSGI / Scripts)
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from notpixel import Ads
|
|
69
|
+
|
|
70
|
+
ads = Ads(publisher_id="pub_10565")
|
|
71
|
+
ad = ads.get_ad(input="Best serverless Postgres database")
|
|
72
|
+
if ad:
|
|
73
|
+
print(ad.text)
|
|
74
|
+
ads.track_click(ad)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Privacy Modes
|
|
78
|
+
|
|
79
|
+
- **`privacy=True` (Default)**: Generates `384`-dimensional embeddings locally using FastEmbed with the same quantized `Xenova/bge-small-en-v1.5` artifact as the TypeScript SDK, attention-mask **mean pooling** and L2 normalization. The built-in FastEmbed BGE entry uses CLS pooling and must not be substituted for this configuration. If local embedding fails, `get_ad()` emits `on_error` and returns `None` — **never** falling back to transmitting raw or sanitized text.
|
|
80
|
+
- **`privacy=False`**: Sanitizes PII (emails, phone numbers), truncates to 256 characters, and sends `mode="standard"` to `https://api.notpixel.ai/api/v1/pick`.
|
|
81
|
+
|
|
82
|
+
## Releases (maintainers)
|
|
83
|
+
|
|
84
|
+
Python releases use their own version and tags (`python-v0.1.0`), independently of npm.
|
|
85
|
+
Keep `pyproject.toml` and `src/notpixel/__init__.py` versions aligned.
|
|
86
|
+
|
|
87
|
+
`.github/workflows/publish-python.yml` builds a wheel and source distribution, checks
|
|
88
|
+
their metadata and contents, and installs the wheel with runtime dependencies to run
|
|
89
|
+
the tests on Python 3.10–3.13. Tests use simulated transports and embeddings; they do
|
|
90
|
+
not call production ad endpoints or download model weights.
|
|
91
|
+
|
|
92
|
+
One-time setup: create the GitHub environment `pypi` and register a pending Trusted
|
|
93
|
+
Publisher in [PyPI account publishing](https://pypi.org/manage/account/publishing/):
|
|
94
|
+
|
|
95
|
+
| Field | Value |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| PyPI project | `notpixel` |
|
|
98
|
+
| GitHub owner | `Cocco-Enterprises` |
|
|
99
|
+
| Repository | `notpixel` |
|
|
100
|
+
| Workflow filename | `publish-python.yml` |
|
|
101
|
+
| Environment | `pypi` |
|
|
102
|
+
|
|
103
|
+
To release an already committed version:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git tag -a python-v0.1.0 -m "notpixel Python 0.1.0"
|
|
107
|
+
git push origin python-v0.1.0
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Only matching `python-v*` tags publish. Pull requests, pushes to `main`, and manual
|
|
111
|
+
runs with `dry_run=true` build and test without uploading. Publishing uses PyPI
|
|
112
|
+
Trusted Publishing (OIDC), with no permanent API token. After publication, confirm
|
|
113
|
+
the version on PyPI and test `pip install notpixel==0.1.0` in a fresh environment.
|
notpixel-0.1.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# NotPixel Python SDK (`notpixel`)
|
|
2
|
+
|
|
3
|
+
Official Python SDK for **NotPixel** — privacy-first contextual monetization for LLM applications, agents, and backends (**FastAPI, Django, Flask, LangChain, LlamaIndex, CrewAI**).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install notpixel
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start (Async — FastAPI / LangChain)
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from notpixel import AsyncAds
|
|
15
|
+
|
|
16
|
+
# Privacy=True by default: generates 384-d embeddings locally (BAAI/bge-small-en-v1.5)
|
|
17
|
+
# User prompts NEVER leave your server.
|
|
18
|
+
ads = AsyncAds(publisher_id="pub_10565")
|
|
19
|
+
|
|
20
|
+
# 1. Fetch a contextual ad + run your LLM in parallel (best-effort)
|
|
21
|
+
result = await ads.respond(
|
|
22
|
+
input="How do I optimize PostgreSQL queries with indexes?",
|
|
23
|
+
llm=lambda: call_my_llm("How do I optimize PostgreSQL queries with indexes?"),
|
|
24
|
+
)
|
|
25
|
+
print(result.text) # LLM response + sponsored ad block (if matched)
|
|
26
|
+
|
|
27
|
+
# 2. Track events
|
|
28
|
+
if result.ad:
|
|
29
|
+
await ads.impression(result.ad)
|
|
30
|
+
await ads.track_click(result.ad, metadata={"source": "fastapi_chat"})
|
|
31
|
+
await ads.track_conversion(
|
|
32
|
+
result.ad,
|
|
33
|
+
type="signup",
|
|
34
|
+
value_cents=2900,
|
|
35
|
+
external_id="order_12345",
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Synchronous Usage (Django WSGI / Scripts)
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from notpixel import Ads
|
|
43
|
+
|
|
44
|
+
ads = Ads(publisher_id="pub_10565")
|
|
45
|
+
ad = ads.get_ad(input="Best serverless Postgres database")
|
|
46
|
+
if ad:
|
|
47
|
+
print(ad.text)
|
|
48
|
+
ads.track_click(ad)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Privacy Modes
|
|
52
|
+
|
|
53
|
+
- **`privacy=True` (Default)**: Generates `384`-dimensional embeddings locally using FastEmbed with the same quantized `Xenova/bge-small-en-v1.5` artifact as the TypeScript SDK, attention-mask **mean pooling** and L2 normalization. The built-in FastEmbed BGE entry uses CLS pooling and must not be substituted for this configuration. If local embedding fails, `get_ad()` emits `on_error` and returns `None` — **never** falling back to transmitting raw or sanitized text.
|
|
54
|
+
- **`privacy=False`**: Sanitizes PII (emails, phone numbers), truncates to 256 characters, and sends `mode="standard"` to `https://api.notpixel.ai/api/v1/pick`.
|
|
55
|
+
|
|
56
|
+
## Releases (maintainers)
|
|
57
|
+
|
|
58
|
+
Python releases use their own version and tags (`python-v0.1.0`), independently of npm.
|
|
59
|
+
Keep `pyproject.toml` and `src/notpixel/__init__.py` versions aligned.
|
|
60
|
+
|
|
61
|
+
`.github/workflows/publish-python.yml` builds a wheel and source distribution, checks
|
|
62
|
+
their metadata and contents, and installs the wheel with runtime dependencies to run
|
|
63
|
+
the tests on Python 3.10–3.13. Tests use simulated transports and embeddings; they do
|
|
64
|
+
not call production ad endpoints or download model weights.
|
|
65
|
+
|
|
66
|
+
One-time setup: create the GitHub environment `pypi` and register a pending Trusted
|
|
67
|
+
Publisher in [PyPI account publishing](https://pypi.org/manage/account/publishing/):
|
|
68
|
+
|
|
69
|
+
| Field | Value |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| PyPI project | `notpixel` |
|
|
72
|
+
| GitHub owner | `Cocco-Enterprises` |
|
|
73
|
+
| Repository | `notpixel` |
|
|
74
|
+
| Workflow filename | `publish-python.yml` |
|
|
75
|
+
| Environment | `pypi` |
|
|
76
|
+
|
|
77
|
+
To release an already committed version:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git tag -a python-v0.1.0 -m "notpixel Python 0.1.0"
|
|
81
|
+
git push origin python-v0.1.0
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Only matching `python-v*` tags publish. Pull requests, pushes to `main`, and manual
|
|
85
|
+
runs with `dry_run=true` build and test without uploading. Publishing uses PyPI
|
|
86
|
+
Trusted Publishing (OIDC), with no permanent API token. After publication, confirm
|
|
87
|
+
the version on PyPI and test `pip install notpixel==0.1.0` in a fresh environment.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "notpixel"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "NotPixel Python SDK — Privacy-first contextual ads for LLM applications (FastAPI, Django, LangChain, CrewAI)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "NotPixel", email = "dev@notpixel.ai" }
|
|
15
|
+
]
|
|
16
|
+
keywords = ["llm", "ads", "monetization", "ai", "Contextual-ads", "fastapi", "langchain"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.25.0",
|
|
29
|
+
"fastembed>=0.7.0,<1",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://notpixel.ai"
|
|
39
|
+
Documentation = "https://docs.notpixel.ai/http-api"
|
|
40
|
+
Repository = "https://github.com/Cocco-Enterprises/notpixel"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/notpixel"]
|
|
44
|
+
core-metadata-version = "2.4"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
include = ["/src", "/tests", "/scripts", "/README.md", "/LICENSE", "/pyproject.toml"]
|
|
48
|
+
core-metadata-version = "2.4"
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Check release archives before upload. Run with Python 3.11+ from any directory."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from email.parser import BytesParser
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import tarfile
|
|
9
|
+
import tomllib
|
|
10
|
+
import zipfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
root = Path(__file__).resolve().parents[1]
|
|
14
|
+
project = tomllib.loads((root / "pyproject.toml").read_text())["project"]
|
|
15
|
+
version = project["version"]
|
|
16
|
+
tree = ast.parse((root / "src/notpixel/__init__.py").read_text())
|
|
17
|
+
code_version = next(ast.literal_eval(n.value) for n in tree.body
|
|
18
|
+
if isinstance(n, ast.Assign)
|
|
19
|
+
and any(isinstance(t, ast.Name) and t.id == "__version__" for t in n.targets))
|
|
20
|
+
assert version == code_version, "Package and runtime versions differ"
|
|
21
|
+
ref = os.environ.get("GITHUB_REF", "")
|
|
22
|
+
if ref.startswith("refs/tags/python-v"):
|
|
23
|
+
assert ref.removeprefix("refs/tags/python-v") == version, "Tag must match package version"
|
|
24
|
+
|
|
25
|
+
wheels = list((root / "dist").glob("*.whl"))
|
|
26
|
+
sdists = list((root / "dist").glob("*.tar.gz"))
|
|
27
|
+
assert len(wheels) == len(sdists) == 1, "Expected exactly one wheel and one sdist"
|
|
28
|
+
with zipfile.ZipFile(wheels[0]) as wheel:
|
|
29
|
+
names = wheel.namelist()
|
|
30
|
+
assert "notpixel/py.typed" in names
|
|
31
|
+
assert "notpixel/client.py" in names and "notpixel/embedding.py" in names
|
|
32
|
+
assert all(n.startswith("notpixel/") or ".dist-info/" in n for n in names)
|
|
33
|
+
metadata = BytesParser().parsebytes(wheel.read(next(n for n in names if n.endswith("/METADATA"))))
|
|
34
|
+
assert metadata["Name"] == project["name"] and metadata["Version"] == version
|
|
35
|
+
assert metadata["Requires-Python"] == project["requires-python"]
|
|
36
|
+
assert metadata["License-Expression"] == "MIT"
|
|
37
|
+
assert any(n.endswith("/licenses/LICENSE") for n in names)
|
|
38
|
+
assert len(metadata.get_all("Requires-Dist", [])) >= len(project["dependencies"])
|
|
39
|
+
with tarfile.open(sdists[0]) as sdist:
|
|
40
|
+
names = [m.name for m in sdist.getmembers() if m.isfile()]
|
|
41
|
+
assert all(n.startswith(f"notpixel-{version}/") for n in names)
|
|
42
|
+
assert any(n.endswith("/LICENSE") for n in names)
|
|
43
|
+
assert any(n.endswith("/src/notpixel/py.typed") for n in names)
|
|
44
|
+
assert not any("/.env" in n or "/__pycache__/" in n for n in names)
|
|
45
|
+
print(f"Validated notpixel {version}: wheel, sdist, metadata, license, typing and release tag")
|
|
@@ -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
|
+
]
|
|
@@ -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=(",", ":"))
|