Skip to content

Commit 316a694

Browse files
committed
initial import: nullrun Python SDK 0.3.0
0 parents  commit 316a694

62 files changed

Lines changed: 16148 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.git
2+
*.log
3+
*.lock
4+
.env
5+
**/node_modules
6+
dist

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python: ["3.10", "3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python ${{ matrix.python }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python }}
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e ".[dev]"
28+
29+
- name: Run tests
30+
run: pytest
31+
32+
- name: Run ruff
33+
run: ruff check src/
34+
35+
- name: Run mypy
36+
run: mypy src/
37+
38+
coverage:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@v4
42+
- uses: actions/setup-python@v5
43+
with:
44+
python-version: "3.12"
45+
- run: pip install -e ".[dev]"
46+
- run: coverage run -m pytest
47+
- uses: codecov/codecov-action@v4
48+
if: always()

.github/workflows/publish.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*' # триггер только по тегу: git tag v0.1.0 && git push --tags
7+
8+
jobs:
9+
test:
10+
name: Run tests
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.10", "3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
23+
- name: Install dependencies
24+
run: |
25+
pip install -e ".[dev]"
26+
27+
- name: Run tests
28+
run: pytest tests/ -v
29+
30+
publish:
31+
name: Build and publish
32+
needs: test # сначала все тесты зелёные — потом публикация
33+
runs-on: ubuntu-latest
34+
35+
environment:
36+
name: pypi
37+
url: https://pypi.org/p/nullrun-sdk
38+
39+
permissions:
40+
id-token: write # для trusted publishing (без токена, рекомендуется PyPI)
41+
42+
steps:
43+
- uses: actions/checkout@v4
44+
45+
- uses: actions/setup-python@v5
46+
with:
47+
python-version: "3.11"
48+
49+
- name: Build package
50+
run: |
51+
pip install hatchling build
52+
python -m build
53+
54+
- name: Check dist contents
55+
run: |
56+
pip install twine
57+
twine check dist/*
58+
59+
# Вариант 1: Trusted Publishing (рекомендуется, не нужен токен)
60+
# Настроить на pypi.org: Account → Publishing → Add publisher
61+
# Publisher: GitHub, repo: maltsev-dev/nullrun-sdk, workflow: publish.yml
62+
- name: Publish to PyPI (Trusted Publishing)
63+
uses: pypa/gh-action-pypi-publish@release/v1
64+
65+
# Вариант 2: API токен (раскомментируй если не используешь Trusted Publishing)
66+
# - name: Publish to PyPI (API token)
67+
# uses: pypa/gh-action-pypi-publish@release/v1
68+
# with:
69+
# password: ${{ secrets.PYPI_API_TOKEN }}

.pre-commit-config.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.5.0
4+
hooks:
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-yaml
8+
- id: check-toml
9+
10+
- repo: https://github.com/astral-sh/ruff-pre-commit
11+
rev: v0.2.0
12+
hooks:
13+
- id: ruff
14+
args: [--fix]
15+
- id: ruff-format
16+
17+
- repo: https://github.com/pre-commit/mypy
18+
rev: v1.8.0
19+
hooks:
20+
- id: mypy
21+
additional_dependencies: [types-all]

CHANGELOG.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Changelog
2+
3+
All notable changes to `nullrun-sdk` will be documented here.
4+
5+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6+
Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7+
8+
---
9+
10+
## [Unreleased]
11+
12+
### Added
13+
14+
- **Async Policy Cache**: `AsyncTransport` now uses `PolicyCache` for CACHED fallback mode. Previously the async transport always fell back to PERMISSIVE when gateway was unreachable. Now it caches successful execute decisions and uses them when gateway is unavailable.
15+
- **Custom Sensitive Tools API**: Added `add_sensitive_tool()`, `remove_sensitive_tool()`, `register_sensitive_tools()`, and `get_sensitive_tools()` methods to `NullRunRuntime`. Users can now register custom tools as sensitive requiring strict mode enforcement.
16+
- **`NullRunBlockedException.tool_name` attribute** (FIX-5): The `tool_name`
17+
kwarg is now a first-class attribute on `NullRunBlockedException`
18+
(and its subclasses `LoopDetectedException`, etc.) instead of being
19+
absorbed into `**details`. Cookbook examples that read `exc.tool_name`
20+
no longer raise `AttributeError`. Backwards-compatible: `tool_name`
21+
defaults to `None` and does not appear in `exc.details` when unset.
22+
The stringified exception now includes `tool={name}` when set.
23+
24+
### Fixed
25+
26+
- **SDK silent runtime fallback removed** (FIX-4): `_get_or_create_runtime`
27+
in `nullrun.decorators` no longer wraps `NullRunRuntime.get_instance()`
28+
in a `try/except Exception` that rebuilds a no-arg `NullRunRuntime()`.
29+
In 0.3.0 (T3-S2) the no-arg constructor requires `api_key` and raises
30+
`NullRunAuthenticationError` — so the fallback swallowed the auth
31+
error from `get_instance()` only to crash with the same error from
32+
the fallback path itself. After this fix, the auth error propagates
33+
cleanly to the first `@protect` invocation, mirroring the fail-loud
34+
contract of `nullrun.init()`. Aligns with the T3-S2 invariant that
35+
the SDK has no local mode: a missing API key is a hard error, not a
36+
silent allow-all.
37+
38+
---
39+
40+
## [0.3.0] — 2026-06-15
41+
42+
### Breaking
43+
44+
- **No-api-key init now raises** (T3-S2): `nullrun.init()` and
45+
`NullRunRuntime(...)` without an `api_key` (and with `NULLRUN_API_KEY`
46+
unset) now raise `NullRunAuthenticationError` instead of falling back
47+
to a `NullRunNoop` stub. The previous silent fallback silently
48+
bypassed every backend gate (budget, policy, control plane) — a real
49+
safety hole in production. **Action required:** ensure
50+
`api_key="nr_live_..."` is passed to `init()` (or `NULLRUN_API_KEY`
51+
is set) in every entry point. The `0.2.0` deprecation warning has
52+
been removed; the new behavior is hard.
53+
- **`local_mode` field removed**: The auto-derived `local_mode` flag
54+
on `NullRunRuntime` is gone. The `is_local_mode` property and the
55+
`NullRunNoop` / `NullRunNoopBreaker` / `_NullContext` classes are
56+
deleted (`nullrun.noop` module removed). All call sites that read
57+
`runtime.local_mode` will see `AttributeError` — there is no
58+
migration path because the field no longer has meaning. Code paths
59+
that previously branched on `local_mode` now always go through the
60+
cloud runtime (auth + policy fetch + control plane).
61+
62+
### Removed
63+
64+
- **Legacy Breaker exports** (T9): The 7 legacy re-exports
65+
(`nullrun.BreakerError`, `nullrun.CostLimitExceeded`,
66+
`nullrun.ApprovalRequired`, `nullrun.BreakerTimeout`,
67+
`nullrun.Policy`, `nullrun.FallbackMode`, `nullrun.PoolConfig`)
68+
are no longer reachable as `from nullrun import X`. The canonical
69+
exception names (`NullRunBlockedException`, `WorkflowPausedException`,
70+
`WorkflowKilledException`, `NullRunAuthenticationError`, …) and the
71+
canonical policy/transport modules
72+
(`from nullrun.runtime import Policy`,
73+
`from nullrun.transport import FallbackMode, PoolConfig`) remain
74+
available. Audited for 0 external callers.
75+
76+
### Migration
77+
78+
- **0.2.x → 0.3.0**:
79+
- `nullrun.init()` calls without `api_key` will raise. Pass
80+
`api_key="nr_live_..."` explicitly or set `NULLRUN_API_KEY`.
81+
- `NullRunRuntime(...)` constructions without `api_key` will raise
82+
(same fix).
83+
- Tests using `NullRunNoop` / `local_mode=True` mocking must switch
84+
to `NullRunRuntime(api_key="test-key", _test_mode=True)`
85+
`_test_mode` skips the network calls without silently bypassing
86+
policy.
87+
- `from nullrun import BreakerError` (and the 6 other legacy names)
88+
must use the canonical paths above.
89+
90+
### Added
91+
92+
- **Async Policy Cache**: `AsyncTransport` now uses `PolicyCache` for CACHED fallback mode. Previously the async transport always fell back to PERMISSIVE when gateway was unreachable. Now it caches successful execute decisions and uses them when gateway is unavailable.
93+
- **Custom Sensitive Tools API**: Added `add_sensitive_tool()`, `remove_sensitive_tool()`, `register_sensitive_tools()`, and `get_sensitive_tools()` methods to `NullRunRuntime`. Users can now register custom tools as sensitive requiring strict mode enforcement.
94+
95+
### Deprecated
96+
97+
- **No-api-key init / local mode** (T3-S1): Calling `nullrun.init()` or constructing `NullRunRuntime(...)` without an `api_key` (and with `NULLRUN_API_KEY` unset) now emits a `DeprecationWarning`. The runtime still falls back to local mode and silently bypasses every backend gate (budget, policy, control plane). The fallback will be **removed in 0.3.0** — passing `api_key='nr_live_...'` explicitly or setting `NULLRUN_API_KEY` is the only supported path going forward. Pin the warning to a hard error with `python -W error::DeprecationWarning` to catch callers in CI.
98+
99+
---
100+
101+
## [0.1.1] — 2026-05-20
102+
103+
### Fixed
104+
105+
- **CR-2**: Fixed buffer overflow when circuit breaker is OPEN. Previously, re-queued events were prepended to buffer, causing newest events to be dropped first. Now appends to buffer end and checks max_buffer_size before re-queue.
106+
- **CR-5**: Async circuit breaker now uses `asyncio.Lock` instead of `threading.Lock` for proper async context handling.
107+
- **CR-1+CR-4**: `runtime.py` now creates Transport before `_authenticate()` and `_fetch_policy()`, reusing the HTTP client for connection pooling and consistent timeout/retry policies.
108+
- **AsyncAwait**: Fixed `_call_async()` not awaiting `_on_success_async()` and `_on_failure_async()` coroutines, causing "coroutine was never awaited" warnings in async transport.
109+
110+
### Changed
111+
112+
- Transport buffer now enforces max_buffer_size **before** re-queuing events on circuit breaker OPEN
113+
114+
---
115+
116+
## [0.1.0] — 2026-05-18
117+
118+
### Added
119+
120+
- Circuit breaker core (`src/nullrun/breaker/`) with STRICT / PERMISSIVE / CACHED fallback modes
121+
- HTTP transport with batch event sending (`transport.py`)
122+
- Async transport for asyncio applications
123+
- Retry logic with jitter and policy-aware backoff
124+
- `@protect` decorator for wrapping functions (`decorators.py`)
125+
- Workflow context support (`context.py`)
126+
- Main runtime entrypoint (`runtime.py`)
127+
- `X-API-Version` header on all outgoing requests
128+
129+
### Notes
130+
131+
- Requires Python ≥ 3.10
132+
- Compatible with NullRun API version `2024-01-15`
133+
134+
---
135+
136+
## How to upgrade
137+
138+
### 0.x → next
139+
140+
_No breaking changes yet. Watch this file._
141+
142+
---
143+
144+
[Unreleased]: https://github.com/maltsev-dev/nullrun-sdk/compare/v0.1.1...HEAD
145+
[0.1.1]: https://github.com/maltsev-dev/nullrun-sdk/releases/tag/v0.1.1
146+
[0.1.0]: https://github.com/maltsev-dev/nullrun-sdk/releases/tag/v0.1.0

Dockerfile

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Build stage for Python SDK
2+
FROM python:3.11-slim as builder
3+
4+
WORKDIR /app
5+
6+
# Install build dependencies
7+
RUN apt-get update && apt-get install -y \
8+
build-essential \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
# Copy source first (needed for build with src layout)
12+
COPY pyproject.toml ./
13+
COPY src ./src
14+
RUN pip install build && python -m build
15+
16+
# Runtime stage
17+
FROM python:3.11-slim
18+
19+
WORKDIR /app
20+
21+
# Install runtime dependencies
22+
RUN apt-get update && apt-get install -y \
23+
curl \
24+
&& rm -rf /var/lib/apt/lists/*
25+
26+
# Copy builder output
27+
COPY --from=builder /app/dist /app/dist
28+
RUN pip install /app/dist/*.whl --force-reinstall
29+
30+
# Non-root user
31+
RUN useradd -m -u 1000 nullrun
32+
USER nullrun
33+
34+
# Install optional dependencies
35+
RUN pip install "nullrun-breaker[langgraph]"
36+
37+
ENTRYPOINT ["python", "-m", "nullrun.breaker"]

Dockerfile.dev

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Development Dockerfile for Python SDK
2+
FROM python:3.11-slim
3+
4+
WORKDIR /app
5+
6+
# Copy source first (needed for editable install with src layout)
7+
COPY pyproject.toml README.md ./
8+
COPY src ./src
9+
10+
# Install dependencies
11+
RUN pip install -e ".[dev,langgraph]"
12+
13+
# Copy tests
14+
COPY tests ./tests
15+
16+
# Stay alive for debugging - user can exec in to run tests manually
17+
CMD ["tail", "-f", "/dev/null"]

0 commit comments

Comments
 (0)