Implement identity graph stitching and configurable profile storage
Issue persistent signed anonymous IDs and accept cookie/token-bound events. Resolve verified email, phone, OAuth, and namespaced third-party IDs into transactionally merged master identities with historical event aliases. Protect shared-device account switching, tenant isolation, and event retries. Store sparse canonical profiles as PostgreSQL JSONB or local SQLite JSON. Support an optional versioned Cassandra wide-column projection through a transactional outbox, retry worker, and immediate canonical read fallback. Document deployment settings and provide opt-in database integration tests. Validation: 43 local tests passed; 6 external database tests skipped. Ruff checks, formatting, and offline uv lock verification passed.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"""Exercise a running local backend without printing service/anonymous tokens."""
|
||||
|
||||
import argparse
|
||||
import http.cookiejar
|
||||
import json
|
||||
import urllib.request
|
||||
from uuid import uuid4
|
||||
|
||||
from adcloud_server.settings import Settings
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--base-url", default="http://127.0.0.1:8000")
|
||||
args = parser.parse_args()
|
||||
settings = Settings.from_env()
|
||||
if not settings.service_token:
|
||||
raise SystemExit("Configure SERVICE_TOKEN in .env first")
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())
|
||||
)
|
||||
|
||||
|
||||
def api(path, body=None, trusted=False, method=None):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if trusted:
|
||||
headers["Authorization"] = "Bearer " + settings.service_token
|
||||
request = urllib.request.Request(
|
||||
args.base_url.rstrip("/") + path,
|
||||
data=None if body is None else json.dumps(body).encode(),
|
||||
headers=headers,
|
||||
method=method,
|
||||
)
|
||||
with opener.open(request, timeout=10) as response:
|
||||
return json.load(response)
|
||||
|
||||
|
||||
anon = api("/v1/identity/anonymous?rotate=true", {})
|
||||
api("/v1/events", {"name": "product.view", "properties": {"sku": "demo-running-shoe"}})
|
||||
api(
|
||||
"/v1/users/" + anon["master_id"],
|
||||
{"attributes": {"interest": "running", "unused": None}, "tags": ["runner"]},
|
||||
trusted=True,
|
||||
method="PATCH",
|
||||
)
|
||||
# Synthetic verified identity for this local demo, not an OTP implementation.
|
||||
stitched = api(
|
||||
"/v1/identity/stitch",
|
||||
{
|
||||
"anonymous_token": anon["anonymous_token"],
|
||||
"identifiers": [
|
||||
{"kind": "email", "value": "demo-" + uuid4().hex + "@example.com"}
|
||||
],
|
||||
},
|
||||
trusted=True,
|
||||
)
|
||||
profile = api("/v1/users/" + stitched["master_id"], trusted=True)
|
||||
assert profile["attributes"] == {"interest": "running"}
|
||||
assert profile["tags"] == ["runner"] and profile["event_count"] == 1
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"master_id": profile["master_id"],
|
||||
"attributes": profile["attributes"],
|
||||
"tags": profile["tags"],
|
||||
"event_count": profile["event_count"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Create ignored local-development secrets without printing their values."""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
text = (root / ".env.example").read_text()
|
||||
text = text.replace(
|
||||
"IDENTITY_SECRET=\n", "IDENTITY_SECRET=" + secrets.token_urlsafe(48) + "\n"
|
||||
)
|
||||
text = text.replace(
|
||||
"SERVICE_TOKEN=\n", "SERVICE_TOKEN=" + secrets.token_urlsafe(48) + "\n"
|
||||
)
|
||||
try:
|
||||
descriptor = os.open(root / ".env", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
except FileExistsError:
|
||||
raise SystemExit(".env already exists; left unchanged") from None
|
||||
with os.fdopen(descriptor, "w") as output:
|
||||
output.write(text)
|
||||
print("Created local .env with generated secrets. Values are not printed.")
|
||||
Reference in New Issue
Block a user