wealthbox-cli 1.0.0__tar.gz → 1.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.
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/CONTRIBUTING.md +19 -4
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/PKG-INFO +40 -10
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/README.md +39 -9
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/pyproject.toml +1 -1
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/_util.py +133 -6
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/contacts.py +2 -5
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/events.py +23 -8
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/main.py +0 -2
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/notes.py +23 -8
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/opportunities.py +25 -17
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/projects.py +18 -7
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/tasks.py +34 -6
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/workflows.py +19 -7
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/comments.py +8 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/enums.py +8 -1
- wealthbox_cli-1.0.0/src/wealthbox_tools/cli/comments.py +0 -51
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/.env.example +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/.github/workflows/ci.yml +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/.gitignore +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/LICENSE +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/SECURITY.md +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/docs/cli-reference.md +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/__init__.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/__init__.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/_config.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/activity.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/categories.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/config.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/households.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/me.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/cli/users.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/__init__.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/activity.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/base.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/categories.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/contacts.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/events.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/households.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/me.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/notes.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/opportunities.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/projects.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/tasks.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/users.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/client/workflows.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/__init__.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/activity.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/comments.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/common.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/contacts.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/custom_fields.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/events.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/households.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/notes.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/opportunities.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/projects.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/tasks.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/models/workflows.py +0 -0
- {wealthbox_cli-1.0.0 → wealthbox_cli-1.0.2}/src/wealthbox_tools/py.typed +0 -0
|
@@ -15,7 +15,13 @@ source .venv/bin/activate # macOS/Linux
|
|
|
15
15
|
pip install -e ".[dev]"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Configure your Wealthbox API token:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
wbox config set-token
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or copy `.env.example` to `.env` and add your token there.
|
|
19
25
|
|
|
20
26
|
## Running Tests
|
|
21
27
|
|
|
@@ -32,8 +38,8 @@ ruff check src/
|
|
|
32
38
|
mypy src/
|
|
33
39
|
```
|
|
34
40
|
|
|
35
|
-
- **ruff** for linting (E, F, I rules;
|
|
36
|
-
- **mypy** in strict mode
|
|
41
|
+
- **ruff** for linting (E, F, I rules; 120-char line length)
|
|
42
|
+
- **mypy** in strict mode (has known `untyped-decorator` warnings from Typer — not currently enforced in CI)
|
|
37
43
|
|
|
38
44
|
## Project Architecture
|
|
39
45
|
|
|
@@ -55,11 +61,20 @@ Three layers under `src/wealthbox_tools/`:
|
|
|
55
61
|
6. If the resource has category types, add them via `make_category_command()` in the resource's `categories` sub-app
|
|
56
62
|
7. Add tests in `tests/test_<resource>_create.py` and `tests/test_<resource>_update.py`
|
|
57
63
|
|
|
64
|
+
## CI
|
|
65
|
+
|
|
66
|
+
Pull requests and pushes to `main` run GitHub Actions CI:
|
|
67
|
+
|
|
68
|
+
- **Lint:** `ruff check src/ tests/`
|
|
69
|
+
- **Test:** `pytest` across Python 3.11, 3.12, 3.13
|
|
70
|
+
|
|
71
|
+
Both must pass before merging.
|
|
72
|
+
|
|
58
73
|
## Pull Requests
|
|
59
74
|
|
|
60
75
|
- Keep PRs focused — one feature or fix per PR
|
|
61
76
|
- All tests must pass (`pytest`)
|
|
62
|
-
- No ruff
|
|
77
|
+
- No ruff errors
|
|
63
78
|
- Update the CLI reference docs if commands change
|
|
64
79
|
|
|
65
80
|
## Reporting Issues
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wealthbox-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: CLI and client library for the Wealthbox CRM API
|
|
5
5
|
Project-URL: Homepage, https://github.com/massive-value/wealthbox-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/massive-value/wealthbox-cli/issues
|
|
@@ -76,6 +76,18 @@ Official API documentation: https://dev.wealthbox.com
|
|
|
76
76
|
pip install wealthbox-cli
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
### With pipx (recommended for Ubuntu/Debian)
|
|
80
|
+
|
|
81
|
+
On systems where the system Python is externally managed (Ubuntu 23.04+, Debian 12+),
|
|
82
|
+
`pip install` outside a virtual environment is blocked by [PEP 668](https://peps.python.org/pep-0668/).
|
|
83
|
+
Use [pipx](https://pipx.pypa.io/) to install CLI tools in isolated environments:
|
|
84
|
+
|
|
85
|
+
``` bash
|
|
86
|
+
pipx install wealthbox-cli
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This puts `wbox` and `wb` on your PATH without touching system Python.
|
|
90
|
+
|
|
79
91
|
### From source (development)
|
|
80
92
|
|
|
81
93
|
``` bash
|
|
@@ -92,26 +104,44 @@ pip install -e ".[dev]"
|
|
|
92
104
|
|
|
93
105
|
## Configuration
|
|
94
106
|
|
|
95
|
-
|
|
107
|
+
### Getting your API token
|
|
96
108
|
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
1. Log in to [Wealthbox](https://app.crmworkspace.com)
|
|
110
|
+
2. Click the three dots menu (**...**) in the top right
|
|
111
|
+
3. Go to **Settings** -> **API Access**
|
|
112
|
+
4. Click **Create Access Token**
|
|
113
|
+
|
|
114
|
+
### Storing your token
|
|
115
|
+
|
|
116
|
+
``` bash
|
|
117
|
+
wbox config set-token
|
|
99
118
|
```
|
|
100
119
|
|
|
101
|
-
|
|
120
|
+
This prompts for your Wealthbox API token (input is masked) and stores it in
|
|
121
|
+
`~/.config/wbox/config.json` (Linux/macOS) or `%APPDATA%\wbox\config.json` (Windows).
|
|
102
122
|
|
|
103
|
-
|
|
123
|
+
Other configuration commands:
|
|
104
124
|
|
|
105
125
|
``` bash
|
|
106
|
-
|
|
126
|
+
wbox config show # display stored config (token masked)
|
|
127
|
+
wbox config clear # remove stored config
|
|
107
128
|
```
|
|
108
129
|
|
|
109
|
-
**
|
|
130
|
+
**Alternative methods** (for CI, scripting, or containers):
|
|
131
|
+
|
|
132
|
+
``` bash
|
|
133
|
+
# Environment variable
|
|
134
|
+
export WEALTHBOX_TOKEN="your_api_token_here"
|
|
110
135
|
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
# .env file in working directory
|
|
137
|
+
echo 'WEALTHBOX_TOKEN=your_api_token_here' > .env
|
|
138
|
+
|
|
139
|
+
# Per-command flag
|
|
140
|
+
wbox contacts list --token your_api_token_here
|
|
113
141
|
```
|
|
114
142
|
|
|
143
|
+
Token is resolved in this order: `--token` flag > `WEALTHBOX_TOKEN` env var > config file > `.env` file.
|
|
144
|
+
|
|
115
145
|
------------------------------------------------------------------------
|
|
116
146
|
|
|
117
147
|
## Usage
|
|
@@ -45,6 +45,18 @@ Official API documentation: https://dev.wealthbox.com
|
|
|
45
45
|
pip install wealthbox-cli
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
### With pipx (recommended for Ubuntu/Debian)
|
|
49
|
+
|
|
50
|
+
On systems where the system Python is externally managed (Ubuntu 23.04+, Debian 12+),
|
|
51
|
+
`pip install` outside a virtual environment is blocked by [PEP 668](https://peps.python.org/pep-0668/).
|
|
52
|
+
Use [pipx](https://pipx.pypa.io/) to install CLI tools in isolated environments:
|
|
53
|
+
|
|
54
|
+
``` bash
|
|
55
|
+
pipx install wealthbox-cli
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This puts `wbox` and `wb` on your PATH without touching system Python.
|
|
59
|
+
|
|
48
60
|
### From source (development)
|
|
49
61
|
|
|
50
62
|
``` bash
|
|
@@ -61,26 +73,44 @@ pip install -e ".[dev]"
|
|
|
61
73
|
|
|
62
74
|
## Configuration
|
|
63
75
|
|
|
64
|
-
|
|
76
|
+
### Getting your API token
|
|
65
77
|
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
1. Log in to [Wealthbox](https://app.crmworkspace.com)
|
|
79
|
+
2. Click the three dots menu (**...**) in the top right
|
|
80
|
+
3. Go to **Settings** -> **API Access**
|
|
81
|
+
4. Click **Create Access Token**
|
|
82
|
+
|
|
83
|
+
### Storing your token
|
|
84
|
+
|
|
85
|
+
``` bash
|
|
86
|
+
wbox config set-token
|
|
68
87
|
```
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
This prompts for your Wealthbox API token (input is masked) and stores it in
|
|
90
|
+
`~/.config/wbox/config.json` (Linux/macOS) or `%APPDATA%\wbox\config.json` (Windows).
|
|
71
91
|
|
|
72
|
-
|
|
92
|
+
Other configuration commands:
|
|
73
93
|
|
|
74
94
|
``` bash
|
|
75
|
-
|
|
95
|
+
wbox config show # display stored config (token masked)
|
|
96
|
+
wbox config clear # remove stored config
|
|
76
97
|
```
|
|
77
98
|
|
|
78
|
-
**
|
|
99
|
+
**Alternative methods** (for CI, scripting, or containers):
|
|
100
|
+
|
|
101
|
+
``` bash
|
|
102
|
+
# Environment variable
|
|
103
|
+
export WEALTHBOX_TOKEN="your_api_token_here"
|
|
79
104
|
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
# .env file in working directory
|
|
106
|
+
echo 'WEALTHBOX_TOKEN=your_api_token_here' > .env
|
|
107
|
+
|
|
108
|
+
# Per-command flag
|
|
109
|
+
wbox contacts list --token your_api_token_here
|
|
82
110
|
```
|
|
83
111
|
|
|
112
|
+
Token is resolved in this order: `--token` flag > `WEALTHBOX_TOKEN` env var > config file > `.env` file.
|
|
113
|
+
|
|
84
114
|
------------------------------------------------------------------------
|
|
85
115
|
|
|
86
116
|
## Usage
|
|
@@ -11,11 +11,42 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
13
|
from pydantic import ValidationError
|
|
14
|
+
from typer.core import TyperGroup
|
|
14
15
|
|
|
15
16
|
from wealthbox_tools.client import WealthboxAPIError, WealthboxClient
|
|
16
17
|
from wealthbox_tools.models import CategoryListQuery, CategoryType, LinkedToRef, TaskResourceType
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
class _GetShortcutGroup(TyperGroup):
|
|
21
|
+
"""Typer Group that routes numeric first arguments to the ``get`` subcommand."""
|
|
22
|
+
|
|
23
|
+
def resolve_command(self, ctx: typer.Context, args: list[str]) -> tuple: # type: ignore[type-arg]
|
|
24
|
+
if args and args[0].isdigit() and args[0] not in self.commands:
|
|
25
|
+
args.insert(0, "get")
|
|
26
|
+
return super().resolve_command(ctx, args)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def make_resource_app(*, help: str) -> typer.Typer:
|
|
30
|
+
"""Create a Typer app that supports ``wbox <resource> <id>`` as shorthand for ``get <id>``."""
|
|
31
|
+
return typer.Typer(
|
|
32
|
+
cls=_GetShortcutGroup,
|
|
33
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
34
|
+
help=help,
|
|
35
|
+
no_args_is_help=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Maps CLI resource names to the CommentResourceType string expected by GET /comments
|
|
40
|
+
COMMENT_RESOURCE_TYPES: dict[str, str] = {
|
|
41
|
+
"tasks": "Task",
|
|
42
|
+
"events": "Event",
|
|
43
|
+
"notes": "StatusUpdates",
|
|
44
|
+
"opportunities": "Opportunity",
|
|
45
|
+
"projects": "Project",
|
|
46
|
+
"workflows": "Workflow",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
19
50
|
def get_client(token: str | None = None) -> WealthboxClient:
|
|
20
51
|
"""Create a WealthboxClient with token resolution: --token flag > env var > config file > .env."""
|
|
21
52
|
import os
|
|
@@ -42,6 +73,95 @@ def run_client(token: str | None, fn: Callable[[WealthboxClient], Awaitable[Any]
|
|
|
42
73
|
return asyncio.run(_execute())
|
|
43
74
|
|
|
44
75
|
|
|
76
|
+
def run_client_with_comments(
|
|
77
|
+
token: str | None,
|
|
78
|
+
fn: Callable[[WealthboxClient], Awaitable[Any]],
|
|
79
|
+
resource_type: str,
|
|
80
|
+
resource_id: int,
|
|
81
|
+
include_comments: bool = True,
|
|
82
|
+
) -> Any:
|
|
83
|
+
"""Run an async client operation and optionally fetch + merge comments concurrently."""
|
|
84
|
+
async def _with_comments(client: WealthboxClient) -> Any:
|
|
85
|
+
if include_comments:
|
|
86
|
+
result, comments = await asyncio.gather(
|
|
87
|
+
fn(client),
|
|
88
|
+
client.get_comments_for_resource(resource_type, resource_id),
|
|
89
|
+
)
|
|
90
|
+
if isinstance(result, dict):
|
|
91
|
+
result["comments"] = comments
|
|
92
|
+
else:
|
|
93
|
+
result = await fn(client)
|
|
94
|
+
return result
|
|
95
|
+
return run_client(token, _with_comments)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_COMMENT_PREVIEW_LEN = 50
|
|
99
|
+
_SLIM_COMMENT_FIELDS = ("updated_at", "created_at", "creator")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _strip_html(text: str) -> str:
|
|
103
|
+
"""Remove HTML tags and decode common HTML entities."""
|
|
104
|
+
import html
|
|
105
|
+
import re
|
|
106
|
+
return html.unescape(re.sub(r"<[^>]+>", "", text)).strip()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def clean_comments(data: dict[str, Any]) -> dict[str, Any]:
|
|
110
|
+
"""Strip HTML from body.text in each comment, keeping full structure (for verbose output)."""
|
|
111
|
+
comments = data.get("comments")
|
|
112
|
+
if not isinstance(comments, list):
|
|
113
|
+
return data
|
|
114
|
+
cleaned = []
|
|
115
|
+
for c in comments:
|
|
116
|
+
body = c.get("body")
|
|
117
|
+
if isinstance(body, dict) and "text" in body:
|
|
118
|
+
c = {**c, "body": {**body, "text": _strip_html(body["text"])}}
|
|
119
|
+
cleaned.append(c)
|
|
120
|
+
return {**data, "comments": cleaned}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def slim_comments(data: dict[str, Any]) -> dict[str, Any]:
|
|
124
|
+
"""Strip HTML, trim to essential fields, and unnest body.text → text (single pass)."""
|
|
125
|
+
comments = data.get("comments")
|
|
126
|
+
if not isinstance(comments, list):
|
|
127
|
+
return data
|
|
128
|
+
slimmed = []
|
|
129
|
+
for c in comments:
|
|
130
|
+
entry = {k: c[k] for k in _SLIM_COMMENT_FIELDS if k in c}
|
|
131
|
+
body = c.get("body")
|
|
132
|
+
raw = body.get("text", "") if isinstance(body, dict) else str(body or "")
|
|
133
|
+
entry["text"] = _strip_html(raw)
|
|
134
|
+
slimmed.append(entry)
|
|
135
|
+
return {**data, "comments": slimmed}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def summarize_comments(data: dict[str, Any]) -> dict[str, Any]:
|
|
139
|
+
"""Replace a ``comments`` list with ``comment_count`` and ``latest_comment`` summary fields."""
|
|
140
|
+
comments = data.get("comments")
|
|
141
|
+
if comments is None:
|
|
142
|
+
return data
|
|
143
|
+
data = {k: v for k, v in data.items() if k != "comments"}
|
|
144
|
+
data["comment_count"] = len(comments)
|
|
145
|
+
if comments:
|
|
146
|
+
newest = max(comments, key=lambda c: c.get("created_at", c.get("updated_at", "")))
|
|
147
|
+
text = newest.get("text", "")
|
|
148
|
+
preview = text[:_COMMENT_PREVIEW_LEN] + "..." if len(text) > _COMMENT_PREVIEW_LEN else text
|
|
149
|
+
data["latest_comment"] = preview
|
|
150
|
+
else:
|
|
151
|
+
data["latest_comment"] = ""
|
|
152
|
+
return data
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def output_get_result(
|
|
156
|
+
result: dict[str, Any], fmt: OutputFormat, fields: list[str] | None = None
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Standard output pipeline for get commands with comments: slim → summarize → output."""
|
|
159
|
+
result = slim_comments(result)
|
|
160
|
+
if fmt != OutputFormat.JSON:
|
|
161
|
+
result = summarize_comments(result)
|
|
162
|
+
output_result(result, fmt, fields=fields)
|
|
163
|
+
|
|
164
|
+
|
|
45
165
|
class OutputFormat(StrEnum):
|
|
46
166
|
JSON = "json"
|
|
47
167
|
TABLE = "table"
|
|
@@ -118,8 +238,12 @@ def _flatten_value(value: Any) -> Any:
|
|
|
118
238
|
return item["address"]
|
|
119
239
|
return first["address"]
|
|
120
240
|
if "id" in first and "type" in first:
|
|
121
|
-
# e.g. linked_to, invitees
|
|
122
|
-
|
|
241
|
+
# e.g. linked_to, invitees — prefer name if available
|
|
242
|
+
parts = []
|
|
243
|
+
for item in value:
|
|
244
|
+
name = item.get("name")
|
|
245
|
+
parts.append(name if name else f"{item['type']}:{item['id']}")
|
|
246
|
+
return ", ".join(parts)
|
|
123
247
|
return f"[{len(value)} items]"
|
|
124
248
|
if isinstance(value, dict):
|
|
125
249
|
return json.dumps(value)
|
|
@@ -143,10 +267,13 @@ def _extract_collection(data: Any) -> tuple[list[dict] | None, int | None]: # t
|
|
|
143
267
|
break
|
|
144
268
|
total = data["meta"].get("total_count") or data["meta"].get("total_entries")
|
|
145
269
|
return rows, total
|
|
146
|
-
# Check if any value is a list (collection without meta)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
270
|
+
# Check if any value is a list (collection without meta).
|
|
271
|
+
# Skip when the dict looks like a single resource (has an "id" key) —
|
|
272
|
+
# nested lists like linked_to, subtasks, comments are not collections.
|
|
273
|
+
if "id" not in data:
|
|
274
|
+
for v in data.values():
|
|
275
|
+
if isinstance(v, list) and v and isinstance(v[0], dict):
|
|
276
|
+
return v, None
|
|
150
277
|
return None, None
|
|
151
278
|
|
|
152
279
|
|
|
@@ -22,16 +22,13 @@ from ._util import (
|
|
|
22
22
|
active_to_status,
|
|
23
23
|
handle_errors,
|
|
24
24
|
make_category_command,
|
|
25
|
+
make_resource_app,
|
|
25
26
|
output_result,
|
|
26
27
|
parse_more_fields,
|
|
27
28
|
run_client,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
|
-
app =
|
|
31
|
-
context_settings={"help_option_names": ["-h", "--help"]},
|
|
32
|
-
help="Manage Wealthbox contacts.",
|
|
33
|
-
no_args_is_help=True,
|
|
34
|
-
)
|
|
31
|
+
app = make_resource_app(help="Manage Wealthbox contacts.")
|
|
35
32
|
|
|
36
33
|
_DEFAULT_FIELDS = ["id", "name", "type", "contact_type", "assigned_to", "status"]
|
|
37
34
|
|
|
@@ -14,16 +14,26 @@ from wealthbox_tools.models import (
|
|
|
14
14
|
TaskResourceType,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
from ._util import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
from ._util import (
|
|
18
|
+
COMMENT_RESOURCE_TYPES,
|
|
19
|
+
OutputFormat,
|
|
20
|
+
build_linked_to,
|
|
21
|
+
handle_errors,
|
|
22
|
+
make_category_command,
|
|
23
|
+
make_resource_app,
|
|
24
|
+
output_get_result,
|
|
25
|
+
output_result,
|
|
26
|
+
run_client,
|
|
27
|
+
run_client_with_comments,
|
|
23
28
|
)
|
|
29
|
+
|
|
30
|
+
app = make_resource_app(help="Manage Wealthbox events.")
|
|
24
31
|
app.command("categories", help="List event category options.")(make_category_command(CategoryType.EVENT_CATEGORIES))
|
|
25
32
|
|
|
26
|
-
_DEFAULT_FIELDS = [
|
|
33
|
+
_DEFAULT_FIELDS = [
|
|
34
|
+
"id", "title", "starts_at", "ends_at", "state", "event_category",
|
|
35
|
+
"comments", "comment_count", "latest_comment",
|
|
36
|
+
]
|
|
27
37
|
|
|
28
38
|
|
|
29
39
|
@app.command("list", help="List events with optional filters.")
|
|
@@ -71,11 +81,16 @@ def list_events(
|
|
|
71
81
|
@handle_errors
|
|
72
82
|
def get_event(
|
|
73
83
|
event_id: int = typer.Argument(..., help="Event ID"),
|
|
84
|
+
no_comments: bool = typer.Option(False, "--no-comments", help="Omit comments from output"),
|
|
74
85
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show all fields"),
|
|
75
86
|
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
76
87
|
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
77
88
|
) -> None:
|
|
78
|
-
|
|
89
|
+
result = run_client_with_comments(
|
|
90
|
+
token, lambda c: c.get_event(event_id),
|
|
91
|
+
COMMENT_RESOURCE_TYPES["events"], event_id, include_comments=not no_comments,
|
|
92
|
+
)
|
|
93
|
+
output_get_result(result, fmt, fields=None if verbose else _DEFAULT_FIELDS)
|
|
79
94
|
|
|
80
95
|
|
|
81
96
|
@app.command("add", help="Create a new event.")
|
|
@@ -6,7 +6,6 @@ import typer
|
|
|
6
6
|
|
|
7
7
|
from .activity import app as activity_app
|
|
8
8
|
from .categories import app as categories_app
|
|
9
|
-
from .comments import app as comments_app
|
|
10
9
|
from .config import app as config_app
|
|
11
10
|
from .contacts import app as contacts_app
|
|
12
11
|
from .events import app as events_app
|
|
@@ -38,7 +37,6 @@ def _main(
|
|
|
38
37
|
app.add_typer(activity_app, name="activity")
|
|
39
38
|
app.add_typer(categories_app, name="categories")
|
|
40
39
|
app.add_typer(config_app, name="config")
|
|
41
|
-
app.add_typer(comments_app, name="comments")
|
|
42
40
|
app.add_typer(contacts_app, name="contacts")
|
|
43
41
|
app.add_typer(events_app, name="events")
|
|
44
42
|
app.add_typer(households_app, name="households")
|
|
@@ -6,15 +6,25 @@ import typer
|
|
|
6
6
|
|
|
7
7
|
from wealthbox_tools.models import NoteCreateInput, NoteListQuery, NoteResourceType, NotesOrder, NoteUpdateInput
|
|
8
8
|
|
|
9
|
-
from ._util import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
from ._util import (
|
|
10
|
+
COMMENT_RESOURCE_TYPES,
|
|
11
|
+
OutputFormat,
|
|
12
|
+
build_linked_to,
|
|
13
|
+
handle_errors,
|
|
14
|
+
make_resource_app,
|
|
15
|
+
output_get_result,
|
|
16
|
+
output_result,
|
|
17
|
+
run_client,
|
|
18
|
+
run_client_with_comments,
|
|
19
|
+
truncate_field,
|
|
15
20
|
)
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
app = make_resource_app(help="Manage Wealthbox notes.")
|
|
23
|
+
|
|
24
|
+
_DEFAULT_FIELDS = [
|
|
25
|
+
"id", "content", "linked_to", "creator_id", "updated_at",
|
|
26
|
+
"comments", "comment_count", "latest_comment",
|
|
27
|
+
]
|
|
18
28
|
_CONTENT_PREVIEW_LEN = 500
|
|
19
29
|
|
|
20
30
|
|
|
@@ -51,11 +61,16 @@ def list_notes(
|
|
|
51
61
|
@handle_errors
|
|
52
62
|
def get_note(
|
|
53
63
|
note_id: int = typer.Argument(..., help="Note ID"),
|
|
64
|
+
no_comments: bool = typer.Option(False, "--no-comments", help="Omit comments from output"),
|
|
54
65
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show all fields"),
|
|
55
66
|
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
56
67
|
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
57
68
|
) -> None:
|
|
58
|
-
|
|
69
|
+
result = run_client_with_comments(
|
|
70
|
+
token, lambda c: c.get_note(note_id),
|
|
71
|
+
COMMENT_RESOURCE_TYPES["notes"], note_id, include_comments=not no_comments,
|
|
72
|
+
)
|
|
73
|
+
output_get_result(result, fmt, fields=None if verbose else _DEFAULT_FIELDS)
|
|
59
74
|
|
|
60
75
|
|
|
61
76
|
@app.command("add", help="Create a new note.")
|
|
@@ -13,15 +13,26 @@ from wealthbox_tools.models import (
|
|
|
13
13
|
OpportunityUpdateInput,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
from ._util import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
from ._util import (
|
|
17
|
+
COMMENT_RESOURCE_TYPES,
|
|
18
|
+
OutputFormat,
|
|
19
|
+
build_linked_to,
|
|
20
|
+
handle_errors,
|
|
21
|
+
make_resource_app,
|
|
22
|
+
output_get_result,
|
|
23
|
+
output_result,
|
|
24
|
+
parse_more_fields,
|
|
25
|
+
run_client,
|
|
26
|
+
run_client_with_comments,
|
|
22
27
|
)
|
|
23
28
|
|
|
29
|
+
app = make_resource_app(help="Manage Wealthbox opportunities.")
|
|
30
|
+
|
|
24
31
|
_DEFAULT_FIELDS = ["id", "name", "stage", "probability", "target_close", "manager", "linked_to"]
|
|
32
|
+
_MORE_FIELDS_RESERVED = {
|
|
33
|
+
"name", "target_close", "probability", "stage", "description",
|
|
34
|
+
"manager", "visible_to", "linked_to", "amounts",
|
|
35
|
+
}
|
|
25
36
|
|
|
26
37
|
|
|
27
38
|
def _build_amounts(
|
|
@@ -83,10 +94,15 @@ def list_opportunities(
|
|
|
83
94
|
@handle_errors
|
|
84
95
|
def get_opportunity(
|
|
85
96
|
opportunity_id: int = typer.Argument(..., help="Opportunity ID"),
|
|
97
|
+
no_comments: bool = typer.Option(False, "--no-comments", help="Omit comments from output"),
|
|
86
98
|
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
87
99
|
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
88
100
|
) -> None:
|
|
89
|
-
|
|
101
|
+
result = run_client_with_comments(
|
|
102
|
+
token, lambda c: c.get_opportunity(opportunity_id),
|
|
103
|
+
COMMENT_RESOURCE_TYPES["opportunities"], opportunity_id, include_comments=not no_comments,
|
|
104
|
+
)
|
|
105
|
+
output_get_result(result, fmt)
|
|
90
106
|
|
|
91
107
|
|
|
92
108
|
@app.command("add", help="Create a new opportunity.")
|
|
@@ -125,11 +141,7 @@ def add_opportunity(
|
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
if more_fields:
|
|
128
|
-
|
|
129
|
-
"name", "target_close", "probability", "stage", "description",
|
|
130
|
-
"manager", "visible_to", "linked_to", "amounts",
|
|
131
|
-
}
|
|
132
|
-
payload.update(parse_more_fields(more_fields, _reserved))
|
|
144
|
+
payload.update(parse_more_fields(more_fields, _MORE_FIELDS_RESERVED))
|
|
133
145
|
|
|
134
146
|
input_model = OpportunityCreateInput(**{k: v for k, v in payload.items() if v is not None})
|
|
135
147
|
output_result(run_client(token, lambda c: c.create_opportunity(input_model)), fmt)
|
|
@@ -178,11 +190,7 @@ def update_opportunity(
|
|
|
178
190
|
payload["amounts"] = amounts
|
|
179
191
|
|
|
180
192
|
if more_fields:
|
|
181
|
-
|
|
182
|
-
"name", "target_close", "probability", "stage", "description",
|
|
183
|
-
"manager", "visible_to", "linked_to", "amounts",
|
|
184
|
-
}
|
|
185
|
-
payload.update(parse_more_fields(more_fields, _update_reserved))
|
|
193
|
+
payload.update(parse_more_fields(more_fields, _MORE_FIELDS_RESERVED))
|
|
186
194
|
|
|
187
195
|
input_model = OpportunityUpdateInput(**payload)
|
|
188
196
|
output_result(run_client(token, lambda c: c.update_opportunity(opportunity_id, input_model)), fmt)
|
|
@@ -6,14 +6,20 @@ import typer
|
|
|
6
6
|
|
|
7
7
|
from wealthbox_tools.models import ProjectCreateInput, ProjectListQuery, ProjectUpdateInput
|
|
8
8
|
|
|
9
|
-
from ._util import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
from ._util import (
|
|
10
|
+
COMMENT_RESOURCE_TYPES,
|
|
11
|
+
OutputFormat,
|
|
12
|
+
handle_errors,
|
|
13
|
+
make_resource_app,
|
|
14
|
+
output_get_result,
|
|
15
|
+
output_result,
|
|
16
|
+
parse_more_fields,
|
|
17
|
+
run_client,
|
|
18
|
+
run_client_with_comments,
|
|
15
19
|
)
|
|
16
20
|
|
|
21
|
+
app = make_resource_app(help="Manage Wealthbox projects.")
|
|
22
|
+
|
|
17
23
|
_DEFAULT_FIELDS = ["id", "name", "description", "organizer", "updated_at"]
|
|
18
24
|
|
|
19
25
|
|
|
@@ -41,10 +47,15 @@ def list_projects(
|
|
|
41
47
|
@handle_errors
|
|
42
48
|
def get_project(
|
|
43
49
|
project_id: int = typer.Argument(..., help="Project ID"),
|
|
50
|
+
no_comments: bool = typer.Option(False, "--no-comments", help="Omit comments from output"),
|
|
44
51
|
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
45
52
|
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
46
53
|
) -> None:
|
|
47
|
-
|
|
54
|
+
result = run_client_with_comments(
|
|
55
|
+
token, lambda c: c.get_project(project_id),
|
|
56
|
+
COMMENT_RESOURCE_TYPES["projects"], project_id, include_comments=not no_comments,
|
|
57
|
+
)
|
|
58
|
+
output_get_result(result, fmt)
|
|
48
59
|
|
|
49
60
|
|
|
50
61
|
@app.command("add", help="Create a new project.")
|
|
@@ -15,24 +15,37 @@ from wealthbox_tools.models import (
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
from ._util import (
|
|
18
|
+
COMMENT_RESOURCE_TYPES,
|
|
18
19
|
OutputFormat,
|
|
19
20
|
build_linked_to,
|
|
20
21
|
build_resource_filter,
|
|
22
|
+
clean_comments,
|
|
21
23
|
handle_errors,
|
|
22
24
|
make_category_command,
|
|
25
|
+
make_resource_app,
|
|
23
26
|
output_result,
|
|
24
27
|
parse_more_fields,
|
|
25
28
|
run_client,
|
|
29
|
+
run_client_with_comments,
|
|
30
|
+
slim_comments,
|
|
31
|
+
summarize_comments,
|
|
26
32
|
)
|
|
27
33
|
|
|
28
|
-
app =
|
|
29
|
-
context_settings={"help_option_names": ["-h", "--help"]},
|
|
30
|
-
help="Manage Wealthbox tasks.",
|
|
31
|
-
no_args_is_help=True,
|
|
32
|
-
)
|
|
34
|
+
app = make_resource_app(help="Manage Wealthbox tasks.")
|
|
33
35
|
app.command("categories", help="List task category options.")(make_category_command(CategoryType.TASK_CATEGORIES))
|
|
34
36
|
|
|
35
37
|
_DEFAULT_FIELDS = ["id", "name", "due_date", "frame", "complete", "category"]
|
|
38
|
+
_GET_FIELDS = [
|
|
39
|
+
"id", "name", "description", "due_date", "created_at", "complete",
|
|
40
|
+
"priority", "assigned_to", "category", "linked_to",
|
|
41
|
+
"comment_count", "latest_comment",
|
|
42
|
+
]
|
|
43
|
+
_GET_JSON_FIELDS = [
|
|
44
|
+
"id", "name", "description", "due_date", "created_at", "updated_at",
|
|
45
|
+
"frame", "complete", "repeats", "priority",
|
|
46
|
+
"assigned_to", "assigned_to_team", "creator", "completer",
|
|
47
|
+
"category", "linked_to", "comments",
|
|
48
|
+
]
|
|
36
49
|
|
|
37
50
|
_TASK_CREATE_RESERVED = {"name", "due_date", "frame", "priority", "assigned_to", "linked_to"}
|
|
38
51
|
|
|
@@ -87,10 +100,25 @@ def list_tasks(
|
|
|
87
100
|
@handle_errors
|
|
88
101
|
def get_task(
|
|
89
102
|
task_id: int = typer.Argument(..., help="Task ID"),
|
|
103
|
+
no_comments: bool = typer.Option(False, "--no-comments", help="Omit comments from output"),
|
|
104
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show all fields"),
|
|
90
105
|
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
91
106
|
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
92
107
|
) -> None:
|
|
93
|
-
|
|
108
|
+
result = run_client_with_comments(
|
|
109
|
+
token, lambda c: c.get_task(task_id),
|
|
110
|
+
COMMENT_RESOURCE_TYPES["tasks"], task_id, include_comments=not no_comments,
|
|
111
|
+
)
|
|
112
|
+
result = clean_comments(result)
|
|
113
|
+
if not verbose:
|
|
114
|
+
result = {k: result[k] for k in _GET_JSON_FIELDS if k in result}
|
|
115
|
+
result = slim_comments(result)
|
|
116
|
+
if fmt != OutputFormat.JSON:
|
|
117
|
+
result = summarize_comments(result)
|
|
118
|
+
desc = result.get("description", "")
|
|
119
|
+
if isinstance(desc, str) and len(desc) > 50:
|
|
120
|
+
result = {**result, "description": desc[:50] + "..."}
|
|
121
|
+
output_result(result, fmt, fields=None if (verbose or fmt == OutputFormat.JSON) else _GET_FIELDS)
|
|
94
122
|
|
|
95
123
|
|
|
96
124
|
@app.command("add", help="Create a new task. Required: name, and either due_date or frame.")
|
|
@@ -13,14 +13,21 @@ from wealthbox_tools.models import (
|
|
|
13
13
|
WorkflowTemplateListQuery,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
from ._util import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
from ._util import (
|
|
17
|
+
COMMENT_RESOURCE_TYPES,
|
|
18
|
+
OutputFormat,
|
|
19
|
+
build_linked_to,
|
|
20
|
+
handle_errors,
|
|
21
|
+
make_resource_app,
|
|
22
|
+
output_get_result,
|
|
23
|
+
output_result,
|
|
24
|
+
parse_more_fields,
|
|
25
|
+
run_client,
|
|
26
|
+
run_client_with_comments,
|
|
22
27
|
)
|
|
23
28
|
|
|
29
|
+
app = make_resource_app(help="Manage Wealthbox workflows.")
|
|
30
|
+
|
|
24
31
|
templates_app = typer.Typer(
|
|
25
32
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
26
33
|
help="List workflow templates.",
|
|
@@ -68,10 +75,15 @@ def list_workflows(
|
|
|
68
75
|
@handle_errors
|
|
69
76
|
def get_workflow(
|
|
70
77
|
workflow_id: int = typer.Argument(..., help="Workflow ID"),
|
|
78
|
+
no_comments: bool = typer.Option(False, "--no-comments", help="Omit comments from output"),
|
|
71
79
|
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
72
80
|
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
73
81
|
) -> None:
|
|
74
|
-
|
|
82
|
+
result = run_client_with_comments(
|
|
83
|
+
token, lambda c: c.get_workflow(workflow_id),
|
|
84
|
+
COMMENT_RESOURCE_TYPES["workflows"], workflow_id, include_comments=not no_comments,
|
|
85
|
+
)
|
|
86
|
+
output_get_result(result, fmt)
|
|
75
87
|
|
|
76
88
|
|
|
77
89
|
@app.command("add", help="Create a new workflow from a template.")
|
|
@@ -12,3 +12,11 @@ class CommentsMixin:
|
|
|
12
12
|
params = query.model_dump(exclude_none=True) if query else None
|
|
13
13
|
resp = await self._request("GET", "/comments", params=params) # type: ignore[attr-defined]
|
|
14
14
|
return resp.json()
|
|
15
|
+
|
|
16
|
+
async def get_comments_for_resource(
|
|
17
|
+
self, resource_type: str, resource_id: int
|
|
18
|
+
) -> list[dict[str, Any]]:
|
|
19
|
+
"""Fetch all comments for a specific resource."""
|
|
20
|
+
params = {"resource_type": resource_type, "resource_id": resource_id}
|
|
21
|
+
resp = await self._request("GET", "/comments", params=params) # type: ignore[attr-defined]
|
|
22
|
+
return resp.json().get("comments", [])
|
|
@@ -148,9 +148,16 @@ class NoteResourceType(StrEnum):
|
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
class CommentResourceType(StrEnum):
|
|
151
|
-
CONTACT = "Contact"
|
|
152
151
|
TASK = "Task"
|
|
153
152
|
EVENT = "Event"
|
|
153
|
+
STATUS_UPDATES = "StatusUpdates" # Notes
|
|
154
|
+
# CONTACT = "Contact"
|
|
155
|
+
OPPORTUNITY = "Opportunity"
|
|
156
|
+
PROJECT = "Project"
|
|
157
|
+
WORKFLOW = "Workflow"
|
|
158
|
+
WORKFLOW_STEP = "WorkflowStep"
|
|
159
|
+
SELECTED_OUTCOME = "SelectedOutcome"
|
|
160
|
+
MEETING = "Meeting"
|
|
154
161
|
|
|
155
162
|
|
|
156
163
|
class OpportunityResourceType(StrEnum):
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import typer
|
|
4
|
-
|
|
5
|
-
from wealthbox_tools.models import CommentListQuery, CommentResourceType
|
|
6
|
-
|
|
7
|
-
from ._util import OutputFormat, handle_errors, output_result, run_client, truncate_nested_field
|
|
8
|
-
|
|
9
|
-
app = typer.Typer(
|
|
10
|
-
context_settings={"help_option_names": ["-h", "--help"]},
|
|
11
|
-
help="Retrieve Wealthbox comments.",
|
|
12
|
-
no_args_is_help=True,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
_DEFAULT_FIELDS = ["id", "creator", "resource_type", "resource_id", "created_at", "updated_at", "body"]
|
|
16
|
-
_BODY_PREVIEW_LEN = 500
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@app.command("list", help="List comments. Filter by resource ID/type and/or updated date range.")
|
|
20
|
-
@handle_errors
|
|
21
|
-
def list_comments(
|
|
22
|
-
resource_id: int | None = typer.Option(
|
|
23
|
-
None, "--resource-id", help="Filter by resource ID (requires --resource-type)"
|
|
24
|
-
),
|
|
25
|
-
resource_type: CommentResourceType | None = typer.Option(
|
|
26
|
-
None, "--resource-type", help="Filter by resource type: Contact, Task, Event"
|
|
27
|
-
),
|
|
28
|
-
updated_since: str | None = typer.Option(
|
|
29
|
-
None, "--updated-since", help="Only comments updated on or after this timestamp"
|
|
30
|
-
),
|
|
31
|
-
updated_before: str | None = typer.Option(
|
|
32
|
-
None, "--updated-before", help="Only comments updated on or before this timestamp"
|
|
33
|
-
),
|
|
34
|
-
page: int | None = typer.Option(None),
|
|
35
|
-
per_page: int | None = typer.Option(None, "--per-page", help="Results per page (max 100)"),
|
|
36
|
-
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show all fields"),
|
|
37
|
-
token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
|
|
38
|
-
fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
|
|
39
|
-
) -> None:
|
|
40
|
-
query = CommentListQuery(
|
|
41
|
-
resource_id=resource_id,
|
|
42
|
-
resource_type=resource_type,
|
|
43
|
-
updated_since=updated_since,
|
|
44
|
-
updated_before=updated_before,
|
|
45
|
-
page=page,
|
|
46
|
-
per_page=per_page,
|
|
47
|
-
)
|
|
48
|
-
result = run_client(token, lambda c: c.list_comments(query))
|
|
49
|
-
if not verbose:
|
|
50
|
-
result = truncate_nested_field(result, "body", ["text", "html"], _BODY_PREVIEW_LEN)
|
|
51
|
-
output_result(result, fmt, fields=None if verbose else _DEFAULT_FIELDS)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|