wealthbox-cli 1.0.0__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.
Files changed (54) hide show
  1. wealthbox_cli-1.0.0.dist-info/METADATA +230 -0
  2. wealthbox_cli-1.0.0.dist-info/RECORD +54 -0
  3. wealthbox_cli-1.0.0.dist-info/WHEEL +4 -0
  4. wealthbox_cli-1.0.0.dist-info/entry_points.txt +3 -0
  5. wealthbox_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
  6. wealthbox_tools/__init__.py +51 -0
  7. wealthbox_tools/cli/__init__.py +3 -0
  8. wealthbox_tools/cli/_config.py +43 -0
  9. wealthbox_tools/cli/_util.py +315 -0
  10. wealthbox_tools/cli/activity.py +42 -0
  11. wealthbox_tools/cli/categories.py +41 -0
  12. wealthbox_tools/cli/comments.py +51 -0
  13. wealthbox_tools/cli/config.py +47 -0
  14. wealthbox_tools/cli/contacts.py +412 -0
  15. wealthbox_tools/cli/events.py +168 -0
  16. wealthbox_tools/cli/households.py +41 -0
  17. wealthbox_tools/cli/main.py +55 -0
  18. wealthbox_tools/cli/me.py +25 -0
  19. wealthbox_tools/cli/notes.py +97 -0
  20. wealthbox_tools/cli/opportunities.py +198 -0
  21. wealthbox_tools/cli/projects.py +102 -0
  22. wealthbox_tools/cli/tasks.py +180 -0
  23. wealthbox_tools/cli/users.py +24 -0
  24. wealthbox_tools/cli/workflows.py +172 -0
  25. wealthbox_tools/client/__init__.py +45 -0
  26. wealthbox_tools/client/activity.py +13 -0
  27. wealthbox_tools/client/base.py +216 -0
  28. wealthbox_tools/client/categories.py +14 -0
  29. wealthbox_tools/client/comments.py +14 -0
  30. wealthbox_tools/client/contacts.py +44 -0
  31. wealthbox_tools/client/events.py +31 -0
  32. wealthbox_tools/client/households.py +30 -0
  33. wealthbox_tools/client/me.py +11 -0
  34. wealthbox_tools/client/notes.py +28 -0
  35. wealthbox_tools/client/opportunities.py +31 -0
  36. wealthbox_tools/client/projects.py +28 -0
  37. wealthbox_tools/client/tasks.py +31 -0
  38. wealthbox_tools/client/users.py +14 -0
  39. wealthbox_tools/client/workflows.py +44 -0
  40. wealthbox_tools/models/__init__.py +134 -0
  41. wealthbox_tools/models/activity.py +13 -0
  42. wealthbox_tools/models/comments.py +13 -0
  43. wealthbox_tools/models/common.py +123 -0
  44. wealthbox_tools/models/contacts.py +131 -0
  45. wealthbox_tools/models/custom_fields.py +19 -0
  46. wealthbox_tools/models/enums.py +209 -0
  47. wealthbox_tools/models/events.py +63 -0
  48. wealthbox_tools/models/households.py +17 -0
  49. wealthbox_tools/models/notes.py +28 -0
  50. wealthbox_tools/models/opportunities.py +48 -0
  51. wealthbox_tools/models/projects.py +27 -0
  52. wealthbox_tools/models/tasks.py +76 -0
  53. wealthbox_tools/models/workflows.py +50 -0
  54. wealthbox_tools/py.typed +0 -0
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from wealthbox_tools.models import ProjectCreateInput, ProjectListQuery, ProjectUpdateInput
6
+
7
+
8
+ class ProjectsMixin:
9
+ """Projects Resource"""
10
+
11
+ async def list_projects(self, query: ProjectListQuery | None = None) -> dict[str, Any]:
12
+ params = query.model_dump(exclude_none=True) if query else None
13
+ resp = await self._request("GET", "/projects", params=params) # type: ignore[attr-defined]
14
+ return resp.json()
15
+
16
+ async def get_project(self, project_id: int) -> dict[str, Any]:
17
+ resp = await self._request("GET", f"/projects/{project_id}") # type: ignore[attr-defined]
18
+ return resp.json()
19
+
20
+ async def create_project(self, data: ProjectCreateInput) -> dict[str, Any]:
21
+ payload = data.model_dump(exclude_none=True)
22
+ resp = await self._request("POST", "/projects", json=payload) # type: ignore[attr-defined]
23
+ return resp.json()
24
+
25
+ async def update_project(self, project_id: int, data: ProjectUpdateInput) -> dict[str, Any]:
26
+ payload = data.model_dump(exclude_unset=True)
27
+ resp = await self._request("PUT", f"/projects/{project_id}", json=payload) # type: ignore[attr-defined]
28
+ return resp.json()
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from wealthbox_tools.models import TaskCreateInput, TaskListQuery, TaskUpdateInput
6
+
7
+
8
+ class TasksMixin:
9
+ """Task resource methods. Mixed into WealthboxClient."""
10
+
11
+ async def list_tasks(self, query: TaskListQuery | None = None) -> dict[str, Any]:
12
+ params = query.model_dump(exclude_none=True) if query else None
13
+ resp = await self._request("GET", "/tasks", params=params) # type: ignore[attr-defined]
14
+ return resp.json()
15
+
16
+ async def get_task(self, task_id: int) -> dict[str, Any]:
17
+ resp = await self._request("GET", f"/tasks/{task_id}") # type: ignore[attr-defined]
18
+ return resp.json()
19
+
20
+ async def create_task(self, data: TaskCreateInput) -> dict[str, Any]:
21
+ payload = data.model_dump(exclude_none=True)
22
+ resp = await self._request("POST", "/tasks", json=payload) # type: ignore[attr-defined]
23
+ return resp.json()
24
+
25
+ async def update_task(self, task_id: int, data: TaskUpdateInput) -> dict[str, Any]:
26
+ payload = data.model_dump(exclude_unset=True)
27
+ resp = await self._request("PUT", f"/tasks/{task_id}", json=payload) # type: ignore[attr-defined]
28
+ return resp.json()
29
+
30
+ async def delete_task(self, task_id: int) -> None:
31
+ await self._request("DELETE", f"/tasks/{task_id}") # type: ignore[attr-defined]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from wealthbox_tools.models.common import PaginationQuery
6
+
7
+
8
+ class UsersMixin:
9
+ """Users Resource"""
10
+
11
+ async def list_users(self, query: PaginationQuery | None = None) -> dict[str, Any]:
12
+ params = query.model_dump(exclude_none=True) if query else None
13
+ resp = await self._request("GET", "/users", params=params) # type: ignore[attr-defined]
14
+ return resp.json()
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from wealthbox_tools.models import (
6
+ WorkflowCreateInput,
7
+ WorkflowListQuery,
8
+ WorkflowStepCompleteInput,
9
+ WorkflowTemplateListQuery,
10
+ )
11
+
12
+
13
+ class WorkflowsMixin:
14
+ """Workflows Resource"""
15
+
16
+ async def list_workflows(self, query: WorkflowListQuery | None = None) -> dict[str, Any]:
17
+ params = query.model_dump(exclude_none=True) if query else None
18
+ resp = await self._request("GET", "/workflows", params=params) # type: ignore[attr-defined]
19
+ return resp.json()
20
+
21
+ async def get_workflow(self, workflow_id: int) -> dict[str, Any]:
22
+ resp = await self._request("GET", f"/workflows/{workflow_id}") # type: ignore[attr-defined]
23
+ return resp.json()
24
+
25
+ async def create_workflow(self, data: WorkflowCreateInput) -> dict[str, Any]:
26
+ payload = data.model_dump(exclude_none=True)
27
+ resp = await self._request("POST", "/workflows", json=payload) # type: ignore[attr-defined]
28
+ return resp.json()
29
+
30
+ async def complete_workflow_step(
31
+ self, workflow_id: int, step_id: int, data: WorkflowStepCompleteInput
32
+ ) -> dict[str, Any]:
33
+ payload = data.model_dump(exclude_none=True)
34
+ resp = await self._request("PUT", f"/workflows/{workflow_id}/steps/{step_id}", json=payload) # type: ignore[attr-defined]
35
+ return resp.json()
36
+
37
+ async def revert_workflow_step(self, workflow_id: int, step_id: int) -> dict[str, Any]:
38
+ resp = await self._request("PUT", f"/workflows/{workflow_id}/steps/{step_id}", json={"revert": True}) # type: ignore[attr-defined]
39
+ return resp.json()
40
+
41
+ async def list_workflow_templates(self, query: WorkflowTemplateListQuery | None = None) -> dict[str, Any]:
42
+ params = query.model_dump(exclude_none=True) if query else None
43
+ resp = await self._request("GET", "/workflow_templates", params=params) # type: ignore[attr-defined]
44
+ return resp.json()
@@ -0,0 +1,134 @@
1
+ """Pydantic models for Wealthbox tool payload/query validation."""
2
+
3
+ from .activity import ActivityListQuery
4
+ from .comments import CommentListQuery
5
+ from .common import (
6
+ ContactRoleAssignment,
7
+ EmailAddress,
8
+ LinkedToRef,
9
+ PhoneNumber,
10
+ StreetAddress,
11
+ )
12
+ from .contacts import ContactCreateInput, ContactListQuery, ContactUpdateInput
13
+ from .custom_fields import CategoryListQuery, CustomFieldValue
14
+ from .enums import (
15
+ ActivityType,
16
+ CategoryType,
17
+ CommentResourceType,
18
+ ContactsOrder,
19
+ DocumentType,
20
+ EventsOrder,
21
+ EventsState,
22
+ Gender,
23
+ HouseholdTitle,
24
+ MaritalStatus,
25
+ NoteResourceType,
26
+ NotesOrder,
27
+ OpportunityAmountKind,
28
+ OpportunityOrder,
29
+ OpportunityResourceType,
30
+ RecordType,
31
+ TaskFrame,
32
+ TaskPriority,
33
+ TaskResourceType,
34
+ TaskType,
35
+ WorkflowResourceType,
36
+ WorkflowStatus,
37
+ )
38
+ from .events import EventCreateInput, EventListQuery, EventUpdateInput
39
+ from .households import HouseholdMemberInput
40
+ from .notes import NoteCreateInput, NoteListQuery, NoteUpdateInput
41
+ from .opportunities import OpportunityAmount, OpportunityCreateInput, OpportunityListQuery, OpportunityUpdateInput
42
+ from .projects import ProjectCreateInput, ProjectListQuery, ProjectUpdateInput
43
+ from .tasks import TaskCreateInput, TaskListQuery, TaskUpdateInput
44
+ from .workflows import (
45
+ WorkflowCreateInput,
46
+ WorkflowListQuery,
47
+ WorkflowMilestone,
48
+ WorkflowStepCompleteInput,
49
+ WorkflowStepRevertInput,
50
+ WorkflowTemplateListQuery,
51
+ )
52
+
53
+ __all__ = [
54
+ # Enums
55
+ "RecordType",
56
+ "Gender",
57
+ "MaritalStatus",
58
+ "EventsOrder",
59
+ "HouseholdTitle",
60
+ "DocumentType",
61
+ "ContactsOrder",
62
+ "ActivityType",
63
+ "CategoryType",
64
+ "CommentResourceType",
65
+ "NotesOrder",
66
+ "NoteResourceType",
67
+ "OpportunityAmountKind",
68
+ "OpportunityOrder",
69
+ "OpportunityResourceType",
70
+ "TaskFrame",
71
+ "TaskPriority",
72
+ "TaskResourceType",
73
+ "TaskType",
74
+ "EventsState",
75
+ "WorkflowResourceType",
76
+ "WorkflowStatus",
77
+
78
+ # Activity
79
+ "ActivityListQuery",
80
+
81
+ # Comments
82
+ "CommentListQuery",
83
+
84
+ # Contacts
85
+ "ContactCreateInput",
86
+ "ContactListQuery",
87
+ "ContactUpdateInput",
88
+ "ContactRoleAssignment",
89
+ "EmailAddress",
90
+ "LinkedToRef",
91
+ "PhoneNumber",
92
+ "StreetAddress",
93
+
94
+ # Events
95
+ "EventCreateInput",
96
+ "EventListQuery",
97
+ "EventUpdateInput",
98
+
99
+ # Households
100
+ "HouseholdMemberInput",
101
+
102
+ # Categories
103
+ "CategoryListQuery",
104
+ "CustomFieldValue",
105
+
106
+ # Notes
107
+ "NoteCreateInput",
108
+ "NoteListQuery",
109
+ "NoteUpdateInput",
110
+
111
+ # Opportunities
112
+ "OpportunityAmount",
113
+ "OpportunityCreateInput",
114
+ "OpportunityListQuery",
115
+ "OpportunityUpdateInput",
116
+
117
+ # Projects
118
+ "ProjectCreateInput",
119
+ "ProjectListQuery",
120
+ "ProjectUpdateInput",
121
+
122
+ # Tasks
123
+ "TaskCreateInput",
124
+ "TaskListQuery",
125
+ "TaskUpdateInput",
126
+
127
+ # Workflows
128
+ "WorkflowCreateInput",
129
+ "WorkflowListQuery",
130
+ "WorkflowMilestone",
131
+ "WorkflowStepCompleteInput",
132
+ "WorkflowStepRevertInput",
133
+ "WorkflowTemplateListQuery",
134
+ ]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from .common import DateTimeField, WealthboxModel
4
+ from .enums import ActivityType
5
+
6
+
7
+ class ActivityListQuery(WealthboxModel):
8
+ contact: int | None = None
9
+ cursor: str | None = None
10
+ type: ActivityType | None = None
11
+ updated_since: DateTimeField = None
12
+ updated_before: DateTimeField = None
13
+
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field
4
+
5
+ from .common import DateTimeField, PaginationQuery
6
+ from .enums import CommentResourceType
7
+
8
+
9
+ class CommentListQuery(PaginationQuery):
10
+ resource_id: int | None = Field(default=None, ge=1)
11
+ resource_type: CommentResourceType | None = None
12
+ updated_since: DateTimeField = None
13
+ updated_before: DateTimeField = None
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Annotated
5
+
6
+ from pydantic import AfterValidator, BaseModel, ConfigDict, Field, field_validator, model_validator
7
+
8
+ # ---------------------------------------------------------------------------
9
+ # Reusable date / datetime validators
10
+ # ---------------------------------------------------------------------------
11
+
12
+ _DATE_RE = re.compile(r'^\d{4}-\d{2}-\d{2}$')
13
+ # ISO 8601 datetime: YYYY-MM-DDTHH:MM:SS with optional Z or ±HH:MM offset
14
+ _DATETIME_ISO_RE = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})?$')
15
+ _EMAIL_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
16
+
17
+ _LINKED_TO_TYPES = frozenset({"Contact", "Project", "Opportunity"})
18
+ _LINKED_TO_TYPES_DISPLAY = ", ".join(sorted(_LINKED_TO_TYPES))
19
+
20
+ _DATETIME_EXAMPLE = "e.g. '2026-04-01T10:00:00-07:00' or '2026-04-01T10:00:00Z'"
21
+
22
+
23
+ def _check_date(v: str | None) -> str | None:
24
+ if v is not None and not _DATE_RE.match(v):
25
+ raise ValueError("must be in YYYY-MM-DD format (e.g. '1975-10-27')")
26
+ return v
27
+
28
+
29
+ def _check_datetime(v: str | None) -> str | None:
30
+ if v is not None and not _DATETIME_ISO_RE.match(v):
31
+ raise ValueError(f"must be ISO 8601 datetime ({_DATETIME_EXAMPLE})")
32
+ return v
33
+
34
+
35
+ def _check_email(v: str | None) -> str | None:
36
+ if v is not None and not _EMAIL_RE.match(v):
37
+ raise ValueError(f"'{v}' is not a valid email address")
38
+ return v
39
+
40
+
41
+ # Annotated type aliases — use these as field type annotations so format
42
+ # validation is applied automatically. Import into each model module that uses them.
43
+ DateField = Annotated[str | None, AfterValidator(_check_date)]
44
+ DateTimeField = Annotated[str | None, AfterValidator(_check_datetime)]
45
+ # For required (non-optional) datetime string fields.
46
+ NonNullDateTimeField = Annotated[str, AfterValidator(_check_datetime)]
47
+ EmailField = Annotated[str | None, AfterValidator(_check_email)]
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Base models
52
+ # ---------------------------------------------------------------------------
53
+
54
+ class WealthboxModel(BaseModel):
55
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
56
+
57
+
58
+ class RequireAnyFieldModel(WealthboxModel):
59
+ @model_validator(mode="after")
60
+ def ensure_any_field_present(self) -> "RequireAnyFieldModel":
61
+ # Updates should allow explicit null for clearable fields while still
62
+ # rejecting completely empty payloads.
63
+ if not self.model_fields_set:
64
+ raise ValueError("At least one field must be provided.")
65
+ return self
66
+
67
+
68
+ class PaginationQuery(WealthboxModel):
69
+ page: int | None = Field(default=None, ge=1)
70
+ per_page: int | None = Field(default=25, ge=1, le=100)
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Nested contact sub-objects
75
+ # ---------------------------------------------------------------------------
76
+
77
+ class EmailAddress(WealthboxModel):
78
+ id: int | None = Field(default=None, ge=1)
79
+ address: EmailField = None
80
+ kind: str | None = None
81
+ principal: bool | None = None
82
+ destroy: bool | None = None
83
+
84
+
85
+ class PhoneNumber(WealthboxModel):
86
+ id: int | None = Field(default=None, ge=1)
87
+ address: str | None = None
88
+ kind: str | None = None
89
+ principal: bool | None = None
90
+ extension: str | None = None
91
+ destroy: bool | None = None
92
+
93
+
94
+ class StreetAddress(WealthboxModel):
95
+ id: int | None = Field(default=None, ge=1)
96
+ kind: str | None = None
97
+ principal: bool | None = None
98
+ street_line_1: str | None = None
99
+ street_line_2: str | None = None
100
+ city: str | None = None
101
+ state: str | None = None
102
+ zip_code: str | None = None
103
+ country: str | None = None
104
+ destroy: bool | None = None
105
+
106
+
107
+ class ContactRoleAssignment(WealthboxModel):
108
+ id: int | None = Field(default=None, ge=1)
109
+ type: str | None = None
110
+
111
+
112
+ class LinkedToRef(WealthboxModel):
113
+ id: int = Field(ge=1, description="Wealthbox object ID (positive integer)")
114
+ type: str = Field(description="Wealthbox object type; allowed: Contact, Project, Opportunity")
115
+
116
+ @field_validator("type")
117
+ @classmethod
118
+ def validate_linked_to_type(cls, v: str) -> str:
119
+ if v not in _LINKED_TO_TYPES:
120
+ raise ValueError(
121
+ f"'{v}' is not a valid linked_to type; must be one of: {_LINKED_TO_TYPES_DISPLAY}"
122
+ )
123
+ return v
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field, model_validator
4
+
5
+ from .common import (
6
+ ContactRoleAssignment,
7
+ DateField,
8
+ DateTimeField,
9
+ EmailAddress,
10
+ PaginationQuery,
11
+ PhoneNumber,
12
+ RequireAnyFieldModel,
13
+ StreetAddress,
14
+ WealthboxModel,
15
+ )
16
+ from .custom_fields import CustomFieldValue
17
+ from .enums import (
18
+ ContactsOrder,
19
+ Gender,
20
+ HouseholdTitle,
21
+ MaritalStatus,
22
+ RecordType,
23
+ )
24
+
25
+
26
+ class ContactListQuery(PaginationQuery):
27
+ id: int | None = Field(default=None, ge=1)
28
+ contact_type: str | None = None
29
+ name: str | None = None
30
+ email: str | None = None
31
+ phone: str | None = None
32
+ external_unique_id: str | None = None
33
+ active: bool | None = None
34
+ tags: list[str] | None = None
35
+ deleted: bool | None = None
36
+ deleted_since: DateTimeField = None
37
+ household_title: HouseholdTitle | None = None
38
+ type: RecordType | None = None
39
+ order: ContactsOrder | None = None
40
+ updated_since: DateTimeField = None
41
+ updated_before: DateTimeField = None
42
+
43
+
44
+ class ContactCreateInput(WealthboxModel):
45
+ # Core identity
46
+ type: RecordType | None = None
47
+ prefix: str | None = None
48
+ first_name: str | None = None
49
+ middle_name: str | None = None
50
+ last_name: str | None = None
51
+ suffix: str | None = None
52
+ nickname: str | None = None
53
+ name: str | None = None
54
+
55
+ # Person/company attributes
56
+ job_title: str | None = None
57
+ company_name: str | None = None
58
+ marital_status: MaritalStatus | None = None
59
+ gender: Gender | None = None
60
+ birth_date: DateField = None
61
+ anniversary: DateField = None
62
+ client_since: DateField = None
63
+ date_of_death: DateField = None
64
+
65
+ contact_type: str | None = None
66
+ contact_source: str | None = None
67
+ status: str | None = None
68
+ assigned_to: int | None = Field(default=None, ge=1)
69
+ visible_to: str | None = None
70
+ external_unique_id: str | None = None
71
+ background_information: str | None = None
72
+ important_information: str | None = None
73
+
74
+
75
+ # Nested arrays
76
+ email_addresses: list[EmailAddress] | None = None
77
+ phone_numbers: list[PhoneNumber] | None = None
78
+ street_addresses: list[StreetAddress] | None = None
79
+ tags: list[str] | None = None
80
+ custom_fields: list[CustomFieldValue] | None = None
81
+ contact_roles: list[ContactRoleAssignment] | None = None
82
+
83
+
84
+ @model_validator(mode="after")
85
+ def ensure_non_empty_create(self) -> "ContactCreateInput":
86
+ # Enforce non-empty payload for create only; update has its own rules.
87
+ if type(self) is ContactCreateInput and not self.model_dump(exclude_none=True):
88
+ raise ValueError("At least one field must be provided.")
89
+ return self
90
+
91
+
92
+ class ContactUpdateInput(RequireAnyFieldModel):
93
+ # Core identity
94
+ type: RecordType | None = None
95
+ prefix: str | None = None
96
+ first_name: str | None = None
97
+ middle_name: str | None = None
98
+ last_name: str | None = None
99
+ suffix: str | None = None
100
+ nickname: str | None = None
101
+ name: str | None = None
102
+
103
+ # Person/company attributes
104
+ job_title: str | None = None
105
+ company_name: str | None = None
106
+ marital_status: MaritalStatus | None = None
107
+ gender: Gender | None = None
108
+ birth_date: DateField = None
109
+ anniversary: DateField = None
110
+ client_since: DateField = None
111
+ date_of_death: DateField = None
112
+
113
+ contact_type: str | None = None
114
+ contact_source: str | None = None
115
+ status: str | None = None
116
+ assigned_to: int | None = Field(default=None, ge=1)
117
+ visible_to: str | None = None
118
+ external_unique_id: str | None = None
119
+ background_information: str | None = None
120
+ important_information: str | None = None
121
+
122
+ # Nested arrays
123
+ email_addresses: list[EmailAddress] | None = None
124
+ phone_numbers: list[PhoneNumber] | None = None
125
+ street_addresses: list[StreetAddress] | None = None
126
+ tags: list[str] | None = None
127
+ custom_fields: list[CustomFieldValue] | None = None
128
+ contact_roles: list[ContactRoleAssignment] | None = None
129
+
130
+ # Update-only fields
131
+ destroy: bool | None = None
@@ -0,0 +1,19 @@
1
+ """Pydantic models for metadata tool validation."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from pydantic import Field
7
+
8
+ from .common import PaginationQuery, WealthboxModel
9
+ from .enums import DocumentType
10
+
11
+
12
+ class CustomFieldValue(WealthboxModel):
13
+ id: int | None = Field(default=None, ge=1)
14
+ name: str | None = None
15
+ value: Any | None = None
16
+
17
+
18
+ class CategoryListQuery(PaginationQuery):
19
+ document_type: DocumentType | None = None