Skip to content
Open
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
19 changes: 14 additions & 5 deletions .github/workflows/dependency-testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ on:
branches: [main]
paths:
- 'pyproject.toml'
- 'packages/**'
- 'requirements/**'
- 'validmind/**'
- 'tests/**'
pull_request:
branches: ['*']
paths:
- 'pyproject.toml'
- 'packages/**'
- 'requirements/**'
- 'validmind/**'
- 'tests/**'
Expand Down Expand Up @@ -68,9 +70,9 @@ jobs:
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: Build wheel and sdist
- name: Build wheels and sdists
run: |
python -m build
uv build --all-packages

- name: Install and test via pip artifacts (${{ matrix.deps-type }})
run: |
Expand All @@ -82,7 +84,9 @@ jobs:
if [[ "${{ matrix.deps-type }}" == "core" ]]; then
# Exercise the unbounded core dependency set separately from the
# constrained optional extras (for example, latest scikit-learn).
WHEEL=$(ls dist/*.whl | head -n 1)
WHEEL=$(ls dist/validmind-*.whl | head -n 1)
CORE_WHEEL=$(ls dist/validmind_tracking_core*.whl | head -n 1)
python -m pip install "${CORE_WHEEL}"
# Jinja2 is currently imported by the library but is only present
# transitively through optional extras. Install it as test tooling
# so this leg can exercise the otherwise unbounded core set.
Expand All @@ -95,7 +99,9 @@ jobs:
| cat
elif [[ "${{ matrix.deps-type }}" == "default" ]]; then
# Install only from built artifacts (let pip resolve deps normally)
WHEEL=$(ls dist/*.whl | head -n 1)
WHEEL=$(ls dist/validmind-*.whl | head -n 1)
CORE_WHEEL=$(ls dist/validmind_tracking_core*.whl | head -n 1)
python -m pip install "${CORE_WHEEL}"
python -m pip install "${WHEEL}[all]"
pip check
python -m tests.test_unit_tests | cat
Expand All @@ -105,7 +111,9 @@ jobs:
uv pip compile pyproject.toml -p "${{ matrix.python-version }}" --all-extras --no-emit-index-url --no-annotate --no-strip-markers --output-file "$OUT" --upgrade
# Install constraints then the wheel without reinstalling deps
pip install -r "$OUT"
WHEEL=$(ls dist/*.whl | head -n 1)
WHEEL=$(ls dist/validmind-*.whl | head -n 1)
CORE_WHEEL=$(ls dist/validmind_tracking_core*.whl | head -n 1)
pip install "${CORE_WHEEL}" --no-deps
python -m pip install "${WHEEL}[all]" --no-deps
pip check
python -m tests.test_unit_tests | cat
Expand Down Expand Up @@ -146,6 +154,7 @@ jobs:
fi
python -m pip install --upgrade pip
pip install -r "$FREEZE_FILE"
python -m pip install packages/validmind-tracking-core --no-deps
python -m pip install . --no-deps
python -m tests.test_unit_tests | cat

Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,19 @@ jobs:
sudo apt install r-base r-base-dev

- name: Build the package
run: uv build
run: uv build --all-packages

- name: Remove Build Environment
run: rm -rf .venv

- name: 'Setup Virtual Environment for [all]'
run: python -m venv all-venv

# This proves that the [all] install target works
# This proves that the [all] install target works
- name: 'Install Built Package for [all]'
run: all-venv/bin/pip install --no-cache-dir "$(ls dist/validmind*.whl | head -n 1)[all]"
run: |
all-venv/bin/pip install --no-cache-dir "$(ls dist/validmind_tracking_core*.whl | head -n 1)"
all-venv/bin/pip install --no-cache-dir "$(ls dist/validmind-*.whl | head -n 1)[all]"

- name: Install Additional Dependencies
run: all-venv/bin/pip install nbformat papermill jupyter
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/pypi-metrics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Publish the lightweight metric SDK independently from the full library.

name: Publish Metrics SDK to PyPI

on:
push:
tags:
- 'metrics-v*.*.*'

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Build metrics SDK
run: uv build --package validmind-metrics --out-dir dist

- name: Publish metrics SDK
env:
UV_PUBLISH_USERNAME: __token__
UV_PUBLISH_PASSWORD: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }}
run: uv publish dist/validmind_metrics-*
37 changes: 37 additions & 0 deletions .github/workflows/pypi-tracking-core.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Publish the dependency-light tracking core independently from the full library.

name: Publish Tracking Core to PyPI

on:
push:
tags:
- 'tracking-core-v*.*.*'

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Build tracking core
run: uv build --package validmind-tracking-core --out-dir dist

- name: Publish tracking core
env:
UV_PUBLISH_USERNAME: __token__
UV_PUBLISH_PASSWORD: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }}
run: uv publish dist/validmind_tracking_core-*
3 changes: 2 additions & 1 deletion .github/workflows/pypi.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# This workflow pushes the ValidMind Library package to PyPI when a tag is created
# This workflow pushes the ValidMind Library package to PyPI when a tag is created.
# The validmind-tracking-core package must be published first for new releases.

name: Publish to PyPI

Expand Down
11 changes: 8 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ __check_defined = \
$(error Undefined $1$(if $2, ($2))))

format:
uv run black validmind
uv run isort validmind
uv run black validmind packages
uv run isort validmind packages

lint:
# don't check max line length for now since black already takes care of it
# and flake8 is too strict where it doesn't need to be
uv run flake8 validmind --config .flake8
uv run flake8 validmind packages --config .flake8

install:
uv sync --all-extras --group dev
Expand All @@ -28,8 +28,13 @@ ifdef ONLY
uv run python -m unittest $(ONLY)
else
uv run python -m unittest discover tests
$(MAKE) test-packages
endif

test-packages:
uv run --package validmind-tracking-core python -m unittest discover packages/validmind-tracking-core/tests
uv run --package validmind-metrics python -m unittest discover packages/validmind-metrics/tests

test-unit:
uv run python -m unittest "tests.test_unit_tests"

Expand Down
25 changes: 25 additions & 0 deletions packages/validmind-metrics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ValidMind Metrics

`validmind-metrics` is a lightweight client for sending unit metrics to the
ValidMind Platform. It supports API-key authentication and OIDC device-flow
authentication without installing or importing the full `validmind` library.

```python
from validmind_metrics import MetricsClient

client = MetricsClient(
api_host="https://app.validmind.ai/api/v1/tracking",
model="model-cuid",
api_key="api-key",
api_secret="api-secret",
)

client.log_metric("accuracy", 0.95)
```

OIDC authentication uses the same `issuer`, `client_id`, optional `scope`, and
optional `audience` settings as the full library. Cached credentials are stored
under `~/.validmind/credentials.json`.

For an async HTTP handler, use `await client.alog_metric(...)` so the blocking
transport runs outside the event loop.
27 changes: 27 additions & 0 deletions packages/validmind-metrics/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[build-system]
requires = ["hatchling>=1.26.0"]
build-backend = "hatchling.build"

[project]
name = "validmind-metrics"
version = "0.1.0"
description = "Lightweight metric logging client for the ValidMind Platform"
readme = "README.md"
requires-python = ">=3.9,<3.15"
license = { text = "AGPL-3.0 AND ValidMind Commercial" }
dependencies = [
"validmind-tracking-core>=0.1.0,<0.2.0",
]

[tool.uv.sources]
validmind-tracking-core = { workspace = true }

[tool.hatch.build.targets.sdist]
include = [
"/src",
"/README.md",
"/pyproject.toml",
]

[tool.hatch.build.targets.wheel]
packages = ["src/validmind_metrics"]
85 changes: 85 additions & 0 deletions packages/validmind-metrics/src/validmind_metrics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright © 2023-2026 ValidMind Inc. All rights reserved.
# Refer to the LICENSE file in the root directory for details.
# SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial

"""Lightweight metric logging client for the ValidMind Platform."""

from typing import Any, Dict, List, Optional

from validmind_tracking_core import (
MetricsClient,
TrackingAPIError,
TrackingAuthError,
TrackingConfigurationError,
)

__version__ = "0.1.0"

_client: Optional[MetricsClient] = None


def init(**kwargs: Any) -> MetricsClient:
"""Create and retain the default metric client."""
global _client
kwargs.setdefault("client_version", __version__)
_client = MetricsClient(**kwargs)
return _client


def _get_client() -> MetricsClient:
if _client is None:
return init()
return _client


def log_metric(
key: str,
value: Any,
inputs: Optional[List[str]] = None,
params: Optional[Dict[str, Any]] = None,
recorded_at: Optional[str] = None,
thresholds: Optional[Dict[str, Any]] = None,
passed: Optional[bool] = None,
) -> Dict[str, Any]:
"""Log one metric using the default client."""
return _get_client().log_metric(
key,
value,
inputs=inputs,
params=params,
recorded_at=recorded_at,
thresholds=thresholds,
passed=passed,
)


async def alog_metric(
key: str,
value: Any,
inputs: Optional[List[str]] = None,
params: Optional[Dict[str, Any]] = None,
recorded_at: Optional[str] = None,
thresholds: Optional[Dict[str, Any]] = None,
passed: Optional[bool] = None,
) -> Dict[str, Any]:
"""Log one metric without blocking the current event loop."""
return await _get_client().alog_metric(
key,
value,
inputs=inputs,
params=params,
recorded_at=recorded_at,
thresholds=thresholds,
passed=passed,
)


__all__ = [
"MetricsClient",
"TrackingAPIError",
"TrackingAuthError",
"TrackingConfigurationError",
"alog_metric",
"init",
"log_metric",
]
Loading
Loading