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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,19 @@ Useful flags:
```python
import wildedge

client = wildedge.WildEdge(dsn="...") # or WILDEDGE_DSN env var
client.instrument("transformers", hubs=["huggingface"])
client = wildedge.init(
dsn="...", # or WILDEDGE_DSN env var
integrations=["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.

`init(...)` is a convenience wrapper for `WildEdge(...)` + `instrument(...)`.

## Supported integrations

**On-device**
Expand Down
5 changes: 4 additions & 1 deletion docs/manual-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ Every model needs a handle before you can track events against it. Pass the mode
```python
import wildedge

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

# Optional: enable auto-instrumentation alongside manual tracking.
# client = wildedge.init(integrations=["transformers"], hubs=["huggingface"])

handle = client.register_model(
my_model,
Expand Down
4 changes: 2 additions & 2 deletions examples/feedback_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@

CONFIDENCE_THRESHOLD = 0.6

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

model = timm.create_model("resnet18", pretrained=True)
model.eval()
Expand Down
5 changes: 3 additions & 2 deletions examples/gguf_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@

import wildedge

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

model_path = hf_hub_download(
"bartowski/Llama-3.2-1B-Instruct-GGUF",
Expand Down
11 changes: 6 additions & 5 deletions examples/mlx_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ def main() -> None:
)
args = parser.parse_args()

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

print(f"\nLoading {args.model} ...")
model, tokenizer = mlx_lm.load(args.model) # load + download tracked automatically
Expand Down
5 changes: 3 additions & 2 deletions examples/onnx_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@

import wildedge

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

model_path = hf_hub_download("Xenova/resnet-50", "onnx/model.onnx")
session = ort.InferenceSession(model_path)
Expand Down
10 changes: 5 additions & 5 deletions examples/openai_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
inference tracking happens automatically for every chat.completions.create call.

Run with: uv run openai_example.py
Requires: WILDEDGE_DSN and OPENAI_API_KEY environment variables.
Requires: OPENAI_API_KEY environment variable. Set WILDEDGE_DSN to send events.
"""

from openai import OpenAI

import wildedge

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

openai_client = OpenAI() # set OPENAI_API_KEY env var or pass api_key= explicitly

Expand Down
6 changes: 3 additions & 3 deletions examples/tensorflow_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@

import wildedge

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


def build_and_save_model(save_path: Path) -> None:
Expand All @@ -40,7 +40,7 @@ def build_and_save_model(save_path: Path) -> None:
model_path = Path(temp_dir) / "demo_model.keras"
build_and_save_model(model_path)

# load_model is auto-instrumented by client.instrument("tensorflow")
# load_model is auto-instrumented by init(..., integrations="tensorflow")
loaded = tf.keras.models.load_model(model_path)

batch = np.random.randn(4, 16).astype(np.float32)
Expand Down
5 changes: 3 additions & 2 deletions examples/timm_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@

import wildedge

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

model = timm.create_model("resnet18", pretrained=True)
model.eval()
Expand Down
9 changes: 5 additions & 4 deletions examples/transformers_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@ def main() -> None:
)
args = parser.parse_args()

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

print()
{"classify": run_classify, "generate": run_generate, "embed": run_embed}[
Expand Down
69 changes: 69 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from __future__ import annotations

import wildedge
import wildedge.convenience as convenience


def test_init_calls_instrument_for_integrations(monkeypatch):

calls: list[tuple[str | None, list[str] | None]] = []

class DummyClient:
def __init__(self, **kwargs):
self.kwargs = kwargs

def instrument(self, integration, *, hubs=None):
calls.append((integration, hubs))

monkeypatch.setattr(convenience, "WildEdge", DummyClient)

client = wildedge.init(
dsn="https://secret@ingest.wildedge.dev/key",
integrations=["onnx", "timm"],
hubs=["huggingface"],
)

assert isinstance(client, DummyClient)
assert calls == [("onnx", ["huggingface"]), ("timm", ["huggingface"])]


def test_init_hubs_only(monkeypatch):

calls: list[tuple[str | None, list[str] | None]] = []

class DummyClient:
def __init__(self, **kwargs):
self.kwargs = kwargs

def instrument(self, integration, *, hubs=None):
calls.append((integration, hubs))

monkeypatch.setattr(convenience, "WildEdge", DummyClient)

client = wildedge.init(
dsn="https://secret@ingest.wildedge.dev/key",
hubs=["huggingface"],
)

assert isinstance(client, DummyClient)
assert calls == [(None, ["huggingface"])]


def test_init_logs_debug_when_no_integrations_or_hubs(monkeypatch):
logs: list[str] = []

class DummyClient:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.debug = True

def instrument(self, integration, *, hubs=None):
raise AssertionError("instrument should not be called")

monkeypatch.setattr(convenience, "WildEdge", DummyClient)
monkeypatch.setattr(convenience.logger, "debug", lambda msg: logs.append(msg))

client = wildedge.init(dsn="https://secret@ingest.wildedge.dev/key")

assert isinstance(client, DummyClient)
assert logs == ["wildedge: init called without integrations or hubs"]
2 changes: 2 additions & 0 deletions wildedge/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""WildEdge Python SDK."""

from wildedge.client import WildEdge
from wildedge.convenience import init
from wildedge.decorators import track
from wildedge.events import (
AdapterDownload,
Expand All @@ -24,6 +25,7 @@

__all__ = [
"WildEdge",
"init",
"capture_hardware",
"HardwareContext",
"ThermalContext",
Expand Down
41 changes: 41 additions & 0 deletions wildedge/convenience.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

from collections.abc import Iterable
from typing import Any

from wildedge.client import WildEdge
from wildedge.logging import logger


def _normalize_list(value: str | Iterable[str] | None) -> list[str]:
if value is None:
return []
if isinstance(value, str):
return [value]
return [item for item in value if item]


def init(
*,
integrations: str | Iterable[str] | None = None,
hubs: str | Iterable[str] | None = None,
**kwargs: Any,
) -> WildEdge:
"""
Convenience initializer: construct a WildEdge client and instrument integrations.

Additional keyword arguments are forwarded to WildEdge(...).
"""
client = WildEdge(**kwargs)
normalized_integrations = _normalize_list(integrations)
normalized_hubs = _normalize_list(hubs)

if normalized_integrations:
for integration in normalized_integrations:
client.instrument(integration, hubs=normalized_hubs or None)
elif normalized_hubs:
client.instrument(None, hubs=normalized_hubs)
elif getattr(client, "debug", False):
logger.debug("wildedge: init called without integrations or hubs")

return client
Loading