Python SDK for Dooray.com with Socket Mode support
Project description
Dooray! SDK - Python
A Python SDK for building agents on Dooray!. This document covers the core classes, decorators, and usage patterns.
Highlights
- One SDK surface for the messenger, task, and wiki APIs — plus real-time events from other Dooray! services
- Real-time task / wiki events via the
commonWebSocket (@agent.on("task", action="createTask")) - 13 task and 16 wiki REST methods — 29 in total (
agent.task.create_task(...),agent.wiki.create_page(...)) - Unified LLM access for Dooray! AI, OpenAI, and Azure OpenAI (
dooray_sdk.query)
Installation
Install via pip
# Currently in beta (requires --pre flag)
pip install --pre dooray-sdk
Using Virtual Environment (Recommended)
# Create virtual environment
python3 -m venv .venv
# Activate (Linux/macOS)
source .venv/bin/activate
# Activate (Windows)
.venv\Scripts\activate
# Install
pip install --pre dooray-sdk
Dependencies
Installed automatically with the package:
| Package | Version | Description |
|---|---|---|
requests |
>= 2.25.0 | Sync HTTP client |
websocket-client |
>= 1.0.0 | WebSocket client |
python-dotenv |
>= 1.0.0 | .env file support |
aiohttp |
>= 3.8.0 | Async HTTP/WebSocket transport (powers Agent real-time runs) |
Optional Extras
| Extra | Installs | When to use |
|---|---|---|
[aiohttp] |
aiohttp>=3.8.0 |
Included by default since 0.1.0b1 — kept as a backward-compat alias |
[templates] |
cookiecutter>=2.0.0 |
dooray-sdk init project templates |
[llm] |
langchain-core, langchain-openai, pydantic |
LLM helpers (query, QueryOptions) |
[all] |
Everything above | Full installation |
The LLM helpers expose a simple query() API on top of LangChain — see examples/ai_agent.py in the repository for a working agent.
Quick Start (Templates)
Use the CLI tool to quickly bootstrap your project.
Install with Templates
pip install --pre "dooray-sdk[templates]"
Usage
# List available templates
dooray-sdk list
# Create an echo agent project
dooray-sdk init echo-agent
# Create with default values (no prompts)
dooray-sdk init echo-agent --no-input
# Create in a specific directory
dooray-sdk init echo-agent -o ./projects
Available Templates
| Template | Description |
|---|---|
echo-agent |
A simple agent that echoes messages back |
Environment Variables
The SDK uses the following environment variables:
| Variable | Required | Description | Example |
|---|---|---|---|
DOORAY_AGENT_TOKEN |
Yes | Agent authentication token | your_agent_token |
DOORAY_DOMAIN |
Yes | Dooray! domain | company.dooray.com |
DOORAY_BOT_TOKEN |
No | Fallback token (checked when DOORAY_AGENT_TOKEN is unset) |
your_agent_token |
DOORAY_SERVICES |
No | Comma-separated service list (default: messenger) |
messenger,common |
Classes
Agent
The core class for building Dooray! agents. Supports message handling, auto-replies, and event processing.
class Agent:
def __init__(
self,
token: str = None,
domain: str = None,
services: List[str] = None,
**kwargs
)
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
token |
str | None |
None |
Agent token (falls back to DOORAY_AGENT_TOKEN env var) |
domain |
str | None |
None |
Dooray! domain (falls back to DOORAY_DOMAIN env var) |
services |
List[str] | None |
["messenger"] |
WebSocket services to connect: "messenger" and/or "common". Falls back to the DOORAY_SERVICES env var. Task/wiki events arrive over the common connection |
Properties
| Property | Type | Description |
|---|---|---|
token |
str |
Agent authentication token |
domain |
str |
Dooray! domain |
services |
List[str] |
Connected service list |
base_url |
str |
API base URL |
is_connected |
bool |
Connection status. Only valid after run()/start() — accessing it earlier raises AttributeError |
messenger |
ServiceNamespace |
Messenger API namespace (agent.messenger.send_message(...) etc.) |
task |
ServiceNamespace |
Task API namespace (agent.task.create_task(...) etc., 13 methods) |
wiki |
ServiceNamespace |
Wiki API namespace (agent.wiki.create_page(...) etc., 16 methods) |
Methods
| Method | Description |
|---|---|
run() |
Start the agent (blocking) |
on(event_type, action, service) |
Event handler decorator (preferred) |
add_handler(event_type, handler, action, service) |
Register handler (non-decorator) |
reply_to(trigger, response) |
Simple response mapping |
send_message(channel, text) |
Deprecated. Use agent.messenger.send_message(...) |
Service Namespace Clients
agent.messenger, agent.task, and agent.wiki expose service-scoped API clients that group related methods together (Slack Bolt / atlassian-python-api style).
| Namespace | Method | Description |
|---|---|---|
agent.messenger |
send_message(channel, text, **kw) |
Post a new message to a channel/DM |
agent.messenger |
reply_to_message(channel, message_id, text, **kw) |
Quote-reply to a specific message |
agent.messenger |
reply_in_thread(parent_channel_id, text, **kw) |
Reply inside a thread (raises NotImplementedError — implementation pending) |
agent.task |
create_task, get_task, list_tasks, modify_task, set_done, ... |
Task (project post) CRUD — 13 methods, see TaskAPI |
agent.wiki |
list_wikis, create_page, get_page, modify_page, move_page, ... |
Wiki page/comment CRUD — 16 methods, see WikiAPI |
The same names also still accept decorator usage (@agent.messenger etc.) for backward compatibility, but that path emits a DeprecationWarning. Use @agent.on(...) instead.
Example - Basic Usage (recommended)
Handlers now receive (req, client) — req carries the event data and client (the Agent instance) exposes the service namespaces.
from dooray_sdk import Agent
agent = Agent()
@agent.on("message")
async def handle_message(req, client):
"""Handle messenger messages."""
if req.text:
await client.messenger.send_message(
channel=req.channel_info.id,
text=f"Received: {req.text}",
)
agent.run()
A handler that only accepts (req) keeps working for one minor version.
Example - Multi-Service
Task and wiki events are delivered over the common WebSocket connection — pass services=["messenger", "common"] to receive them alongside messenger events.
from dooray_sdk import Agent
agent = Agent(services=["messenger", "common"])
@agent.on("message")
async def handle_messenger(req, client):
await client.messenger.send_message(
channel=req.channel_info.id,
text="This is a messenger message",
)
@agent.on("task", action="createTask")
async def handle_task_created(req, client):
# Send a notification message about the new task
notify_channel = "<channel-id>"
subject = req.entity.data.subject if req.entity.data else "(no subject)"
await client.messenger.send_message(
channel=notify_channel,
text=f"New task: {subject}",
)
@agent.on("page")
async def handle_wiki(req, client):
print(f"Wiki event: {req.action}")
agent.run()
Example - Reply to a Specific Message
@agent.on("message")
async def handle(req, client):
if req.text == "quote me":
await client.messenger.reply_to_message(
channel=req.channel_info.id,
message_id=req.entity.data.id,
text="Quoted reply",
)
SocketModeRequest
Represents an incoming request received via WebSocket.
@dataclass
class SocketModeRequest:
envelope_id: str
type: str
payload: Dict[str, Any]
accepts_response_payload: bool = False
service: str = "messenger"
action: str = "" # wrapped into ActionWrapper
entity: Any = None # wrapped into EntityWrapper
actor: Any = None # wrapped into ActorWrapper
action_data: Optional[Dict[str, Any]] = None # internal; access via action.data
channel_data: Optional[Dict[str, Any]] = None # access via channel_info
Raw dicts passed to entity, actor, and action are automatically converted to wrapper objects (EntityWrapper, ActorWrapper, ActionWrapper) in __post_init__.
Properties
| Property | Type | Description |
|---|---|---|
envelope_id |
str |
Unique message ID |
type |
str |
Event type (message, task, taskComment, page, pageComment, etc.) |
payload |
Dict[str, Any] |
Raw payload data |
service |
str |
Service name (messenger, task, wiki) |
action |
ActionWrapper |
Action info; compares equal to strings (req.action == "create") |
entity |
EntityWrapper |
Entity information wrapper |
actor |
ActorWrapper |
Actor information wrapper |
Convenience Properties
| Property | Type | Description |
|---|---|---|
is_message |
bool |
Whether this is a message type |
text |
str | None |
Message text |
channel_info |
ChannelInfo | None |
Channel info (id, scope, type). None when the raw event has no channel field |
channel |
str | None |
Deprecated. Use channel_info.id |
data |
Dict[str, Any] |
Raw data (alias for payload) |
Methods
| Method | Description |
|---|---|
reply(text) |
Deprecated. Delegates to client.messenger.send_message(channel=req.channel_info.id, text=text) with a DeprecationWarning. Use the (req, client) handler signature and client.messenger.<method>(...) directly. |
is_message / is_task / is_page |
Type check properties |
is_action_type(action) |
Check action (accepts a string or a list of strings). req.action == "create" also works |
is_service(service) |
Check service |
to_dict() |
Convert to dictionary |
from_dict(data) |
Create from dictionary (class method) |
SocketModeRequest is now a pure event-data object — action methods have moved to the service namespace clients (client.messenger.*).
Example - Request Handling
@agent.on("message")
async def handle(req, client):
# Check message type
if req.is_message:
print(f"Text: {req.text}")
print(f"Channel: {req.channel_info.id}")
# Check action
if req.action == "create": # or req.is_action_type("create")
print("This is a new message")
# Check service
if req.is_service("messenger"):
await client.messenger.send_message(
channel=req.channel_info.id,
text="Responding from messenger",
)
WebClient
Synchronous REST API client (HTTP transport).
class WebClient:
def __init__(
self,
token: str,
base_url: str = "https://api.dooray.com",
timeout: int = 30
)
Service Namespace
| Namespace | Type | Description |
|---|---|---|
client.messenger |
MessengerAPI |
Messenger service methods (send_message, reply_to_message, reply_in_thread) |
Methods
| Method | Return Type | Description |
|---|---|---|
get_member(member_id) |
Dict |
Get organization member info |
send_message(channel, text, **kwargs) |
Dict |
Deprecated. Use client.messenger.send_message(...). |
get_channels(**kwargs) |
Dict |
Deprecated. Will move under client.messenger.* in a future release. |
get_messages(channel, limit, **kwargs) |
Dict |
Deprecated. Will move under client.messenger.* in a future release. |
get_user_info(user_id) |
Dict |
Deprecated. Will move under client.messenger.* in a future release. |
AsyncWebClient
Asynchronous REST API client (HTTP transport).
class AsyncWebClient:
def __init__(
self,
token: str,
base_url: str = "https://api.dooray.com",
timeout: int = 30
)
Service Namespace
| Namespace | Type | Description |
|---|---|---|
client.messenger |
AsyncMessengerAPI |
Messenger service methods (send_message, reply_to_message, reply_in_thread) |
Methods
| Method | Return Type | Description |
|---|---|---|
get_member(member_id) |
Awaitable[Dict] |
Get organization member info |
send_message(channel, text, **kwargs) |
Awaitable[Dict] |
Deprecated. Use client.messenger.send_message(...). |
get_channels(**kwargs) |
Awaitable[Dict] |
Deprecated. Will move under client.messenger.* in a future release. |
get_messages(channel, limit, **kwargs) |
Awaitable[Dict] |
Deprecated. Will move under client.messenger.* in a future release. |
get_user_info(user_id) |
Awaitable[Dict] |
Deprecated. Will move under client.messenger.* in a future release. |
MessengerAPI / AsyncMessengerAPI
Messenger service clients. Reached via agent.messenger, WebClient.messenger, or AsyncWebClient.messenger.
| Method | Description | HTTP |
|---|---|---|
send_message(channel, text, **kw) |
Post a new message to a channel/DM | POST /messenger/v1/channels/{channel}/logs |
reply_to_message(channel, message_id, text, **kw) |
Quote-reply to a specific message | POST /messenger/v1/channels/{channel}/logs/{message_id}/reply |
reply_in_thread(parent_channel_id, text, **kw) |
Reply inside a thread | Not implemented yet — raises NotImplementedError. Implementation pending. |
Each method is provided in both sync (MessengerAPI) and async (AsyncMessengerAPI) flavours.
TaskAPI / AsyncTaskAPI
Task (project post) service clients — 13 methods. Reached via agent.task.<method>(...) inside a running agent, or constructed directly with a transport (TaskAPI(WebClient(...)) / AsyncTaskAPI(AsyncWebClient(...))). Both classes are importable from dooray_sdk.
| Method | Description |
|---|---|
create_task(project_id, subject, body, **kw) |
Create a task. A str body is auto-wrapped as text/x-markdown |
get_task(project_id, post_id) |
Get task details |
get_task_by_id(post_id) |
Get a task without a project ID |
list_tasks(project_id, *, page=0, size=20, **filters) |
List tasks. size is clamped to 100 with a UserWarning |
modify_task(project_id, post_id, *, version=None, **kw) |
Modify a task (version enables optimistic locking) |
set_workflow(project_id, post_id, workflow_id) |
Change the task workflow state |
set_assignee_workflow(project_id, post_id, member_id, workflow_id) |
Change one assignee's state (member_id="me" allowed) |
set_done(project_id, post_id) |
Mark the task as done |
create_comment(project_id, post_id, body, *, attach_file_ids=None) |
Create a comment |
list_comments(project_id, post_id, *, page=0, size=20, order="createdAt") |
List comments |
get_comment(project_id, post_id, log_id) |
Get one comment |
modify_comment(project_id, post_id, log_id, body, *, attach_file_ids=None) |
Modify a comment |
delete_comment(project_id, post_id, log_id) |
Delete a comment |
@agent.on("message")
async def create_task_from_message(req, client):
result = await client.task.create_task(
project_id="<project-id>",
subject="New task from chat",
body=req.text or "(no text)", # str is wrapped as text/x-markdown
)
print(result)
WikiAPI / AsyncWikiAPI
Wiki service clients — 16 methods (11 page + 5 comment). Reached via agent.wiki.<method>(...), or constructed directly (WikiAPI(WebClient(...)) / AsyncWikiAPI(AsyncWebClient(...))). Both classes are importable from dooray_sdk.
| Method | Description |
|---|---|
list_wikis(*, page=0, size=20) |
List accessible wikis |
create_page(wiki_id, subject, body, *, parent_page_id=None, attach_file_ids=None, referrers=None) |
Create a page |
list_pages(wiki_id, *, parent_page_id=None) |
List sibling pages (top-level when parent_page_id is omitted) |
get_page(wiki_id, page_id) |
Get a page |
get_page_by_id(page_id) |
Get a page without a wiki ID |
modify_page(wiki_id, page_id, *, subject=None, body=None, referrers=None) |
Modify a page. None fields are omitted; all-None raises ValueError |
set_page_title(wiki_id, page_id, subject) |
Update the title only |
set_page_content(wiki_id, page_id, body) |
Update the content only |
set_page_referrers(wiki_id, page_id, referrers) |
Update the referrers only |
move_page(wiki_id, page_id, target_parent_page_id, *, target_wiki_id=None, with_children=True, before_page_id=None) |
Move / reorder a page |
delete_page(wiki_id, page_id) |
Delete a page |
create_comment(wiki_id, page_id, body) |
Create a comment |
list_comments(wiki_id, page_id, *, page=0, size=20) |
List comments |
get_comment(wiki_id, page_id, comment_id) |
Get one comment |
modify_comment(wiki_id, page_id, comment_id, body) |
Modify a comment |
delete_comment(wiki_id, page_id, comment_id) |
Delete a comment |
@agent.on("message")
async def note_to_wiki(req, client):
page = await client.wiki.get_page(wiki_id="<wiki-id>", page_id="<page-id>")
await client.wiki.create_comment(
wiki_id="<wiki-id>",
page_id="<page-id>",
body=f"From chat: {req.text}",
)
As with the messenger methods, body accepts either a raw str (auto-wrapped as {"mimeType": "text/x-markdown", "content": ...}) or a dict for full control (e.g. text/html). Every method is provided in both sync and async flavours.
Decorators
@agent.on() Decorator (recommended)
Unified event handler decorator. Use service=... and event_type=... to scope.
@agent.on("message") # all message events
@agent.on(service="messenger") # all messenger events
@agent.on("task", action="createTask") # task registration only
@agent.on("page") # all wiki page events
Service Decorators (deprecated)
@agent.messenger, @agent.task, and @agent.wiki still work for one minor version but emit a DeprecationWarning. They are kept because the same names (agent.messenger etc.) now also expose service namespace clients (see above) — the decorator form will be retired in a future release.
@agent.messenger # deprecated — use @agent.on(service="messenger") or @agent.on("message")
@agent.task # deprecated — use @agent.on(service="task")
@agent.wiki # deprecated — use @agent.on(service="wiki") or @agent.on("page")
@agent.on() Signature
def on(
event_type: Union[str, List[str]] = "all",
action: Union[str, List[str]] = None,
service: Union[str, List[str]] = None
) -> Callable
| Parameter | Type | Default | Description |
|---|---|---|---|
event_type |
str | List[str] |
"all" |
Event type filter |
action |
str | List[str] | None |
None |
Action filter |
service |
str | List[str] | None |
None |
Service filter |
Handlers can accept (req, client) (recommended) or (req) (one-minor backward compatibility). client is the Agent instance — use client.messenger.<method>(...) etc. to send.
Examples
# Messenger messages only
@agent.on("message")
async def handle_message(req, client):
pass
# Specific action
@agent.on("message", action="create")
async def handle_new_message(req, client):
pass
# common service — new task created
@agent.on("task", action="createTask")
async def on_task_created(req, client):
pass
# common service — wiki page created / referrers added
@agent.on("page", action=["createPage", "addPageUser"])
async def on_wiki_changed(req, client):
pass
# common service — task comment created
# comment events use event_type "taskComment" — subscribing to "task" does NOT match
@agent.on("taskComment", action="createComment")
async def on_task_comment(req, client):
pass
# Everything (debugging / unified logging)
@agent.on("all")
async def log_all(req, client):
pass
Events delivered via the common service
When connected with Agent(services=["messenger", "common"]), a single WebSocket delivers task / wiki and other Dooray! events together. Register handlers by event type (req.type) and narrow them down with service / action:
Event type (event_type) |
service |
action (as observed) |
Description |
|---|---|---|---|
task |
task |
createTask / addTaskUser |
Task created / assignees·cc added |
taskComment |
task |
createComment |
Task comment created |
page |
wiki |
createPage / addPageUser |
Wiki page created / referrers added |
pageComment |
wiki |
createComment |
Wiki page comment created |
driveFile |
drive |
(as published by the server) | Drive file changes |
Handlers are routed by entity.type. Comment events are routed separately with event_type taskComment / pageComment — @agent.on("task", action="createComment") does not match; subscribe with @agent.on("taskComment", action="createComment") instead.
This table reflects events as published by the server. The SDK forwards the
service/type/actionvalues ofcommonpayloads as-is without filtering, so new event kinds added on the server side arrive without an SDK update.
Request Data Structure
The SDK provides wrapper types that enable dot notation access to dictionary data.
Type Structure Overview
SocketModeRequest
├── entity: EntityWrapper # Entity info (task, page, etc.)
│ ├── type: str # Entity type
│ └── data: typed wrapper # message→MessageData, task→TaskData,
│ # taskComment→TaskCommentData, page→PageData,
│ # pageComment→PageCommentData, unknown→DataWrapper
├── actor: ActorWrapper # Action performer info
│ ├── type: str # Actor type
│ └── data: DataWrapper # Actor data
└── action: ActionWrapper # Action details
├── type: str # Action type
└── data: DataWrapper # Action data
Entity types with typed wrappers (message / task / taskComment / page / pageComment) support nested dot access such as req.entity.data.project.id and req.entity.data.workflow.class_. Unknown types (driveFile, etc.) fall back to the generic DataWrapper, which supports one-level dot access only.
DataWrapper
The base class for all wrapper types. Enables dot notation access to dictionary data.
class DataWrapper:
def __getattr__(self, name: str) -> Any # Dot notation access
def __getitem__(self, key: str) -> Any # Dictionary access
def get(self, key: str, default=None) # Safe access
Usage:
# Dot notation access (one-level fields only on the generic DataWrapper)
data.id
data.subject
# Dictionary access (use this for nested dicts)
data["id"]
data.get("subject", "default")
data["nested"]["value"]
The generic DataWrapper does not wrap nested dicts, so nested dot access like data.nested.value is not available on it. Entity types with typed wrappers (task, page, etc. — see Task/Wiki Event Data below) do support nested dot access.
EntityWrapper
Wraps entity information (tasks, pages, etc.).
| Property | Type | Description |
|---|---|---|
type |
str |
Entity type (e.g., "task", "taskComment", "page", "pageComment") |
data |
typed wrapper | Entity detail data — message→MessageData, task→TaskData, taskComment→TaskCommentData, page→PageData, pageComment→PageCommentData, unknown types→DataWrapper |
Usage:
@agent.on("task", action="createTask")
async def handle_task(req, client):
print(req.entity.type) # "task"
data = req.entity.data # TaskData
print(data.id) # Task ID
print(data.subject) # Task subject
print(data.project.id) # Project ID (nested dot access)
print(data.workflow.class_) # Workflow class — 'class' is a reserved word, hence class_
print(data.workflow["class"]) # dict access also supported
if data.to:
print(data.to[0].organizationMember.name) # Assignee name
Message Data
For messenger messages, entity.type is "message".
| Property | Type | Description |
|---|---|---|
id |
str |
Message ID |
channelId |
str |
Channel ID |
senderId |
str |
Sender member ID |
text |
str |
Message text |
sentAt |
int |
Sent time (timestamp ms) |
seq |
int |
Message sequence number |
directMemberId |
str |
DM recipient member ID |
parentChannelId |
str |
Parent channel ID (threads) |
@agent.on("message")
async def handle(req, client):
print(req.entity.message.text)
print(req.entity.message.channelId)
print(req.entity.message.senderId)
print(req.entity.message.sentAt)
Task/Wiki Event Data
For task / wiki events, entity.data is wrapped in a typed wrapper that supports nested dot access.
entity.type |
Wrapper | Key fields |
|---|---|---|
task |
TaskData |
id, number, subject, createdAt, project(id, code), workflow(id, name, class_), to[]/cc[](organizationMember/emailUser), files[] |
taskComment |
TaskCommentData |
id, task(id, subject), project, createdAt, files[] — the spec includes no comment body; re-fetch via the API when needed |
page |
PageData |
id, subject, createdAt, project, referrers[], files[] |
pageComment |
PageCommentData |
id, page(id, subject), project, createdAt, body(mimeType, content — optional), files[] |
- The workflow class field is the Python reserved word
class, so it is exposed as theworkflow.class_property;workflow["class"]dict access also works. - Fields defined in the spec return safe defaults (
""/0/[]) instead of raising when absent, and the["key"]/.get()dict contract is preserved. - These wrappers live in the
dooray_sdk.socket_mode.requestmodule and are not exported at the top level (dooray_sdk) — for isinstance checks etc., import them asfrom dooray_sdk.socket_mode.request import TaskData.
@agent.on("taskComment", action="createComment")
async def on_task_comment(req, client):
c = req.entity.data # TaskCommentData
print(c.task.subject) # Subject of the commented task
print(c.project.code) # Project code
for f in c.files:
print(f.name, f.size) # Attached files
ActorWrapper
Wraps information about the user who performed the action.
| Property | Type | Description |
|---|---|---|
type |
str |
Actor type (e.g., "organizationMember") |
data |
DataWrapper |
Actor detail data |
organizationMember |
LazyMemberData | None |
Organization member info (supports lazy loading) |
Usage:
@agent.on("message")
async def handle(req, client):
if req.actor:
print(req.actor.type) # "organizationMember"
# Access via organizationMember (recommended)
member = req.actor.organizationMember
print(member.id) # Member ID (local data, no API call)
name = await member.name # Member name (API call when needed)
print(name)
Inside an async handler, always fetch remote fields with await member.<field>. Sync access (print / comparison) only works for fields present in the local data or already fetched via await; sync-accessing an unfetched field inside an async handler raises RuntimeError (see Lazy Loading below).
organizationMember Fields
| Property | Type | Description |
|---|---|---|
id |
str |
Member ID |
name |
str |
Member name |
externalEmailAddress |
str |
Email address |
nickname |
str |
Nickname |
englishName |
str |
English name |
nativeName |
str |
Native name |
userCode |
str |
User code |
locale |
str |
Locale |
timezoneName |
str |
Timezone |
Lazy Loading
organizationMember returns data included in the WebSocket message immediately, and fetches missing fields via API when accessed.
@agent.on("message")
async def handle(req, client):
member = req.actor.organizationMember
# Local data (no API call)
member_id = member.id
# Async access - API call when needed (always use this form in async handlers)
name = await member.name
email = await member.externalEmailAddress
# Sync access (print, comparison, etc.) — only for fields present in
# local data or already fetched via await, as above
print(member.name)
if member.name == "John Doe":
await client.messenger.send_message(
channel=req.channel_info.id, text="Hello!"
)
# Get full data
full_data = await member
Caution: Sync-accessing a remote field that has not been fetched yet inside an async handler raises
RuntimeError: Sync access not available in async context. Useawait member.<field>in async contexts. Sync access that triggers an API call only works outside async contexts (plain sync code).
ActionWrapper
Wraps action detail information.
| Property | Type | Description |
|---|---|---|
type |
str |
Action type (e.g., "create", "update") |
data |
DataWrapper |
Action detail data |
Usage Examples
Echo Agent
from dooray_sdk import Agent
agent = Agent()
@agent.on("message")
async def echo(req, client):
if req.text:
await client.messenger.send_message(
channel=req.channel_info.id,
text=f"Echo: {req.text}",
)
agent.run()
Auto-Reply Agent
from dooray_sdk import Agent
agent = Agent()
# Simple mapping (unchanged)
agent.reply_to("ping", "pong")
agent.reply_to("hello", "Hello there!")
# Custom handler
@agent.on("message")
async def handle(req, client):
text = req.text or ""
if text.startswith("help"):
await client.messenger.send_message(
channel=req.channel_info.id,
text="Available commands: ping, hello, help",
)
agent.run()
Multi-Service Agent
Task/wiki events arrive over the common WebSocket connection.
from dooray_sdk import Agent
agent = Agent(services=["messenger", "common"])
@agent.on("message")
async def on_message(req, client):
print(f"[Messenger] {req.text}")
await client.messenger.send_message(
channel=req.channel_info.id, text="Message received!"
)
@agent.on("task")
async def on_task(req, client):
print(f"[Task] {req.action}: {req.entity.data.subject}")
@agent.on("page")
async def on_wiki(req, client):
print(f"[Wiki] {req.action}: {req.entity.data.subject}")
@agent.on("all")
async def on_any(req, client):
print(f"[All] {req.service}/{req.type}/{req.action}")
agent.run()
Reply to a Specific Message (Quote Reply)
@agent.on("message")
async def handle(req, client):
if req.text == "quote me":
await client.messenger.reply_to_message(
channel=req.channel_info.id,
message_id=req.entity.data.id,
text="This is a quoted reply",
)
Error Handling
Common Exceptions
All SDK exceptions inherit from DooraySDKError and are importable from dooray_sdk.
| Exception | Cause | Solution |
|---|---|---|
DoorayConfigurationError |
Token/domain not set or invalid | Set DOORAY_AGENT_TOKEN and DOORAY_DOMAIN |
DoorayConnectionError |
WebSocket connection failed | Check network and token. (DoorayWebSocketError is defined but not currently raised by the SDK) |
RuntimeError |
Calling reply before agent starts | Use after agent.run() |
Exception Handling Example
from dooray_sdk import Agent, DoorayConfigurationError
try:
agent = Agent()
except DoorayConfigurationError as e:
print(f"Configuration error: {e}")
print("Please set DOORAY_AGENT_TOKEN and DOORAY_DOMAIN")
exit(1)
@agent.on("message")
async def handle(req, client):
try:
# Business logic
process_message(req)
except Exception as e:
# Log error (avoid exposing errors to users)
logger.error(f"Processing error: {e}")
agent.run()
Migration Guide (Service Namespace Refactor)
Starting with this release, the SDK separates event data from action methods and groups actions under per-service namespaces. Older surfaces keep working for one minor version with DeprecationWarning; the next minor release will remove them.
Handler signature
# Before (deprecated; still works for one minor)
@agent.messenger
async def handle(req):
await req.reply("pong")
# After
@agent.on("message")
async def handle(req, client):
await client.messenger.send_message(
channel=req.channel_info.id, text="pong"
)
Three messenger methods
req.reply() historically meant three different things in Dooray!. Each is now its own method:
| Intent | New method |
|---|---|
| Send a fresh message to a channel/DM | client.messenger.send_message(channel, text) |
| Quote-reply to a specific message | client.messenger.reply_to_message(channel, message_id, text) |
| Reply inside a thread | client.messenger.reply_in_thread(parent_channel_id, text) (implementation pending — raises NotImplementedError) |
High availability (active/standby)
The SDK transparently supports running multiple agent processes against the same token: the server enforces one active WebSocket per token (close code 1008 AGENT_ALREADY_CONNECTED), and the SDK puts duplicates into a STANDBY state that retries every 15 seconds. When the active process exits, a standby is promoted automatically — no extra configuration required.
Summary of deprecated surfaces
| Old | New |
|---|---|
req.reply(text) |
await client.messenger.send_message(channel=req.channel_info.id, text=text) |
@agent.messenger / @agent.task / @agent.wiki (decorator) |
@agent.on(service="messenger") (or @agent.on("message"), @agent.on("task"), @agent.on("page")) |
Agent.send_message / get_channels / get_messages / get_user_info |
agent.messenger.send_message(...) (and future agent.messenger.* methods) |
WebClient / AsyncWebClient .send_message / .get_channels / .get_messages / .get_user_info |
client.messenger.<method>(...) |
Run under python -W error::DeprecationWarning to discover remaining call sites.
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dooray_sdk-0.1.0b1.tar.gz.
File metadata
- Download URL: dooray_sdk-0.1.0b1.tar.gz
- Upload date:
- Size: 150.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d78d520c976eeb76a0b5aed652fe397acb1a58a9d8ce8e6e4f22ac99eaa9ddfe
|
|
| MD5 |
573064cb04c7ccc1e42b9987ba576667
|
|
| BLAKE2b-256 |
6e23886d7d4dae15544d8ba36c4bb0cb6f7d859e4b58a43966a515faaccd7a9c
|
File details
Details for the file dooray_sdk-0.1.0b1-py3-none-any.whl.
File metadata
- Download URL: dooray_sdk-0.1.0b1-py3-none-any.whl
- Upload date:
- Size: 93.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c5a9d41710d3f6fb1ca9cae9af19378f580287c5c81886c6d57a5e24396b38c
|
|
| MD5 |
6d58e2fc6d1ebb6606ad224a18fb7254
|
|
| BLAKE2b-256 |
61a017298fe3069b7e1fef98bd3f82e0b62f64b892f4dfad7cb0ea8fbf4a9779
|