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,412 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from wealthbox_tools.models import (
|
|
9
|
+
CategoryType,
|
|
10
|
+
ContactCreateInput,
|
|
11
|
+
ContactListQuery,
|
|
12
|
+
ContactsOrder,
|
|
13
|
+
ContactUpdateInput,
|
|
14
|
+
Gender,
|
|
15
|
+
HouseholdTitle,
|
|
16
|
+
MaritalStatus,
|
|
17
|
+
RecordType,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ._util import (
|
|
21
|
+
OutputFormat,
|
|
22
|
+
active_to_status,
|
|
23
|
+
handle_errors,
|
|
24
|
+
make_category_command,
|
|
25
|
+
output_result,
|
|
26
|
+
parse_more_fields,
|
|
27
|
+
run_client,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
app = typer.Typer(
|
|
31
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
32
|
+
help="Manage Wealthbox contacts.",
|
|
33
|
+
no_args_is_help=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_DEFAULT_FIELDS = ["id", "name", "type", "contact_type", "assigned_to", "status"]
|
|
37
|
+
|
|
38
|
+
# -- categories sub-app -------------------------------------------------------
|
|
39
|
+
categories_app = typer.Typer(
|
|
40
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
41
|
+
help="List available category values for contact fields.",
|
|
42
|
+
no_args_is_help=True,
|
|
43
|
+
)
|
|
44
|
+
categories_app.command("contact-types", help="List contact type options.")(
|
|
45
|
+
make_category_command(CategoryType.CONTACT_TYPES)
|
|
46
|
+
)
|
|
47
|
+
categories_app.command("contact-sources", help="List contact source options.")(
|
|
48
|
+
make_category_command(CategoryType.CONTACT_SOURCES)
|
|
49
|
+
)
|
|
50
|
+
categories_app.command("email-types", help="List email type options.")(make_category_command(CategoryType.EMAIL_TYPES))
|
|
51
|
+
categories_app.command("phone-types", help="List phone type options.")(make_category_command(CategoryType.PHONE_TYPES))
|
|
52
|
+
categories_app.command("address-types", help="List address type options.")(
|
|
53
|
+
make_category_command(CategoryType.ADDRESS_TYPES)
|
|
54
|
+
)
|
|
55
|
+
categories_app.command("website-types", help="List website type options.")(
|
|
56
|
+
make_category_command(CategoryType.WEBSITE_TYPES)
|
|
57
|
+
)
|
|
58
|
+
categories_app.command("contact-roles", help="List contact role options.")(
|
|
59
|
+
make_category_command(CategoryType.CONTACT_ROLES)
|
|
60
|
+
)
|
|
61
|
+
app.add_typer(categories_app, name="categories")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command("list", help="List contacts with optional filters.")
|
|
65
|
+
@handle_errors
|
|
66
|
+
def list_contacts(
|
|
67
|
+
type_: RecordType | None = typer.Option(
|
|
68
|
+
None, "--type", help="Record Type - Person, Household, Organization, or Trust"
|
|
69
|
+
),
|
|
70
|
+
name: str | None = typer.Option(None, help="Filter by name - Contains"),
|
|
71
|
+
email: str | None = typer.Option(None, help="Filter by email - Full Match"),
|
|
72
|
+
phone: str | None = typer.Option(None, help="Filter by phone - Full Match - Parsing handled by Wealthbox"),
|
|
73
|
+
contact_type: str | None = typer.Option(
|
|
74
|
+
None, "--contact-type", help="Client, Prospect, Vendor, etc. - see wbox contacts categories contact-types"
|
|
75
|
+
),
|
|
76
|
+
active: bool | None = typer.Option(None, "--active/--inactive", help="Filter by active status"),
|
|
77
|
+
deleted: bool | None = typer.Option(
|
|
78
|
+
None, "--deleted", help="Filter to deleted contacts only (omit to see non-deleted, which is the API default)"
|
|
79
|
+
),
|
|
80
|
+
household_title: HouseholdTitle | None = typer.Option(
|
|
81
|
+
None, help="The household title you wish to filter the household title"
|
|
82
|
+
),
|
|
83
|
+
tags: str | None = typer.Option(None, help="Comma-separated tags"),
|
|
84
|
+
order: ContactsOrder = typer.Option(ContactsOrder.ASC, help="The order that the contacts should be returned in"),
|
|
85
|
+
updated_since: str | None = typer.Option(None, "--updated-since", help="Format of 'YYYY-MM-DD 07:00 AM -0700'"),
|
|
86
|
+
updated_before: str | None = typer.Option(None, "--updated-before", help="Format of 'YYYY-MM-DD 07:00 AM -0700'"),
|
|
87
|
+
deleted_since: str | None = typer.Option(
|
|
88
|
+
None, help="Only returns deleted contacts that were deleted on or after this timestamp"
|
|
89
|
+
),
|
|
90
|
+
assigned_to: int | None = typer.Option(
|
|
91
|
+
None, "--assigned-to", help="Filter by assigned user ID (client-side scan — fetches all pages)."
|
|
92
|
+
),
|
|
93
|
+
page: int | None = typer.Option(None, help="Page number"),
|
|
94
|
+
per_page: int | None = typer.Option(None, "--per-page", help="Results per page (max 100)"),
|
|
95
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show all fields"),
|
|
96
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
97
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
98
|
+
) -> None:
|
|
99
|
+
if assigned_to is not None:
|
|
100
|
+
if page is not None or per_page is not None:
|
|
101
|
+
typer.echo("Warning: --page and --per-page are ignored when --assigned-to is active.", err=True)
|
|
102
|
+
typer.echo("Note: --assigned-to requires fetching all contacts. This may take a moment.", err=True)
|
|
103
|
+
|
|
104
|
+
tag_list = [t.strip() for t in tags.split(",")] if tags else None
|
|
105
|
+
query = ContactListQuery(
|
|
106
|
+
type=type_,
|
|
107
|
+
name=name,
|
|
108
|
+
email=email,
|
|
109
|
+
phone=phone,
|
|
110
|
+
contact_type=contact_type,
|
|
111
|
+
active=active,
|
|
112
|
+
deleted=deleted,
|
|
113
|
+
household_title=household_title,
|
|
114
|
+
tags=tag_list,
|
|
115
|
+
order=order,
|
|
116
|
+
updated_since=updated_since,
|
|
117
|
+
updated_before=updated_before,
|
|
118
|
+
deleted_since=deleted_since,
|
|
119
|
+
page=None if assigned_to is not None else page,
|
|
120
|
+
per_page=None if assigned_to is not None else per_page,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if assigned_to is not None:
|
|
124
|
+
def _progress(page_num: int, total_fetched: int) -> None:
|
|
125
|
+
typer.echo(f"Scanning page {page_num}... ({total_fetched} fetched so far)", err=True)
|
|
126
|
+
|
|
127
|
+
raw = run_client(token, lambda c: c.list_all_contacts(query, on_progress=_progress))
|
|
128
|
+
matched = [c for c in raw.get("contacts", []) if c.get("assigned_to") == assigned_to]
|
|
129
|
+
result = {"contacts": matched, "meta": {"total_count": len(matched)}}
|
|
130
|
+
else:
|
|
131
|
+
result = run_client(token, lambda c: c.list_contacts(query))
|
|
132
|
+
|
|
133
|
+
output_result(result, fmt, fields=None if verbose else _DEFAULT_FIELDS)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command("get", help="Get a single contact by ID.")
|
|
137
|
+
@handle_errors
|
|
138
|
+
def get_contact(
|
|
139
|
+
contact_id: int = typer.Argument(..., help="Contact ID"),
|
|
140
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
141
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
142
|
+
) -> None:
|
|
143
|
+
output_result(run_client(token, lambda c: c.get_contact(contact_id)), fmt)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# -- add sub-app --------------------------------------------------------------
|
|
147
|
+
add_app = typer.Typer(
|
|
148
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
149
|
+
help="Create a new contact.",
|
|
150
|
+
no_args_is_help=True,
|
|
151
|
+
)
|
|
152
|
+
app.add_typer(add_app, name="add")
|
|
153
|
+
|
|
154
|
+
_PERSON_RESERVED = {
|
|
155
|
+
"type", "first_name", "middle_name", "last_name", "prefix", "suffix", "nickname",
|
|
156
|
+
"gender", "marital_status", "birth_date", "anniversary", "job_title", "company_name",
|
|
157
|
+
"contact_type", "contact_source", "status", "assigned_to", "email_addresses", "phone_numbers",
|
|
158
|
+
}
|
|
159
|
+
_HOUSEHOLD_RESERVED = {"type", "name", "contact_type", "contact_source", "status", "assigned_to", "email_addresses"}
|
|
160
|
+
_ORG_TRUST_RESERVED = _HOUSEHOLD_RESERVED | {"phone_numbers"}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _build_contact_entry(value: str | None, kind: str | None) -> list[dict[str, Any]] | None:
|
|
164
|
+
if not value:
|
|
165
|
+
return None
|
|
166
|
+
entry: dict[str, Any] = {"address": value, "principal": True}
|
|
167
|
+
if kind:
|
|
168
|
+
entry["kind"] = kind
|
|
169
|
+
return [entry]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _create_named_contact(
|
|
173
|
+
record_type: RecordType,
|
|
174
|
+
reserved: set[str],
|
|
175
|
+
name: str,
|
|
176
|
+
contact_type: str | None,
|
|
177
|
+
contact_source: str | None,
|
|
178
|
+
active: bool | None,
|
|
179
|
+
assigned_to: int | None,
|
|
180
|
+
email: str | None,
|
|
181
|
+
email_type: str | None,
|
|
182
|
+
phone: str | None,
|
|
183
|
+
phone_type: str | None,
|
|
184
|
+
more_fields: str | None,
|
|
185
|
+
token: str | None,
|
|
186
|
+
fmt: OutputFormat,
|
|
187
|
+
) -> None:
|
|
188
|
+
payload: dict[str, Any] = {k: v for k, v in {
|
|
189
|
+
"type": record_type,
|
|
190
|
+
"name": name,
|
|
191
|
+
"contact_type": contact_type,
|
|
192
|
+
"contact_source": contact_source,
|
|
193
|
+
"status": active_to_status(active),
|
|
194
|
+
"assigned_to": assigned_to,
|
|
195
|
+
}.items() if v is not None}
|
|
196
|
+
emails = _build_contact_entry(email, email_type)
|
|
197
|
+
if emails:
|
|
198
|
+
payload["email_addresses"] = emails
|
|
199
|
+
phones = _build_contact_entry(phone, phone_type)
|
|
200
|
+
if phones:
|
|
201
|
+
payload["phone_numbers"] = phones
|
|
202
|
+
if more_fields:
|
|
203
|
+
payload.update(parse_more_fields(more_fields, reserved))
|
|
204
|
+
input_model = ContactCreateInput(**payload)
|
|
205
|
+
output_result(run_client(token, lambda c: c.create_contact(input_model)), fmt)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@add_app.command("person", help="Create a Person contact.")
|
|
209
|
+
@handle_errors
|
|
210
|
+
def add_person(
|
|
211
|
+
first_name: str | None = typer.Option(None, "--first-name"),
|
|
212
|
+
middle_name: str | None = typer.Option(None, "--middle-name"),
|
|
213
|
+
last_name: str | None = typer.Option(None, "--last-name"),
|
|
214
|
+
prefix: str | None = typer.Option(None, "--prefix"),
|
|
215
|
+
suffix: str | None = typer.Option(None, "--suffix"),
|
|
216
|
+
nickname: str | None = typer.Option(None, "--nickname"),
|
|
217
|
+
gender: Gender | None = typer.Option(None, "--gender"),
|
|
218
|
+
marital_status: MaritalStatus | None = typer.Option(None, "--marital-status"),
|
|
219
|
+
birth_date: str | None = typer.Option(None, "--birth-date", help="Format: YYYY-MM-DD"),
|
|
220
|
+
anniversary: str | None = typer.Option(None, "--anniversary", help="Format: YYYY-MM-DD"),
|
|
221
|
+
job_title: str | None = typer.Option(None, "--job-title"),
|
|
222
|
+
company_name: str | None = typer.Option(None, "--company-name"),
|
|
223
|
+
contact_type: str | None = typer.Option(None, "--contact-type", help="e.g. Client, Prospect"),
|
|
224
|
+
contact_source: str | None = typer.Option(None, "--contact-source"),
|
|
225
|
+
active: bool | None = typer.Option(None, "--active/--inactive", help="Set contact status to Active or Inactive"),
|
|
226
|
+
assigned_to: int | None = typer.Option(None, "--assigned-to", help="Assign to a user by ID"),
|
|
227
|
+
email: str | None = typer.Option(None, "--email", help="Primary email address"),
|
|
228
|
+
email_type: str | None = typer.Option(
|
|
229
|
+
None, "--email-type", help="Email kind (e.g. Work, Personal) — see: wbox contacts categories email-types"
|
|
230
|
+
),
|
|
231
|
+
phone: str | None = typer.Option(None, "--phone", help="Primary phone number"),
|
|
232
|
+
phone_type: str | None = typer.Option(
|
|
233
|
+
None, "--phone-type", help="Phone kind (e.g. Work, Mobile) — see: wbox contacts categories phone-types"
|
|
234
|
+
),
|
|
235
|
+
more_fields: str | None = typer.Option(
|
|
236
|
+
None, "--more-fields", help="Extra fields as JSON object (merged with flags; cannot override explicit flags)"
|
|
237
|
+
),
|
|
238
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
239
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
240
|
+
) -> None:
|
|
241
|
+
payload: dict[str, Any] = {k: v for k, v in {
|
|
242
|
+
"type": RecordType.PERSON,
|
|
243
|
+
"first_name": first_name,
|
|
244
|
+
"middle_name": middle_name,
|
|
245
|
+
"last_name": last_name,
|
|
246
|
+
"prefix": prefix,
|
|
247
|
+
"suffix": suffix,
|
|
248
|
+
"nickname": nickname,
|
|
249
|
+
"gender": gender,
|
|
250
|
+
"marital_status": marital_status,
|
|
251
|
+
"birth_date": birth_date,
|
|
252
|
+
"anniversary": anniversary,
|
|
253
|
+
"job_title": job_title,
|
|
254
|
+
"company_name": company_name,
|
|
255
|
+
"contact_type": contact_type,
|
|
256
|
+
"contact_source": contact_source,
|
|
257
|
+
"status": active_to_status(active),
|
|
258
|
+
"assigned_to": assigned_to,
|
|
259
|
+
}.items() if v is not None}
|
|
260
|
+
emails = _build_contact_entry(email, email_type)
|
|
261
|
+
if emails:
|
|
262
|
+
payload["email_addresses"] = emails
|
|
263
|
+
phones = _build_contact_entry(phone, phone_type)
|
|
264
|
+
if phones:
|
|
265
|
+
payload["phone_numbers"] = phones
|
|
266
|
+
if more_fields:
|
|
267
|
+
payload.update(parse_more_fields(more_fields, _PERSON_RESERVED))
|
|
268
|
+
input_model = ContactCreateInput(**payload)
|
|
269
|
+
output_result(run_client(token, lambda c: c.create_contact(input_model)), fmt)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@add_app.command("household", help="Create a Household contact.")
|
|
273
|
+
@handle_errors
|
|
274
|
+
def add_household(
|
|
275
|
+
name: str = typer.Option(..., "--name", help="Household name (required)"),
|
|
276
|
+
contact_type: str | None = typer.Option(None, "--contact-type", help="e.g. Client, Prospect"),
|
|
277
|
+
contact_source: str | None = typer.Option(None, "--contact-source"),
|
|
278
|
+
active: bool | None = typer.Option(None, "--active/--inactive", help="Set contact status to Active or Inactive"),
|
|
279
|
+
assigned_to: int | None = typer.Option(None, "--assigned-to", help="Assign to a user by ID"),
|
|
280
|
+
email: str | None = typer.Option(None, "--email", help="Primary email address"),
|
|
281
|
+
email_type: str | None = typer.Option(
|
|
282
|
+
None, "--email-type", help="Email kind (e.g. Work, Personal) — see: wbox contacts categories email-types"
|
|
283
|
+
),
|
|
284
|
+
more_fields: str | None = typer.Option(
|
|
285
|
+
None, "--more-fields", help="Extra fields as JSON object (merged with flags; cannot override explicit flags)"
|
|
286
|
+
),
|
|
287
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
288
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
289
|
+
) -> None:
|
|
290
|
+
payload: dict[str, Any] = {k: v for k, v in {
|
|
291
|
+
"type": RecordType.HOUSEHOLD,
|
|
292
|
+
"name": name,
|
|
293
|
+
"contact_type": contact_type,
|
|
294
|
+
"contact_source": contact_source,
|
|
295
|
+
"status": active_to_status(active),
|
|
296
|
+
"assigned_to": assigned_to,
|
|
297
|
+
}.items() if v is not None}
|
|
298
|
+
emails = _build_contact_entry(email, email_type)
|
|
299
|
+
if emails:
|
|
300
|
+
payload["email_addresses"] = emails
|
|
301
|
+
if more_fields:
|
|
302
|
+
payload.update(parse_more_fields(more_fields, _HOUSEHOLD_RESERVED))
|
|
303
|
+
input_model = ContactCreateInput(**payload)
|
|
304
|
+
output_result(run_client(token, lambda c: c.create_contact(input_model)), fmt)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@add_app.command("org", help="Create an Organization contact.")
|
|
308
|
+
@handle_errors
|
|
309
|
+
def add_org(
|
|
310
|
+
name: str = typer.Option(..., "--name", help="Organization name (required)"),
|
|
311
|
+
contact_type: str | None = typer.Option(None, "--contact-type", help="e.g. Client, Prospect"),
|
|
312
|
+
contact_source: str | None = typer.Option(None, "--contact-source"),
|
|
313
|
+
active: bool | None = typer.Option(None, "--active/--inactive", help="Set contact status to Active or Inactive"),
|
|
314
|
+
assigned_to: int | None = typer.Option(None, "--assigned-to", help="Assign to a user by ID"),
|
|
315
|
+
email: str | None = typer.Option(None, "--email", help="Primary email address"),
|
|
316
|
+
email_type: str | None = typer.Option(
|
|
317
|
+
None, "--email-type", help="Email kind (e.g. Work, Personal) — see: wbox contacts categories email-types"
|
|
318
|
+
),
|
|
319
|
+
phone: str | None = typer.Option(None, "--phone", help="Primary phone number"),
|
|
320
|
+
phone_type: str | None = typer.Option(
|
|
321
|
+
None, "--phone-type", help="Phone kind (e.g. Work, Mobile) — see: wbox contacts categories phone-types"
|
|
322
|
+
),
|
|
323
|
+
more_fields: str | None = typer.Option(
|
|
324
|
+
None, "--more-fields", help="Extra fields as JSON object (merged with flags; cannot override explicit flags)"
|
|
325
|
+
),
|
|
326
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
327
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
328
|
+
) -> None:
|
|
329
|
+
_create_named_contact(
|
|
330
|
+
RecordType.ORGANIZATION, _ORG_TRUST_RESERVED, name, contact_type, contact_source,
|
|
331
|
+
active, assigned_to, email, email_type, phone, phone_type, more_fields, token, fmt,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@add_app.command("trust", help="Create a Trust contact.")
|
|
336
|
+
@handle_errors
|
|
337
|
+
def add_trust(
|
|
338
|
+
name: str = typer.Option(..., "--name", help="Trust name (required)"),
|
|
339
|
+
contact_type: str | None = typer.Option(None, "--contact-type", help="e.g. Client, Prospect"),
|
|
340
|
+
contact_source: str | None = typer.Option(None, "--contact-source"),
|
|
341
|
+
active: bool | None = typer.Option(None, "--active/--inactive", help="Set contact status to Active or Inactive"),
|
|
342
|
+
assigned_to: int | None = typer.Option(None, "--assigned-to", help="Assign to a user by ID"),
|
|
343
|
+
email: str | None = typer.Option(None, "--email", help="Primary email address"),
|
|
344
|
+
email_type: str | None = typer.Option(
|
|
345
|
+
None, "--email-type", help="Email kind (e.g. Work, Personal) — see: wbox contacts categories email-types"
|
|
346
|
+
),
|
|
347
|
+
phone: str | None = typer.Option(None, "--phone", help="Primary phone number"),
|
|
348
|
+
phone_type: str | None = typer.Option(
|
|
349
|
+
None, "--phone-type", help="Phone kind (e.g. Work, Mobile) — see: wbox contacts categories phone-types"
|
|
350
|
+
),
|
|
351
|
+
more_fields: str | None = typer.Option(
|
|
352
|
+
None, "--more-fields", help="Extra fields as JSON object (merged with flags; cannot override explicit flags)"
|
|
353
|
+
),
|
|
354
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
355
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
356
|
+
) -> None:
|
|
357
|
+
_create_named_contact(
|
|
358
|
+
RecordType.TRUST, _ORG_TRUST_RESERVED, name, contact_type, contact_source,
|
|
359
|
+
active, assigned_to, email, email_type, phone, phone_type, more_fields, token, fmt,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@app.command("update", help="Update an existing contact. Pass only the fields you want to change.")
|
|
364
|
+
@handle_errors
|
|
365
|
+
def update_contact(
|
|
366
|
+
contact_id: int = typer.Argument(..., help="Contact ID"),
|
|
367
|
+
# Advanced path for nested arrays (email_addresses, phone_numbers, etc.)
|
|
368
|
+
json_data: str | None = typer.Option(
|
|
369
|
+
None, "--json", help="Full update as JSON (for nested fields like email_addresses)"
|
|
370
|
+
),
|
|
371
|
+
# Scalar flags
|
|
372
|
+
first_name: str | None = typer.Option(None, "--first-name"),
|
|
373
|
+
middle_name: str | None = typer.Option(None, "--middle-name"),
|
|
374
|
+
last_name: str | None = typer.Option(None, "--last-name"),
|
|
375
|
+
name: str | None = typer.Option(None, "--name", help="Full name (for Household/Org/Trust)"),
|
|
376
|
+
job_title: str | None = typer.Option(None, "--job-title"),
|
|
377
|
+
company_name: str | None = typer.Option(None, "--company-name"),
|
|
378
|
+
contact_type: str | None = typer.Option(None, "--contact-type", help="e.g. Client, Prospect"),
|
|
379
|
+
contact_source: str | None = typer.Option(None, "--contact-source"),
|
|
380
|
+
active: bool | None = typer.Option(None, "--active/--inactive", help="Set contact status to Active or Inactive"),
|
|
381
|
+
assigned_to: int | None = typer.Option(None, "--assigned-to", help="Reassign to a user by ID"),
|
|
382
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
383
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
384
|
+
) -> None:
|
|
385
|
+
if json_data is not None:
|
|
386
|
+
input_model = ContactUpdateInput(**json.loads(json_data))
|
|
387
|
+
else:
|
|
388
|
+
payload: dict[str, Any] = {k: v for k, v in {
|
|
389
|
+
"first_name": first_name,
|
|
390
|
+
"middle_name": middle_name,
|
|
391
|
+
"last_name": last_name,
|
|
392
|
+
"name": name,
|
|
393
|
+
"job_title": job_title,
|
|
394
|
+
"company_name": company_name,
|
|
395
|
+
"contact_type": contact_type,
|
|
396
|
+
"contact_source": contact_source,
|
|
397
|
+
"status": active_to_status(active),
|
|
398
|
+
"assigned_to": assigned_to,
|
|
399
|
+
}.items() if v is not None}
|
|
400
|
+
input_model = ContactUpdateInput(**payload)
|
|
401
|
+
|
|
402
|
+
output_result(run_client(token, lambda c: c.update_contact(contact_id, input_model)), fmt)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@app.command("delete", help="Delete an existing contact.")
|
|
406
|
+
@handle_errors
|
|
407
|
+
def delete_contact(
|
|
408
|
+
contact_id: int = typer.Argument(..., help="Contact ID"),
|
|
409
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
410
|
+
) -> None:
|
|
411
|
+
run_client(token, lambda c: c.delete_contact(contact_id))
|
|
412
|
+
typer.echo(f"Contact {contact_id} deleted.")
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from wealthbox_tools.models import (
|
|
8
|
+
CategoryType,
|
|
9
|
+
EventCreateInput,
|
|
10
|
+
EventListQuery,
|
|
11
|
+
EventsOrder,
|
|
12
|
+
EventsState,
|
|
13
|
+
EventUpdateInput,
|
|
14
|
+
TaskResourceType,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from ._util import OutputFormat, build_linked_to, handle_errors, make_category_command, output_result, run_client
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
21
|
+
help="Manage Wealthbox events.",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
app.command("categories", help="List event category options.")(make_category_command(CategoryType.EVENT_CATEGORIES))
|
|
25
|
+
|
|
26
|
+
_DEFAULT_FIELDS = ["id", "title", "starts_at", "ends_at", "state", "event_category"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command("list", help="List events with optional filters.")
|
|
30
|
+
@handle_errors
|
|
31
|
+
def list_events(
|
|
32
|
+
resource_id: int | None = typer.Option(None, "--resource-id", help="Filter by resource ID"),
|
|
33
|
+
resource_type: TaskResourceType | None = typer.Option(
|
|
34
|
+
None, "--resource-type", help="Supports: Contact, Project, Opportunity"
|
|
35
|
+
),
|
|
36
|
+
start_date_min: str | None = typer.Option(
|
|
37
|
+
None, "--start-date-min", help="Format example: '2015-05-24 10:00 AM -0400'"
|
|
38
|
+
),
|
|
39
|
+
start_date_max: str | None = typer.Option(
|
|
40
|
+
None, "--start-date-max", help="Format example: '2015-05-24 10:00 AM -0400'"
|
|
41
|
+
),
|
|
42
|
+
order: EventsOrder | None = typer.Option(None, "--order", help="Sort order: asc, desc, recent, created"),
|
|
43
|
+
updated_since: str | None = typer.Option(
|
|
44
|
+
None, "--updated-since", help="Format example: '2015-05-24 10:00 AM -0400'"
|
|
45
|
+
),
|
|
46
|
+
updated_before: str | None = typer.Option(
|
|
47
|
+
None, "--updated-before", help="Format example: '2015-05-24 10:00 AM -0400'"
|
|
48
|
+
),
|
|
49
|
+
page: int | None = typer.Option(None, help="Page number"),
|
|
50
|
+
per_page: int | None = typer.Option(None, "--per-page", help="Results per page (max 100)"),
|
|
51
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show all fields"),
|
|
52
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
53
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
54
|
+
) -> None:
|
|
55
|
+
query = EventListQuery(
|
|
56
|
+
resource_id=resource_id,
|
|
57
|
+
resource_type=resource_type,
|
|
58
|
+
start_date_min=start_date_min,
|
|
59
|
+
start_date_max=start_date_max,
|
|
60
|
+
order=order,
|
|
61
|
+
updated_since=updated_since,
|
|
62
|
+
updated_before=updated_before,
|
|
63
|
+
page=page,
|
|
64
|
+
per_page=per_page,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
output_result(run_client(token, lambda c: c.list_events(query)), fmt, fields=None if verbose else _DEFAULT_FIELDS)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("get", help="Get a single event by ID.")
|
|
71
|
+
@handle_errors
|
|
72
|
+
def get_event(
|
|
73
|
+
event_id: int = typer.Argument(..., help="Event ID"),
|
|
74
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show all fields"),
|
|
75
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
76
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
77
|
+
) -> None:
|
|
78
|
+
output_result(run_client(token, lambda c: c.get_event(event_id)), fmt, fields=None if verbose else _DEFAULT_FIELDS)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command("add", help="Create a new event.")
|
|
82
|
+
@handle_errors
|
|
83
|
+
def add_event(
|
|
84
|
+
title: str = typer.Argument(..., help="Event title"),
|
|
85
|
+
starts_at: str = typer.Option(
|
|
86
|
+
..., "--starts-at", help="Start datetime in ISO 8601, e.g. 2026-01-15T10:00:00-07:00"
|
|
87
|
+
),
|
|
88
|
+
ends_at: str = typer.Option(
|
|
89
|
+
..., "--ends-at", help="End datetime in ISO 8601, e.g. 2026-01-15T11:00:00-07:00"
|
|
90
|
+
),
|
|
91
|
+
location: str | None = typer.Option(None, "--location"),
|
|
92
|
+
state: EventsState | None = typer.Option(
|
|
93
|
+
None, "--state", help="unconfirmed, confirmed, tentative, completed, cancelled"
|
|
94
|
+
),
|
|
95
|
+
all_day: bool | None = typer.Option(None, "--all-day/--no-all-day"),
|
|
96
|
+
description: str | None = typer.Option(None, "--description"),
|
|
97
|
+
event_category: int | None = typer.Option(None, "--category", help="Event category ID"),
|
|
98
|
+
contact: int | None = typer.Option(None, "--contact", help="Link to a Contact by ID"),
|
|
99
|
+
project: int | None = typer.Option(None, "--project", help="Link to a Project by ID"),
|
|
100
|
+
opportunity: int | None = typer.Option(None, "--opportunity", help="Link to an Opportunity by ID"),
|
|
101
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
102
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
103
|
+
) -> None:
|
|
104
|
+
input_model = EventCreateInput(
|
|
105
|
+
title=title,
|
|
106
|
+
starts_at=starts_at,
|
|
107
|
+
ends_at=ends_at,
|
|
108
|
+
location=location,
|
|
109
|
+
state=state,
|
|
110
|
+
all_day=all_day,
|
|
111
|
+
description=description,
|
|
112
|
+
event_category=event_category,
|
|
113
|
+
linked_to=build_linked_to(contact, project, opportunity),
|
|
114
|
+
)
|
|
115
|
+
output_result(run_client(token, lambda c: c.create_event(input_model)), fmt)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command("update", help="Update an existing event. Pass only the fields you want to change.")
|
|
119
|
+
@handle_errors
|
|
120
|
+
def update_event(
|
|
121
|
+
event_id: int = typer.Argument(..., help="Event ID"),
|
|
122
|
+
title: str | None = typer.Option(None, "--title", help="Event title"),
|
|
123
|
+
starts_at: str | None = typer.Option(
|
|
124
|
+
None, "--starts-at", help="Start datetime in ISO 8601, e.g. 2026-01-15T10:00:00-07:00"
|
|
125
|
+
),
|
|
126
|
+
ends_at: str | None = typer.Option(
|
|
127
|
+
None, "--ends-at", help="End datetime in ISO 8601, e.g. 2026-01-15T11:00:00-07:00"
|
|
128
|
+
),
|
|
129
|
+
location: str | None = typer.Option(None, "--location"),
|
|
130
|
+
state: EventsState | None = typer.Option(
|
|
131
|
+
None, "--state", help="unconfirmed, confirmed, tentative, completed, cancelled"
|
|
132
|
+
),
|
|
133
|
+
all_day: bool | None = typer.Option(None, "--all-day/--no-all-day"),
|
|
134
|
+
description: str | None = typer.Option(None, "--description"),
|
|
135
|
+
event_category: int | None = typer.Option(None, "--category", help="Event category ID"),
|
|
136
|
+
contact: int | None = typer.Option(None, "--contact", help="Replace linked Contact (by ID)"),
|
|
137
|
+
project: int | None = typer.Option(None, "--project", help="Replace linked Project (by ID)"),
|
|
138
|
+
opportunity: int | None = typer.Option(None, "--opportunity", help="Replace linked Opportunity (by ID)"),
|
|
139
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
140
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
141
|
+
) -> None:
|
|
142
|
+
payload: dict[str, Any] = {k: v for k, v in {
|
|
143
|
+
"title": title,
|
|
144
|
+
"starts_at": starts_at,
|
|
145
|
+
"ends_at": ends_at,
|
|
146
|
+
"location": location,
|
|
147
|
+
"state": state,
|
|
148
|
+
"description": description,
|
|
149
|
+
"event_category": event_category,
|
|
150
|
+
}.items() if v is not None}
|
|
151
|
+
if all_day is not None:
|
|
152
|
+
payload["all_day"] = all_day
|
|
153
|
+
linked = build_linked_to(contact, project, opportunity)
|
|
154
|
+
if linked is not None:
|
|
155
|
+
payload["linked_to"] = linked
|
|
156
|
+
input_model = EventUpdateInput(**payload)
|
|
157
|
+
|
|
158
|
+
output_result(run_client(token, lambda c: c.update_event(event_id, input_model)), fmt)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command("delete", help="Delete an existing event.")
|
|
162
|
+
@handle_errors
|
|
163
|
+
def delete_event(
|
|
164
|
+
event_id: int = typer.Argument(..., help="Event ID"),
|
|
165
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
166
|
+
) -> None:
|
|
167
|
+
run_client(token, lambda c: c.delete_event(event_id))
|
|
168
|
+
typer.echo(f"Event {event_id} deleted.")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from wealthbox_tools.models import HouseholdMemberInput, HouseholdTitle
|
|
6
|
+
|
|
7
|
+
from ._util import OutputFormat, handle_errors, output_result, run_client
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
11
|
+
help="Manage household members.",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command(
|
|
17
|
+
"add-member",
|
|
18
|
+
help="Add a member to a household. Usage: add-member <household_id> <member_id> --title <title>",
|
|
19
|
+
)
|
|
20
|
+
@handle_errors
|
|
21
|
+
def add_member(
|
|
22
|
+
household_id: int = typer.Argument(..., help="Household contact ID"),
|
|
23
|
+
member_id: int = typer.Argument(..., help="Member contact ID to add"),
|
|
24
|
+
title: HouseholdTitle = typer.Option(..., help="Household title for member (e.g. Spouse, Head)"),
|
|
25
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
26
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
27
|
+
) -> None:
|
|
28
|
+
payload = HouseholdMemberInput(id=member_id, title=title)
|
|
29
|
+
|
|
30
|
+
output_result(run_client(token, lambda c: c.add_household_member(household_id, payload)), fmt)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command("remove-member", help="Remove a member from a household.")
|
|
34
|
+
@handle_errors
|
|
35
|
+
def remove_member(
|
|
36
|
+
household_id: int = typer.Argument(..., help="Household contact ID"),
|
|
37
|
+
member_id: int = typer.Argument(..., help="Member contact ID to remove"),
|
|
38
|
+
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
39
|
+
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
40
|
+
) -> None:
|
|
41
|
+
output_result(run_client(token, lambda c: c.remove_household_member(household_id, member_id)), fmt)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version as _pkg_version
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .activity import app as activity_app
|
|
8
|
+
from .categories import app as categories_app
|
|
9
|
+
from .comments import app as comments_app
|
|
10
|
+
from .config import app as config_app
|
|
11
|
+
from .contacts import app as contacts_app
|
|
12
|
+
from .events import app as events_app
|
|
13
|
+
from .households import app as households_app
|
|
14
|
+
from .me import app as me_app
|
|
15
|
+
from .notes import app as notes_app
|
|
16
|
+
from .opportunities import app as opportunities_app
|
|
17
|
+
from .projects import app as projects_app
|
|
18
|
+
from .tasks import app as tasks_app
|
|
19
|
+
from .users import app as users_app
|
|
20
|
+
from .workflows import app as workflows_app
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]},
|
|
23
|
+
name="wbox",
|
|
24
|
+
help="Wealthbox CRM CLI — interact with contacts, households, tasks, events, opportunities, and notes.",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
@app.callback(invoke_without_command=True)
|
|
29
|
+
def _main(
|
|
30
|
+
ctx: typer.Context,
|
|
31
|
+
version: bool = typer.Option(False, "--version", "-v", is_eager=True, help="Show version and exit."),
|
|
32
|
+
) -> None:
|
|
33
|
+
if version:
|
|
34
|
+
typer.echo(_pkg_version("wealthbox-cli"))
|
|
35
|
+
raise typer.Exit()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
app.add_typer(activity_app, name="activity")
|
|
39
|
+
app.add_typer(categories_app, name="categories")
|
|
40
|
+
app.add_typer(config_app, name="config")
|
|
41
|
+
app.add_typer(comments_app, name="comments")
|
|
42
|
+
app.add_typer(contacts_app, name="contacts")
|
|
43
|
+
app.add_typer(events_app, name="events")
|
|
44
|
+
app.add_typer(households_app, name="households")
|
|
45
|
+
app.add_typer(me_app, name="me")
|
|
46
|
+
app.add_typer(notes_app, name="notes")
|
|
47
|
+
app.add_typer(opportunities_app, name="opportunities")
|
|
48
|
+
app.add_typer(projects_app, name="projects")
|
|
49
|
+
app.add_typer(tasks_app, name="tasks")
|
|
50
|
+
app.add_typer(users_app, name="users")
|
|
51
|
+
app.add_typer(workflows_app, name="workflows")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
app()
|