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.
Files changed (92) hide show
  1. affinity/__init__.py +139 -0
  2. affinity/cli/__init__.py +7 -0
  3. affinity/cli/click_compat.py +27 -0
  4. affinity/cli/commands/__init__.py +1 -0
  5. affinity/cli/commands/_entity_files_dump.py +219 -0
  6. affinity/cli/commands/_list_entry_fields.py +41 -0
  7. affinity/cli/commands/_v1_parsing.py +77 -0
  8. affinity/cli/commands/company_cmds.py +2139 -0
  9. affinity/cli/commands/completion_cmd.py +33 -0
  10. affinity/cli/commands/config_cmds.py +540 -0
  11. affinity/cli/commands/entry_cmds.py +33 -0
  12. affinity/cli/commands/field_cmds.py +413 -0
  13. affinity/cli/commands/interaction_cmds.py +875 -0
  14. affinity/cli/commands/list_cmds.py +3152 -0
  15. affinity/cli/commands/note_cmds.py +433 -0
  16. affinity/cli/commands/opportunity_cmds.py +1174 -0
  17. affinity/cli/commands/person_cmds.py +1980 -0
  18. affinity/cli/commands/query_cmd.py +444 -0
  19. affinity/cli/commands/relationship_strength_cmds.py +62 -0
  20. affinity/cli/commands/reminder_cmds.py +595 -0
  21. affinity/cli/commands/resolve_url_cmd.py +127 -0
  22. affinity/cli/commands/session_cmds.py +84 -0
  23. affinity/cli/commands/task_cmds.py +110 -0
  24. affinity/cli/commands/version_cmd.py +29 -0
  25. affinity/cli/commands/whoami_cmd.py +36 -0
  26. affinity/cli/config.py +108 -0
  27. affinity/cli/context.py +749 -0
  28. affinity/cli/csv_utils.py +195 -0
  29. affinity/cli/date_utils.py +42 -0
  30. affinity/cli/decorators.py +77 -0
  31. affinity/cli/errors.py +28 -0
  32. affinity/cli/field_utils.py +355 -0
  33. affinity/cli/formatters.py +551 -0
  34. affinity/cli/help_json.py +283 -0
  35. affinity/cli/logging.py +100 -0
  36. affinity/cli/main.py +261 -0
  37. affinity/cli/options.py +53 -0
  38. affinity/cli/paths.py +32 -0
  39. affinity/cli/progress.py +183 -0
  40. affinity/cli/query/__init__.py +163 -0
  41. affinity/cli/query/aggregates.py +357 -0
  42. affinity/cli/query/dates.py +194 -0
  43. affinity/cli/query/exceptions.py +147 -0
  44. affinity/cli/query/executor.py +1236 -0
  45. affinity/cli/query/filters.py +248 -0
  46. affinity/cli/query/models.py +333 -0
  47. affinity/cli/query/output.py +331 -0
  48. affinity/cli/query/parser.py +619 -0
  49. affinity/cli/query/planner.py +430 -0
  50. affinity/cli/query/progress.py +270 -0
  51. affinity/cli/query/schema.py +439 -0
  52. affinity/cli/render.py +1589 -0
  53. affinity/cli/resolve.py +222 -0
  54. affinity/cli/resolvers.py +249 -0
  55. affinity/cli/results.py +308 -0
  56. affinity/cli/runner.py +218 -0
  57. affinity/cli/serialization.py +65 -0
  58. affinity/cli/session_cache.py +276 -0
  59. affinity/cli/types.py +70 -0
  60. affinity/client.py +771 -0
  61. affinity/clients/__init__.py +19 -0
  62. affinity/clients/http.py +3664 -0
  63. affinity/clients/pipeline.py +165 -0
  64. affinity/compare.py +501 -0
  65. affinity/downloads.py +114 -0
  66. affinity/exceptions.py +615 -0
  67. affinity/filters.py +1128 -0
  68. affinity/hooks.py +198 -0
  69. affinity/inbound_webhooks.py +302 -0
  70. affinity/models/__init__.py +163 -0
  71. affinity/models/entities.py +798 -0
  72. affinity/models/pagination.py +513 -0
  73. affinity/models/rate_limit_snapshot.py +48 -0
  74. affinity/models/secondary.py +413 -0
  75. affinity/models/types.py +663 -0
  76. affinity/policies.py +40 -0
  77. affinity/progress.py +22 -0
  78. affinity/py.typed +0 -0
  79. affinity/services/__init__.py +42 -0
  80. affinity/services/companies.py +1286 -0
  81. affinity/services/lists.py +1892 -0
  82. affinity/services/opportunities.py +1330 -0
  83. affinity/services/persons.py +1348 -0
  84. affinity/services/rate_limits.py +173 -0
  85. affinity/services/tasks.py +193 -0
  86. affinity/services/v1_only.py +2445 -0
  87. affinity/types.py +83 -0
  88. affinity_sdk-0.9.5.dist-info/METADATA +622 -0
  89. affinity_sdk-0.9.5.dist-info/RECORD +92 -0
  90. affinity_sdk-0.9.5.dist-info/WHEEL +4 -0
  91. affinity_sdk-0.9.5.dist-info/entry_points.txt +2 -0
  92. 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
+ [![CI](https://github.com/yaniv-golan/affinity-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/yaniv-golan/affinity-sdk/actions/workflows/ci.yml)
61
+ [![Coverage](https://codecov.io/gh/yaniv-golan/affinity-sdk/branch/main/graph/badge.svg)](https://codecov.io/gh/yaniv-golan/affinity-sdk)
62
+ [![PyPI version](https://img.shields.io/pypi/v/affinity-sdk.svg)](https://pypi.org/project/affinity-sdk/)
63
+ [![Python versions](https://img.shields.io/pypi/pyversions/affinity-sdk.svg)](https://pypi.org/project/affinity-sdk/)
64
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
65
+ [![Typed](https://img.shields.io/badge/typed-mypy-blue.svg)](https://mypy-lang.org/)
66
+ [![Pydantic v2](https://img.shields.io/badge/Pydantic-v2-orange.svg)](https://docs.pydantic.dev/)
67
+ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://yaniv-golan.github.io/affinity-sdk/latest/)
68
+ [![MCP](https://img.shields.io/badge/MCP-server-green?logo=modelcontextprotocol)](https://yaniv-golan.github.io/affinity-sdk/latest/mcp/)
69
+ [![MCP Bash Framework](https://img.shields.io/badge/MCP-MCP_Bash_Framework-green?logo=modelcontextprotocol)](https://github.com/yaniv-golan/mcp-bash-framework)
70
+ [![Claude Code](https://img.shields.io/badge/Claude%20Code-plugins-blueviolet.svg)](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)