solidaritytechtools 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
File without changes
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
from typing import Any, Final
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from .models import (
|
|
7
|
+
EventRsvpCreate,
|
|
8
|
+
EventRsvpUpdate,
|
|
9
|
+
ScheduledTaskCreate,
|
|
10
|
+
UserCreate,
|
|
11
|
+
UserNoteCreate,
|
|
12
|
+
UserUpdate,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
DEFAULT_V1_ST_API_BASE_URL: Final[str] = "https://api.solidarity.tech/v1/"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class STClient:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
api_key: str | None = None,
|
|
22
|
+
base_url: str | None = None,
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Initializes the Solidarity Tech API client.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
api_key: Optional API key. If not provided, it will be loaded
|
|
29
|
+
from the ST_API_KEY env var.
|
|
30
|
+
base_url: Optional base URL. If not provided, it will be loaded o
|
|
31
|
+
from ST_BASE_URL env var or default.
|
|
32
|
+
"""
|
|
33
|
+
if not api_key:
|
|
34
|
+
raise ValueError("Must pass api_key")
|
|
35
|
+
self.api_key: str = api_key
|
|
36
|
+
|
|
37
|
+
if not base_url:
|
|
38
|
+
base_url = DEFAULT_V1_ST_API_BASE_URL
|
|
39
|
+
self.base_url: str = base_url
|
|
40
|
+
|
|
41
|
+
self._client = httpx.Client(
|
|
42
|
+
base_url=self.base_url,
|
|
43
|
+
headers={
|
|
44
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
45
|
+
"Accept": "application/json",
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# --- HTTP Helpers ---
|
|
50
|
+
|
|
51
|
+
def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
52
|
+
response = self._client.get(path, params=params)
|
|
53
|
+
response.raise_for_status()
|
|
54
|
+
return response.json()
|
|
55
|
+
|
|
56
|
+
def _post(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
57
|
+
response = self._client.post(path, json=json)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
return response.json()
|
|
60
|
+
|
|
61
|
+
def _put(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
62
|
+
response = self._client.put(path, json=json)
|
|
63
|
+
response.raise_for_status()
|
|
64
|
+
return response.json()
|
|
65
|
+
|
|
66
|
+
def _delete(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
67
|
+
response = self._client.delete(path, params=params)
|
|
68
|
+
response.raise_for_status()
|
|
69
|
+
return response.json()
|
|
70
|
+
|
|
71
|
+
def _build_params(
|
|
72
|
+
self,
|
|
73
|
+
limit: int = 20,
|
|
74
|
+
offset: int = 0,
|
|
75
|
+
since: int = 0,
|
|
76
|
+
extra: dict[str, Any] | None = None,
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
params = {"_limit": limit, "_offset": offset, "_since": since}
|
|
79
|
+
if extra:
|
|
80
|
+
params.update({k: v for k, v in extra.items() if v is not None})
|
|
81
|
+
return params
|
|
82
|
+
|
|
83
|
+
def _to_dict(self, data: BaseModel | dict[str, Any]) -> dict[str, Any]:
|
|
84
|
+
if isinstance(data, BaseModel):
|
|
85
|
+
return data.model_dump(exclude_unset=True)
|
|
86
|
+
return data
|
|
87
|
+
|
|
88
|
+
# --- Users ---
|
|
89
|
+
|
|
90
|
+
def get_users(
|
|
91
|
+
self,
|
|
92
|
+
limit: int = 20,
|
|
93
|
+
offset: int = 0,
|
|
94
|
+
since: int = 0,
|
|
95
|
+
user_list_ids: list[int] | str | None = None,
|
|
96
|
+
) -> list[dict[str, Any]]:
|
|
97
|
+
extra = {}
|
|
98
|
+
if user_list_ids:
|
|
99
|
+
extra["user_list_ids"] = (
|
|
100
|
+
",".join([str(i) for i in user_list_ids])
|
|
101
|
+
if isinstance(user_list_ids, list)
|
|
102
|
+
else user_list_ids
|
|
103
|
+
)
|
|
104
|
+
params = self._build_params(limit, offset, since, extra)
|
|
105
|
+
return self._get("users", params=params)
|
|
106
|
+
|
|
107
|
+
def get_user(self, user_id: int) -> dict[str, Any]:
|
|
108
|
+
return self._get(f"users/{user_id}")
|
|
109
|
+
|
|
110
|
+
def create_user(self, user_data: UserCreate | dict[str, Any]) -> dict[str, Any]:
|
|
111
|
+
return self._post("users", json=self._to_dict(user_data))
|
|
112
|
+
|
|
113
|
+
def update_user(self, user_id: int, user_data: UserUpdate | dict[str, Any]) -> dict[str, Any]:
|
|
114
|
+
return self._put(f"users/{user_id}", json=self._to_dict(user_data))
|
|
115
|
+
|
|
116
|
+
# --- Activities ---
|
|
117
|
+
|
|
118
|
+
def get_activities(self, limit: int = 20, offset: int = 0, since: int = 0) -> dict[str, Any]:
|
|
119
|
+
return self._get("activities", params=self._build_params(limit, offset, since))
|
|
120
|
+
|
|
121
|
+
# --- Agent Assignments ---
|
|
122
|
+
|
|
123
|
+
def get_agent_assignments(
|
|
124
|
+
self,
|
|
125
|
+
limit: int = 20,
|
|
126
|
+
offset: int = 0,
|
|
127
|
+
since: int = 0,
|
|
128
|
+
user_id: int | None = None,
|
|
129
|
+
agent_user_id: int | None = None,
|
|
130
|
+
) -> list[dict[str, Any]]:
|
|
131
|
+
extra = {"user_id": user_id, "agent_user_id": agent_user_id}
|
|
132
|
+
params = self._build_params(limit, offset, since, extra)
|
|
133
|
+
return self._get("agent_assignments", params=params)
|
|
134
|
+
|
|
135
|
+
def create_agent_assignment(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
136
|
+
return self._post("agent_assignments", json=data)
|
|
137
|
+
|
|
138
|
+
def update_agent_assignment(self, assignment_id: int, data: dict[str, Any]) -> dict[str, Any]:
|
|
139
|
+
return self._put(f"agent_assignments/{assignment_id}", json=data)
|
|
140
|
+
|
|
141
|
+
def delete_agent_assignment(self, assignment_id: int) -> dict[str, Any]:
|
|
142
|
+
return self._delete(f"agent_assignments/{assignment_id}")
|
|
143
|
+
|
|
144
|
+
# --- Calls ---
|
|
145
|
+
|
|
146
|
+
def get_calls(
|
|
147
|
+
self,
|
|
148
|
+
limit: int = 20,
|
|
149
|
+
offset: int = 0,
|
|
150
|
+
since: int = 0,
|
|
151
|
+
user_id: int | None = None,
|
|
152
|
+
) -> list[dict[str, Any]]:
|
|
153
|
+
extra = {"user_id": user_id}
|
|
154
|
+
params = self._build_params(limit, offset, since, extra)
|
|
155
|
+
return self._get("calls", params=params)
|
|
156
|
+
|
|
157
|
+
# --- Chapters ---
|
|
158
|
+
|
|
159
|
+
def get_chapters(
|
|
160
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
161
|
+
) -> list[dict[str, Any]]:
|
|
162
|
+
return self._get("chapters", params=self._build_params(limit, offset, since))
|
|
163
|
+
|
|
164
|
+
def get_chapter_phone_numbers(
|
|
165
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
166
|
+
) -> list[dict[str, Any]]:
|
|
167
|
+
return self._get("chapter_phone_numbers", params=self._build_params(limit, offset, since))
|
|
168
|
+
|
|
169
|
+
# --- Custom User Properties ---
|
|
170
|
+
|
|
171
|
+
def get_custom_user_properties(
|
|
172
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
173
|
+
) -> list[dict[str, Any]]:
|
|
174
|
+
return self._get("custom_user_properties", params=self._build_params(limit, offset, since))
|
|
175
|
+
|
|
176
|
+
# --- Donations ---
|
|
177
|
+
|
|
178
|
+
def get_donation_charges(
|
|
179
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
180
|
+
) -> list[dict[str, Any]]:
|
|
181
|
+
return self._get("donation_charges", params=self._build_params(limit, offset, since))
|
|
182
|
+
|
|
183
|
+
def get_donation_charge(self, charge_id: int) -> dict[str, Any]:
|
|
184
|
+
return self._get(f"donation_charges/{charge_id}")
|
|
185
|
+
|
|
186
|
+
# --- Events & RSVPs ---
|
|
187
|
+
|
|
188
|
+
def get_events(self, limit: int = 20, offset: int = 0, since: int = 0) -> list[dict[str, Any]]:
|
|
189
|
+
return self._get("events", params=self._build_params(limit, offset, since))
|
|
190
|
+
|
|
191
|
+
def get_event(self, event_id: int) -> dict[str, Any]:
|
|
192
|
+
return self._get(f"events/{event_id}")
|
|
193
|
+
|
|
194
|
+
def get_event_sessions(
|
|
195
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
196
|
+
) -> list[dict[str, Any]]:
|
|
197
|
+
return self._get("event_sessions", params=self._build_params(limit, offset, since))
|
|
198
|
+
|
|
199
|
+
def get_event_rsvps(
|
|
200
|
+
self,
|
|
201
|
+
limit: int = 20,
|
|
202
|
+
offset: int = 0,
|
|
203
|
+
since: int = 0,
|
|
204
|
+
event_id: int | None = None,
|
|
205
|
+
user_id: int | None = None,
|
|
206
|
+
) -> list[dict[str, Any]]:
|
|
207
|
+
extra = {"event_id": event_id, "user_id": user_id}
|
|
208
|
+
params = self._build_params(limit, offset, since, extra)
|
|
209
|
+
return self._get("event_rsvps", params=params)
|
|
210
|
+
|
|
211
|
+
def create_event_rsvp(self, rsvp_data: EventRsvpCreate | dict[str, Any]) -> dict[str, Any]:
|
|
212
|
+
return self._post("event_rsvps", json=self._to_dict(rsvp_data))
|
|
213
|
+
|
|
214
|
+
def update_event_rsvp(
|
|
215
|
+
self, rsvp_id: int, rsvp_data: EventRsvpUpdate | dict[str, Any]
|
|
216
|
+
) -> dict[str, Any]:
|
|
217
|
+
return self._put(f"event_rsvps/{rsvp_id}", json=self._to_dict(rsvp_data))
|
|
218
|
+
|
|
219
|
+
# --- User Actions ---
|
|
220
|
+
|
|
221
|
+
def submit_user_action(self, action_data: dict[str, Any]) -> dict[str, Any]:
|
|
222
|
+
"""
|
|
223
|
+
Submits a user action (e.g., form submission, petition signature).
|
|
224
|
+
Matches POST /user_actions.
|
|
225
|
+
"""
|
|
226
|
+
return self._post("user_actions", json=action_data)
|
|
227
|
+
|
|
228
|
+
# --- Texts ---
|
|
229
|
+
|
|
230
|
+
def get_texts(
|
|
231
|
+
self,
|
|
232
|
+
limit: int = 20,
|
|
233
|
+
offset: int = 0,
|
|
234
|
+
since: int = 0,
|
|
235
|
+
user_id: int | None = None,
|
|
236
|
+
) -> list[dict[str, Any]]:
|
|
237
|
+
extra = {"user_id": user_id}
|
|
238
|
+
params = self._build_params(limit, offset, since, extra)
|
|
239
|
+
return self._get("texts", params=params)
|
|
240
|
+
|
|
241
|
+
def create_text(self, text_data: dict[str, Any]) -> dict[str, Any]:
|
|
242
|
+
return self._post("texts", json=text_data)
|
|
243
|
+
|
|
244
|
+
# --- Emails ---
|
|
245
|
+
|
|
246
|
+
def send_email(self, email_data: dict[str, Any]) -> dict[str, Any]:
|
|
247
|
+
return self._post("emails", json=email_data)
|
|
248
|
+
|
|
249
|
+
def get_email_blasts(
|
|
250
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
251
|
+
) -> list[dict[str, Any]]:
|
|
252
|
+
return self._get("email_blasts", params=self._build_params(limit, offset, since))
|
|
253
|
+
|
|
254
|
+
# --- Scheduled Tasks ---
|
|
255
|
+
|
|
256
|
+
def get_scheduled_tasks(
|
|
257
|
+
self,
|
|
258
|
+
limit: int = 20,
|
|
259
|
+
offset: int = 0,
|
|
260
|
+
since: int = 0,
|
|
261
|
+
user_id: int | None = None,
|
|
262
|
+
agent_user_id: int | None = None,
|
|
263
|
+
) -> list[dict[str, Any]]:
|
|
264
|
+
extra = {"user_id": user_id, "agent_user_id": agent_user_id}
|
|
265
|
+
params = self._build_params(limit, offset, since, extra)
|
|
266
|
+
return self._get("scheduled_tasks", params=params)
|
|
267
|
+
|
|
268
|
+
def create_scheduled_task(
|
|
269
|
+
self, task_data: ScheduledTaskCreate | dict[str, Any]
|
|
270
|
+
) -> dict[str, Any]:
|
|
271
|
+
return self._post("scheduled_tasks", json=self._to_dict(task_data))
|
|
272
|
+
|
|
273
|
+
def update_scheduled_task(self, task_id: int, task_data: dict[str, Any]) -> dict[str, Any]:
|
|
274
|
+
return self._put(f"scheduled_tasks/{task_id}", json=task_data)
|
|
275
|
+
|
|
276
|
+
# --- User Notes ---
|
|
277
|
+
|
|
278
|
+
def create_user_note(self, note_data: UserNoteCreate | dict[str, Any]) -> dict[str, Any]:
|
|
279
|
+
return self._post("user_notes", json=self._to_dict(note_data))
|
|
280
|
+
|
|
281
|
+
def delete_user_note(self, note_id: int) -> dict[str, Any]:
|
|
282
|
+
return self._delete(f"user_notes/{note_id}")
|
|
283
|
+
|
|
284
|
+
# --- Organizations ---
|
|
285
|
+
|
|
286
|
+
def get_organizations(
|
|
287
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
288
|
+
) -> list[dict[str, Any]]:
|
|
289
|
+
return self._get("organizations", params=self._build_params(limit, offset, since))
|
|
290
|
+
|
|
291
|
+
# --- Automation Enrollments ---
|
|
292
|
+
|
|
293
|
+
def enroll_in_automation(self, enrollment_data: dict[str, Any]) -> dict[str, Any]:
|
|
294
|
+
return self._post("automation_enrollments", json=enrollment_data)
|
|
295
|
+
|
|
296
|
+
# --- Phonebanks & Textbanks ---
|
|
297
|
+
|
|
298
|
+
def get_phonebanks(
|
|
299
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
300
|
+
) -> list[dict[str, Any]]:
|
|
301
|
+
return self._get("phonebanks", params=self._build_params(limit, offset, since))
|
|
302
|
+
|
|
303
|
+
def get_phonebank(self, phonebank_id: int) -> dict[str, Any]:
|
|
304
|
+
return self._get(f"phonebanks/{phonebank_id}")
|
|
305
|
+
|
|
306
|
+
def get_textbanks(
|
|
307
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
308
|
+
) -> list[dict[str, Any]]:
|
|
309
|
+
return self._get("textbanks", params=self._build_params(limit, offset, since))
|
|
310
|
+
|
|
311
|
+
def get_textbank(self, textbank_id: int) -> dict[str, Any]:
|
|
312
|
+
return self._get(f"textbanks/{textbank_id}")
|
|
313
|
+
|
|
314
|
+
def get_text_blasts(
|
|
315
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
316
|
+
) -> list[dict[str, Any]]:
|
|
317
|
+
return self._get("text_blasts", params=self._build_params(limit, offset, since))
|
|
318
|
+
|
|
319
|
+
def get_text_blast(self, text_blast_id: int) -> dict[str, Any]:
|
|
320
|
+
return self._get(f"text_blasts/{text_blast_id}")
|
|
321
|
+
|
|
322
|
+
# --- User Lists ---
|
|
323
|
+
|
|
324
|
+
def get_user_lists(
|
|
325
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
326
|
+
) -> list[dict[str, Any]]:
|
|
327
|
+
return self._get("user_lists", params=self._build_params(limit, offset, since))
|
|
328
|
+
|
|
329
|
+
def create_user_list(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
330
|
+
return self._post("user_lists", json=data)
|
|
331
|
+
|
|
332
|
+
def get_user_list(self, list_id: int) -> dict[str, Any]:
|
|
333
|
+
return self._get(f"user_lists/{list_id}")
|
|
334
|
+
|
|
335
|
+
def update_user_list(self, list_id: int, data: dict[str, Any]) -> dict[str, Any]:
|
|
336
|
+
return self._put(f"user_lists/{list_id}", json=data)
|
|
337
|
+
|
|
338
|
+
def delete_user_list(self, list_id: int) -> dict[str, Any]:
|
|
339
|
+
return self._delete(f"user_lists/{list_id}")
|
|
340
|
+
|
|
341
|
+
# --- Relationships ---
|
|
342
|
+
|
|
343
|
+
def get_user_relationships(self, user_id: int) -> list[dict[str, Any]]:
|
|
344
|
+
return self._get("user_relationships", params={"user_id": user_id})
|
|
345
|
+
|
|
346
|
+
def create_user_relationship(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
347
|
+
return self._post("user_relationships", json=data)
|
|
348
|
+
|
|
349
|
+
def delete_user_relationship(self, relationship_id: int, user_id: int) -> dict[str, Any]:
|
|
350
|
+
return self._delete(f"user_relationships/{relationship_id}", params={"user_id": user_id})
|
|
351
|
+
|
|
352
|
+
# --- Task Agents & Assignments ---
|
|
353
|
+
|
|
354
|
+
def get_task_agents(
|
|
355
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
356
|
+
) -> list[dict[str, Any]]:
|
|
357
|
+
return self._get("task_agents", params=self._build_params(limit, offset, since))
|
|
358
|
+
|
|
359
|
+
def create_task_agent(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
360
|
+
return self._post("task_agents", json=data)
|
|
361
|
+
|
|
362
|
+
def get_task_agent(self, agent_id: int) -> dict[str, Any]:
|
|
363
|
+
return self._get(f"task_agents/{agent_id}")
|
|
364
|
+
|
|
365
|
+
def delete_task_agent(self, agent_id: int) -> dict[str, Any]:
|
|
366
|
+
return self._delete(f"task_agents/{agent_id}")
|
|
367
|
+
|
|
368
|
+
def get_task_assignments(
|
|
369
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
370
|
+
) -> list[dict[str, Any]]:
|
|
371
|
+
return self._get("task_assignments", params=self._build_params(limit, offset, since))
|
|
372
|
+
|
|
373
|
+
def create_task_assignment(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
374
|
+
return self._post("task_assignments", json=data)
|
|
375
|
+
|
|
376
|
+
def get_task_assignment(self, assignment_id: int) -> dict[str, Any]:
|
|
377
|
+
return self._get(f"task_assignments/{assignment_id}")
|
|
378
|
+
|
|
379
|
+
def update_task_assignment(self, assignment_id: int, data: dict[str, Any]) -> dict[str, Any]:
|
|
380
|
+
return self._put(f"task_assignments/{assignment_id}", json=data)
|
|
381
|
+
|
|
382
|
+
def delete_task_assignment(self, assignment_id: int) -> dict[str, Any]:
|
|
383
|
+
return self._delete(f"task_assignments/{assignment_id}")
|
|
384
|
+
|
|
385
|
+
# --- Team Members ---
|
|
386
|
+
|
|
387
|
+
def get_team_members(
|
|
388
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
389
|
+
) -> list[dict[str, Any]]:
|
|
390
|
+
return self._get("team_members", params=self._build_params(limit, offset, since))
|
|
391
|
+
|
|
392
|
+
def create_team_member(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
393
|
+
return self._post("team_members", json=data)
|
|
394
|
+
|
|
395
|
+
def update_team_member(self, member_id: int, data: dict[str, Any]) -> dict[str, Any]:
|
|
396
|
+
return self._put(f"team_members/{member_id}", json=data)
|
|
397
|
+
|
|
398
|
+
# --- Text Templates ---
|
|
399
|
+
|
|
400
|
+
def get_text_templates(
|
|
401
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
402
|
+
) -> list[dict[str, Any]]:
|
|
403
|
+
return self._get("text_templates", params=self._build_params(limit, offset, since))
|
|
404
|
+
|
|
405
|
+
def create_text_template(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
406
|
+
return self._post("text_templates", json=data)
|
|
407
|
+
|
|
408
|
+
def get_text_template(self, template_id: int) -> dict[str, Any]:
|
|
409
|
+
return self._get(f"text_templates/{template_id}")
|
|
410
|
+
|
|
411
|
+
def update_text_template(self, template_id: int, data: dict[str, Any]) -> dict[str, Any]:
|
|
412
|
+
return self._put(f"text_templates/{template_id}", json=data)
|
|
413
|
+
|
|
414
|
+
def delete_text_template(self, template_id: int) -> dict[str, Any]:
|
|
415
|
+
return self._delete(f"text_templates/{template_id}")
|
|
416
|
+
|
|
417
|
+
# --- Scheduled Calls ---
|
|
418
|
+
|
|
419
|
+
def get_scheduled_calls(
|
|
420
|
+
self, limit: int = 20, offset: int = 0, since: int = 0
|
|
421
|
+
) -> list[dict[str, Any]]:
|
|
422
|
+
return self._get("scheduled_calls", params=self._build_params(limit, offset, since))
|
|
423
|
+
|
|
424
|
+
def get_scheduled_call(self, call_id: int) -> dict[str, Any]:
|
|
425
|
+
return self._get(f"scheduled_calls/{call_id}")
|
|
426
|
+
|
|
427
|
+
# --- Other ---
|
|
428
|
+
|
|
429
|
+
def create_field_survey_url(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
430
|
+
return self._post("field_survey_urls", json=data)
|
|
431
|
+
|
|
432
|
+
def create_custom_user_property_option(
|
|
433
|
+
self, property_id: int, data: dict[str, Any]
|
|
434
|
+
) -> dict[str, Any]:
|
|
435
|
+
return self._post(f"custom_user_properties/{property_id}/options", json=data)
|
|
436
|
+
|
|
437
|
+
def delete_custom_user_property_option(
|
|
438
|
+
self, property_id: int, option_id: int
|
|
439
|
+
) -> dict[str, Any]:
|
|
440
|
+
return self._delete(f"custom_user_properties/{property_id}/options/{option_id}")
|
|
441
|
+
|
|
442
|
+
# --- Lifecycle ---
|
|
443
|
+
|
|
444
|
+
def __enter__(self):
|
|
445
|
+
return self
|
|
446
|
+
|
|
447
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
448
|
+
self._client.close()
|
|
449
|
+
|
|
450
|
+
def close(self):
|
|
451
|
+
"""Closes the underlying httpx client."""
|
|
452
|
+
self._client.close()
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
ScopeType = Literal["Organization", "Chapter", "Team", "User"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserAddressUpdate(BaseModel):
|
|
10
|
+
address1: str | None = None
|
|
11
|
+
address2: str | None = None
|
|
12
|
+
city: str | None = None
|
|
13
|
+
state: str | None = None
|
|
14
|
+
zip_code: str | None = None
|
|
15
|
+
country: str | None = None
|
|
16
|
+
latitude: float | None = None
|
|
17
|
+
longitude: float | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UserUpdate(BaseModel):
|
|
21
|
+
phone_number: str | None = None
|
|
22
|
+
email: str | None = None
|
|
23
|
+
first_name: str | None = None
|
|
24
|
+
last_name: str | None = None
|
|
25
|
+
preferred_language: str | None = None
|
|
26
|
+
chapter_id: int | None = None
|
|
27
|
+
chapter_ids: list[int] | None = None
|
|
28
|
+
add_chapter_ids: list[int] | None = None
|
|
29
|
+
remove_chapter_ids: list[int] | None = None
|
|
30
|
+
set_exclusive_chapter: bool | None = None
|
|
31
|
+
second_language: str | None = None
|
|
32
|
+
referred_by_user_id: int | None = None
|
|
33
|
+
custom_user_properties: dict[str, str] | None = None
|
|
34
|
+
address: UserAddressUpdate | None = None
|
|
35
|
+
sms_permission: bool | None = None
|
|
36
|
+
call_permission: bool | None = None
|
|
37
|
+
email_permission: bool | None = None
|
|
38
|
+
timezone: str | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UserCreate(BaseModel):
|
|
42
|
+
phone_number: str | None = None
|
|
43
|
+
email: str | None = None
|
|
44
|
+
first_name: str | None = None
|
|
45
|
+
last_name: str | None = None
|
|
46
|
+
preferred_language: str | None = "en"
|
|
47
|
+
second_language: str | None = None
|
|
48
|
+
chapter_id: int | None = None
|
|
49
|
+
chapter_ids: list[int] | None = None
|
|
50
|
+
referred_by_user_id: int | None = None
|
|
51
|
+
custom_user_properties: dict[str, str] | None = None
|
|
52
|
+
add_tags: list[str] | None = None
|
|
53
|
+
remove_tags: list[str] | None = None
|
|
54
|
+
address: UserAddressUpdate | None = None
|
|
55
|
+
sms_permission: bool | None = None
|
|
56
|
+
call_permission: bool | None = None
|
|
57
|
+
email_permission: bool | None = None
|
|
58
|
+
timezone: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class EventRsvpCreate(BaseModel):
|
|
62
|
+
event_id: int
|
|
63
|
+
event_session_id: int | None = None
|
|
64
|
+
user_id: int
|
|
65
|
+
is_attending: Literal["yes", "no", "maybe"] | None = "yes"
|
|
66
|
+
is_confirmed: bool | None = False
|
|
67
|
+
agent_user_id: int | None = None
|
|
68
|
+
source: str | None = None
|
|
69
|
+
source_system: str | None = None
|
|
70
|
+
skip_email_confirmation: bool | None = False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class EventRsvpUpdate(BaseModel):
|
|
74
|
+
is_attending: Literal["yes", "no", "maybe"] | None = None
|
|
75
|
+
is_confirmed: bool | None = None
|
|
76
|
+
agent_user_id: int | None = None
|
|
77
|
+
source: str | None = None
|
|
78
|
+
source_system: str | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ScheduledTaskCreate(BaseModel):
|
|
82
|
+
due_at: int | None = None
|
|
83
|
+
remind_at: int | None = None
|
|
84
|
+
agent_user_id: int | None = None
|
|
85
|
+
user_id: int
|
|
86
|
+
notes: str | None = None
|
|
87
|
+
marked_as_completed: bool | None = False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class UserNoteCreate(BaseModel):
|
|
91
|
+
content: str
|
|
92
|
+
user_id: int
|
|
93
|
+
agent_user_id: int | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TextMessage(BaseModel):
|
|
97
|
+
sent_at: int
|
|
98
|
+
content: str
|
|
99
|
+
direction: Literal["in", "out"] | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CallRecord(BaseModel):
|
|
103
|
+
called_at: int
|
|
104
|
+
duration: int
|
|
105
|
+
notes: str | None = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Note(BaseModel):
|
|
109
|
+
id: int
|
|
110
|
+
content: str
|
|
111
|
+
agent_user_id: int
|
|
112
|
+
created_at: datetime
|
|
113
|
+
updated_at: datetime
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Donation(BaseModel):
|
|
117
|
+
model_config = ConfigDict(extra="allow")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Person(BaseModel):
|
|
121
|
+
model_config = ConfigDict(
|
|
122
|
+
populate_by_name=True,
|
|
123
|
+
extra="allow",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
id: int
|
|
127
|
+
name: str
|
|
128
|
+
first_name: str
|
|
129
|
+
last_name: str
|
|
130
|
+
|
|
131
|
+
phone_number: str | None = None
|
|
132
|
+
email: str | None = None
|
|
133
|
+
|
|
134
|
+
chapter: str | None = None
|
|
135
|
+
preferred_language: str | None = None
|
|
136
|
+
|
|
137
|
+
full_address: str | None = None
|
|
138
|
+
address1: str | None = None
|
|
139
|
+
address2: str | None = None
|
|
140
|
+
city: str | None = None
|
|
141
|
+
state: str | None = None
|
|
142
|
+
postal_code: str | None = None
|
|
143
|
+
country: str | None = None
|
|
144
|
+
|
|
145
|
+
created_at: datetime
|
|
146
|
+
paid_dues_since: datetime | None = None
|
|
147
|
+
last_ip_address: str | None = None
|
|
148
|
+
|
|
149
|
+
tags: list[str] = Field(default_factory=list)
|
|
150
|
+
|
|
151
|
+
yes_rsvp_count: int | None = None
|
|
152
|
+
confirmed_rsvp_count: int | None = None
|
|
153
|
+
|
|
154
|
+
most_recent_yes_rsvp_session: str | None = None
|
|
155
|
+
most_recent_confirmed_yes_rsvp_session: str | None = None
|
|
156
|
+
|
|
157
|
+
action_interests: str | None = Field(default=None, alias="action-interests")
|
|
158
|
+
cause_interests: str | None = Field(default=None, alias="cause-interests")
|
|
159
|
+
|
|
160
|
+
# list_date: date | None = Field(
|
|
161
|
+
# default=None, alias="list-date"
|
|
162
|
+
# )
|
|
163
|
+
|
|
164
|
+
membership_status: str | None = Field(default=None, alias="membership-status")
|
|
165
|
+
|
|
166
|
+
ydsa_chapter: str | None = Field(default=None, alias="ydsa-chapter")
|
|
167
|
+
|
|
168
|
+
texts: list[TextMessage] = Field(default_factory=list)
|
|
169
|
+
calls: list[CallRecord] = Field(default_factory=list)
|
|
170
|
+
notes: list[Note] = Field(default_factory=list)
|
|
171
|
+
donations: list[Donation] = Field(default_factory=list)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Chapter(BaseModel):
|
|
175
|
+
id: int
|
|
176
|
+
name: str
|
|
177
|
+
logo_url: str | None = None
|
|
178
|
+
organization_id: int
|
|
179
|
+
chapter_phone_number: str | None = None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class Organization(BaseModel):
|
|
183
|
+
id: int
|
|
184
|
+
name: str
|
|
185
|
+
image_url: str | None = None
|
|
186
|
+
parent_organization_id: int | None = None
|
|
187
|
+
default_language: str | None = None
|
|
188
|
+
supported_languages: list[str] | None = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class User(BaseModel):
|
|
192
|
+
id: int
|
|
193
|
+
hash_id: str | None = None
|
|
194
|
+
phone_number: str | None = None
|
|
195
|
+
email: str | None = None
|
|
196
|
+
first_name: str | None = None
|
|
197
|
+
last_name: str | None = None
|
|
198
|
+
preferred_language: str
|
|
199
|
+
second_language: str | None = None
|
|
200
|
+
chapter_id: int | None = None
|
|
201
|
+
chapter_ids: list[int] = Field(default_factory=list)
|
|
202
|
+
branch_id: int | None = None
|
|
203
|
+
created_at: datetime
|
|
204
|
+
custom_user_properties: dict[str, str] = Field(default_factory=dict)
|
|
205
|
+
address: dict[str, Any] | None = None
|
|
206
|
+
sms_permission: bool = False
|
|
207
|
+
call_permission: bool = False
|
|
208
|
+
email_permission: bool = False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class EventSession(BaseModel):
|
|
212
|
+
id: int
|
|
213
|
+
event_id: int
|
|
214
|
+
start_time: datetime
|
|
215
|
+
end_time: datetime
|
|
216
|
+
title: str | None = None
|
|
217
|
+
location_name: str | None = None
|
|
218
|
+
location_address: str | None = None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class Event(BaseModel):
|
|
222
|
+
id: int
|
|
223
|
+
title: str
|
|
224
|
+
scope_id: int
|
|
225
|
+
scope_type: ScopeType
|
|
226
|
+
event_type: str
|
|
227
|
+
location_name: str | None = None
|
|
228
|
+
created_at: datetime
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Call(BaseModel):
|
|
232
|
+
id: int
|
|
233
|
+
user_id: int
|
|
234
|
+
direction: str
|
|
235
|
+
agent_user_id: int | None = None
|
|
236
|
+
duration: int
|
|
237
|
+
picked_up: bool
|
|
238
|
+
left_voicemail: bool
|
|
239
|
+
created_at: datetime
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class Text(BaseModel):
|
|
243
|
+
id: int
|
|
244
|
+
user_id: int
|
|
245
|
+
direction: str
|
|
246
|
+
body: str | None = None
|
|
247
|
+
media_urls: list[str] = Field(default_factory=list)
|
|
248
|
+
created_at: datetime
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class UserNote(BaseModel):
|
|
252
|
+
id: int
|
|
253
|
+
content: str
|
|
254
|
+
user_id: int
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class DonationCharge(BaseModel):
|
|
258
|
+
id: int
|
|
259
|
+
amount: int
|
|
260
|
+
|
|
261
|
+
success: bool
|
|
262
|
+
refunded: bool
|
|
263
|
+
created_at: datetime
|
|
264
|
+
user: dict[str, Any] | None = None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class Activity(BaseModel):
|
|
268
|
+
id: int
|
|
269
|
+
user_id: int
|
|
270
|
+
name: str
|
|
271
|
+
actionable_id: int
|
|
272
|
+
actionable_type: str
|
|
273
|
+
created_at: datetime
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class ChapterPhoneNumber(BaseModel):
|
|
277
|
+
phone_number: str
|
|
278
|
+
assigned_user_count: int | None = None
|
|
279
|
+
created_at: datetime
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solidaritytechtools
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Requires-Dist: httpx>=0.28.1
|
|
7
|
+
Requires-Dist: pydantic>=2.12.5
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# Solidarity Tech Tools
|
|
11
|
+
|
|
12
|
+
A python library to help you automate solidarity tech (ST).
|
|
13
|
+
|
|
14
|
+
This is still in beta, and there is a bit more work to do. But you can still use this in production if you are bold.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
### Client
|
|
19
|
+
Call python methods to interact with the ST api. You can pass pydantic models and receive pydantic models in return, so you can rely on the response structure.
|
|
20
|
+
|
|
21
|
+
## Todo: Contact Matching
|
|
22
|
+
Given a contact export and a ST account with contacts, attempt to match the two, so you can perform operations.
|
|
23
|
+
|
|
24
|
+
This is useful for example if you want to export migrate resources from one ST account to another.
|
|
25
|
+
|
|
26
|
+
## Using
|
|
27
|
+
|
|
28
|
+
1. Add `solidaritytechtools` as a dependency via `uv add solidaritytechtools`, `pip install solidaritytechtools`, etc
|
|
29
|
+
2. Initialize the client with the api key: `st_client = STClient(api_key="...")`
|
|
30
|
+
3. Perform calls, like `st_client.get_users()`
|
|
31
|
+
|
|
32
|
+
## Contributing
|
|
33
|
+
|
|
34
|
+
1. Clone the repo
|
|
35
|
+
2. Install pre-commit hooks (`uv run pre-commit install`)
|
|
36
|
+
3. Start coding and make a MR :)
|
|
37
|
+
|
|
38
|
+
Please use `ty` to check the type safety of your code. When possible, this will be added to pre-commit hooks.
|
|
39
|
+
|
|
40
|
+
## Publishing
|
|
41
|
+
|
|
42
|
+
1. `uv build`
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
solidaritytechtools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
solidaritytechtools/client/base_client.py,sha256=fO-dJ-hh-FzKZhM3A-91rj4JE-zMXwl6BxsrQmlYtZA,16403
|
|
3
|
+
solidaritytechtools/client/models.py,sha256=sJzje0srV5Wy_FfuV0CPkdS0ZD_iel7g9n01tGugNYQ,7121
|
|
4
|
+
solidaritytechtools-0.0.2.dist-info/METADATA,sha256=KAUMTV5CsqJw8jxn_iXm1xoTSImVP55t0yGX72i8Uls,1379
|
|
5
|
+
solidaritytechtools-0.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
solidaritytechtools-0.0.2.dist-info/RECORD,,
|