Skip to content

Commit 204af00

Browse files
docs and tests for daisychain connector
1 parent 8c60cfa commit 204af00

File tree

4 files changed

+211
-0
lines changed

4 files changed

+211
-0
lines changed

docs/daisychain.rst

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
Daisychain
2+
==========
3+
4+
********
5+
Overview
6+
********
7+
8+
`Daisychain <https://www.daisychain.app/>`_ is a digital organizing automation tool that allows organizers to string together automated digital outreach steps, similar to Zapier but designed specifically for digital organizing. The Parsons connector provides methods for finding contacts and triggering events through the Daisychain API.
9+
10+
This connector implements the ``people/match`` and ``actions`` endpoints of the Daisychain API, allowing you to find existing people records and post actions that can trigger automations.
11+
12+
.. note::
13+
Authentication
14+
The Daisychain API uses API token authentication. You can generate your API token from your Daisychain account settings.
15+
16+
***********
17+
Quick Start
18+
***********
19+
20+
To instantiate the class, you can either pass in the API token as an argument or set the ``DAISYCHAIN_API_TOKEN`` environmental variable.
21+
22+
.. code-block:: python
23+
24+
from parsons import Daisychain
25+
26+
# First approach: Use API credentials via environmental variables
27+
dc = Daisychain()
28+
29+
# Second approach: Pass API credentials as arguments
30+
dc = Daisychain(api_token='MY_API_TOKEN')
31+
32+
You can then call various endpoints:
33+
34+
.. code-block:: python
35+
36+
# Find a person by email address
37+
people = dc.find_person(email_address='person@example.com')
38+
39+
# Find a person by phone number
40+
people = dc.find_person(phone_number='+15555551234')
41+
42+
# Find a person by email and phone (AND logic)
43+
people = dc.find_person(email_address='person@example.com', phone_number='+15555551234')
44+
45+
# Post an action to trigger an automation
46+
# This will create a new person if they don't exist, or associate the action with an existing person
47+
person_id = dc.post_action(
48+
email_address='person@example.com',
49+
phone_number='+15555551234',
50+
first_name='Jane',
51+
last_name='Doe',
52+
email_opt_in=True,
53+
sms_opt_in=True,
54+
action_data={'type': 'petition_signature', 'petition_id': '12345'}
55+
)
56+
57+
# Use action_data with custom fields to trigger specific automations
58+
# The action_data can contain any JSON and be matched in Daisychain automation builder
59+
person_id = dc.post_action(
60+
email_address='volunteer@example.com',
61+
action_data={'event_type': 'volunteer_signup', 'event_id': 'canvass_2024'}
62+
)
63+
64+
***
65+
API
66+
***
67+
.. autoclass :: parsons.Daisychain
68+
:inherited-members:

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ Indices and tables
198198
controlshift
199199
copper
200200
crowdtangle
201+
daisychain
201202
databases
202203
donorbox
203204
empower

test/test_daisychain/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Empty init file for test module
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import unittest
2+
3+
import pytest
4+
import requests_mock
5+
6+
from parsons import Daisychain
7+
8+
9+
class TestDaisychain(unittest.TestCase):
10+
def setUp(self):
11+
self.api_token = "test_api_token"
12+
self.connector = Daisychain(api_token=self.api_token)
13+
14+
def test_initialization(self):
15+
"""Test that Daisychain initializes with correct API token header."""
16+
assert self.connector.connection.headers["X-API-Token"] == self.api_token
17+
assert self.connector.connection.uri == "https://go.daisychain.app/api/v1/"
18+
19+
@requests_mock.Mocker()
20+
def test_find_person_email(self, mocker):
21+
"""Test finding a person by email address."""
22+
expected_response = {
23+
"people": [
24+
{
25+
"id": "person123",
26+
"email": "test@example.com",
27+
"first_name": "Test",
28+
"last_name": "User",
29+
}
30+
]
31+
}
32+
33+
mocker.post("https://go.daisychain.app/api/v1/people/match", json=expected_response)
34+
35+
result = self.connector.find_person(email_address="test@example.com")
36+
37+
assert result == expected_response["people"]
38+
assert len(result) == 1
39+
assert result[0]["email"] == "test@example.com"
40+
41+
@requests_mock.Mocker()
42+
def test_find_person_phone(self, mocker):
43+
"""Test finding a person by phone number."""
44+
expected_response = {
45+
"people": [
46+
{
47+
"id": "person456",
48+
"phone_number": "+15555551234",
49+
"first_name": "Jane",
50+
"last_name": "Doe",
51+
}
52+
]
53+
}
54+
55+
mocker.post("https://go.daisychain.app/api/v1/people/match", json=expected_response)
56+
57+
result = self.connector.find_person(phone_number="+15555551234")
58+
59+
assert result == expected_response["people"]
60+
assert result[0]["phone_number"] == "+15555551234"
61+
62+
@requests_mock.Mocker()
63+
def test_find_person_email_and_phone(self, mocker):
64+
"""Test finding a person by both email and phone."""
65+
expected_response = {
66+
"people": [
67+
{
68+
"id": "person789",
69+
"email": "combo@example.com",
70+
"phone_number": "+15555559999",
71+
"first_name": "Combo",
72+
"last_name": "Test",
73+
}
74+
]
75+
}
76+
77+
mocker.post("https://go.daisychain.app/api/v1/people/match", json=expected_response)
78+
79+
result = self.connector.find_person(
80+
email_address="combo@example.com", phone_number="+15555559999"
81+
)
82+
83+
assert result == expected_response["people"]
84+
85+
def test_find_person_requires_parameter(self):
86+
"""Test that find_person requires at least one parameter."""
87+
with pytest.raises(AssertionError):
88+
self.connector.find_person()
89+
90+
@requests_mock.Mocker()
91+
def test_post_action_minimal(self, mocker):
92+
"""Test posting an action with minimal data."""
93+
expected_response = {"person": {"id": "person123"}}
94+
95+
mocker.post("https://go.daisychain.app/api/v1/actions", json=expected_response)
96+
97+
person_id = self.connector.post_action(email_address="test@example.com")
98+
99+
assert person_id == "person123"
100+
101+
@requests_mock.Mocker()
102+
def test_post_action_full(self, mocker):
103+
"""Test posting an action with full person data."""
104+
expected_response = {"person": {"id": "person456"}}
105+
106+
mocker.post("https://go.daisychain.app/api/v1/actions", json=expected_response)
107+
108+
person_id = self.connector.post_action(
109+
email_address="test@example.com",
110+
phone_number="+15555551234",
111+
first_name="Jane",
112+
last_name="Doe",
113+
addresses=[{"city": "New York", "state": "NY"}],
114+
email_opt_in=True,
115+
sms_opt_in=True,
116+
action_data={"type": "petition_signature", "petition_id": "12345"},
117+
)
118+
119+
assert person_id == "person456"
120+
121+
@requests_mock.Mocker()
122+
def test_post_action_custom_data(self, mocker):
123+
"""Test posting an action with custom action data."""
124+
expected_response = {"person": {"id": "person789"}}
125+
126+
mocker.post("https://go.daisychain.app/api/v1/actions", json=expected_response)
127+
128+
custom_data = {
129+
"event_type": "volunteer_signup",
130+
"event_id": "canvass_2024",
131+
"custom_field": "custom_value",
132+
}
133+
134+
person_id = self.connector.post_action(phone_number="+15555559999", action_data=custom_data)
135+
136+
assert person_id == "person789"
137+
138+
def test_post_action_requires_contact(self):
139+
"""Test that post_action requires at least email or phone."""
140+
with pytest.raises(AssertionError):
141+
self.connector.post_action(first_name="Test", last_name="User")

0 commit comments

Comments
 (0)