Skip to content

Commit 05ae90f

Browse files
committed
Merge branch 'main' into remove-deprecated-workflow-operations
Signed-off-by: Albert Callarisa <albert@diagrid.io>
2 parents c54aad2 + bcfa893 commit 05ae90f

15 files changed

Lines changed: 1260 additions & 11 deletions

File tree

AGENTS.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# AGENTS.md — Dapr Python SDK
2+
3+
This file provides context for AI agents working on the Dapr Python SDK.
4+
The project is the official Python SDK for [Dapr](https://dapr.io/) (Distributed Application Runtime),
5+
enabling Python developers to build distributed applications using Dapr building blocks.
6+
7+
Repository: https://github.com/dapr/python-sdk
8+
License: Apache 2.0
9+
10+
> **Deeper documentation lives alongside the code.** This root file gives you the big picture and
11+
> tells you where to look. Each extension and the examples directory has its own `AGENTS.md` with
12+
> detailed architecture, APIs, and patterns.
13+
14+
## Project structure
15+
16+
```
17+
dapr/ # Core SDK package
18+
├── actor/ # Actor framework (virtual actor model)
19+
├── aio/ # Async I/O modules
20+
├── clients/ # Dapr clients (gRPC and HTTP)
21+
├── common/ # Shared utilities
22+
├── conf/ # Configuration (settings, environment)
23+
├── proto/ # Auto-generated gRPC protobuf stubs (DO NOT EDIT)
24+
├── serializers/ # JSON and pluggable serializers
25+
└── version/ # Version metadata
26+
27+
ext/ # Extension packages (each is a separate PyPI package)
28+
├── dapr-ext-workflow/ # Workflow authoring ← see ext/dapr-ext-workflow/AGENTS.md
29+
├── dapr-ext-grpc/ # gRPC App extension ← see ext/dapr-ext-grpc/AGENTS.md
30+
├── dapr-ext-fastapi/ # FastAPI integration ← see ext/dapr-ext-fastapi/AGENTS.md
31+
├── dapr-ext-langgraph/ # LangGraph checkpointer ← see ext/dapr-ext-langgraph/AGENTS.md
32+
├── dapr-ext-strands/ # Strands agent sessions ← see ext/dapr-ext-strands/AGENTS.md
33+
└── flask_dapr/ # Flask integration ← see ext/flask_dapr/AGENTS.md
34+
35+
tests/ # Unit tests (mirrors dapr/ package structure)
36+
examples/ # Integration test suite ← see examples/AGENTS.md
37+
docs/ # Sphinx documentation source
38+
tools/ # Build and release scripts
39+
```
40+
41+
## Key architectural patterns
42+
43+
- **Namespace packages**: The `dapr` namespace is shared across the core SDK and extensions via `find_namespace_packages`. Extensions live in `ext/` but install into the `dapr.ext.*` namespace. Do not add `__init__.py` to namespace package roots in extensions.
44+
- **Client architecture**: `DaprGrpcClient` (primary, high-performance) and HTTP-based clients. Both implement shared interfaces.
45+
- **Actor model**: `Actor` base class, `ActorInterface` with `@actormethod` decorator, `ActorProxy`/`ActorProxyFactory` for client-side references, `ActorRuntime` for server-side hosting.
46+
- **Serialization**: Pluggable via `Serializer` base class. `DefaultJSONSerializer` is the default.
47+
- **Proto files**: Auto-generated from Dapr proto definitions. Never edit files under `dapr/proto/` directly.
48+
49+
## Extension overview
50+
51+
Each extension is a **separate PyPI package** with its own `setup.cfg`, `setup.py`, `tests/`, and `AGENTS.md`.
52+
53+
| Extension | Package | Purpose | Active development |
54+
|-----------|---------|---------|-------------------|
55+
| `dapr-ext-workflow` | `dapr.ext.workflow` | Durable workflow orchestration via durabletask-dapr | **High** — major focus area |
56+
| `dapr-ext-grpc` | `dapr.ext.grpc` | gRPC server for Dapr callbacks (methods, pub/sub, bindings, jobs) | Moderate |
57+
| `dapr-ext-fastapi` | `dapr.ext.fastapi` | FastAPI integration for pub/sub and actors | Moderate |
58+
| `flask_dapr` | `flask_dapr` | Flask integration for pub/sub and actors | Low |
59+
| `dapr-ext-langgraph` | `dapr.ext.langgraph` | LangGraph checkpoint persistence to Dapr state store | Moderate |
60+
| `dapr-ext-strands` | `dapr.ext.strands` | Strands agent session management via Dapr state store | New |
61+
62+
## Examples (integration test suite)
63+
64+
The `examples/` directory serves as both user-facing documentation and the project's integration test suite. Examples are validated in CI using [mechanical-markdown](https://pypi.org/project/mechanical-markdown/), which executes bash code blocks from README files and asserts expected output.
65+
66+
**See `examples/AGENTS.md`** for the full guide on example structure, validation, mechanical-markdown STEP blocks, and how to add new examples.
67+
68+
Quick reference:
69+
```bash
70+
tox -e examples # Run all examples (needs Dapr runtime)
71+
tox -e example-component -- state_store # Run a single example
72+
cd examples && ./validate.sh state_store # Run directly
73+
```
74+
75+
## Python version support
76+
77+
- **Minimum**: Python 3.10
78+
- **Tested**: 3.10, 3.11, 3.12, 3.13, 3.14
79+
- **Target version for tooling**: `py310` (ruff, mypy)
80+
81+
## Development setup
82+
83+
Install all packages in editable mode with dev dependencies:
84+
85+
```bash
86+
pip install -r dev-requirements.txt \
87+
-e . \
88+
-e ext/dapr-ext-workflow/ \
89+
-e ext/dapr-ext-grpc/ \
90+
-e ext/dapr-ext-fastapi/ \
91+
-e ext/dapr-ext-langgraph/ \
92+
-e ext/dapr-ext-strands/ \
93+
-e ext/flask_dapr/
94+
```
95+
96+
## Running tests
97+
98+
Tests use Python's built-in `unittest` framework with `coverage`. Run via tox:
99+
100+
```bash
101+
# Run all unit tests (replace 311 with your Python version)
102+
tox -e py311
103+
104+
# Run linting and formatting
105+
tox -e ruff
106+
107+
# Run type checking
108+
tox -e type
109+
110+
# Validate examples (requires Dapr runtime)
111+
tox -e examples
112+
```
113+
114+
To run tests directly without tox:
115+
116+
```bash
117+
# Core SDK tests
118+
python -m unittest discover -v ./tests
119+
120+
# Extension tests (run each separately)
121+
python -m unittest discover -v ./ext/dapr-ext-workflow/tests
122+
python -m unittest discover -v ./ext/dapr-ext-grpc/tests
123+
python -m unittest discover -v ./ext/dapr-ext-fastapi/tests
124+
python -m unittest discover -v ./ext/dapr-ext-langgraph/tests
125+
python -m unittest discover -v ./ext/dapr-ext-strands/tests
126+
python -m unittest discover -v ./ext/flask_dapr/tests
127+
```
128+
129+
## Code style and linting
130+
131+
**Formatter/Linter**: Ruff (v0.14.1)
132+
133+
Key rules:
134+
- **Line length**: 100 characters (E501 is currently ignored, but respect the 100-char target)
135+
- **Quote style**: Single quotes
136+
- **Import sorting**: isort-compatible (ruff `I` rules)
137+
- **Target**: Python 3.10
138+
- **Excluded from linting**: `.github/`, `dapr/proto/`
139+
140+
Run formatting and lint fixes:
141+
142+
```bash
143+
ruff check --fix
144+
ruff format
145+
```
146+
147+
**Type checking**: MyPy
148+
149+
```bash
150+
mypy --config-file mypy.ini
151+
```
152+
153+
MyPy is configured to check: `dapr/actor/`, `dapr/clients/`, `dapr/conf/`, `dapr/serializers/`, `ext/dapr-ext-grpc/`, `ext/dapr-ext-fastapi/`, `ext/flask_dapr/`, and `examples/demo_actor/`. Proto stubs (`dapr.proto.*`) have errors ignored.
154+
155+
## Commit and PR conventions
156+
157+
- **DCO required**: Every commit must include a `Signed-off-by` line. Use `git commit -s` to add it automatically.
158+
- **CI checks**: Linting (ruff), unit tests (Python 3.10-3.14), type checking (mypy), and DCO verification run on all PRs.
159+
- **Branch targets**: PRs go to `main` or `release-*` branches.
160+
- **Tag-based releases**: Tags like `v*`, `workflow-v*`, `grpc-v*`, `fastapi-v*`, `flask-v*`, `langgraph-v*`, `strands-v*` trigger PyPI publishing for the corresponding package.
161+
162+
## Agent task checklist
163+
164+
When completing any task on this project, work through this checklist. Not every item applies to every change — use judgment — but always consider each one.
165+
166+
### Before writing code
167+
168+
- [ ] Read the relevant existing source files before making changes
169+
- [ ] Understand the existing patterns in the area you're modifying (naming, error handling, async vs sync)
170+
- [ ] Check if there's both a sync and async variant that needs updating (see `dapr/aio/` and extension `aio/` subdirectories)
171+
- [ ] Read the relevant extension's `AGENTS.md` for architecture and gotchas specific to that area
172+
173+
### Implementation
174+
175+
- [ ] Follow existing code style: single quotes, 100-char lines, Python 3.10+ syntax
176+
- [ ] Do not edit files under `dapr/proto/` — these are auto-generated
177+
- [ ] Do not add `__init__.py` files to namespace package roots in extensions
178+
179+
### Unit tests
180+
181+
- [ ] Add or update unit tests under `tests/` (core SDK) or `ext/*/tests/` (extensions)
182+
- [ ] Tests use `unittest` — follow the existing test patterns in the relevant directory
183+
- [ ] Verify tests pass: `python -m unittest discover -v ./tests` (or the relevant test directory)
184+
185+
### Linting and type checking
186+
187+
- [ ] Run `ruff check --fix && ruff format` and fix any remaining issues
188+
- [ ] Run `mypy --config-file mypy.ini` if you changed files covered by mypy (actor, clients, conf, serializers, ext-grpc, ext-fastapi, flask_dapr)
189+
190+
### Examples (integration tests)
191+
192+
- [ ] If you added a new user-facing feature or building block, add or update an example in `examples/`
193+
- [ ] Ensure the example README has `<!-- STEP -->` blocks with `expected_stdout_lines` so it is validated in CI
194+
- [ ] If you added a new example, register it in `tox.ini` under `[testenv:examples]`
195+
- [ ] If you changed output format of existing functionality, update `expected_stdout_lines` in affected example READMEs
196+
- [ ] See `examples/AGENTS.md` for full details on writing examples
197+
198+
### Documentation
199+
200+
- [ ] Update docstrings if you changed a public API's signature or behavior
201+
- [ ] Update the relevant example README if the usage pattern changed
202+
203+
### Final verification
204+
205+
- [ ] Run `tox -e ruff` — linting must be clean
206+
- [ ] Run `tox -e py311` (or your Python version) — all unit tests must pass
207+
- [ ] If you touched examples: `tox -e example-component -- <example-name>` to validate locally
208+
- [ ] Commits must be signed off for DCO: `git commit -s`
209+
210+
## Important files
211+
212+
| File | Purpose |
213+
|------|---------|
214+
| `setup.cfg` | Core package metadata and dependencies |
215+
| `setup.py` | Package build script (handles dev version suffixing) |
216+
| `pyproject.toml` | Ruff configuration |
217+
| `tox.ini` | Test environments and CI commands |
218+
| `mypy.ini` | Type checking configuration |
219+
| `dev-requirements.txt` | Development/test dependencies |
220+
| `dapr/version/__init__.py` | SDK version string |
221+
| `ext/*/setup.cfg` | Extension package metadata and dependencies |
222+
| `examples/validate.sh` | Entry point for mechanical-markdown example validation |
223+
224+
## Gotchas
225+
226+
- **Namespace packages**: Do not add `__init__.py` to the top-level `dapr/` directory in extensions — it will break namespace package resolution.
227+
- **Proto files**: Never manually edit anything under `dapr/proto/`. These are generated.
228+
- **Extension independence**: Each extension is a separate PyPI package. Core SDK changes should not break extensions; extension changes should not require core SDK changes unless intentional.
229+
- **DCO signoff**: PRs will be blocked by the DCO bot if commits lack `Signed-off-by`. Always use `git commit -s`.
230+
- **Ruff version pinned**: Dev requirements pin `ruff === 0.14.1`. Use this exact version to match CI.
231+
- **Examples are integration tests**: Changing output format (log messages, print statements) can break example validation. Always check `expected_stdout_lines` in example READMEs when modifying user-visible output.
232+
- **Background processes in examples**: Examples that start background services (servers, subscribers) must include a cleanup step to stop them, or CI will hang.
233+
- **Workflow is the most active area**: See `ext/dapr-ext-workflow/AGENTS.md` for workflow-specific architecture and constraints.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

dapr/aio/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
Copyright 2026 The Dapr Authors
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
"""

dapr/aio/clients/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ async def invoke_method(
103103
self,
104104
app_id: str,
105105
method_name: str,
106-
data: Union[bytes, str, GrpcMessage],
106+
data: Union[bytes, str, GrpcMessage] = b'',
107107
content_type: Optional[str] = None,
108108
metadata: Optional[MetadataTuple] = None,
109109
http_verb: Optional[str] = None,

dapr/aio/clients/grpc/client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import socket
1818
import time
1919
import uuid
20-
from typing import Any, Awaitable, Callable, Dict, List, Optional, Sequence, Text, Union
20+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Sequence, Text, Tuple, Union
2121
from urllib.parse import urlencode
2222
from warnings import warn
2323

@@ -149,7 +149,7 @@ def __init__(
149149

150150
useragent = f'dapr-sdk-python/{__version__}'
151151
if not max_grpc_message_length:
152-
options = [
152+
options: List[Tuple[str, Any]] = [
153153
('grpc.primary_user_agent', useragent),
154154
]
155155
else:
@@ -200,7 +200,7 @@ def __init__(
200200

201201
@staticmethod
202202
def get_credentials():
203-
return grpc.ssl_channel_credentials()
203+
return grpc.ssl_channel_credentials() # type: ignore[attr-defined]
204204

205205
async def close(self):
206206
"""Closes Dapr runtime gRPC channel."""
@@ -599,7 +599,7 @@ async def subscribe_with_handler(
599599
self,
600600
pubsub_name: str,
601601
topic: str,
602-
handler_fn: Callable[..., TopicEventResponse],
602+
handler_fn: Callable[..., Awaitable[TopicEventResponse]],
603603
metadata: Optional[dict] = None,
604604
dead_letter_topic: Optional[str] = None,
605605
) -> Callable[[], Awaitable[None]]:
@@ -1015,7 +1015,7 @@ async def execute_state_transaction(
10151015
operationType=o.operation_type.value,
10161016
request=common_v1.StateItem(
10171017
key=o.key,
1018-
value=to_bytes(o.data),
1018+
value=to_bytes(o.data) if o.data is not None else to_bytes(''),
10191019
etag=common_v1.Etag(value=o.etag) if o.etag is not None else None,
10201020
),
10211021
)
@@ -1355,7 +1355,7 @@ async def try_lock(
13551355
response = await call
13561356
return TryLockResponse(
13571357
success=response.success,
1358-
client=self,
1358+
client=self, # type: ignore[arg-type]
13591359
store_name=store_name,
13601360
resource_id=resource_id,
13611361
lock_owner=lock_owner,

dapr/aio/clients/grpc/interceptors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ async def _intercept_call(self, client_call_details: ClientCallDetails) -> Clien
9292
:class: `ClientCallDetails` modified call details
9393
"""
9494

95-
metadata = []
95+
metadata: List[Tuple[str, str]] = []
9696
if client_call_details.metadata is not None:
97-
metadata = list(client_call_details.metadata)
97+
metadata = list(client_call_details.metadata) # type: ignore[arg-type]
9898
metadata.extend(self._metadata)
9999

100100
new_call_details = _ClientCallDetailsAsync(

dapr/aio/clients/grpc/subscription.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
22

3-
from grpc import StatusCode
3+
from grpc import StatusCode # type: ignore[attr-defined]
44
from grpc.aio import AioRpcError
55

66
from dapr.aio.clients.health import DaprHealth
@@ -21,7 +21,7 @@ def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=No
2121
self._metadata = metadata or {}
2222
self._dead_letter_topic = dead_letter_topic or ''
2323
self._stream = None
24-
self._send_queue = asyncio.Queue()
24+
self._send_queue: asyncio.Queue[api_v1.SubscribeTopicEventsRequestAlpha1] = asyncio.Queue()
2525
self._stream_active = asyncio.Event()
2626

2727
async def start(self):

0 commit comments

Comments
 (0)