solidaritytechtools 0.0.2__tar.gz

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.
@@ -0,0 +1,48 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distribution
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.14"
19
+ - name: Install uv
20
+ run: |
21
+ curl -LsSf https://astral.sh/uv/install.sh | sh
22
+ - name: Build a binary wheel and a source tarball
23
+ run: uv build
24
+
25
+ - name: Store the distribution packages
26
+ uses: actions/upload-artifact@v4
27
+ with:
28
+ name: python-package-distributions
29
+ path: dist/
30
+
31
+ publish-to-pypi:
32
+ name: Publish Python distribution to PyPI
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ environment:
36
+ name: pypi
37
+ url: https://pypi.org/p/solidaritytechtools
38
+ permissions:
39
+ id-token: write # IMPORTANT: mandatory for trusted publishing
40
+
41
+ steps:
42
+ - name: Download all the dists
43
+ uses: actions/download-artifact@v4
44
+ with:
45
+ name: python-package-distributions
46
+ path: dist/
47
+ - name: Publish distribution to PyPI
48
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,27 @@
1
+ *.env
2
+
3
+ *.swp
4
+
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
@@ -0,0 +1,16 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ # Ruff version
4
+ rev: v0.9.4
5
+ hooks:
6
+ # Run the linter.
7
+ - id: ruff
8
+ args: [ --fix ]
9
+ # Run the formatter.
10
+ - id: ruff-format
11
+ # - repo: local
12
+ # hooks:
13
+ # - id: ty
14
+ # name: ty check
15
+ # entry: uvx ty check .
16
+ # language: python
@@ -0,0 +1 @@
1
+ 3.14
@@ -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,33 @@
1
+ # Solidarity Tech Tools
2
+
3
+ A python library to help you automate solidarity tech (ST).
4
+
5
+ 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.
6
+
7
+ ## Features
8
+
9
+ ### Client
10
+ 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.
11
+
12
+ ## Todo: Contact Matching
13
+ Given a contact export and a ST account with contacts, attempt to match the two, so you can perform operations.
14
+
15
+ This is useful for example if you want to export migrate resources from one ST account to another.
16
+
17
+ ## Using
18
+
19
+ 1. Add `solidaritytechtools` as a dependency via `uv add solidaritytechtools`, `pip install solidaritytechtools`, etc
20
+ 2. Initialize the client with the api key: `st_client = STClient(api_key="...")`
21
+ 3. Perform calls, like `st_client.get_users()`
22
+
23
+ ## Contributing
24
+
25
+ 1. Clone the repo
26
+ 2. Install pre-commit hooks (`uv run pre-commit install`)
27
+ 3. Start coding and make a MR :)
28
+
29
+ Please use `ty` to check the type safety of your code. When possible, this will be added to pre-commit hooks.
30
+
31
+ ## Publishing
32
+
33
+ 1. `uv build`
@@ -0,0 +1,6 @@
1
+ def main():
2
+ print("Hello from solidaritytechtools!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "solidaritytechtools"
3
+ version = "0.0.2"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "httpx>=0.28.1",
9
+ "pydantic>=2.12.5",
10
+ ]
11
+
12
+ [dependency-groups]
13
+ dev = [
14
+ "pre-commit>=4.5.1",
15
+ "ruff>=0.14.13",
16
+ "ty>=0.0.13",
17
+ ]
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.ruff]
24
+ line-length = 100
25
+ lint.select = ["E", "F", "I", "UP", "B", "UP"]
@@ -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,343 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.14"
4
+
5
+ [[package]]
6
+ name = "annotated-types"
7
+ version = "0.7.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "anyio"
16
+ version = "4.12.1"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ dependencies = [
19
+ { name = "idna" },
20
+ ]
21
+ sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "certifi"
28
+ version = "2026.1.4"
29
+ source = { registry = "https://pypi.org/simple" }
30
+ sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
31
+ wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "cfgv"
37
+ version = "3.5.0"
38
+ source = { registry = "https://pypi.org/simple" }
39
+ sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
40
+ wheels = [
41
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
42
+ ]
43
+
44
+ [[package]]
45
+ name = "distlib"
46
+ version = "0.4.0"
47
+ source = { registry = "https://pypi.org/simple" }
48
+ sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
51
+ ]
52
+
53
+ [[package]]
54
+ name = "filelock"
55
+ version = "3.20.3"
56
+ source = { registry = "https://pypi.org/simple" }
57
+ sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
58
+ wheels = [
59
+ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
60
+ ]
61
+
62
+ [[package]]
63
+ name = "h11"
64
+ version = "0.16.0"
65
+ source = { registry = "https://pypi.org/simple" }
66
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
67
+ wheels = [
68
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
69
+ ]
70
+
71
+ [[package]]
72
+ name = "httpcore"
73
+ version = "1.0.9"
74
+ source = { registry = "https://pypi.org/simple" }
75
+ dependencies = [
76
+ { name = "certifi" },
77
+ { name = "h11" },
78
+ ]
79
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
80
+ wheels = [
81
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
82
+ ]
83
+
84
+ [[package]]
85
+ name = "httpx"
86
+ version = "0.28.1"
87
+ source = { registry = "https://pypi.org/simple" }
88
+ dependencies = [
89
+ { name = "anyio" },
90
+ { name = "certifi" },
91
+ { name = "httpcore" },
92
+ { name = "idna" },
93
+ ]
94
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
95
+ wheels = [
96
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
97
+ ]
98
+
99
+ [[package]]
100
+ name = "identify"
101
+ version = "2.6.16"
102
+ source = { registry = "https://pypi.org/simple" }
103
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" }
104
+ wheels = [
105
+ { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" },
106
+ ]
107
+
108
+ [[package]]
109
+ name = "idna"
110
+ version = "3.11"
111
+ source = { registry = "https://pypi.org/simple" }
112
+ sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
113
+ wheels = [
114
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
115
+ ]
116
+
117
+ [[package]]
118
+ name = "nodeenv"
119
+ version = "1.10.0"
120
+ source = { registry = "https://pypi.org/simple" }
121
+ sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
122
+ wheels = [
123
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
124
+ ]
125
+
126
+ [[package]]
127
+ name = "platformdirs"
128
+ version = "4.5.1"
129
+ source = { registry = "https://pypi.org/simple" }
130
+ sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
131
+ wheels = [
132
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
133
+ ]
134
+
135
+ [[package]]
136
+ name = "pre-commit"
137
+ version = "4.5.1"
138
+ source = { registry = "https://pypi.org/simple" }
139
+ dependencies = [
140
+ { name = "cfgv" },
141
+ { name = "identify" },
142
+ { name = "nodeenv" },
143
+ { name = "pyyaml" },
144
+ { name = "virtualenv" },
145
+ ]
146
+ sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
147
+ wheels = [
148
+ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
149
+ ]
150
+
151
+ [[package]]
152
+ name = "pydantic"
153
+ version = "2.12.5"
154
+ source = { registry = "https://pypi.org/simple" }
155
+ dependencies = [
156
+ { name = "annotated-types" },
157
+ { name = "pydantic-core" },
158
+ { name = "typing-extensions" },
159
+ { name = "typing-inspection" },
160
+ ]
161
+ sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
162
+ wheels = [
163
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
164
+ ]
165
+
166
+ [[package]]
167
+ name = "pydantic-core"
168
+ version = "2.41.5"
169
+ source = { registry = "https://pypi.org/simple" }
170
+ dependencies = [
171
+ { name = "typing-extensions" },
172
+ ]
173
+ sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
174
+ wheels = [
175
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
176
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
177
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
178
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
179
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
180
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
181
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
182
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
183
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
184
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
185
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
186
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
187
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
188
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
189
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
190
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
191
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
192
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
193
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
194
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
195
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
196
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
197
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
198
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
199
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
200
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
201
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
202
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
203
+ ]
204
+
205
+ [[package]]
206
+ name = "pyyaml"
207
+ version = "6.0.3"
208
+ source = { registry = "https://pypi.org/simple" }
209
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
210
+ wheels = [
211
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
212
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
213
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
214
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
215
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
216
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
217
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
218
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
219
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
220
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
221
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
222
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
223
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
224
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
225
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
226
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
227
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
228
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
229
+ ]
230
+
231
+ [[package]]
232
+ name = "ruff"
233
+ version = "0.14.13"
234
+ source = { registry = "https://pypi.org/simple" }
235
+ sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" }
236
+ wheels = [
237
+ { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" },
238
+ { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" },
239
+ { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" },
240
+ { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" },
241
+ { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" },
242
+ { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" },
243
+ { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" },
244
+ { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" },
245
+ { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" },
246
+ { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" },
247
+ { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" },
248
+ { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" },
249
+ { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" },
250
+ { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" },
251
+ { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" },
252
+ { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" },
253
+ { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" },
254
+ { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" },
255
+ ]
256
+
257
+ [[package]]
258
+ name = "solidaritytechtools"
259
+ version = "0.0.1"
260
+ source = { editable = "." }
261
+ dependencies = [
262
+ { name = "httpx" },
263
+ { name = "pydantic" },
264
+ ]
265
+
266
+ [package.dev-dependencies]
267
+ dev = [
268
+ { name = "pre-commit" },
269
+ { name = "ruff" },
270
+ { name = "ty" },
271
+ ]
272
+
273
+ [package.metadata]
274
+ requires-dist = [
275
+ { name = "httpx", specifier = ">=0.28.1" },
276
+ { name = "pydantic", specifier = ">=2.12.5" },
277
+ ]
278
+
279
+ [package.metadata.requires-dev]
280
+ dev = [
281
+ { name = "pre-commit", specifier = ">=4.5.1" },
282
+ { name = "ruff", specifier = ">=0.14.13" },
283
+ { name = "ty", specifier = ">=0.0.13" },
284
+ ]
285
+
286
+ [[package]]
287
+ name = "ty"
288
+ version = "0.0.13"
289
+ source = { registry = "https://pypi.org/simple" }
290
+ sdist = { url = "https://files.pythonhosted.org/packages/5a/dc/b607f00916f5a7c52860b84a66dc17bc6988e8445e96b1d6e175a3837397/ty-0.0.13.tar.gz", hash = "sha256:7a1d135a400ca076407ea30012d1f75419634160ed3b9cad96607bf2956b23b3", size = 4999183, upload-time = "2026-01-21T13:21:16.133Z" }
291
+ wheels = [
292
+ { url = "https://files.pythonhosted.org/packages/1a/df/3632f1918f4c0a33184f107efc5d436ab6da147fd3d3b94b3af6461efbf4/ty-0.0.13-py3-none-linux_armv6l.whl", hash = "sha256:1b2b8e02697c3a94c722957d712a0615bcc317c9b9497be116ef746615d892f2", size = 9993501, upload-time = "2026-01-21T13:21:26.628Z" },
293
+ { url = "https://files.pythonhosted.org/packages/92/87/6a473ced5ac280c6ce5b1627c71a8a695c64481b99aabc798718376a441e/ty-0.0.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f15cdb8e233e2b5adfce673bb21f4c5e8eaf3334842f7eea3c70ac6fda8c1de5", size = 9860986, upload-time = "2026-01-21T13:21:24.425Z" },
294
+ { url = "https://files.pythonhosted.org/packages/5d/9b/d89ae375cf0a7cd9360e1164ce017f8c753759be63b6a11ed4c944abe8c6/ty-0.0.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0819e89ac9f0d8af7a062837ce197f0461fee2fc14fd07e2c368780d3a397b73", size = 9350748, upload-time = "2026-01-21T13:21:28.502Z" },
295
+ { url = "https://files.pythonhosted.org/packages/a8/a6/9ad58518056fab344b20c0bb2c1911936ebe195318e8acc3bc45ac1c6b6b/ty-0.0.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de79f481084b7cc7a202ba0d7a75e10970d10ffa4f025b23f2e6b7324b74886", size = 9849884, upload-time = "2026-01-21T13:21:21.886Z" },
296
+ { url = "https://files.pythonhosted.org/packages/b1/c3/8add69095fa179f523d9e9afcc15a00818af0a37f2b237a9b59bc0046c34/ty-0.0.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4fb2154cff7c6e95d46bfaba283c60642616f20d73e5f96d0c89c269f3e1bcec", size = 9822975, upload-time = "2026-01-21T13:21:14.292Z" },
297
+ { url = "https://files.pythonhosted.org/packages/a4/05/4c0927c68a0a6d43fb02f3f0b6c19c64e3461dc8ed6c404dde0efb8058f7/ty-0.0.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00be58d89337c27968a20d58ca553458608c5b634170e2bec82824c2e4cf4d96", size = 10294045, upload-time = "2026-01-21T13:21:30.505Z" },
298
+ { url = "https://files.pythonhosted.org/packages/b4/86/6dc190838aba967557fe0bfd494c595d00b5081315a98aaf60c0e632aaeb/ty-0.0.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72435eade1fa58c6218abb4340f43a6c3ff856ae2dc5722a247d3a6dd32e9737", size = 10916460, upload-time = "2026-01-21T13:21:07.788Z" },
299
+ { url = "https://files.pythonhosted.org/packages/04/40/9ead96b7c122e1109dfcd11671184c3506996bf6a649306ec427e81d9544/ty-0.0.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77a548742ee8f621d718159e7027c3b555051d096a49bb580249a6c5fc86c271", size = 10597154, upload-time = "2026-01-21T13:21:18.064Z" },
300
+ { url = "https://files.pythonhosted.org/packages/aa/7d/e832a2c081d2be845dc6972d0c7998914d168ccbc0b9c86794419ab7376e/ty-0.0.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da067c57c289b7cf914669704b552b6207c2cc7f50da4118c3e12388642e6b3f", size = 10410710, upload-time = "2026-01-21T13:21:12.388Z" },
301
+ { url = "https://files.pythonhosted.org/packages/31/e3/898be3a96237a32f05c4c29b43594dc3b46e0eedfe8243058e46153b324f/ty-0.0.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d1b50a01fffa140417fca5a24b658fbe0734074a095d5b6f0552484724474343", size = 9826299, upload-time = "2026-01-21T13:21:00.845Z" },
302
+ { url = "https://files.pythonhosted.org/packages/bb/eb/db2d852ce0ed742505ff18ee10d7d252f3acfd6fc60eca7e9c7a0288a6d8/ty-0.0.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f33c46f52e5e9378378eca0d8059f026f3c8073ace02f7f2e8d079ddfe5207e", size = 9831610, upload-time = "2026-01-21T13:21:05.842Z" },
303
+ { url = "https://files.pythonhosted.org/packages/9e/61/149f59c8abaddcbcbb0bd13b89c7741ae1c637823c5cf92ed2c644fcadef/ty-0.0.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:168eda24d9a0b202cf3758c2962cc295878842042b7eca9ed2965259f59ce9f2", size = 9978885, upload-time = "2026-01-21T13:21:10.306Z" },
304
+ { url = "https://files.pythonhosted.org/packages/a0/cd/026d4e4af60a80918a8d73d2c42b8262dd43ab2fa7b28d9743004cb88d57/ty-0.0.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4917678b95dc8cb399cc459fab568ba8d5f0f33b7a94bf840d9733043c43f29", size = 10506453, upload-time = "2026-01-21T13:20:56.633Z" },
305
+ { url = "https://files.pythonhosted.org/packages/63/06/8932833a4eca2df49c997a29afb26721612de8078ae79074c8fe87e17516/ty-0.0.13-py3-none-win32.whl", hash = "sha256:c1f2ec40daa405508b053e5b8e440fbae5fdb85c69c9ab0ee078f8bc00eeec3d", size = 9433482, upload-time = "2026-01-21T13:20:58.717Z" },
306
+ { url = "https://files.pythonhosted.org/packages/aa/fd/e8d972d1a69df25c2cecb20ea50e49ad5f27a06f55f1f5f399a563e71645/ty-0.0.13-py3-none-win_amd64.whl", hash = "sha256:8b7b1ab9f187affbceff89d51076038363b14113be29bda2ddfa17116de1d476", size = 10319156, upload-time = "2026-01-21T13:21:03.266Z" },
307
+ { url = "https://files.pythonhosted.org/packages/2d/c2/05fdd64ac003a560d4fbd1faa7d9a31d75df8f901675e5bed1ee2ceeff87/ty-0.0.13-py3-none-win_arm64.whl", hash = "sha256:1c9630333497c77bb9bcabba42971b96ee1f36c601dd3dcac66b4134f9fa38f0", size = 9808316, upload-time = "2026-01-21T13:20:54.053Z" },
308
+ ]
309
+
310
+ [[package]]
311
+ name = "typing-extensions"
312
+ version = "4.15.0"
313
+ source = { registry = "https://pypi.org/simple" }
314
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
315
+ wheels = [
316
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
317
+ ]
318
+
319
+ [[package]]
320
+ name = "typing-inspection"
321
+ version = "0.4.2"
322
+ source = { registry = "https://pypi.org/simple" }
323
+ dependencies = [
324
+ { name = "typing-extensions" },
325
+ ]
326
+ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
327
+ wheels = [
328
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
329
+ ]
330
+
331
+ [[package]]
332
+ name = "virtualenv"
333
+ version = "20.36.1"
334
+ source = { registry = "https://pypi.org/simple" }
335
+ dependencies = [
336
+ { name = "distlib" },
337
+ { name = "filelock" },
338
+ { name = "platformdirs" },
339
+ ]
340
+ sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
341
+ wheels = [
342
+ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
343
+ ]