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.
- wealthbox_cli-1.0.0.dist-info/METADATA +230 -0
- wealthbox_cli-1.0.0.dist-info/RECORD +54 -0
- wealthbox_cli-1.0.0.dist-info/WHEEL +4 -0
- wealthbox_cli-1.0.0.dist-info/entry_points.txt +3 -0
- wealthbox_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
- wealthbox_tools/__init__.py +51 -0
- wealthbox_tools/cli/__init__.py +3 -0
- wealthbox_tools/cli/_config.py +43 -0
- wealthbox_tools/cli/_util.py +315 -0
- wealthbox_tools/cli/activity.py +42 -0
- wealthbox_tools/cli/categories.py +41 -0
- wealthbox_tools/cli/comments.py +51 -0
- wealthbox_tools/cli/config.py +47 -0
- wealthbox_tools/cli/contacts.py +412 -0
- wealthbox_tools/cli/events.py +168 -0
- wealthbox_tools/cli/households.py +41 -0
- wealthbox_tools/cli/main.py +55 -0
- wealthbox_tools/cli/me.py +25 -0
- wealthbox_tools/cli/notes.py +97 -0
- wealthbox_tools/cli/opportunities.py +198 -0
- wealthbox_tools/cli/projects.py +102 -0
- wealthbox_tools/cli/tasks.py +180 -0
- wealthbox_tools/cli/users.py +24 -0
- wealthbox_tools/cli/workflows.py +172 -0
- wealthbox_tools/client/__init__.py +45 -0
- wealthbox_tools/client/activity.py +13 -0
- wealthbox_tools/client/base.py +216 -0
- wealthbox_tools/client/categories.py +14 -0
- wealthbox_tools/client/comments.py +14 -0
- wealthbox_tools/client/contacts.py +44 -0
- wealthbox_tools/client/events.py +31 -0
- wealthbox_tools/client/households.py +30 -0
- wealthbox_tools/client/me.py +11 -0
- wealthbox_tools/client/notes.py +28 -0
- wealthbox_tools/client/opportunities.py +31 -0
- wealthbox_tools/client/projects.py +28 -0
- wealthbox_tools/client/tasks.py +31 -0
- wealthbox_tools/client/users.py +14 -0
- wealthbox_tools/client/workflows.py +44 -0
- wealthbox_tools/models/__init__.py +134 -0
- wealthbox_tools/models/activity.py +13 -0
- wealthbox_tools/models/comments.py +13 -0
- wealthbox_tools/models/common.py +123 -0
- wealthbox_tools/models/contacts.py +131 -0
- wealthbox_tools/models/custom_fields.py +19 -0
- wealthbox_tools/models/enums.py +209 -0
- wealthbox_tools/models/events.py +63 -0
- wealthbox_tools/models/households.py +17 -0
- wealthbox_tools/models/notes.py +28 -0
- wealthbox_tools/models/opportunities.py +48 -0
- wealthbox_tools/models/projects.py +27 -0
- wealthbox_tools/models/tasks.py +76 -0
- wealthbox_tools/models/workflows.py +50 -0
- 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
|