Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ client.instrument("transformers", hubs=["huggingface"])
# models loaded after this point are tracked automatically
```

If no DSN is configured, the client becomes a no-op and logs a warning.

## Supported integrations

**On-device**
Expand Down Expand Up @@ -90,7 +92,7 @@ For unsupported frameworks, see [Manual tracking](https://github.com/wild-edge/w

| Parameter | Default | Description |
|---|---|---|
| `dsn` | - | `https://<secret>@ingest.wildedge.dev/<key>` (or `WILDEDGE_DSN`) |
| `dsn` | - | `https://<secret>@ingest.wildedge.dev/<key>` (or `WILDEDGE_DSN`). If unset, the client is a no-op. |
| `app_version` | `None` | Your app's version string |
| `app_identity` | `<project_key>` | Namespace for offline persistence. Set per-app in multi-process workloads (or `WILDEDGE_APP_IDENTITY`) |
| `enable_offline_persistence` | `true` | Persist unsent events to disk and replay on restart |
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Full reference for all `WildEdge` client parameters.

| Parameter | Default | Env var | Description |
|---|---|---|---|
| `dsn` | - | `WILDEDGE_DSN` | `https://<secret>@ingest.wildedge.dev/<key>` |
| `dsn` | - | `WILDEDGE_DSN` | `https://<secret>@ingest.wildedge.dev/<key>`. If unset, the client is a no-op. |
| `app_version` | `None` | - | Your app's version string |
| `app_identity` | `<project_key>` | `WILDEDGE_APP_IDENTITY` | Namespace for offline persistence. Set per-app in multi-process workloads |
| `enable_offline_persistence` | `true` | - | Persist unsent events to disk and replay on restart |
Expand Down
2 changes: 1 addition & 1 deletion docs/manual-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Every model needs a handle before you can track events against it. Pass the mode
```python
import wildedge

client = wildedge.WildEdge() # set WILDEDGE_DSN env var
client = wildedge.WildEdge() # uses WILDEDGE_DSN if set; otherwise no-op

handle = client.register_model(
my_model,
Expand Down
2 changes: 1 addition & 1 deletion examples/feedback_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
CONFIDENCE_THRESHOLD = 0.6

client = wildedge.WildEdge(
app_version="1.0.0", # set WILDEDGE_DSN env var
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
)
client.instrument("timm")

Expand Down
2 changes: 1 addition & 1 deletion examples/gguf_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import wildedge

client = wildedge.WildEdge(
app_version="1.0.0", # set WILDEDGE_DSN env var
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
)
client.instrument("gguf", hubs=["huggingface"])

Expand Down
2 changes: 1 addition & 1 deletion examples/gguf_gemma_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
FILE = "gemma-2-2b-it-Q4_K_M.gguf"

client = wildedge.WildEdge(
app_version="1.0.0", # set WILDEDGE_DSN env var
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
)

# --- Download ---
Expand Down
2 changes: 1 addition & 1 deletion examples/keras_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
exit(1)

client = wildedge.WildEdge(
app_version="1.0.0", # set WILDEDGE_DSN env var
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
)

# Load a pre-trained MobileNetV2 model using client to track construction and lifecycle
Expand Down
4 changes: 3 additions & 1 deletion examples/mlx_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def main() -> None:

# instrument() patches mlx_lm.load and mlx_lm.generate; must be called
# before any model is loaded.
client = wildedge.WildEdge(app_version="1.0.0") # set WILDEDGE_DSN env var
client = wildedge.WildEdge(
app_version="1.0.0"
) # uses WILDEDGE_DSN if set; otherwise no-op
client.instrument("mlx", hubs=["huggingface"])

print(f"\nLoading {args.model} ...")
Expand Down
2 changes: 1 addition & 1 deletion examples/onnx_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import wildedge

client = wildedge.WildEdge(
app_version="1.0.0", # set WILDEDGE_DSN env var
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
)
client.instrument("onnx", hubs=["huggingface"])

Expand Down
4 changes: 3 additions & 1 deletion examples/openai_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

import wildedge

client = wildedge.WildEdge(app_version="1.0.0") # set WILDEDGE_DSN env var
client = wildedge.WildEdge(
app_version="1.0.0"
) # uses WILDEDGE_DSN if set; otherwise no-op
client.instrument("openai")

openai_client = OpenAI() # set OPENAI_API_KEY env var or pass api_key= explicitly
Expand Down
2 changes: 1 addition & 1 deletion examples/pytorch_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import wildedge

client = wildedge.WildEdge(
app_version="1.0.0", # set WILDEDGE_DSN env var
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
)


Expand Down
2 changes: 1 addition & 1 deletion examples/tensorflow_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import wildedge

client = wildedge.WildEdge(
app_version="1.0.0", # set WILDEDGE_DSN env var
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
)
client.instrument("tensorflow")

Expand Down
2 changes: 1 addition & 1 deletion examples/timm_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import wildedge

client = wildedge.WildEdge(
app_version="1.0.0", # set WILDEDGE_DSN env var
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
)
client.instrument("timm", hubs=["huggingface", "torchhub"])

Expand Down
4 changes: 3 additions & 1 deletion examples/transformers_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ def main() -> None:
)
args = parser.parse_args()

client = wildedge.WildEdge(app_version="1.0.0") # set WILDEDGE_DSN env var
client = wildedge.WildEdge(
app_version="1.0.0"
) # uses WILDEDGE_DSN if set; otherwise no-op
client.instrument("transformers", hubs=["huggingface"])

print()
Expand Down
32 changes: 32 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,38 @@ def test_batch_size_too_low():
WildEdge(dsn="https://test@test.com/key", batch_size=0)


def test_no_dsn_is_noop(monkeypatch, caplog):
from wildedge.client import WildEdge

monkeypatch.delenv(constants.ENV_DSN, raising=False)
with caplog.at_level("WARNING"):
client = WildEdge()

assert client.noop is True
assert client.closed is True
assert "no DSN configured" in caplog.text


def test_no_dsn_instrument_does_not_raise(monkeypatch):
from wildedge.client import WildEdge

monkeypatch.delenv(constants.ENV_DSN, raising=False)
client = WildEdge()

# No-DSN client should ignore instrument() entirely (even for unknown integration).
client.instrument("definitely-not-real")


def test_no_dsn_publish_does_not_enqueue(monkeypatch):
from wildedge.client import WildEdge

monkeypatch.delenv(constants.ENV_DSN, raising=False)
client = WildEdge()

client.publish({"event_type": "inference", "model_id": "m"})
client.queue.add.assert_not_called()


def test_batch_size_too_high():
from wildedge.client import WildEdge

Expand Down
47 changes: 43 additions & 4 deletions wildedge/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@
)


class _NoopConsumer:
def flush(self, timeout: float = 0) -> None:
return None

def close(self, timeout: float | None = None) -> None:
return None


def parse_dsn(dsn: str) -> tuple[str, str, str]:
"""Parse DSN into (secret, host, project_key)."""
parsed = urlparse(dsn)
Expand Down Expand Up @@ -153,10 +161,12 @@ def __init__(
):
env = read_client_env(dsn=dsn, debug=debug, app_identity=app_identity)
dsn = env.dsn
debug = env.debug
self.noop = False
if not dsn:
raise ValueError(ERROR_DSN_REQUIRED)
self._init_noop(debug=debug, device=device)
return
api_key, host, project_key = parse_dsn(dsn)
debug = env.debug
app_identity = resolve_app_identity(
explicit=env.app_identity,
project_key=project_key,
Expand Down Expand Up @@ -258,8 +268,32 @@ def __init__(
if debug:
logger.debug("wildedge: client initialized (session=%s)", self.session_id)

def _init_noop(self, *, debug: bool, device: DeviceInfo | None) -> None:
self.noop = True
self.debug = debug
self.closed = True
logger.warning(
"wildedge: no DSN configured; client is disabled (events will be dropped)"
)
self.api_key = None
self.device = device
self.session_id = str(uuid.uuid4())
self.created_at = datetime.now(timezone.utc)
self.queue = EventQueue(
max_size=1,
policy=QueuePolicy.OPPORTUNISTIC,
persist_to_disk=False,
)
self.registry = ModelRegistry(persist_path=None)
self.transmitter = None
self.dead_letter_store = None
self.consumer = _NoopConsumer()
self.auto_loaded = set()
self._auto_load_finalizers = {}
self.hub_trackers = {}

def publish(self, event_dict: dict) -> None:
if self.closed:
if self.closed or self.noop:
return

if self.debug:
Expand Down Expand Up @@ -472,6 +506,10 @@ def instrument(
Each integration or hub tracker is installed at most once per process
regardless of how many times ``instrument()`` is called.
"""
if self.noop:
if self.debug:
logger.debug("wildedge: instrument skipped (no DSN configured)")
return
if integration is None:
if not hubs:
raise ValueError(
Expand Down Expand Up @@ -658,7 +696,8 @@ def flush(self, timeout: float = 5.0) -> None:
def close(self, timeout: float | None = None) -> None:
"""Best-effort shutdown; pass timeout to attempt bounded flush first."""
self.closed = True
stop_sampler()
if not self.noop:
stop_sampler()
if timeout is None:
self.consumer.close()
else:
Expand Down
Loading