billdogeng 1.0.0b1__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.
@@ -0,0 +1,48 @@
1
+ # billdogeng-* SDK fleet — build & dependency artifacts (do not commit)
2
+
3
+ # JS / TS (node, react, vue, next, svelte, angular, nuxt, astro, capacitor-eng)
4
+ **/node_modules/
5
+ **/dist/
6
+ **/.next/
7
+ **/.nuxt/
8
+ **/.svelte-kit/
9
+ **/.astro/
10
+ **/*.tsbuildinfo
11
+
12
+ # Python
13
+ **/__pycache__/
14
+ **/.venv/
15
+ **/*.egg-info/
16
+ **/.pytest_cache/
17
+ **/build/
18
+ **/.mypy_cache/
19
+
20
+ # PHP
21
+ php/vendor/
22
+ **/composer.lock
23
+
24
+ # Ruby
25
+ ruby/.bundle/
26
+ **/Gemfile.lock
27
+ **/*.gem
28
+
29
+ # Java / Gradle
30
+ **/.gradle/
31
+ java/build/
32
+ **/*.class
33
+
34
+ # Rust
35
+ rust/target/
36
+ **/Cargo.lock
37
+
38
+ # .NET
39
+ **/bin/
40
+ **/obj/
41
+
42
+ # Elixir
43
+ elixir/_build/
44
+ elixir/deps/
45
+ **/*.ez
46
+
47
+ # Go
48
+ **/*.test
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: billdogeng
3
+ Version: 1.0.0b1
4
+ Summary: Official BilldogEng server SDK for Python — Analytics, Feature Flags (remote + local eval), Surveys, Messaging, and LLM observability.
5
+ Project-URL: Homepage, https://billdog.io
6
+ Project-URL: Repository, https://github.com/billdog/billdogeng-python
7
+ Author: BillDog
8
+ License: MIT
9
+ Keywords: analytics,billdog,billdogeng,feature-flags,llm,messaging,server-sdk,surveys
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.8
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.0; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # billdogeng (Python)
22
+
23
+ Official **BilldogEng** server SDK for Python — the engagement suite for
24
+ server-side use: **Analytics**, **Feature Flags** (remote + local evaluation),
25
+ **Surveys** (data API), **Messaging** dispatch, and **LLM** observability.
26
+
27
+ This is the Python port of the canonical Node reference SDK; the wire protocol
28
+ and method surface are identical across all `billdogeng-*` server SDKs.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install billdogeng
34
+ ```
35
+
36
+ Zero runtime dependencies (stdlib only). Requires Python 3.8+.
37
+
38
+ ## Quickstart
39
+
40
+ ```python
41
+ from billdogeng import BilldogEng
42
+
43
+ bd = BilldogEng("bd_test_xxx", local_evaluation=True)
44
+
45
+ # Analytics (batched + background flush + gzip + retry)
46
+ bd.capture("user-123", "order_completed", {"revenue": 49.99}, {"company": "acme"})
47
+ bd.identify("user-123", {"email": "a@b.com", "plan": "pro"})
48
+ bd.group_identify("company", "acme", {"seats": 50})
49
+ bd.alias("user-123", "anon-abc")
50
+
51
+ # Feature flags (local evaluation = deterministic murmurhash3 bucketing)
52
+ if bd.is_feature_enabled("new_checkout", "user-123"):
53
+ ...
54
+ variant = bd.get_feature_flag("pricing_test", "user-123")
55
+ payload = bd.get_feature_flag_payload("pricing_test", "user-123")
56
+ all_flags = bd.get_all_flags("user-123")
57
+
58
+ # Surveys (data API — no rendering)
59
+ surveys = bd.surveys.list("user-123")
60
+ config = bd.surveys.fetch(survey_id, "user-123")
61
+ started = bd.surveys.start(survey_id, {"customer_id": "user-123"})
62
+ bd.surveys.submit(survey_id, [{"question_id": q, "answer_number": 9}],
63
+ {"respondent_id": started["respondent_id"]})
64
+
65
+ # Messaging dispatch (Bearer JWT auth)
66
+ bd.messaging.dispatch(
67
+ project_id="…", channel="push",
68
+ content={"title": "Hi", "body": "There"},
69
+ access_token="<supabase-jwt>",
70
+ targeting={"type": "all"},
71
+ scheduling={"deliveryType": "immediate"},
72
+ )
73
+
74
+ # LLM observability
75
+ bd.llm.capture_trace(
76
+ trace_id="t-1", span_id="s-1", model="claude-opus-4-8",
77
+ input_text="…", output_text="…",
78
+ prompt_tokens=10, completion_tokens=5, duration_ms=123, cost_usd=0.002,
79
+ )
80
+
81
+ bd.shutdown() # flushes remaining events + stops the background timer
82
+
83
+ # Or as a context manager:
84
+ with BilldogEng("bd_test_xxx") as bd:
85
+ bd.capture("user-123", "page_view")
86
+ ```
87
+
88
+ ## Configuration
89
+
90
+ | Option | Default | Description |
91
+ | --- | --- | --- |
92
+ | `host` | `https://api.billdog.io/v1` | Base URL for all requests |
93
+ | `flush_at` | `20` | Batch size that triggers a flush |
94
+ | `flush_interval` | `10000` | Background flush cadence (ms) |
95
+ | `max_queue_size` | `1000` | Max queued events before oldest are dropped |
96
+ | `gzip` | `True` | Gzip request bodies large enough to benefit |
97
+ | `local_evaluation` | `False` | Enable local (server-side) flag evaluation |
98
+ | `request_timeout` | `10000` | Per-request timeout (ms) |
99
+ | `max_retries` | `3` | Retry attempts for 5xx / network errors |
100
+ | `group_type_index` | `None` | Stable group-type → positional index map |
101
+
102
+ Auth: `x-api-key: <apiKey>` on every request (`bd_test_*` sandbox /
103
+ `bd_live_*` live). Messaging dispatch authenticates with a Bearer JWT instead.
104
+
105
+ ## Tests
106
+
107
+ ```bash
108
+ python3 -m venv .venv && . .venv/bin/activate
109
+ pip install -e ".[dev]"
110
+ pytest -q
111
+ ```
112
+
113
+ The suite covers: batching (10 captures → 1 POST), identify/group/alias event
114
+ shapes, the 12 canonical murmurhash3 vectors + rollout/targeting/multivariate
115
+ evaluation, 503-then-200 retry with backoff (local stub server), and the
116
+ survey round-trip + messaging dispatch + LLM trace payload shapes.
@@ -0,0 +1,96 @@
1
+ # billdogeng (Python)
2
+
3
+ Official **BilldogEng** server SDK for Python — the engagement suite for
4
+ server-side use: **Analytics**, **Feature Flags** (remote + local evaluation),
5
+ **Surveys** (data API), **Messaging** dispatch, and **LLM** observability.
6
+
7
+ This is the Python port of the canonical Node reference SDK; the wire protocol
8
+ and method surface are identical across all `billdogeng-*` server SDKs.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install billdogeng
14
+ ```
15
+
16
+ Zero runtime dependencies (stdlib only). Requires Python 3.8+.
17
+
18
+ ## Quickstart
19
+
20
+ ```python
21
+ from billdogeng import BilldogEng
22
+
23
+ bd = BilldogEng("bd_test_xxx", local_evaluation=True)
24
+
25
+ # Analytics (batched + background flush + gzip + retry)
26
+ bd.capture("user-123", "order_completed", {"revenue": 49.99}, {"company": "acme"})
27
+ bd.identify("user-123", {"email": "a@b.com", "plan": "pro"})
28
+ bd.group_identify("company", "acme", {"seats": 50})
29
+ bd.alias("user-123", "anon-abc")
30
+
31
+ # Feature flags (local evaluation = deterministic murmurhash3 bucketing)
32
+ if bd.is_feature_enabled("new_checkout", "user-123"):
33
+ ...
34
+ variant = bd.get_feature_flag("pricing_test", "user-123")
35
+ payload = bd.get_feature_flag_payload("pricing_test", "user-123")
36
+ all_flags = bd.get_all_flags("user-123")
37
+
38
+ # Surveys (data API — no rendering)
39
+ surveys = bd.surveys.list("user-123")
40
+ config = bd.surveys.fetch(survey_id, "user-123")
41
+ started = bd.surveys.start(survey_id, {"customer_id": "user-123"})
42
+ bd.surveys.submit(survey_id, [{"question_id": q, "answer_number": 9}],
43
+ {"respondent_id": started["respondent_id"]})
44
+
45
+ # Messaging dispatch (Bearer JWT auth)
46
+ bd.messaging.dispatch(
47
+ project_id="…", channel="push",
48
+ content={"title": "Hi", "body": "There"},
49
+ access_token="<supabase-jwt>",
50
+ targeting={"type": "all"},
51
+ scheduling={"deliveryType": "immediate"},
52
+ )
53
+
54
+ # LLM observability
55
+ bd.llm.capture_trace(
56
+ trace_id="t-1", span_id="s-1", model="claude-opus-4-8",
57
+ input_text="…", output_text="…",
58
+ prompt_tokens=10, completion_tokens=5, duration_ms=123, cost_usd=0.002,
59
+ )
60
+
61
+ bd.shutdown() # flushes remaining events + stops the background timer
62
+
63
+ # Or as a context manager:
64
+ with BilldogEng("bd_test_xxx") as bd:
65
+ bd.capture("user-123", "page_view")
66
+ ```
67
+
68
+ ## Configuration
69
+
70
+ | Option | Default | Description |
71
+ | --- | --- | --- |
72
+ | `host` | `https://api.billdog.io/v1` | Base URL for all requests |
73
+ | `flush_at` | `20` | Batch size that triggers a flush |
74
+ | `flush_interval` | `10000` | Background flush cadence (ms) |
75
+ | `max_queue_size` | `1000` | Max queued events before oldest are dropped |
76
+ | `gzip` | `True` | Gzip request bodies large enough to benefit |
77
+ | `local_evaluation` | `False` | Enable local (server-side) flag evaluation |
78
+ | `request_timeout` | `10000` | Per-request timeout (ms) |
79
+ | `max_retries` | `3` | Retry attempts for 5xx / network errors |
80
+ | `group_type_index` | `None` | Stable group-type → positional index map |
81
+
82
+ Auth: `x-api-key: <apiKey>` on every request (`bd_test_*` sandbox /
83
+ `bd_live_*` live). Messaging dispatch authenticates with a Bearer JWT instead.
84
+
85
+ ## Tests
86
+
87
+ ```bash
88
+ python3 -m venv .venv && . .venv/bin/activate
89
+ pip install -e ".[dev]"
90
+ pytest -q
91
+ ```
92
+
93
+ The suite covers: batching (10 captures → 1 POST), identify/group/alias event
94
+ shapes, the 12 canonical murmurhash3 vectors + rollout/targeting/multivariate
95
+ evaluation, 503-then-200 retry with backoff (local stub server), and the
96
+ survey round-trip + messaging dispatch + LLM trace payload shapes.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "billdogeng"
7
+ version = "1.0.0-beta.1"
8
+ description = "Official BilldogEng server SDK for Python — Analytics, Feature Flags (remote + local eval), Surveys, Messaging, and LLM observability."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "BillDog" }]
12
+ requires-python = ">=3.8"
13
+ keywords = [
14
+ "billdog",
15
+ "billdogeng",
16
+ "analytics",
17
+ "feature-flags",
18
+ "surveys",
19
+ "messaging",
20
+ "llm",
21
+ "server-sdk",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 5 - Production/Stable",
25
+ "Intended Audience :: Developers",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3 :: Only",
29
+ "Topic :: Software Development :: Libraries",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.urls]
34
+ Homepage = "https://billdog.io"
35
+ Repository = "https://github.com/billdog/billdogeng-python"
36
+
37
+ [project.optional-dependencies]
38
+ dev = ["pytest>=7.0"]
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/billdogeng"]
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ include = ["src/billdogeng", "README.md", "tests"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ addopts = "-q"
@@ -0,0 +1,51 @@
1
+ """billdogeng -- Official BilldogEng server SDK for Python.
2
+
3
+ Engagement suite: Analytics + Feature Flags (remote + local eval) + Surveys +
4
+ Messaging + LLM observability.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .analytics import Analytics
10
+ from .client import BilldogEng
11
+ from .errors import BilldogEngError
12
+ from .flags import Flags
13
+ from .llm import Llm
14
+ from .messaging import Messaging
15
+ from .murmur import bucket_of, murmurhash3
16
+ from .surveys import Surveys
17
+ from .types import (
18
+ FeatureFlagDefinition,
19
+ FlagVariant,
20
+ Groups,
21
+ LlmTraceParams,
22
+ MessagingScheduling,
23
+ MessagingTargeting,
24
+ Properties,
25
+ SurveyContext,
26
+ TargetingRule,
27
+ )
28
+
29
+ __version__ = "1.0.0"
30
+
31
+ __all__ = [
32
+ "BilldogEng",
33
+ "BilldogEngError",
34
+ "murmurhash3",
35
+ "bucket_of",
36
+ "Analytics",
37
+ "Flags",
38
+ "Surveys",
39
+ "Messaging",
40
+ "Llm",
41
+ "FeatureFlagDefinition",
42
+ "FlagVariant",
43
+ "TargetingRule",
44
+ "SurveyContext",
45
+ "MessagingTargeting",
46
+ "MessagingScheduling",
47
+ "LlmTraceParams",
48
+ "Properties",
49
+ "Groups",
50
+ "__version__",
51
+ ]
@@ -0,0 +1,174 @@
1
+ """Analytics client: capture/identify/groupIdentify/alias + batching.
2
+
3
+ Mirrors the Node reference ``analytics.ts``. Events accumulate in an in-memory
4
+ queue and are flushed as a single batched POST to ``/ingest-events`` when:
5
+ - the queue reaches ``flush_at``,
6
+ - the background timer fires every ``flush_interval``, or
7
+ - ``flush()`` / ``shutdown()`` is called.
8
+
9
+ A failed flush re-queues its batch (front of the queue) so events are never
10
+ lost on a single transient failure; the :class:`Transport` also retries with
11
+ backoff before a flush is considered failed.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import threading
17
+ import time
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from .transport import Transport
21
+ from .types import Groups, Properties
22
+
23
+
24
+ class Analytics:
25
+ def __init__(self, transport: Transport, api_key: str, options: Dict[str, Any]) -> None:
26
+ self._transport = transport
27
+ self._api_key = api_key
28
+ self._opts = options
29
+ self._queue: List[Dict[str, Any]] = []
30
+ self._queue_lock = threading.Lock()
31
+ #: Serializes flushes so re-queued batches don't interleave.
32
+ self._flush_lock = threading.Lock()
33
+ self._timer: Optional[threading.Timer] = None
34
+ self._stopped = False
35
+ self._start_timer()
36
+
37
+ def _log(self, *args: Any) -> None:
38
+ if self._opts.get("enable_logging"):
39
+ print("[BilldogEng:analytics]", *args)
40
+
41
+ # --- Public API ---------------------------------------------------------
42
+
43
+ def capture(
44
+ self,
45
+ distinct_id: str,
46
+ event: str,
47
+ properties: Optional[Properties] = None,
48
+ groups: Optional[Groups] = None,
49
+ ) -> None:
50
+ self._enqueue(
51
+ distinct_id, event, self._with_groups(properties or {}, groups or {})
52
+ )
53
+
54
+ def identify(self, distinct_id: str, properties: Optional[Properties] = None) -> None:
55
+ props = properties or {}
56
+ merged: Properties = {"$set": props}
57
+ merged.update(props)
58
+ self._enqueue(distinct_id, "$identify", merged)
59
+
60
+ def group_identify(
61
+ self, group_type: str, group_key: str, properties: Optional[Properties] = None
62
+ ) -> None:
63
+ self._enqueue(
64
+ group_key,
65
+ "$groupidentify",
66
+ {
67
+ "$group_type": group_type,
68
+ "$group_key": group_key,
69
+ "$group_set": properties or {},
70
+ },
71
+ )
72
+
73
+ def alias(self, distinct_id: str, alias: str) -> None:
74
+ self._enqueue(distinct_id, "$create_alias", {"alias": alias, "distinct_id": distinct_id})
75
+
76
+ def flush(self) -> None:
77
+ """Force-flush the queue immediately.
78
+
79
+ Raises :class:`BilldogEngError` if the flush ultimately fails (after
80
+ retries); the batch is re-queued in that case.
81
+ """
82
+ with self._flush_lock:
83
+ self._do_flush()
84
+
85
+ def stop_timer(self) -> None:
86
+ self._stopped = True
87
+ if self._timer is not None:
88
+ self._timer.cancel()
89
+ self._timer = None
90
+
91
+ def shutdown(self) -> None:
92
+ self.stop_timer()
93
+ self.flush()
94
+
95
+ @property
96
+ def queue_length(self) -> int:
97
+ with self._queue_lock:
98
+ return len(self._queue)
99
+
100
+ # --- Internals ----------------------------------------------------------
101
+
102
+ def _start_timer(self) -> None:
103
+ interval = self._opts.get("flush_interval", 10000) / 1000.0
104
+ self._timer = threading.Timer(interval, self._on_timer)
105
+ self._timer.daemon = True # don't keep the process alive
106
+ self._timer.start()
107
+
108
+ def _on_timer(self) -> None:
109
+ try:
110
+ with self._flush_lock:
111
+ self._do_flush()
112
+ except Exception as e: # noqa: BLE001 - background flush must not crash
113
+ self._log("background flush failed:", e)
114
+ finally:
115
+ if not self._stopped:
116
+ self._start_timer()
117
+
118
+ def _with_groups(self, properties: Properties, groups: Groups) -> Properties:
119
+ if not groups:
120
+ return dict(properties)
121
+ out: Properties = dict(properties)
122
+ out["$groups"] = dict(groups)
123
+ index_map = self._opts.get("group_type_index")
124
+ if index_map:
125
+ for gtype, gkey in groups.items():
126
+ idx = index_map.get(gtype)
127
+ if isinstance(idx, int) and 0 <= idx <= 4:
128
+ out[f"$group_{idx}"] = gkey
129
+ return out
130
+
131
+ def _enqueue(self, distinct_id: str, event_name: str, properties: Properties) -> None:
132
+ wire_event = {
133
+ "event_name": event_name,
134
+ "event_timestamp": int(time.time() * 1000),
135
+ "properties": properties,
136
+ "user_id": distinct_id,
137
+ }
138
+ should_flush = False
139
+ with self._queue_lock:
140
+ self._queue.append(wire_event)
141
+ max_size = self._opts.get("max_queue_size", 1000)
142
+ if len(self._queue) > max_size:
143
+ dropped = len(self._queue) - max_size
144
+ del self._queue[0:dropped]
145
+ self._log(f"max queue size exceeded, dropped {dropped} oldest event(s)")
146
+ if len(self._queue) >= self._opts.get("flush_at", 20):
147
+ should_flush = True
148
+
149
+ if should_flush:
150
+ try:
151
+ self.flush()
152
+ except Exception as e: # noqa: BLE001 - auto-flush must not crash caller
153
+ self._log("auto-flush failed:", e)
154
+
155
+ def _do_flush(self) -> None:
156
+ with self._queue_lock:
157
+ if not self._queue:
158
+ return
159
+ batch = self._queue
160
+ self._queue = []
161
+
162
+ body = {"api_key": self._api_key, "events": batch}
163
+
164
+ try:
165
+ self._transport.request(
166
+ "/ingest-events", body=body, headers={"x-api-key": self._api_key}
167
+ )
168
+ self._log(f"flushed {len(batch)} event(s)")
169
+ except Exception as err:
170
+ # Re-queue the batch at the front so it is retried on the next flush.
171
+ with self._queue_lock:
172
+ self._queue = batch + self._queue
173
+ self._log(f"flush failed, re-queued {len(batch)} event(s): {err}")
174
+ raise