Repository navigation
Expand file tree
/
Copy pathpyproject.toml
More file actions
727 lines (683 loc) · 33.4 KB
/
Copy pathpyproject.toml
File metadata and controls
727 lines (683 loc) · 33.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
[build-system]
requires = ["hatchling>=1.27"] # PEP 639 license metadata
build-backend = "hatchling.build"
[project]
name = "donkey-kit"
# The version is declared once, as `__version__` in src/donkey_kit/__init__.py,
# and hatch reads it from there ([tool.hatch.version] below, #767). Bump only
# that file (scripts/bump-version.sh does it); test_release_version.py checks
# nothing here re-declares it.
dynamic = ["version"]
description = "An SDK for consuming Agent Fabric capabilities (governed model and tool access) from your own agent framework."
readme = "README.md"
requires-python = ">=3.10"
license = "Apache-2.0"
# A copy of the repo-root LICENSE: hatch cannot read files above python/.
# tests/unit/test_house_style_config.py keeps the two identical.
license-files = ["LICENSE"]
# Community-maintained (see the README support statement); report issues and
# contact the maintainers through the Issues URL below.
authors = [{ name = "Donkey Development Kit maintainers" }]
maintainers = [{ name = "Donkey Development Kit maintainers" }]
keywords = ["donkey-kit", "mulesoft", "anypoint", "agent", "llm", "mcp", "gateway"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
# One per CI `test` matrix leg (docs/python-support.md).
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
# core/ has ZERO agent-framework dependencies. httpx only. (the layered architecture hard rule)
# No pydantic: nothing in the base install imports it (#730, tested by
# tests/unit/test_base_install_deps.py).
dependencies = [
"httpx>=0.27",
"typing-extensions>=4.10; python_version < '3.12'",
# stdlib tomllib is 3.11+; backfill for the 3.10 floor (core/config.py).
"tomli>=2.0; python_version < '3.11'",
]
[project.urls]
# Wired to the org/repo/Pages targets at the DDK identity (#339). Homepage +
# Repository point at the repo; Documentation at the GitHub Pages docs site
# (basePath /donkey-development-kit); Changelog is the GitHub Releases feed —
# the GitHub Release IS the changelog (no CHANGELOG.md), per the ddk-release skill.
Homepage = "https://github.com/Donkey-Development-Kit/donkey-development-kit"
Documentation = "https://docs.donkey-kit.dev"
Repository = "https://github.com/Donkey-Development-Kit/donkey-development-kit"
Issues = "https://github.com/Donkey-Development-Kit/donkey-development-kit/issues"
Changelog = "https://github.com/Donkey-Development-Kit/donkey-development-kit/releases"
[project.optional-dependencies]
# --- LLM client (raw, framework-free) ---
# 1.66 is the first openai with the Responses API, the data plane `client()` speaks (§2).
llm = ["openai>=1.66"]
# --- Per-framework integrations (one extra each) ---
# Floors, never ceilings (§8.4) — but every allowed version must work, so each
# floor is the lowest release verified for every kwarg its adapter passes, per
# its docs/verified-apis.md §8 row; §8.3 records each floor (#743). Class names / kwargs inside the
# adapters stay UNVERIFIED until the verification discipline confirms them.
# The first deep, conformance-gated adapter (BG §1.8, #198): its extra ships the graph
# runtime so `interrupt()` composition and the conformance harness run against a
# real LangGraph app, not just a bare ChatOpenAI. langchain-openai 0.3.9 added
# `use_responses_api`; below it the kwarg lands in the request body instead.
# langgraph 0.4 is the first whose `ainvoke` returns `__interrupt__` on a pause;
# langchain-core 0.3.45 is what langchain-openai 0.3.9 requires.
langgraph = ["langgraph>=0.4", "langchain-openai>=0.3.9", "langchain-core>=0.3.45"]
# 2.4 is the first google-adk whose `Gemini` has `client_kwargs`; below it
# `gemini()` silently drops the governed client (#735).
adk = ["google-adk>=2.4", "litellm>=1.84"]
# 1.13 is the first whose `OpenAIChatClient(model=...)` call reaches the proxy:
# 1.12 builds it but sends elsewhere, and 1.11 and earlier lack the class path.
agent_framework = ["agent-framework>=1.13"]
# The second deep, conformance-gated adapter (#223). openai-agents 0.20 itself
# requires openai>=2.45, so that is the real floor (the nightly lowest-direct
# leg checks it, #769).
openai-agents = ["openai-agents>=0.20", "openai>=2.45"]
anthropic = ["anthropic>=0.116"]
# 1.15.3 keeps `openai/<id>` + `base_url` on the native OpenAI provider for any
# model id; earlier releases fall back to LiteLLM for non-OpenAI-looking ids.
crewai = ["crewai>=1.15.3"]
llamaindex = ["llama-index-llms-openai-like>=0.8"]
# The OpenAIModel the adapter builds needs strands' own `openai` extra.
strands = ["strands-agents[openai]>=1.57.1"]
# --- Local gateway simulator (BG §1.4): `donkey mock` ---
# Pure-Python ASGI (starlette) served by uvicorn. Plain `uvicorn`, NOT
# `uvicorn[standard]`: uvloop/httptools are C extensions and would break BG §1.4's
# pure-Python promise. Floors only, no upper pins.
local = ["starlette>=0.37", "uvicorn>=0.30"]
# --- A2A server (BG §2.9.1): `donkey.serve()` ---
# The official a2a-sdk serves the protocol, never a home-grown one (#224). Its
# `http-server` extra brings starlette and sse-starlette; uvicorn runs the app,
# plain as in [local]. 1.2.0 is the lowest release with
# DefaultRequestHandler.aclose(), which build_app()'s lifespan calls to drain
# in-flight tasks on shutdown; see docs/verified-apis.md §8.3.
a2a = ["a2a-sdk[http-server]>=1.2.0", "uvicorn>=0.30"]
# --- Telemetry (BG §1.6) ---
# The OTLP exporter is what makes zero-config export real (#194): with it and
# `OTEL_EXPORTER_OTLP_ENDPOINT` set, spans flow to the customer's own sink with no
# SDK-specific env var. We ship the http/protobuf exporter (the OTel-recommended
# default protocol) rather than the `opentelemetry-exporter-otlp` bundle, which
# would also drag in `grpcio` (a heavy C-extension wheel). grpc stays opt-in:
# `OTEL_EXPORTER_OTLP_PROTOCOL=grpc` is honoured only if the caller also installs
# `opentelemetry-exporter-otlp-proto-grpc` (a one-time warning says so otherwise).
# Floors only, no upper pins.
otel = [
"opentelemetry-api>=1.24",
"opentelemetry-sdk>=1.24",
"opentelemetry-exporter-otlp-proto-http>=1.24",
]
# --- CLI: `donkey init|doctor|mock|test` (donkey_kit/cli) ---
# typer 0.26.0 is the first that keeps a `typer.Argument(help=...)` against a
# fresh click: every typer up to 0.25.1 drops it under click 8.5+ (#1196), and
# 0.26.0 vendors click. Below 0.15.4 typer also breaks against click 8.2+, and
# below 0.13 it rejects `Path | None` options. rich 13.8.0 is what typer 0.26.0
# requires, so a lower rich floor could never be installed.
cli = ["typer>=0.26.0", "rich>=13.8.0"]
# --- Conformance pytest plugin (BG §1.5): `donkey-kit[test]` ---
# The public, run-it-against-your-own-agent plugin
# (`pytest --donkey-conformance --donkey-agent=my_app:build`), registered via the
# pytest11 entry point below. pytest-asyncio so a customer's own async agent
# tests — and the `donkey` fixture used with `simulate()` — run under the
# repo-standard asyncio_mode="auto". The plugin itself needs no framework and no
# `[llm]`; it drives the agent through the customer's own Donkey. Floors only,
# no upper pins. pytest-asyncio 0.23.0 crashes collection (INTERNALERROR) on a
# module-level `importorskip`; 0.23.1 is the lowest that runs the suite.
test = ["pytest>=8.0", "pytest-asyncio>=0.23.1"]
# --- Everything a user runs, together ---
# The framework-free extras plus LangGraph, the first deep adapter (BG §1.8). No
# test runner or linter (#744): add `donkey-kit[all,test]` for the conformance
# plugin. The seven other framework extras are deliberately NOT here:
# whether they co-install is an upstream property no floor can guarantee
# (§8.4), and pip once failed `[all]` with resolution-too-deep when it bundled
# them (#697). Which ones resolve together today is the generated
# docs/co-installability.md table, re-checked nightly (#769). See
# docs/verified-apis.md §8.1.
all = [
"donkey-kit[llm,langgraph,otel,cli,local,a2a]",
]
# Contributor tooling is a PEP 735 dependency group, not an extra, so it stays
# out of the published metadata (#744): `pip install -e . --group dev` (pip
# 25.1+) or `uv sync --group dev`. pytest + pytest-asyncio repeat the [test]
# extra's floors because a group cannot include an extra; typer repeats [cli]'s
# because the base-only CI job installs the group alone and still runs the CLI
# unit tests. pyyaml is test-only: no SDK module imports it (#730), but
# tests/unit/test_integration_checklist.py parses .github/workflows/ci.yml.
[dependency-groups]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23.1",
# Randomises test order every run and prints the seed by default (#750), so
# a leaked module-level cache or warning de-dupe set surfaces as a flaky
# failure instead of hiding behind one fixed collection order. CI reruns a
# failure's exact order with `--randomly-seed=<printed seed>`.
"pytest-randomly>=3.15",
"respx>=0.21",
"mypy>=1.10",
"ruff>=0.5",
# 2.3+: the whole-package layers contract (#729) fails on 2.0-2.2 with
# "Modules have shared descendants".
"import-linter>=2.3",
# dead-code check in CI (#720); allowed names live in vulture_whitelist.py.
"vulture>=2.11",
"typer>=0.26.0",
"pyyaml>=6.0",
# Coverage floor (#751): 7.10 is the first release documenting `[run]
# patch = ["subprocess"]`, which this repo relies on to measure the
# conformance plugin/report through pytester's runpytest_subprocess()
# children (see [tool.coverage.run] below).
"coverage[toml]>=7.10",
# Hermetic test suite (#747): fails fast on any non-loopback socket the
# default/local_gateway suites open, instead of a real network call
# succeeding, hanging, or silently depending on the shell's proxy.
"pytest-socket>=0.7",
# Hermetic test suite (#747): caps every test at timeout (below), so a test
# that stalls on a dead proxy or an unreachable collector fails fast
# instead of hanging CI.
# 2.3.0 was never published; 2.3.1 is the lowest 2.3 release (#769).
"pytest-timeout>=2.3.1",
]
# The release toolchain (#763). The publish workflows and ci.yml's base-only
# wheel build install these from constraints/release-py3.11.txt and build with
# `python -m build --no-isolation`, so the backend that builds a shipped dist is
# the locked one, not whatever hatchling was newest that day. [build-system]
# above keeps its floor (§8.4): it is what an sdist consumer's build resolves.
release = [
"build>=1.2.1",
# 6.1 drops pkginfo for packaging's metadata parser, so `twine check`
# accepts the Metadata-Version 2.4 (PEP 639) dists hatchling>=1.27 writes
# whatever pkginfo is installed; 6.0.0 rejects them.
"twine>=6.1",
"hatchling>=1.27",
]
# The lock's own refresh tool (#763): scripts/compile_constraints.py drives
# `uv pip compile`. lock-refresh.yml installs it from constraints/lock-py3.11.txt.
lock = [
# 0.6.7: the first release whose `uv pip compile` takes `--group`.
"uv>=0.6.7",
]
[project.scripts]
donkey = "donkey_kit.cli:main"
[project.entry-points.pytest11]
# Registers the conformance plugin (BG §1.5). Entry points are package metadata,
# so this loads on EVERY pytest run once donkey-kit is installed — even under
# the dev group alone with no `[test]` extra. That is why plugin.py and its module-top
# imports stay framework-free and openai-free (only stdlib + pytest + the
# framework-free suite/report siblings); the harness that pulls in Donkey is
# imported lazily. The plugin is inert unless `--donkey-conformance` is passed.
donkey_kit_conformance = "donkey_kit.conformance.plugin"
# The one version source (#767): hatch's default pattern reads the
# `__version__ = "..."` line, and the wheel, the sdist and `__version__` can no
# longer disagree.
[tool.hatch.version]
path = "src/donkey_kit/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/donkey_kit"]
# The captured fixtures the local gateway simulator (BG §1.4) replays are package
# data under src/donkey_kit/simulator/_fixtures/ (#746), so the wheel ships them
# with no force-include and `pip install "donkey-kit[local]" && donkey mock`
# finds them. Their READMEs are capture provenance for reviewers, not runtime
# data, and stay out of the published (immutable) wheel (#821).
exclude = ["src/donkey_kit/simulator/_fixtures/**/README.md"]
# An explicit sdist file list, so local caches and scratch files never ship (#746).
[tool.hatch.build.targets.sdist]
include = [
"/src",
"/tests",
"/examples",
"/scripts",
"/README.md",
"/dependency_allowlist.toml",
"/vulture_whitelist.py",
]
exclude = ["__pycache__", "*.py[cod]"]
[tool.mypy]
# Working instruction #8: mypy --strict, blocking in CI. Typecheck fixtures pin
# downstream package-root usage that source-only analysis cannot exercise.
strict = true
python_version = "3.10"
files = ["src/donkey_kit", "tests/typecheck"]
[[tool.mypy.overrides]]
# Optional extras are never followed, installed or not (#763), so `mypy` gives the
# same result in the typecheck job's [llm,cli] + dev venv and in a venv with
# every extra. Before this, mypy followed whatever happened to be installed: a
# venv with a framework extra type-checked the adapters against that release's
# own annotations (and once reached numpy's 3.12-only `type` statements through
# a framework, failing at the 3.10 target), while CI checked them against Any.
# `skip` makes the CI behaviour the only behaviour: each package below is Any
# whether or not it is present, and the adapters' real signatures are checked
# by the adapter-contract jobs and scripts/verify_frameworks.py (§8), not here.
# `follow_imports_for_stubs` extends the skip to packages that ship .pyi files
# (numpy, opentelemetry). tests/unit/test_house_style_config.py keeps every
# optional extra outside [llm,cli] in this list. langgraph itself is NOT listed:
# src/ never imports it statically (approval_interrupt() loads it by name), and
# the langgraph-demo job checks examples/langgraph against the real package.
module = [
"langchain_openai.*",
"langchain_core.*",
"opentelemetry.*",
"google.adk.*",
"google.genai.*",
"agent_framework.*",
"agents.*",
"anthropic.*",
"crewai.*",
"llama_index.*",
"strands.*",
"litellm.*",
# [local] simulator deps — absent under the typecheck job's [llm,cli]
# install; build_app()/serve() confine their use to function bodies and the
# public return type is a framework-free Protocol, so no Any leaks out.
"starlette.*",
"uvicorn.*",
# [a2a] — the same: build_app() imports the a2a-sdk inside its body and
# returns Any, so none of its types leak into the public signatures.
"a2a.*",
# The `mcp` SDK, which no extra installs but several frameworks pull in:
# registry/introspect.py imports it inside _live_mcp_tools() only (#1024).
"mcp.*",
# Never imported by src/, but reachable through a framework's own imports.
"numpy.*",
]
follow_imports = "skip"
follow_imports_for_stubs = true
ignore_missing_imports = true
[[tool.mypy.overrides]]
# Absence only: these are part of the typecheck environment and typed, so mypy
# follows them when present. tomli is the <3.11 tomllib backfill (core/config.py):
# mypy targets py3.10 so it analyzes the `import tomli` branch, but it may be
# absent on 3.11+. httpx2 arrives with openai>=3 or anthropic>=1.0 and
# tests/typecheck/ checks against its real types; it is absent under an older
# openai with no anthropic (#701).
module = [
"tomli",
"httpx2.*",
]
ignore_missing_imports = true
[[tool.mypy.overrides]]
# The httpx2 bridge subclasses httpx2's transport and byte-stream bases. When
# httpx2 is absent those bases are Any, so strict's disallow_subclassing_any would
# fail the base typecheck; an inline ignore would go stale whenever it is present.
# The CrewAI adapter's interceptor subclasses crewai's BaseInterceptor, likewise,
# and the LangGraph adapter's last_call handler langchain_core's BaseCallbackHandler.
# The Strands adapter's model subclasses the OpenAIModel class it is handed (#951),
# and the ADK adapter's llm_client the LiteLLMClient class it is handed (#969).
# The OpenAI Agents adapter's gated tool subclasses agents.FunctionTool (#213).
module = [
"donkey_kit.a2a._server",
"donkey_kit.core.transport.httpx2",
"donkey_kit.integrations.adk",
"donkey_kit.integrations.crewai",
"donkey_kit.integrations.langgraph",
"donkey_kit.integrations.openai_agents",
"donkey_kit.integrations.strands",
]
disallow_subclassing_any = false
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
# The two live rungs are off by default (a command-line -m overrides this, so
# `pytest -m local_gateway` still opts in). Aligns collection with the "off by
# default" statement in CLAUDE.md, which was previously unenforced.
#
# --strict-markers/--strict-config (#747): an unregistered `@pytest.mark.*` or
# an invalid ini option is a hard collection error, not a silent typo. -ra
# prints a short summary of every non-passed test (skips included), so a clean
# skip is visible in CI output instead of hiding in the dot-progress line.
# --allow-hosts/--allow-unix-socket (pytest-socket, #747): the default and
# local_gateway suites talk only to loopback/unix sockets (DonkeyAsyncClient's
# swapped-in fixture transport, the local gateway simulator's real TCP bind);
# any other outbound socket is the exact "offline suite makes a real network
# call" bug this issue is closing, so it now fails fast with a clear error
# instead of hanging or racing a live host.
addopts = """
-m 'not local_gateway and not sandbox and not benchmark'
--strict-markers --strict-config -ra
--allow-hosts=127.0.0.1,::1,localhost
--allow-unix-socket
"""
markers = [
"local_gateway: boots the pure-Python local gateway simulator (BG §1.4) on a real TCP port; off by default",
"sandbox: requires a real Anypoint sandbox (DONKEY_SANDBOX_TESTS=1); off by default",
"benchmark: measures per-call span overhead against the <1ms bar (BG §1.6, #194); off by default, run with `pytest -m benchmark` in its own CI job",
]
# Every test is capped at 60s (#747): a test that stalls on a dead proxy or an
# unreachable OTLP collector fails fast with a traceback instead of hanging CI
# past the job's own timeout. A test that legitimately needs longer sets its
# own `@pytest.mark.timeout(...)` override.
timeout = 60
# A test marked `xfail` that unexpectedly passes is now a failure, not a
# silent pass-through (#747) — an `xfail` whose bug got fixed should fail loud
# so the marker gets removed, not keep reporting green forever.
xfail_strict = true
# --- coverage (#751): measured in ONE CI `test` leg, not every matrix leg ---
[tool.coverage.run]
branch = true
source = ["donkey_kit"]
# `patch = ["subprocess"]` installs the env var + startup hook coverage.py
# needs to measure a *subprocess* interpreter — specifically the ones
# `pytester.runpytest_subprocess()` spawns for the conformance plugin/report
# tests (BG §1.5), which previously ran with no coverage attached at all. It
# implies `parallel = true` on its own; the explicit setting below just makes
# the "many data files, one combine step" intent visible without relying on
# that side effect.
patch = ["subprocess"]
parallel = true
relative_files = true
[tool.coverage.report]
# Today's measured baseline (python -m coverage report, #751), rounded down to
# an integer so a one-line fluctuation doesn't flap CI. Raise it when tests
# lift the measured total past the next integer; never lower it to make a PR
# pass.
fail_under = 94
show_missing = true
skip_covered = false
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.ruff.format]
# #765: `ruff format --check .` runs in the typecheck-and-lint job and as a
# pre-commit hook. The captured fixtures are byte-exact (their manifest hashes
# them, scripts/scrub_fixtures.py reads them as captured), so the formatter
# never rewrites them, including their README.md files.
exclude = ["src/donkey_kit/simulator/_fixtures/**", "tests/fixtures/**"]
[tool.ruff.lint]
# SLF001: no reaching into another object's private members (#719); RUF022:
# every __all__ is sorted (#719).
# BLE/LOG/G: no blind except without a reason, and logging-call hygiene (#717).
# D101-D103: every public class, method and function in src/ has a docstring
# (#722). Only these three D rules are on; tests/examples/scripts are exempt below.
# #721, "the style guide is the config": each CONTRIBUTING §3 rule maps to an
# enforcing rule. I002 (with isort.required-imports below) makes
# `from __future__ import annotations` mandatory; FA backs it for PEP 604/585
# syntax; RUF100 drops stale noqa; TRY203 forbids bare re-raise handlers; ARG
# flags unused arguments; ANN401 forbids `Any` in signatures outside the
# pass-through seams listed in per-file-ignores.
select = [
"E", "F", "I", "UP", "B", "BLE", "LOG", "G", "SLF001", "RUF022",
"D101", "D102", "D103",
"I002", "FA", "RUF100", "TRY203", "ARG", "ANN401",
]
[tool.ruff.lint.pydocstyle]
# Google-style "Args:" / "Raises:" sections; the prose-only docstrings fit it too.
convention = "google"
[tool.ruff.lint.isort]
required-imports = ["from __future__ import annotations"]
[tool.ruff.lint.flake8-annotations]
# `*args: Any` / `**kwargs: Any` that pass straight through to a framework or
# httpx constructor are the sanctioned use of Any (ANN401); everything else is
# either typed or listed per-file below with its reason.
allow-star-arg-any = true
[tool.ruff.lint.flake8-unused-arguments]
# Same pass-through rule for ARG: a `**kwargs` accepted to honour an upstream
# callback signature is not an unused argument.
ignore-variadic-names = true
[tool.ruff.lint.per-file-ignores]
# typer's API requires calling typer.Option(...) in argument defaults (B008).
# The blocked CLI commands also keep their final options (ARG001, see below).
"src/donkey_kit/cli/**" = ["B008", "ARG001"]
# Verification-blocked stubs (§0.3): each raises `_verify.blocked(...)` but keeps
# its final, documented signature, so its arguments are unused until the API is
# verified. Remove the entry when the module's stubs are implemented.
"src/donkey_kit/donkey.py" = ["ARG002"]
"src/donkey_kit/registry/exchange.py" = ["ARG002"]
"src/donkey_kit/registry/publication.py" = ["ARG001", "ARG002"]
# The one sanctioned seam module: the dev-only simulator and conformance siblings
# reach Donkey's and its clients' private members only through it (#719). Its
# transport arguments stay `Any` (ANN401): the sync and async clients take
# different httpx transport types, and a union would break the Protocol match.
"src/donkey_kit/_testing.py" = ["SLF001", "ANN401"]
# ANN401 seams: these modules bind objects from optional dependencies that are
# imported lazily (framework extras, §1.1; opentelemetry; starlette) or from the
# user's own agent and pytest, so `Any` is the honest annotation there.
"src/donkey_kit/integrations/**" = ["ANN401"]
"src/donkey_kit/conformance/**" = ["ANN401"]
"src/donkey_kit/simulator/app.py" = ["ANN401"]
"src/donkey_kit/core/telemetry.py" = ["ANN401"]
"src/donkey_kit/core/inbound.py" = ["ANN401"]
"src/donkey_kit/registry/introspect.py" = ["ANN401"]
# White-box tests inspect and swap private state by design (#719); docstring
# rules apply to the shipped package only (#722). Tests also request fixtures for
# their side effects and write doubles that mirror third-party signatures, so
# unused and `Any`-typed arguments are expected there.
"tests/**" = ["SLF001", "D101", "D102", "D103", "ARG", "ANN401"]
"examples/**" = ["D101", "D102", "D103"]
"scripts/**" = ["D101", "D102", "D103"]
# The §10 capture reads framework objects the way a user would, private ADK
# method included, and its reference function is a schema, not logic (#1024).
"scripts/capture_descriptor_fixtures.py" = ["ANN401", "ARG001", "SLF001"]
# --- vulture: dead-code check in CI (#720) ---
# Reads src/ only, so code that only the tests use is reported as dead; names
# used from outside src/ are listed in vulture_whitelist.py.
[tool.vulture]
paths = ["src", "vulture_whitelist.py"]
min_confidence = 60
# --- import-linter: enforce the layered architecture rule in CI, from day one ---
[tool.importlinter]
root_package = "donkey_kit"
# Third-party packages are nodes in the graph too, so a contract can forbid one
# (core's dependencies, below, #729). grimp records an external import by its
# top-level name; the package does not need to be installed. Imports inside
# functions and under TYPE_CHECKING count, which the base-only job can't see.
include_external_packages = true
[[tool.importlinter.contracts]]
name = "core is framework-free (nothing in integrations may be imported by lower layers)"
type = "forbidden"
source_modules = [
"donkey_kit.core",
"donkey_kit.llm",
"donkey_kit.registry",
"donkey_kit.tools",
]
forbidden_modules = ["donkey_kit.integrations"]
[[tool.importlinter.contracts]]
name = "layered architecture (§1.1)"
type = "layers"
# The whole package, top to bottom; a lower layer never imports a higher one,
# directly or indirectly (#729). The production stack is donkey → integrations →
# tools → registry → llm → core, with experimental (#730) beside donkey: it
# re-exports registry and core names that are not first-class API (ADR 0008,
# docs/adr/0008-*.md), and neither of the two imports the other. a2a (the
# donkey.serve() A2A server, ADR 0014) sits beside integrations: it wraps a
# third-party protocol library like an adapter does, and the two are
# independent. Above it:
# - cli: the `donkey` console script (#730). It may import any layer, and being
# on top means no library module imports it back (typer stays off the base
# install).
# - conformance > simulator > _testing: the dev-only siblings (BG §1.4/§1.5). The
# harness reuses the simulator, both reach Donkey's private state only through
# the _testing seam (#719), and _testing names Donkey's types.
# `exhaustive` makes a new top-level module fail until it is placed here.
containers = ["donkey_kit"]
exhaustive = true
layers = [
"cli",
"conformance",
"simulator",
"_testing",
"donkey | experimental",
"integrations | a2a",
"tools",
"registry",
"llm",
"core",
]
ignore_imports = [
# Donkey.simulate() (BG §1.4) is public API on the orchestrator, and its
# implementation is the simulator's base-path-safe inject module (httpx
# only). The import is lazy, inside the method, so `import donkey_kit` never
# loads the simulator.
"donkey_kit.donkey -> donkey_kit.simulator.inject",
]
[[tool.importlinter.contracts]]
name = "the top-level package exports only the production layers"
type = "forbidden"
# `import donkey_kit` (the root __init__ on its own, as_packages = false) must
# never load the dev-only siblings or the CLI, directly or through Donkey
# (#729). The deprecated aliases load experimental lazily, which is allowed. With as_packages off, each package is named twice: the package
# module and its children.
as_packages = false
source_modules = ["donkey_kit"]
forbidden_modules = [
"donkey_kit._testing",
"donkey_kit.cli",
"donkey_kit.cli.**",
"donkey_kit.conformance",
"donkey_kit.conformance.**",
"donkey_kit.simulator",
"donkey_kit.simulator.**",
]
ignore_imports = [
# The lazy Donkey.simulate() import, as in the layers contract above.
"donkey_kit.donkey -> donkey_kit.simulator.inject",
]
[[tool.importlinter.contracts]]
name = "core depends on httpx only: no framework, LLM SDK or schema library (§1.1)"
type = "forbidden"
# core's third-party imports may be httpx, the stdlib backfills (tomli,
# typing_extensions) and opentelemetry (the optional [otel] extra, imported
# lazily). This contract forbids the packages that have drifted, or could drift,
# into a lower layer: the LLM SDKs, the httpx2 bridge, pydantic (ADR 0001), every
# agent framework the adapters wrap, the MCP SDK (no extra installs it yet), and the
# dev-only and CLI dependencies (#729). A listed package nothing imports yet
# (respx, rich) is still enforced: it fails the moment core imports it.
# tests/unit/test_architecture.py checks the same rule as an allowlist.
# The one exception is core's own httpx2 bridge (#728), which nothing imports at
# package import (pinned by test_import_donkey_kit_loads_only_the_production_layers).
source_modules = ["donkey_kit.core"]
forbidden_modules = [
"a2a",
"agent_framework",
"agents",
"anthropic",
"crewai",
"google",
"httpx2",
"langchain",
"langchain_core",
"langchain_openai",
"langgraph",
"litellm",
"llama_index",
"mcp",
"openai",
"pydantic",
"pytest",
"respx",
"rich",
"starlette",
"strands",
"typer",
"uvicorn",
"yaml",
]
ignore_imports = ["donkey_kit.core.transport.httpx2 -> httpx2"]
[[tool.importlinter.contracts]]
name = "layers inside core"
type = "layers"
# Top to bottom (#729). The order is the real import graph: runtime builds the
# transports; transport attaches budget, telemetry and cache control; auth and
# those features read config; config reads cost, endpoints and header names;
# everything reports through errors. classifier (the classify() registry,
# #210) sits beside cost and the rest, over _policy_spec (its match-spec
# checks); it fills a hook slot in errors, so errors never imports it.
# approvals (the @donkey.tool approval gate, #211) sits beside transport: it
# reads correlation, errors and telemetry, and neither imports the other.
# inbound (the run an inbound A2A task binds, #225) sits there too: it binds
# correlation and opens a telemetry span, and imports neither sibling. Two
# deviations from the order sketched in #729: lastcall sits BELOW errors, because a DonkeyError reads its request id
# through lastcall.request_id; and _wire sits below _verify, because the
# verification placeholders name wire headers. A sibling group with `|` is
# independent: its members never import each other. `exhaustive` makes a new
# core module fail until it is placed. _response (the structural response
# Protocols, #933) imports nothing from core, so it sits at the bottom, as does
# _budget_headers (budget's header parsers and receive-time stamp, #311) and
# _window (the window state every budget window shares, #1025).
containers = ["donkey_kit.core"]
exhaustive = true
layers = [
"runtime",
"transport | approvals | inbound",
"budget | cache | cachecontrol | telemetry | toolspec",
"auth | correlation | spanflush",
"config",
"classifier | cost | endpoints | header_names | reactions | refusals",
"_policy_spec",
"errors",
"_refusal_body | lastcall | masking",
"_verify",
"_wire | _response | _budget_headers | _window",
]
[[tool.importlinter.contracts]]
name = "integrations are mutually independent (no cross-framework leakage, §1.1)"
type = "independence"
# Each adapter depends on exactly ONE agent framework. No adapter may import
# another, directly or indirectly — a convenient cross-import would drag a
# second framework's optional dependency onto an install that only asked for
# the first, quietly breaking the "extras are genuinely optional" guarantee
# (working instruction #6). The shared `_base` sibling (DonkeyConfig +
# transport wiring) is deliberately NOT listed: every adapter importing it
# downward is allowed and expected, and `_base` imports no adapter.
modules = [
"donkey_kit.integrations.langgraph",
"donkey_kit.integrations.adk",
"donkey_kit.integrations.strands",
"donkey_kit.integrations.agent_framework",
"donkey_kit.integrations.openai_agents",
"donkey_kit.integrations.anthropic",
"donkey_kit.integrations.crewai",
"donkey_kit.integrations.llamaindex",
]
[[tool.importlinter.contracts]]
name = "simulator is a dev-only sibling (BG §1.4)"
type = "forbidden"
# The local gateway simulator pulls in starlette/uvicorn lazily. No production
# layer may import it, so a framework dep can never drift onto the base
# `import donkey_kit` path. The simulator importing core downward is allowed.
source_modules = [
"donkey_kit.core",
"donkey_kit.llm",
"donkey_kit.registry",
"donkey_kit.tools",
"donkey_kit.integrations",
]
forbidden_modules = ["donkey_kit.simulator"]
[[tool.importlinter.contracts]]
name = "conformance is a dev-only sibling (BG §1.5)"
type = "forbidden"
# The conformance kit's pytest plugin auto-loads via a pytest11 entry point and
# its harness reuses the simulator. Like the simulator, it is a dev-only sibling:
# no production layer may import it, so nothing it pulls in (pytest, the harness's
# Donkey wiring) can drift onto the base `import donkey_kit` path. The kit
# importing core/donkey and the simulator downward is allowed.
source_modules = [
"donkey_kit.core",
"donkey_kit.llm",
"donkey_kit.registry",
"donkey_kit.tools",
"donkey_kit.integrations",
]
forbidden_modules = ["donkey_kit.conformance"]
[[tool.importlinter.contracts]]
name = "the CLI is a front end: no library module imports donkey_kit.cli (#730)"
type = "forbidden"
# donkey_kit.cli is the console script's home (ADR 0008 in docs/adr/). It may
# import any layer below it; nothing in the library may import it back, so typer
# (the [cli] extra) never drifts onto the base `import donkey_kit` path.
source_modules = [
"donkey_kit.core",
"donkey_kit.llm",
"donkey_kit.registry",
"donkey_kit.tools",
"donkey_kit.integrations",
"donkey_kit.simulator",
"donkey_kit.conformance",
"donkey_kit.donkey",
"donkey_kit.experimental",
]
forbidden_modules = ["donkey_kit.cli"]