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/types.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stable types import path.
|
|
3
|
+
|
|
4
|
+
This module re-exports ID types, enums, and constants from `affinity.models.types`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .models.types import (
|
|
10
|
+
V1_BASE_URL,
|
|
11
|
+
V2_BASE_URL,
|
|
12
|
+
AnyFieldId,
|
|
13
|
+
CompanyId,
|
|
14
|
+
DropdownOptionColor,
|
|
15
|
+
DropdownOptionId,
|
|
16
|
+
EnrichedFieldId,
|
|
17
|
+
EntityType,
|
|
18
|
+
FieldId,
|
|
19
|
+
FieldType,
|
|
20
|
+
FieldValueChangeAction,
|
|
21
|
+
FieldValueChangeId,
|
|
22
|
+
FieldValueId,
|
|
23
|
+
FieldValueType,
|
|
24
|
+
FileId,
|
|
25
|
+
InteractionId,
|
|
26
|
+
InteractionType,
|
|
27
|
+
ListEntryId,
|
|
28
|
+
ListId,
|
|
29
|
+
ListType,
|
|
30
|
+
NoteId,
|
|
31
|
+
NoteType,
|
|
32
|
+
OpportunityId,
|
|
33
|
+
PersonId,
|
|
34
|
+
PersonType,
|
|
35
|
+
ReminderIdType,
|
|
36
|
+
ReminderResetType,
|
|
37
|
+
ReminderStatus,
|
|
38
|
+
ReminderType,
|
|
39
|
+
SavedViewId,
|
|
40
|
+
TenantId,
|
|
41
|
+
UserId,
|
|
42
|
+
WebhookEvent,
|
|
43
|
+
WebhookId,
|
|
44
|
+
field_id_to_v1_numeric,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"V1_BASE_URL",
|
|
49
|
+
"V2_BASE_URL",
|
|
50
|
+
"AnyFieldId",
|
|
51
|
+
"PersonId",
|
|
52
|
+
"CompanyId",
|
|
53
|
+
"OpportunityId",
|
|
54
|
+
"ListId",
|
|
55
|
+
"ListEntryId",
|
|
56
|
+
"FieldId",
|
|
57
|
+
"FieldValueId",
|
|
58
|
+
"FieldValueChangeId",
|
|
59
|
+
"FieldValueChangeAction",
|
|
60
|
+
"EnrichedFieldId",
|
|
61
|
+
"NoteId",
|
|
62
|
+
"UserId",
|
|
63
|
+
"WebhookId",
|
|
64
|
+
"FileId",
|
|
65
|
+
"SavedViewId",
|
|
66
|
+
"ReminderIdType",
|
|
67
|
+
"InteractionId",
|
|
68
|
+
"DropdownOptionId",
|
|
69
|
+
"TenantId",
|
|
70
|
+
"ListType",
|
|
71
|
+
"PersonType",
|
|
72
|
+
"EntityType",
|
|
73
|
+
"FieldValueType",
|
|
74
|
+
"FieldType",
|
|
75
|
+
"DropdownOptionColor",
|
|
76
|
+
"InteractionType",
|
|
77
|
+
"NoteType",
|
|
78
|
+
"ReminderType",
|
|
79
|
+
"ReminderResetType",
|
|
80
|
+
"ReminderStatus",
|
|
81
|
+
"WebhookEvent",
|
|
82
|
+
"field_id_to_v1_numeric",
|
|
83
|
+
]
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: affinity-sdk
|
|
3
|
+
Version: 0.9.5
|
|
4
|
+
Summary: A modern, strongly-typed Python SDK for the Affinity CRM API
|
|
5
|
+
Project-URL: Homepage, https://github.com/yaniv-golan/affinity-sdk
|
|
6
|
+
Project-URL: Documentation, https://yaniv-golan.github.io/affinity-sdk/latest/
|
|
7
|
+
Project-URL: Repository, https://github.com/yaniv-golan/affinity-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/yaniv-golan/affinity-sdk/issues
|
|
9
|
+
Author: Yaniv Golan
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: affinity,api,client,crm,dealflow,relationship-management,sales,sdk
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: httpx<1.0.0,>=0.25.1
|
|
27
|
+
Requires-Dist: pydantic<3.0.0,>=2.8.0
|
|
28
|
+
Provides-Extra: cli
|
|
29
|
+
Requires-Dist: click>=8.1; extra == 'cli'
|
|
30
|
+
Requires-Dist: platformdirs>=4; extra == 'cli'
|
|
31
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == 'cli'
|
|
32
|
+
Requires-Dist: rich-click>=1.8; extra == 'cli'
|
|
33
|
+
Requires-Dist: rich<15,>=13; extra == 'cli'
|
|
34
|
+
Requires-Dist: tomli-w>=1.0; extra == 'cli'
|
|
35
|
+
Requires-Dist: tomli>=2; (python_version < '3.11') and extra == 'cli'
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: click>=8.1; extra == 'dev'
|
|
38
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: platformdirs>=4; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
44
|
+
Requires-Dist: rich-click>=1.8; extra == 'dev'
|
|
45
|
+
Requires-Dist: rich<15,>=13; extra == 'dev'
|
|
46
|
+
Requires-Dist: ruff>=0.5.0; extra == 'dev'
|
|
47
|
+
Requires-Dist: tomli>=2; extra == 'dev'
|
|
48
|
+
Provides-Extra: docs
|
|
49
|
+
Requires-Dist: mike>=2.0.0; extra == 'docs'
|
|
50
|
+
Requires-Dist: mkdocs-include-markdown-plugin>=7.0.0; extra == 'docs'
|
|
51
|
+
Requires-Dist: mkdocs-material>=9.0.0; extra == 'docs'
|
|
52
|
+
Requires-Dist: mkdocs>=1.5.0; extra == 'docs'
|
|
53
|
+
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == 'docs'
|
|
54
|
+
Provides-Extra: dotenv
|
|
55
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == 'dotenv'
|
|
56
|
+
Description-Content-Type: text/markdown
|
|
57
|
+
|
|
58
|
+
# Affinity Python SDK
|
|
59
|
+
|
|
60
|
+
[](https://github.com/yaniv-golan/affinity-sdk/actions/workflows/ci.yml)
|
|
61
|
+
[](https://codecov.io/gh/yaniv-golan/affinity-sdk)
|
|
62
|
+
[](https://pypi.org/project/affinity-sdk/)
|
|
63
|
+
[](https://pypi.org/project/affinity-sdk/)
|
|
64
|
+
[](https://opensource.org/licenses/MIT)
|
|
65
|
+
[](https://mypy-lang.org/)
|
|
66
|
+
[](https://docs.pydantic.dev/)
|
|
67
|
+
[](https://yaniv-golan.github.io/affinity-sdk/latest/)
|
|
68
|
+
[](https://yaniv-golan.github.io/affinity-sdk/latest/mcp/)
|
|
69
|
+
[](https://github.com/yaniv-golan/mcp-bash-framework)
|
|
70
|
+
[](https://yaniv-golan.github.io/affinity-sdk/latest/guides/claude-code-plugins/)
|
|
71
|
+
|
|
72
|
+
A modern, strongly-typed Python wrapper for the [Affinity CRM API](https://api-docs.affinity.co/).
|
|
73
|
+
|
|
74
|
+
Disclaimer: This is an unofficial community project and is not affiliated with, endorsed by, or sponsored by Affinity. “Affinity” and related marks are trademarks of their respective owners. Use of the Affinity API is subject to Affinity’s Terms of Service.
|
|
75
|
+
|
|
76
|
+
Maintainer: GitHub: `yaniv-golan`
|
|
77
|
+
|
|
78
|
+
Documentation: https://yaniv-golan.github.io/affinity-sdk/latest/
|
|
79
|
+
|
|
80
|
+
## Table of Contents
|
|
81
|
+
|
|
82
|
+
- [Features](#features)
|
|
83
|
+
- [Installation](#installation)
|
|
84
|
+
- [Quick Start](#quick-start)
|
|
85
|
+
- [Usage Examples](#usage-examples)
|
|
86
|
+
- [Type System](#type-system)
|
|
87
|
+
- [API Coverage](#api-coverage)
|
|
88
|
+
- [Configuration](#configuration)
|
|
89
|
+
- [Error Handling](#error-handling)
|
|
90
|
+
- [Async Support](#async-support)
|
|
91
|
+
- [Development](#development)
|
|
92
|
+
|
|
93
|
+
## Features
|
|
94
|
+
|
|
95
|
+
- **Complete API coverage** - Full V1 + V2 support with smart routing
|
|
96
|
+
- **CLI included** - Scriptable command-line interface for automation
|
|
97
|
+
- **Strong typing** - Full Pydantic V2 models with typed ID classes
|
|
98
|
+
- **No magic numbers** - Comprehensive enums for all API constants
|
|
99
|
+
- **Automatic pagination** - Iterator support for seamless pagination
|
|
100
|
+
- **Rate limit handling** - Automatic retry with exponential backoff
|
|
101
|
+
- **Response caching** - Optional caching for field metadata
|
|
102
|
+
- **Both sync and async** - Full support for both patterns
|
|
103
|
+
|
|
104
|
+
### AI Integrations
|
|
105
|
+
|
|
106
|
+
- **Claude Code plugins** - SDK and CLI knowledge for AI-assisted development
|
|
107
|
+
- **MCP Server** - Connect desktop AI tools to Affinity
|
|
108
|
+
|
|
109
|
+
## Installation
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install affinity-sdk
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Requires Python 3.10+.
|
|
116
|
+
|
|
117
|
+
Optional (local dev): load `.env` automatically:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install "affinity-sdk[dotenv]"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Optional: install the CLI:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pipx install "affinity-sdk[cli]"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
CLI docs: https://yaniv-golan.github.io/affinity-sdk/latest/cli/
|
|
130
|
+
|
|
131
|
+
### MCP Server
|
|
132
|
+
|
|
133
|
+
Connect desktop AI tools to Affinity CRM:
|
|
134
|
+
|
|
135
|
+
- Claude Desktop, ChatGPT Desktop, Cursor, Windsurf, VS Code + Copilot, Zed, and more
|
|
136
|
+
|
|
137
|
+
Features: entity search, relationship intelligence, workflow management, interaction logging, meeting prep.
|
|
138
|
+
|
|
139
|
+
MCP docs: https://yaniv-golan.github.io/affinity-sdk/latest/mcp/
|
|
140
|
+
|
|
141
|
+
### Claude Code Plugins
|
|
142
|
+
|
|
143
|
+
If you use [Claude Code](https://docs.anthropic.com/en/docs/claude-code), install plugins for SDK/CLI knowledge:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
/plugin marketplace add yaniv-golan/affinity-sdk
|
|
147
|
+
/plugin install sdk@xaffinity # SDK patterns
|
|
148
|
+
/plugin install cli@xaffinity # CLI patterns + /affinity-help
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Plugin docs: https://yaniv-golan.github.io/affinity-sdk/latest/guides/claude-code-plugins/
|
|
152
|
+
|
|
153
|
+
## Documentation
|
|
154
|
+
|
|
155
|
+
- [Full documentation](https://yaniv-golan.github.io/affinity-sdk/latest/)
|
|
156
|
+
- [MCP Server](https://yaniv-golan.github.io/affinity-sdk/latest/mcp/)
|
|
157
|
+
- [CLI Reference](https://yaniv-golan.github.io/affinity-sdk/latest/cli/)
|
|
158
|
+
- [API Reference](https://yaniv-golan.github.io/affinity-sdk/latest/reference/client/)
|
|
159
|
+
|
|
160
|
+
## Quick Start
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from affinity import Affinity
|
|
164
|
+
from affinity.types import FieldType, PersonId
|
|
165
|
+
|
|
166
|
+
# Recommended: read the API key from the environment (AFFINITY_API_KEY)
|
|
167
|
+
client = Affinity.from_env()
|
|
168
|
+
|
|
169
|
+
# If you use a local `.env` file (requires `affinity-sdk[dotenv]`)
|
|
170
|
+
# client = Affinity.from_env(load_dotenv=True)
|
|
171
|
+
|
|
172
|
+
# Or pass it explicitly
|
|
173
|
+
# client = Affinity(api_key="your-api-key")
|
|
174
|
+
|
|
175
|
+
# Or use as a context manager
|
|
176
|
+
with Affinity.from_env() as client:
|
|
177
|
+
# List all companies
|
|
178
|
+
for company in client.companies.all():
|
|
179
|
+
print(f"{company.name} ({company.domain})")
|
|
180
|
+
|
|
181
|
+
# Get a person with enriched data
|
|
182
|
+
person = client.persons.get(
|
|
183
|
+
PersonId(12345),
|
|
184
|
+
field_types=[FieldType.ENRICHED, FieldType.GLOBAL]
|
|
185
|
+
)
|
|
186
|
+
print(f"{person.first_name} {person.last_name}: {person.primary_email}")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Usage Examples
|
|
190
|
+
|
|
191
|
+
### Working with Companies
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from affinity import Affinity, F
|
|
195
|
+
from affinity.models import CompanyCreate
|
|
196
|
+
from affinity.types import CompanyId, FieldType
|
|
197
|
+
|
|
198
|
+
with Affinity(api_key="your-key") as client:
|
|
199
|
+
# List companies with filtering (V2 API)
|
|
200
|
+
companies = client.companies.list(
|
|
201
|
+
filter=F.field("domain").contains("acme"),
|
|
202
|
+
field_types=[FieldType.ENRICHED],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Iterate through all companies with automatic pagination
|
|
206
|
+
for company in client.companies.all():
|
|
207
|
+
print(f"{company.name}: {company.fields}")
|
|
208
|
+
|
|
209
|
+
# Get a specific company
|
|
210
|
+
company = client.companies.get(CompanyId(123))
|
|
211
|
+
|
|
212
|
+
# Create a company (uses V1 API)
|
|
213
|
+
new_company = client.companies.create(
|
|
214
|
+
CompanyCreate(
|
|
215
|
+
name="Acme Corp",
|
|
216
|
+
domain="acme.com",
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Search by name, domain, or email
|
|
221
|
+
results = client.companies.search("acme.com")
|
|
222
|
+
|
|
223
|
+
# Get list entries for a company
|
|
224
|
+
entries = client.companies.get_list_entries(CompanyId(123))
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Working with Persons
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from affinity import Affinity
|
|
231
|
+
from affinity.models import PersonCreate
|
|
232
|
+
from affinity.types import PersonType
|
|
233
|
+
|
|
234
|
+
with Affinity(api_key="your-key") as client:
|
|
235
|
+
# Get all internal team members
|
|
236
|
+
for person in client.persons.all():
|
|
237
|
+
if person.type == PersonType.INTERNAL:
|
|
238
|
+
print(f"{person.first_name} {person.last_name}")
|
|
239
|
+
|
|
240
|
+
# Create a contact
|
|
241
|
+
person = client.persons.create(
|
|
242
|
+
PersonCreate(
|
|
243
|
+
first_name="Jane",
|
|
244
|
+
last_name="Doe",
|
|
245
|
+
emails=["jane@example.com"],
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Search by email
|
|
250
|
+
results = client.persons.search("jane@example.com")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Working with Lists
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from affinity import Affinity
|
|
257
|
+
from affinity.models import ListCreate
|
|
258
|
+
from affinity.types import CompanyId, FieldId, FieldType, ListId, ListType
|
|
259
|
+
|
|
260
|
+
with Affinity(api_key="your-key") as client:
|
|
261
|
+
# Get all lists
|
|
262
|
+
for lst in client.lists.all():
|
|
263
|
+
print(f"{lst.name} ({lst.type.name})")
|
|
264
|
+
|
|
265
|
+
# Get a specific list with field metadata
|
|
266
|
+
pipeline = client.lists.get(ListId(123))
|
|
267
|
+
print(f"Fields: {[f.name for f in pipeline.fields]}")
|
|
268
|
+
|
|
269
|
+
# Create a new list
|
|
270
|
+
new_list = client.lists.create(
|
|
271
|
+
ListCreate(
|
|
272
|
+
name="Q1 Pipeline",
|
|
273
|
+
type=ListType.OPPORTUNITY,
|
|
274
|
+
is_public=True,
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Work with list entries
|
|
279
|
+
entries = client.lists.entries(ListId(123))
|
|
280
|
+
|
|
281
|
+
# List entries with field data
|
|
282
|
+
for entry in entries.all(field_types=[FieldType.LIST_SPECIFIC]):
|
|
283
|
+
print(f"{entry.entity.name}: {entry.fields}")
|
|
284
|
+
|
|
285
|
+
# Add a company to the list
|
|
286
|
+
entry = entries.add_company(CompanyId(456))
|
|
287
|
+
|
|
288
|
+
# Update field values
|
|
289
|
+
entries.update_field_value(
|
|
290
|
+
entry.id,
|
|
291
|
+
FieldId(101),
|
|
292
|
+
"In Progress"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Batch update multiple fields
|
|
296
|
+
entries.batch_update_fields(
|
|
297
|
+
entry.id,
|
|
298
|
+
{
|
|
299
|
+
FieldId(101): "Closed Won",
|
|
300
|
+
FieldId(102): 100000,
|
|
301
|
+
FieldId(103): "2024-03-15",
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Use saved views
|
|
306
|
+
views = client.lists.get_saved_views(ListId(123))
|
|
307
|
+
for view in views.data:
|
|
308
|
+
results = entries.from_saved_view(view.id)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Notes
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from affinity import Affinity
|
|
315
|
+
from affinity.models import NoteCreate, NoteUpdate
|
|
316
|
+
from affinity.types import NoteType, PersonId
|
|
317
|
+
|
|
318
|
+
with Affinity(api_key="your-key") as client:
|
|
319
|
+
# Create a note
|
|
320
|
+
note = client.notes.create(
|
|
321
|
+
NoteCreate(
|
|
322
|
+
content="<p>Great meeting!</p>",
|
|
323
|
+
type=NoteType.HTML,
|
|
324
|
+
person_ids=[PersonId(123)],
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Get notes for a person
|
|
329
|
+
result = client.notes.list(person_id=PersonId(123))
|
|
330
|
+
for note_item in result.data:
|
|
331
|
+
print(note_item.content)
|
|
332
|
+
|
|
333
|
+
# Update a note
|
|
334
|
+
client.notes.update(note.id, NoteUpdate(content="Updated content"))
|
|
335
|
+
|
|
336
|
+
# Delete a note
|
|
337
|
+
client.notes.delete(note.id)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Reminders
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
from datetime import datetime, timedelta
|
|
344
|
+
from affinity import Affinity
|
|
345
|
+
from affinity.models import ReminderCreate
|
|
346
|
+
from affinity.types import PersonId, ReminderResetType, ReminderType, UserId
|
|
347
|
+
|
|
348
|
+
with Affinity(api_key="your-key") as client:
|
|
349
|
+
# Get current user
|
|
350
|
+
me = client.whoami()
|
|
351
|
+
|
|
352
|
+
# Create a follow-up reminder
|
|
353
|
+
reminder = client.reminders.create(
|
|
354
|
+
ReminderCreate(
|
|
355
|
+
owner_id=UserId(me.user.id),
|
|
356
|
+
type=ReminderType.ONE_TIME,
|
|
357
|
+
content="Follow up on proposal",
|
|
358
|
+
due_date=datetime.now() + timedelta(days=7),
|
|
359
|
+
person_id=PersonId(123),
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Create a recurring reminder
|
|
364
|
+
recurring = client.reminders.create(
|
|
365
|
+
ReminderCreate(
|
|
366
|
+
owner_id=UserId(me.user.id),
|
|
367
|
+
type=ReminderType.RECURRING,
|
|
368
|
+
reset_type=ReminderResetType.INTERACTION,
|
|
369
|
+
reminder_days=30,
|
|
370
|
+
content="Monthly check-in",
|
|
371
|
+
person_id=PersonId(123),
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Files
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
from affinity import Affinity
|
|
380
|
+
from affinity.types import FileId, PersonId
|
|
381
|
+
|
|
382
|
+
with Affinity(api_key="your-key") as client:
|
|
383
|
+
# Download into memory (bytes)
|
|
384
|
+
content = client.files.download(FileId(123))
|
|
385
|
+
|
|
386
|
+
# Stream download (for progress bars / piping / large files)
|
|
387
|
+
for chunk in client.files.download_stream(
|
|
388
|
+
FileId(123),
|
|
389
|
+
chunk_size=64_000,
|
|
390
|
+
timeout=60.0, # per-call request timeout override (seconds)
|
|
391
|
+
deadline_seconds=300, # total time budget (includes retries/backoff)
|
|
392
|
+
):
|
|
393
|
+
...
|
|
394
|
+
|
|
395
|
+
# Download to disk
|
|
396
|
+
saved_path = client.files.download_to(
|
|
397
|
+
FileId(123),
|
|
398
|
+
"report.pdf",
|
|
399
|
+
overwrite=False,
|
|
400
|
+
deadline_seconds=300,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Upload (multipart form data)
|
|
404
|
+
client.files.upload(
|
|
405
|
+
files={"file": ("report.pdf", b"hello", "application/pdf")},
|
|
406
|
+
person_id=PersonId(123),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Upload from disk / bytes (ergonomic helpers)
|
|
410
|
+
client.files.upload_path("report.pdf", person_id=PersonId(123))
|
|
411
|
+
client.files.upload_bytes(b"hello", "report.txt", person_id=PersonId(123))
|
|
412
|
+
|
|
413
|
+
# Iterate all files attached to an entity
|
|
414
|
+
for f in client.files.all(person_id=PersonId(123)):
|
|
415
|
+
print(f.name, f.size)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Webhooks
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
from affinity import Affinity
|
|
422
|
+
from affinity.models import WebhookCreate, WebhookUpdate
|
|
423
|
+
from affinity.types import WebhookEvent
|
|
424
|
+
|
|
425
|
+
with Affinity(api_key="your-key") as client:
|
|
426
|
+
# Create a webhook subscription
|
|
427
|
+
webhook = client.webhooks.create(
|
|
428
|
+
WebhookCreate(
|
|
429
|
+
webhook_url="https://your-server.com/webhook",
|
|
430
|
+
subscriptions=[
|
|
431
|
+
WebhookEvent.LIST_ENTRY_CREATED,
|
|
432
|
+
WebhookEvent.LIST_ENTRY_DELETED,
|
|
433
|
+
WebhookEvent.FIELD_VALUE_UPDATED,
|
|
434
|
+
],
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# List all webhooks (max 3 per instance)
|
|
439
|
+
webhooks = client.webhooks.list()
|
|
440
|
+
|
|
441
|
+
# Disable a webhook
|
|
442
|
+
client.webhooks.update(
|
|
443
|
+
webhook.id,
|
|
444
|
+
WebhookUpdate(disabled=True)
|
|
445
|
+
)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Rate Limits
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
from affinity import Affinity
|
|
452
|
+
|
|
453
|
+
with Affinity(api_key="your-key") as client:
|
|
454
|
+
# Fetch/observe current rate limits now (one request)
|
|
455
|
+
limits = client.rate_limits.refresh()
|
|
456
|
+
print(f"API key per minute: {limits.api_key_per_minute.remaining}/{limits.api_key_per_minute.limit}")
|
|
457
|
+
print(f"Org monthly: {limits.org_monthly.remaining}/{limits.org_monthly.limit}")
|
|
458
|
+
|
|
459
|
+
# Best-effort snapshot derived from tracked response headers (no network)
|
|
460
|
+
snapshot = client.rate_limits.snapshot()
|
|
461
|
+
print(f"Snapshot source: {snapshot.source}")
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Type System
|
|
465
|
+
|
|
466
|
+
The SDK uses strongly-typed ID classes (int/str subclasses) to prevent accidental mixing:
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
from affinity.types import PersonId, CompanyId, ListId
|
|
470
|
+
|
|
471
|
+
# These are different types - IDE and type checker will catch mixing
|
|
472
|
+
person_id = PersonId(123)
|
|
473
|
+
company_id = CompanyId(456)
|
|
474
|
+
|
|
475
|
+
# This would be a type error:
|
|
476
|
+
# client.persons.get(company_id) # Wrong type!
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
All magic numbers are replaced with enums:
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
from affinity.types import (
|
|
483
|
+
ListType, # PERSON, ORGANIZATION, OPPORTUNITY
|
|
484
|
+
PersonType, # INTERNAL, EXTERNAL, COLLABORATOR
|
|
485
|
+
FieldValueType, # "text", "number", "datetime", "dropdown-multi", etc.
|
|
486
|
+
InteractionType, # EMAIL, MEETING, CALL, CHAT
|
|
487
|
+
# ... and more
|
|
488
|
+
)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## API Coverage
|
|
492
|
+
|
|
493
|
+
| Feature | V2 | V1 | SDK |
|
|
494
|
+
|---------|:--:|:--:|:---:|
|
|
495
|
+
| Companies (read) | ✅ | ✅ | V2 |
|
|
496
|
+
| Companies (write) | ❌ | ✅ | V1 |
|
|
497
|
+
| Persons (read) | ✅ | ✅ | V2 |
|
|
498
|
+
| Persons (write) | ❌ | ✅ | V1 |
|
|
499
|
+
| Lists (read) | ✅ | ✅ | V2 |
|
|
500
|
+
| Lists (write) | ❌ | ✅ | V1 |
|
|
501
|
+
| List Entries (read) | ✅ | ✅ | V2 |
|
|
502
|
+
| List Entries (write) | ❌ | ✅ | V1 |
|
|
503
|
+
| Field Values (read) | ✅ | ✅ | V2 |
|
|
504
|
+
| Field Values (write) | ✅ | ✅ | V2 |
|
|
505
|
+
| Notes | Read-only | ✅ | V1 |
|
|
506
|
+
| Reminders | ❌ | ✅ | V1 |
|
|
507
|
+
| Webhooks | ❌ | ✅ | V1 |
|
|
508
|
+
| Interactions | Read-only | ✅ | V1 |
|
|
509
|
+
| Entity Files | ❌ | ✅ | V1 |
|
|
510
|
+
| Relationship Strengths | ❌ | ✅ | V1 |
|
|
511
|
+
|
|
512
|
+
## Configuration
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
from affinity import Affinity
|
|
516
|
+
|
|
517
|
+
client = Affinity(
|
|
518
|
+
api_key="your-api-key",
|
|
519
|
+
|
|
520
|
+
# Timeouts and retries
|
|
521
|
+
timeout=30.0, # Request timeout (seconds)
|
|
522
|
+
max_retries=3, # Retries for rate-limited requests
|
|
523
|
+
|
|
524
|
+
# Caching
|
|
525
|
+
enable_cache=True, # Cache field metadata
|
|
526
|
+
cache_ttl=300.0, # Cache TTL (seconds)
|
|
527
|
+
|
|
528
|
+
# Debugging
|
|
529
|
+
log_requests=False, # Log all HTTP requests
|
|
530
|
+
|
|
531
|
+
# Hooks (DX-008)
|
|
532
|
+
# on_event=lambda event: print(event.type),
|
|
533
|
+
# on_request=lambda req: print(req.method, req.url),
|
|
534
|
+
# on_response=lambda resp: print(resp.status_code, resp.request.url),
|
|
535
|
+
)
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Error Handling
|
|
539
|
+
|
|
540
|
+
The SDK provides a comprehensive exception hierarchy:
|
|
541
|
+
|
|
542
|
+
```python
|
|
543
|
+
from affinity import (
|
|
544
|
+
Affinity,
|
|
545
|
+
AffinityError,
|
|
546
|
+
AuthenticationError,
|
|
547
|
+
RateLimitError,
|
|
548
|
+
NotFoundError,
|
|
549
|
+
ValidationError,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
try:
|
|
553
|
+
with Affinity(api_key="your-key") as client:
|
|
554
|
+
person = client.persons.get(PersonId(99999999))
|
|
555
|
+
except AuthenticationError:
|
|
556
|
+
print("Invalid API key")
|
|
557
|
+
except RateLimitError as e:
|
|
558
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
559
|
+
except NotFoundError:
|
|
560
|
+
print("Person not found")
|
|
561
|
+
except ValidationError as e:
|
|
562
|
+
print(f"Invalid request: {e.message}")
|
|
563
|
+
except AffinityError as e:
|
|
564
|
+
print(f"API error: {e}")
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Async Support
|
|
568
|
+
|
|
569
|
+
```python
|
|
570
|
+
import asyncio
|
|
571
|
+
from affinity import AsyncAffinity
|
|
572
|
+
|
|
573
|
+
async def main():
|
|
574
|
+
async with AsyncAffinity(api_key="your-key") as client:
|
|
575
|
+
# Async operations
|
|
576
|
+
companies = await client.companies.list()
|
|
577
|
+
async for company in client.companies.all():
|
|
578
|
+
print(company.name)
|
|
579
|
+
|
|
580
|
+
asyncio.run(main())
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Async support mirrors the sync client surface area (including V1-only services like notes/reminders/webhooks/files).
|
|
584
|
+
|
|
585
|
+
See `docs/public/guides/sync-vs-async.md` for more details.
|
|
586
|
+
|
|
587
|
+
If you don't use `async with`, make sure to `await client.close()` (e.g., in a `finally`) to avoid leaking connections.
|
|
588
|
+
|
|
589
|
+
## Development
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
# Install with dev dependencies
|
|
593
|
+
pip install -e ".[dev]"
|
|
594
|
+
|
|
595
|
+
# Run tests
|
|
596
|
+
pytest
|
|
597
|
+
|
|
598
|
+
# Optional: live API smoke tests (requires a real API key)
|
|
599
|
+
AFFINITY_API_KEY="..." pytest -m integration -q
|
|
600
|
+
|
|
601
|
+
# Type checking
|
|
602
|
+
mypy affinity
|
|
603
|
+
|
|
604
|
+
# Linting
|
|
605
|
+
ruff check affinity
|
|
606
|
+
ruff format affinity
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## License
|
|
610
|
+
|
|
611
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
612
|
+
|
|
613
|
+
## Contributing
|
|
614
|
+
|
|
615
|
+
Contributions welcome! Please read our contributing guidelines first.
|
|
616
|
+
|
|
617
|
+
## Links
|
|
618
|
+
|
|
619
|
+
- Repository: https://github.com/yaniv-golan/affinity-sdk
|
|
620
|
+
- Issues: https://github.com/yaniv-golan/affinity-sdk/issues
|
|
621
|
+
- [Affinity API V2 Documentation](https://api-docs.affinity.co/reference/getting-started-with-your-api)
|
|
622
|
+
- [Affinity API V1 Documentation](https://api-docs.affinity.co/reference)
|