affinity-sdk 0.9.5__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.
- affinity/__init__.py +139 -0
- affinity/cli/__init__.py +7 -0
- affinity/cli/click_compat.py +27 -0
- affinity/cli/commands/__init__.py +1 -0
- affinity/cli/commands/_entity_files_dump.py +219 -0
- affinity/cli/commands/_list_entry_fields.py +41 -0
- affinity/cli/commands/_v1_parsing.py +77 -0
- affinity/cli/commands/company_cmds.py +2139 -0
- affinity/cli/commands/completion_cmd.py +33 -0
- affinity/cli/commands/config_cmds.py +540 -0
- affinity/cli/commands/entry_cmds.py +33 -0
- affinity/cli/commands/field_cmds.py +413 -0
- affinity/cli/commands/interaction_cmds.py +875 -0
- affinity/cli/commands/list_cmds.py +3152 -0
- affinity/cli/commands/note_cmds.py +433 -0
- affinity/cli/commands/opportunity_cmds.py +1174 -0
- affinity/cli/commands/person_cmds.py +1980 -0
- affinity/cli/commands/query_cmd.py +444 -0
- affinity/cli/commands/relationship_strength_cmds.py +62 -0
- affinity/cli/commands/reminder_cmds.py +595 -0
- affinity/cli/commands/resolve_url_cmd.py +127 -0
- affinity/cli/commands/session_cmds.py +84 -0
- affinity/cli/commands/task_cmds.py +110 -0
- affinity/cli/commands/version_cmd.py +29 -0
- affinity/cli/commands/whoami_cmd.py +36 -0
- affinity/cli/config.py +108 -0
- affinity/cli/context.py +749 -0
- affinity/cli/csv_utils.py +195 -0
- affinity/cli/date_utils.py +42 -0
- affinity/cli/decorators.py +77 -0
- affinity/cli/errors.py +28 -0
- affinity/cli/field_utils.py +355 -0
- affinity/cli/formatters.py +551 -0
- affinity/cli/help_json.py +283 -0
- affinity/cli/logging.py +100 -0
- affinity/cli/main.py +261 -0
- affinity/cli/options.py +53 -0
- affinity/cli/paths.py +32 -0
- affinity/cli/progress.py +183 -0
- affinity/cli/query/__init__.py +163 -0
- affinity/cli/query/aggregates.py +357 -0
- affinity/cli/query/dates.py +194 -0
- affinity/cli/query/exceptions.py +147 -0
- affinity/cli/query/executor.py +1236 -0
- affinity/cli/query/filters.py +248 -0
- affinity/cli/query/models.py +333 -0
- affinity/cli/query/output.py +331 -0
- affinity/cli/query/parser.py +619 -0
- affinity/cli/query/planner.py +430 -0
- affinity/cli/query/progress.py +270 -0
- affinity/cli/query/schema.py +439 -0
- affinity/cli/render.py +1589 -0
- affinity/cli/resolve.py +222 -0
- affinity/cli/resolvers.py +249 -0
- affinity/cli/results.py +308 -0
- affinity/cli/runner.py +218 -0
- affinity/cli/serialization.py +65 -0
- affinity/cli/session_cache.py +276 -0
- affinity/cli/types.py +70 -0
- affinity/client.py +771 -0
- affinity/clients/__init__.py +19 -0
- affinity/clients/http.py +3664 -0
- affinity/clients/pipeline.py +165 -0
- affinity/compare.py +501 -0
- affinity/downloads.py +114 -0
- affinity/exceptions.py +615 -0
- affinity/filters.py +1128 -0
- affinity/hooks.py +198 -0
- affinity/inbound_webhooks.py +302 -0
- affinity/models/__init__.py +163 -0
- affinity/models/entities.py +798 -0
- affinity/models/pagination.py +513 -0
- affinity/models/rate_limit_snapshot.py +48 -0
- affinity/models/secondary.py +413 -0
- affinity/models/types.py +663 -0
- affinity/policies.py +40 -0
- affinity/progress.py +22 -0
- affinity/py.typed +0 -0
- affinity/services/__init__.py +42 -0
- affinity/services/companies.py +1286 -0
- affinity/services/lists.py +1892 -0
- affinity/services/opportunities.py +1330 -0
- affinity/services/persons.py +1348 -0
- affinity/services/rate_limits.py +173 -0
- affinity/services/tasks.py +193 -0
- affinity/services/v1_only.py +2445 -0
- affinity/types.py +83 -0
- affinity_sdk-0.9.5.dist-info/METADATA +622 -0
- affinity_sdk-0.9.5.dist-info/RECORD +92 -0
- affinity_sdk-0.9.5.dist-info/WHEEL +4 -0
- affinity_sdk-0.9.5.dist-info/entry_points.txt +2 -0
- affinity_sdk-0.9.5.dist-info/licenses/LICENSE +21 -0
affinity/cli/resolve.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from affinity import Affinity
|
|
7
|
+
from affinity.exceptions import NotFoundError
|
|
8
|
+
from affinity.models.entities import AffinityList, FieldMetadata, SavedView
|
|
9
|
+
from affinity.types import ListId, SavedViewId
|
|
10
|
+
|
|
11
|
+
from .errors import CLIError
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .session_cache import SessionCache
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class ResolvedList:
|
|
19
|
+
list: AffinityList
|
|
20
|
+
resolved: dict[str, Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _looks_int(value: str) -> bool:
|
|
24
|
+
return value.isdigit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_list_selector(
|
|
28
|
+
*,
|
|
29
|
+
client: Affinity,
|
|
30
|
+
selector: str,
|
|
31
|
+
cache: SessionCache | None = None,
|
|
32
|
+
) -> ResolvedList:
|
|
33
|
+
"""Resolve list by name/ID with optional session cache support."""
|
|
34
|
+
selector = selector.strip()
|
|
35
|
+
|
|
36
|
+
# ID lookups don't benefit from name resolution cache
|
|
37
|
+
if _looks_int(selector):
|
|
38
|
+
list_id = ListId(int(selector))
|
|
39
|
+
lst = client.lists.get(list_id)
|
|
40
|
+
return ResolvedList(list=lst, resolved={"list": {"input": selector, "listId": int(lst.id)}})
|
|
41
|
+
|
|
42
|
+
cache_key = f"list_resolve_{selector.lower()}_any"
|
|
43
|
+
|
|
44
|
+
if cache and cache.enabled:
|
|
45
|
+
cached = cache.get(cache_key, AffinityList)
|
|
46
|
+
if cached is not None:
|
|
47
|
+
return ResolvedList(
|
|
48
|
+
list=cached,
|
|
49
|
+
resolved={"list": {"input": selector, "listId": int(cached.id), "cached": True}},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
matches = client.lists.resolve_all(name=selector)
|
|
53
|
+
if not matches:
|
|
54
|
+
raise CLIError(
|
|
55
|
+
f'List not found: "{selector}"',
|
|
56
|
+
exit_code=4,
|
|
57
|
+
error_type="not_found",
|
|
58
|
+
details={"selector": selector},
|
|
59
|
+
)
|
|
60
|
+
if len(matches) > 1:
|
|
61
|
+
raise CLIError(
|
|
62
|
+
f'Ambiguous list name: "{selector}" ({len(matches)} matches)',
|
|
63
|
+
exit_code=2,
|
|
64
|
+
error_type="ambiguous_resolution",
|
|
65
|
+
details={
|
|
66
|
+
"selector": selector,
|
|
67
|
+
"matches": [
|
|
68
|
+
{"listId": int(m.id), "name": m.name, "type": m.type} for m in matches[:20]
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
lst = matches[0]
|
|
74
|
+
|
|
75
|
+
if cache and cache.enabled:
|
|
76
|
+
cache.set(cache_key, lst)
|
|
77
|
+
|
|
78
|
+
return ResolvedList(list=lst, resolved={"list": {"input": selector, "listId": int(lst.id)}})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def resolve_saved_view(
|
|
82
|
+
*,
|
|
83
|
+
client: Affinity,
|
|
84
|
+
list_id: ListId,
|
|
85
|
+
selector: str,
|
|
86
|
+
cache: SessionCache | None = None,
|
|
87
|
+
) -> tuple[SavedView, dict[str, Any]]:
|
|
88
|
+
selector = selector.strip()
|
|
89
|
+
if _looks_int(selector):
|
|
90
|
+
view_id = SavedViewId(int(selector))
|
|
91
|
+
try:
|
|
92
|
+
v = client.lists.get_saved_view(list_id, view_id)
|
|
93
|
+
except NotFoundError as exc:
|
|
94
|
+
raise CLIError(
|
|
95
|
+
f"Saved view not found: {selector}",
|
|
96
|
+
exit_code=4,
|
|
97
|
+
error_type="not_found",
|
|
98
|
+
details={"listId": int(list_id), "selector": selector},
|
|
99
|
+
) from exc
|
|
100
|
+
return v, {
|
|
101
|
+
"savedView": {
|
|
102
|
+
"input": selector,
|
|
103
|
+
"savedViewId": int(v.id),
|
|
104
|
+
"name": v.name,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
views = list_all_saved_views(client=client, list_id=list_id, cache=cache)
|
|
109
|
+
exact = [v for v in views if v.name.lower() == selector.lower()]
|
|
110
|
+
if not exact:
|
|
111
|
+
raise CLIError(
|
|
112
|
+
f'Saved view not found: "{selector}"',
|
|
113
|
+
exit_code=4,
|
|
114
|
+
error_type="not_found",
|
|
115
|
+
details={"listId": int(list_id), "selector": selector},
|
|
116
|
+
)
|
|
117
|
+
if len(exact) > 1:
|
|
118
|
+
raise CLIError(
|
|
119
|
+
f'Ambiguous saved view name: "{selector}"',
|
|
120
|
+
exit_code=2,
|
|
121
|
+
error_type="ambiguous_resolution",
|
|
122
|
+
details={
|
|
123
|
+
"listId": int(list_id),
|
|
124
|
+
"selector": selector,
|
|
125
|
+
"matches": [{"savedViewId": int(v.id), "name": v.name} for v in exact[:20]],
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
v = exact[0]
|
|
129
|
+
return v, {"savedView": {"input": selector, "savedViewId": int(v.id), "name": v.name}}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def list_all_saved_views(
|
|
133
|
+
*,
|
|
134
|
+
client: Affinity,
|
|
135
|
+
list_id: ListId,
|
|
136
|
+
cache: SessionCache | None = None,
|
|
137
|
+
) -> list[SavedView]:
|
|
138
|
+
"""Get saved views for a list with optional session cache support.
|
|
139
|
+
|
|
140
|
+
Note: This caches the full list of saved views. If the fetch is
|
|
141
|
+
interrupted mid-pagination, the cache won't be populated.
|
|
142
|
+
"""
|
|
143
|
+
cache_key = f"saved_views_{list_id}"
|
|
144
|
+
|
|
145
|
+
if cache and cache.enabled:
|
|
146
|
+
cached = cache.get_list(cache_key, SavedView)
|
|
147
|
+
if cached is not None:
|
|
148
|
+
return cached
|
|
149
|
+
|
|
150
|
+
# Eagerly evaluate paginated iterator to cache complete list
|
|
151
|
+
# Note: If pagination is interrupted (e.g., network error), no partial
|
|
152
|
+
# result is cached - the next call will retry from scratch
|
|
153
|
+
views = list(client.lists.saved_views_all(list_id))
|
|
154
|
+
|
|
155
|
+
if cache and cache.enabled:
|
|
156
|
+
cache.set(cache_key, views)
|
|
157
|
+
|
|
158
|
+
return views
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def list_fields_for_list(
|
|
162
|
+
*,
|
|
163
|
+
client: Affinity,
|
|
164
|
+
list_id: ListId,
|
|
165
|
+
cache: SessionCache | None = None,
|
|
166
|
+
) -> list[FieldMetadata]:
|
|
167
|
+
"""Get list fields with optional session cache support."""
|
|
168
|
+
cache_key = f"list_fields_{list_id}"
|
|
169
|
+
|
|
170
|
+
if cache and cache.enabled:
|
|
171
|
+
cached = cache.get_list(cache_key, FieldMetadata)
|
|
172
|
+
if cached is not None:
|
|
173
|
+
return cached
|
|
174
|
+
|
|
175
|
+
fields = client.lists.get_fields(list_id)
|
|
176
|
+
|
|
177
|
+
if cache and cache.enabled:
|
|
178
|
+
cache.set(cache_key, fields)
|
|
179
|
+
|
|
180
|
+
return fields
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_person_fields(
|
|
184
|
+
*,
|
|
185
|
+
client: Affinity,
|
|
186
|
+
cache: SessionCache | None = None,
|
|
187
|
+
) -> list[FieldMetadata]:
|
|
188
|
+
"""Get global person fields with optional session cache support."""
|
|
189
|
+
cache_key = "person_fields_global"
|
|
190
|
+
|
|
191
|
+
if cache and cache.enabled:
|
|
192
|
+
cached = cache.get_list(cache_key, FieldMetadata)
|
|
193
|
+
if cached is not None:
|
|
194
|
+
return cached
|
|
195
|
+
|
|
196
|
+
fields = client.persons.get_fields()
|
|
197
|
+
|
|
198
|
+
if cache and cache.enabled:
|
|
199
|
+
cache.set(cache_key, fields)
|
|
200
|
+
|
|
201
|
+
return fields
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_company_fields(
|
|
205
|
+
*,
|
|
206
|
+
client: Affinity,
|
|
207
|
+
cache: SessionCache | None = None,
|
|
208
|
+
) -> list[FieldMetadata]:
|
|
209
|
+
"""Get global company fields with optional session cache support."""
|
|
210
|
+
cache_key = "company_fields_global"
|
|
211
|
+
|
|
212
|
+
if cache and cache.enabled:
|
|
213
|
+
cached = cache.get_list(cache_key, FieldMetadata)
|
|
214
|
+
if cached is not None:
|
|
215
|
+
return cached
|
|
216
|
+
|
|
217
|
+
fields = client.companies.get_fields()
|
|
218
|
+
|
|
219
|
+
if cache and cache.enabled:
|
|
220
|
+
cache.set(cache_key, fields)
|
|
221
|
+
|
|
222
|
+
return fields
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standard resolver types for CLI commands.
|
|
3
|
+
|
|
4
|
+
These dataclasses provide consistent structure for resolved metadata,
|
|
5
|
+
ensuring all CLI commands follow the same patterns when resolving
|
|
6
|
+
user inputs (IDs, URLs, emails, names, etc.) to API parameters.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import asdict, dataclass
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class ResolvedEntity:
|
|
17
|
+
"""
|
|
18
|
+
Standard structure for entity resolution metadata.
|
|
19
|
+
|
|
20
|
+
This class represents how a user's input selector (ID, URL, email, name)
|
|
21
|
+
was resolved to an entity ID for API calls.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
input: The original user input (e.g., "john@example.com", "123", "acme.com")
|
|
25
|
+
entity_id: The resolved numeric entity ID
|
|
26
|
+
entity_type: Type of entity (person, company, opportunity, list)
|
|
27
|
+
source: How the input was resolved (id, url, email, name, domain)
|
|
28
|
+
canonical_url: Optional canonical Affinity URL for the entity
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> resolved = ResolvedEntity(
|
|
32
|
+
... input="john@example.com",
|
|
33
|
+
... entity_id=12345,
|
|
34
|
+
... entity_type="person",
|
|
35
|
+
... source="email",
|
|
36
|
+
... canonical_url="https://app.affinity.co/persons/12345"
|
|
37
|
+
... )
|
|
38
|
+
>>> resolved.to_dict()
|
|
39
|
+
{
|
|
40
|
+
'input': 'john@example.com',
|
|
41
|
+
'personId': 12345,
|
|
42
|
+
'source': 'email',
|
|
43
|
+
'canonicalUrl': 'https://app.affinity.co/persons/12345'
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
input: str
|
|
48
|
+
entity_id: int
|
|
49
|
+
entity_type: Literal["person", "company", "opportunity", "list"]
|
|
50
|
+
source: Literal["id", "url", "email", "name", "domain"]
|
|
51
|
+
canonical_url: str | None = None
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Convert to dict for resolved metadata.
|
|
56
|
+
|
|
57
|
+
Returns a dictionary suitable for inclusion in CommandOutput.resolved,
|
|
58
|
+
with entity_id renamed to {entityType}Id (e.g., personId, companyId).
|
|
59
|
+
"""
|
|
60
|
+
data = asdict(self)
|
|
61
|
+
# Rename entity_id to {entityType}Id
|
|
62
|
+
entity_id = data.pop("entity_id")
|
|
63
|
+
data[f"{self.entity_type}Id"] = entity_id
|
|
64
|
+
# Remove entity_type from output (it's redundant with the key name)
|
|
65
|
+
data.pop("entity_type", None)
|
|
66
|
+
# Convert snake_case to camelCase for canonical_url
|
|
67
|
+
if "canonical_url" in data and data["canonical_url"] is not None:
|
|
68
|
+
data["canonicalUrl"] = data.pop("canonical_url")
|
|
69
|
+
elif "canonical_url" in data:
|
|
70
|
+
data.pop("canonical_url")
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class ResolvedFieldSelection:
|
|
76
|
+
"""
|
|
77
|
+
Field selection resolution metadata.
|
|
78
|
+
|
|
79
|
+
This class represents which fields were requested by the user
|
|
80
|
+
through --field-id or --field-type flags.
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
field_ids: List of specific field IDs requested (e.g., ["field-123", "field-456"])
|
|
84
|
+
field_types: List of field types requested (e.g., ["global", "enriched"])
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> resolved = ResolvedFieldSelection(
|
|
88
|
+
... field_ids=["field-123"],
|
|
89
|
+
... field_types=["global", "enriched"]
|
|
90
|
+
... )
|
|
91
|
+
>>> resolved.to_dict()
|
|
92
|
+
{'fieldIds': ['field-123'], 'fieldTypes': ['global', 'enriched']}
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
field_ids: list[str] | None = None
|
|
96
|
+
field_types: list[str] | None = None
|
|
97
|
+
|
|
98
|
+
def to_dict(self) -> dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Convert to dict, excluding None values.
|
|
101
|
+
|
|
102
|
+
Returns a dictionary with camelCase field names, omitting any
|
|
103
|
+
fields that are None.
|
|
104
|
+
"""
|
|
105
|
+
result = {}
|
|
106
|
+
if self.field_ids is not None:
|
|
107
|
+
result["fieldIds"] = self.field_ids
|
|
108
|
+
if self.field_types is not None:
|
|
109
|
+
result["fieldTypes"] = self.field_types
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class ResolvedList:
|
|
115
|
+
"""
|
|
116
|
+
List resolution metadata.
|
|
117
|
+
|
|
118
|
+
This class represents how a user's list selector (ID, URL, name)
|
|
119
|
+
was resolved to a list ID.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
input: The original user input
|
|
123
|
+
list_id: The resolved numeric list ID
|
|
124
|
+
source: How the input was resolved (id, url, name)
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
>>> resolved = ResolvedList(
|
|
128
|
+
... input="Sales Pipeline",
|
|
129
|
+
... list_id=789,
|
|
130
|
+
... source="name"
|
|
131
|
+
... )
|
|
132
|
+
>>> resolved.to_dict()
|
|
133
|
+
{'input': 'Sales Pipeline', 'listId': 789, 'source': 'name'}
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
input: str
|
|
137
|
+
list_id: int
|
|
138
|
+
source: Literal["id", "url", "name"]
|
|
139
|
+
|
|
140
|
+
def to_dict(self) -> dict[str, Any]:
|
|
141
|
+
"""Convert to dict with camelCase field names."""
|
|
142
|
+
return {
|
|
143
|
+
"input": self.input,
|
|
144
|
+
"listId": self.list_id,
|
|
145
|
+
"source": self.source,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True, slots=True)
|
|
150
|
+
class ResolvedSavedView:
|
|
151
|
+
"""
|
|
152
|
+
Saved view resolution metadata.
|
|
153
|
+
|
|
154
|
+
This class represents how a user's saved view selector was resolved
|
|
155
|
+
to a saved view ID.
|
|
156
|
+
|
|
157
|
+
Attributes:
|
|
158
|
+
input: The original user input
|
|
159
|
+
saved_view_id: The resolved numeric saved view ID
|
|
160
|
+
name: The name of the saved view
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
>>> resolved = ResolvedSavedView(
|
|
164
|
+
... input="Active Deals",
|
|
165
|
+
... saved_view_id=456,
|
|
166
|
+
... name="Active Deals"
|
|
167
|
+
... )
|
|
168
|
+
>>> resolved.to_dict()
|
|
169
|
+
{'input': 'Active Deals', 'savedViewId': 456, 'name': 'Active Deals'}
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
input: str
|
|
173
|
+
saved_view_id: int
|
|
174
|
+
name: str
|
|
175
|
+
|
|
176
|
+
def to_dict(self) -> dict[str, Any]:
|
|
177
|
+
"""Convert to dict with camelCase field names."""
|
|
178
|
+
return {
|
|
179
|
+
"input": self.input,
|
|
180
|
+
"savedViewId": self.saved_view_id,
|
|
181
|
+
"name": self.name,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_resolved_metadata(
|
|
186
|
+
*,
|
|
187
|
+
entity: ResolvedEntity | None = None,
|
|
188
|
+
list_resolution: ResolvedList | None = None,
|
|
189
|
+
saved_view: ResolvedSavedView | None = None,
|
|
190
|
+
field_selection: ResolvedFieldSelection | None = None,
|
|
191
|
+
expand: list[str] | None = None,
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
"""
|
|
194
|
+
Build a complete resolved metadata dict for CommandOutput.
|
|
195
|
+
|
|
196
|
+
This is a convenience function to construct the resolved metadata
|
|
197
|
+
dictionary in a consistent way across all commands.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
entity: Entity resolution metadata (person, company, opportunity)
|
|
201
|
+
list_resolution: List resolution metadata
|
|
202
|
+
saved_view: Saved view resolution metadata
|
|
203
|
+
field_selection: Field selection metadata
|
|
204
|
+
expand: List of expansion options used
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Dictionary suitable for CommandOutput(resolved=...)
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> person = ResolvedEntity(
|
|
211
|
+
... input="john@example.com",
|
|
212
|
+
... entity_id=12345,
|
|
213
|
+
... entity_type="person",
|
|
214
|
+
... source="email"
|
|
215
|
+
... )
|
|
216
|
+
>>> fields = ResolvedFieldSelection(field_types=["global"])
|
|
217
|
+
>>> resolved = build_resolved_metadata(
|
|
218
|
+
... entity=person,
|
|
219
|
+
... field_selection=fields,
|
|
220
|
+
... expand=["lists"]
|
|
221
|
+
... )
|
|
222
|
+
>>> resolved
|
|
223
|
+
{
|
|
224
|
+
'person': {'input': 'john@example.com', 'personId': 12345, 'source': 'email'},
|
|
225
|
+
'fieldSelection': {'fieldTypes': ['global']},
|
|
226
|
+
'expand': ['lists']
|
|
227
|
+
}
|
|
228
|
+
"""
|
|
229
|
+
result: dict[str, Any] = {}
|
|
230
|
+
|
|
231
|
+
if entity is not None:
|
|
232
|
+
# Use entity_type as the key (e.g., "person", "company")
|
|
233
|
+
result[entity.entity_type] = entity.to_dict()
|
|
234
|
+
|
|
235
|
+
if list_resolution is not None:
|
|
236
|
+
result["list"] = list_resolution.to_dict()
|
|
237
|
+
|
|
238
|
+
if saved_view is not None:
|
|
239
|
+
result["savedView"] = saved_view.to_dict()
|
|
240
|
+
|
|
241
|
+
if field_selection is not None:
|
|
242
|
+
field_dict = field_selection.to_dict()
|
|
243
|
+
if field_dict: # Only include if not empty
|
|
244
|
+
result["fieldSelection"] = field_dict
|
|
245
|
+
|
|
246
|
+
if expand is not None and expand:
|
|
247
|
+
result["expand"] = expand
|
|
248
|
+
|
|
249
|
+
return result
|