From 3056502d92b32b565518a015879b3441a2472cd1 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Sat, 21 Mar 2026 15:50:19 +0100 Subject: [PATCH 1/2] Better sdk init devex --- README.md | 9 +++- docs/manual-tracking.md | 5 ++- examples/feedback_example.py | 4 +- examples/gguf_example.py | 5 ++- examples/mlx_example.py | 11 ++--- examples/onnx_example.py | 5 ++- examples/openai_example.py | 10 ++--- examples/tensorflow_example.py | 6 +-- examples/timm_example.py | 5 ++- examples/transformers_example.py | 9 ++-- tests/test_init.py | 72 ++++++++++++++++++++++++++++++++ wildedge/__init__.py | 2 + wildedge/convenience.py | 41 ++++++++++++++++++ 13 files changed, 156 insertions(+), 28 deletions(-) create mode 100644 tests/test_init.py create mode 100644 wildedge/convenience.py diff --git a/README.md b/README.md index 7c10bae..cbe9c81 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/docs/manual-tracking.md b/docs/manual-tracking.md index e3b1615..f90f58a 100644 --- a/docs/manual-tracking.md +++ b/docs/manual-tracking.md @@ -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, diff --git a/examples/feedback_example.py b/examples/feedback_example.py index 4644e9e..ff06292 100644 --- a/examples/feedback_example.py +++ b/examples/feedback_example.py @@ -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() diff --git a/examples/gguf_example.py b/examples/gguf_example.py index 5af3f25..cf58f88 100644 --- a/examples/gguf_example.py +++ b/examples/gguf_example.py @@ -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", diff --git a/examples/mlx_example.py b/examples/mlx_example.py index 1e70b82..61906e0 100644 --- a/examples/mlx_example.py +++ b/examples/mlx_example.py @@ -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 diff --git a/examples/onnx_example.py b/examples/onnx_example.py index e9c4318..e7a6f78 100644 --- a/examples/onnx_example.py +++ b/examples/onnx_example.py @@ -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) diff --git a/examples/openai_example.py b/examples/openai_example.py index 4515d4e..d9410e4 100644 --- a/examples/openai_example.py +++ b/examples/openai_example.py @@ -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 diff --git a/examples/tensorflow_example.py b/examples/tensorflow_example.py index cd6d1b0..3cf1962 100644 --- a/examples/tensorflow_example.py +++ b/examples/tensorflow_example.py @@ -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: @@ -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) diff --git a/examples/timm_example.py b/examples/timm_example.py index ffea6c3..5b4ffa1 100644 --- a/examples/timm_example.py +++ b/examples/timm_example.py @@ -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() diff --git a/examples/transformers_example.py b/examples/transformers_example.py index cff9277..92e8a1d 100644 --- a/examples/transformers_example.py +++ b/examples/transformers_example.py @@ -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}[ diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..f7b121c --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import wildedge + + +def test_init_calls_instrument_for_integrations(monkeypatch): + import wildedge.convenience as convenience + + 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): + import wildedge.convenience as convenience + + 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): + import wildedge.convenience as convenience + + 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"] diff --git a/wildedge/__init__.py b/wildedge/__init__.py index 73f74ef..f1f449b 100644 --- a/wildedge/__init__.py +++ b/wildedge/__init__.py @@ -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, @@ -24,6 +25,7 @@ __all__ = [ "WildEdge", + "init", "capture_hardware", "HardwareContext", "ThermalContext", diff --git a/wildedge/convenience.py b/wildedge/convenience.py new file mode 100644 index 0000000..a9e61aa --- /dev/null +++ b/wildedge/convenience.py @@ -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 From fa770586d7db57973e35d5050a38e19fb5e71be4 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Sat, 21 Mar 2026 15:52:09 +0100 Subject: [PATCH 2/2] cleanup --- tests/test_init.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index f7b121c..3450446 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,10 +1,10 @@ from __future__ import annotations import wildedge +import wildedge.convenience as convenience def test_init_calls_instrument_for_integrations(monkeypatch): - import wildedge.convenience as convenience calls: list[tuple[str | None, list[str] | None]] = [] @@ -28,7 +28,6 @@ def instrument(self, integration, *, hubs=None): def test_init_hubs_only(monkeypatch): - import wildedge.convenience as convenience calls: list[tuple[str | None, list[str] | None]] = [] @@ -51,8 +50,6 @@ def instrument(self, integration, *, hubs=None): def test_init_logs_debug_when_no_integrations_or_hubs(monkeypatch): - import wildedge.convenience as convenience - logs: list[str] = [] class DummyClient: