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
@@ -0,0 +1,439 @@
1
+ """Entity schema registry.
2
+
3
+ Defines entity types, their fields, and relationships for the query engine.
4
+ This module is CLI-only and NOT part of the public SDK API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum, auto
11
+ from typing import Literal
12
+
13
+
14
+ class FetchStrategy(Enum):
15
+ """How an entity type can be fetched as a top-level query."""
16
+
17
+ # Can call service.all() directly - e.g., persons, companies, opportunities
18
+ GLOBAL = auto()
19
+
20
+ # Requires a parent ID filter - e.g., listEntries needs listId
21
+ REQUIRES_PARENT = auto()
22
+
23
+ # Can only be fetched as a relationship, not directly queried
24
+ RELATIONSHIP_ONLY = auto()
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class RelationshipDef:
29
+ """Defines how to fetch related entities.
30
+
31
+ Attributes:
32
+ target_entity: The entity type being related to
33
+ fetch_strategy: How to fetch the related entities:
34
+ - "entity_method": Call method on entity service
35
+ - "global_service": Call global service with filter
36
+ method_or_service: Method name for entity_method, service attr for global
37
+ filter_field: For global_service: the filter param name
38
+ cardinality: Whether the relationship is one-to-one or one-to-many
39
+ requires_n_plus_1: Does fetching require per-record API calls?
40
+ """
41
+
42
+ target_entity: str
43
+ fetch_strategy: Literal["entity_method", "global_service"]
44
+ method_or_service: str
45
+ filter_field: str | None = None
46
+ cardinality: Literal["one", "many"] = "many"
47
+ requires_n_plus_1: bool = True
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class EntitySchema:
52
+ """Schema definition for an entity type.
53
+
54
+ Attributes:
55
+ name: Entity type name (e.g., "persons", "companies")
56
+ service_attr: Attribute name on Affinity client (e.g., "persons")
57
+ id_field: Name of the ID field (usually "id")
58
+ filterable_fields: Fields that can be used in WHERE clauses
59
+ computed_fields: Fields that are computed (e.g., "firstEmail", "lastEmail")
60
+ relationships: Dict of relationship name -> RelationshipDef
61
+ api_version: Primary API version for this entity ("v1" or "v2")
62
+ fetch_strategy: How to fetch this entity as a top-level query
63
+ required_filters: Filter fields required for REQUIRES_PARENT entities
64
+ parent_filter_field: Field name in where clause (e.g., "listId")
65
+ parent_id_type: Type name to cast to (e.g., "ListId")
66
+ parent_method_name: Method to call on parent service (e.g., "entries")
67
+ """
68
+
69
+ name: str
70
+ service_attr: str
71
+ id_field: str
72
+ filterable_fields: frozenset[str]
73
+ computed_fields: frozenset[str]
74
+ relationships: dict[str, RelationshipDef]
75
+ api_version: Literal["v1", "v2"] = "v2"
76
+ fetch_strategy: FetchStrategy = FetchStrategy.GLOBAL
77
+ required_filters: frozenset[str] = field(default_factory=frozenset)
78
+ parent_filter_field: str | None = None
79
+ parent_id_type: str | None = None
80
+ parent_method_name: str | None = None
81
+
82
+ def __post_init__(self) -> None:
83
+ """Validate schema configuration at definition time."""
84
+ # Validate REQUIRES_PARENT has all required fields
85
+ if self.fetch_strategy == FetchStrategy.REQUIRES_PARENT:
86
+ if not self.required_filters:
87
+ raise ValueError(
88
+ f"Entity '{self.name}' with REQUIRES_PARENT must have required_filters"
89
+ )
90
+ if not self.parent_filter_field:
91
+ raise ValueError(
92
+ f"Entity '{self.name}' with REQUIRES_PARENT must have parent_filter_field"
93
+ )
94
+ if not self.parent_method_name:
95
+ raise ValueError(
96
+ f"Entity '{self.name}' with REQUIRES_PARENT must have parent_method_name"
97
+ )
98
+
99
+ # Validate parent_id_type exists in affinity.types (fail-fast at import time)
100
+ if self.parent_id_type:
101
+ from affinity import types as affinity_types
102
+
103
+ if not hasattr(affinity_types, self.parent_id_type):
104
+ raise ValueError(
105
+ f"Entity '{self.name}' references unknown type '{self.parent_id_type}'. "
106
+ f"Must be a type in affinity.types module."
107
+ )
108
+
109
+
110
+ # =============================================================================
111
+ # Schema Registry
112
+ # =============================================================================
113
+
114
+ SCHEMA_REGISTRY: dict[str, EntitySchema] = {
115
+ "persons": EntitySchema(
116
+ name="persons",
117
+ service_attr="persons",
118
+ id_field="id",
119
+ filterable_fields=frozenset(
120
+ [
121
+ "id",
122
+ "firstName",
123
+ "lastName",
124
+ "primaryEmail",
125
+ "emails",
126
+ "createdAt",
127
+ "updatedAt",
128
+ ]
129
+ ),
130
+ computed_fields=frozenset(["firstEmail", "lastEmail"]),
131
+ relationships={
132
+ "companies": RelationshipDef(
133
+ target_entity="companies",
134
+ fetch_strategy="entity_method",
135
+ method_or_service="get_associated_company_ids",
136
+ requires_n_plus_1=True,
137
+ ),
138
+ "opportunities": RelationshipDef(
139
+ target_entity="opportunities",
140
+ fetch_strategy="entity_method",
141
+ method_or_service="get_associated_opportunity_ids",
142
+ requires_n_plus_1=True,
143
+ ),
144
+ "interactions": RelationshipDef(
145
+ target_entity="interactions",
146
+ fetch_strategy="global_service",
147
+ method_or_service="interactions",
148
+ filter_field="person_id",
149
+ requires_n_plus_1=True, # API requires one entity ID per call
150
+ ),
151
+ "notes": RelationshipDef(
152
+ target_entity="notes",
153
+ fetch_strategy="global_service",
154
+ method_or_service="notes",
155
+ filter_field="person_id",
156
+ requires_n_plus_1=True, # API requires one entity ID per call
157
+ ),
158
+ "listEntries": RelationshipDef(
159
+ target_entity="listEntries",
160
+ fetch_strategy="entity_method",
161
+ method_or_service="get_list_entries",
162
+ requires_n_plus_1=True,
163
+ ),
164
+ },
165
+ fetch_strategy=FetchStrategy.GLOBAL,
166
+ ),
167
+ "companies": EntitySchema(
168
+ name="companies",
169
+ service_attr="companies",
170
+ id_field="id",
171
+ filterable_fields=frozenset(
172
+ [
173
+ "id",
174
+ "name",
175
+ "domain",
176
+ "domains",
177
+ "createdAt",
178
+ "updatedAt",
179
+ ]
180
+ ),
181
+ computed_fields=frozenset([]),
182
+ relationships={
183
+ "people": RelationshipDef(
184
+ target_entity="persons",
185
+ fetch_strategy="entity_method",
186
+ method_or_service="get_associated_person_ids",
187
+ requires_n_plus_1=True,
188
+ ),
189
+ "opportunities": RelationshipDef(
190
+ target_entity="opportunities",
191
+ fetch_strategy="entity_method",
192
+ method_or_service="get_associated_opportunity_ids",
193
+ requires_n_plus_1=True,
194
+ ),
195
+ "interactions": RelationshipDef(
196
+ target_entity="interactions",
197
+ fetch_strategy="global_service",
198
+ method_or_service="interactions",
199
+ filter_field="company_id",
200
+ requires_n_plus_1=True, # API requires one entity ID per call
201
+ ),
202
+ "notes": RelationshipDef(
203
+ target_entity="notes",
204
+ fetch_strategy="global_service",
205
+ method_or_service="notes",
206
+ filter_field="company_id",
207
+ requires_n_plus_1=True, # API requires one entity ID per call
208
+ ),
209
+ "listEntries": RelationshipDef(
210
+ target_entity="listEntries",
211
+ fetch_strategy="entity_method",
212
+ method_or_service="get_list_entries",
213
+ requires_n_plus_1=True,
214
+ ),
215
+ },
216
+ fetch_strategy=FetchStrategy.GLOBAL,
217
+ ),
218
+ "opportunities": EntitySchema(
219
+ name="opportunities",
220
+ service_attr="opportunities",
221
+ id_field="id",
222
+ filterable_fields=frozenset(
223
+ [
224
+ "id",
225
+ "name",
226
+ "listId",
227
+ "createdAt",
228
+ "updatedAt",
229
+ ]
230
+ ),
231
+ computed_fields=frozenset([]),
232
+ relationships={
233
+ "people": RelationshipDef(
234
+ target_entity="persons",
235
+ fetch_strategy="entity_method",
236
+ method_or_service="get_associated_person_ids",
237
+ requires_n_plus_1=True,
238
+ ),
239
+ "companies": RelationshipDef(
240
+ target_entity="companies",
241
+ fetch_strategy="entity_method",
242
+ method_or_service="get_associated_company_ids",
243
+ requires_n_plus_1=True,
244
+ ),
245
+ "interactions": RelationshipDef(
246
+ target_entity="interactions",
247
+ fetch_strategy="global_service",
248
+ method_or_service="interactions",
249
+ filter_field="opportunity_id",
250
+ requires_n_plus_1=True, # API requires one entity ID per call
251
+ ),
252
+ },
253
+ api_version="v1",
254
+ fetch_strategy=FetchStrategy.GLOBAL,
255
+ ),
256
+ "lists": EntitySchema(
257
+ name="lists",
258
+ service_attr="lists",
259
+ id_field="id",
260
+ filterable_fields=frozenset(
261
+ [
262
+ "id",
263
+ "name",
264
+ "type",
265
+ "createdAt",
266
+ ]
267
+ ),
268
+ computed_fields=frozenset([]),
269
+ relationships={
270
+ "entries": RelationshipDef(
271
+ target_entity="listEntries",
272
+ fetch_strategy="entity_method",
273
+ method_or_service="entries",
274
+ cardinality="many",
275
+ requires_n_plus_1=True,
276
+ ),
277
+ },
278
+ fetch_strategy=FetchStrategy.GLOBAL,
279
+ api_version="v2",
280
+ ),
281
+ "listEntries": EntitySchema(
282
+ name="listEntries",
283
+ service_attr="lists", # Uses list service, then .entries()
284
+ id_field="id",
285
+ filterable_fields=frozenset(
286
+ [
287
+ "id",
288
+ "listId",
289
+ "listName", # Alternative to listId (resolved at execution time)
290
+ "entityId",
291
+ "entityType",
292
+ "createdAt",
293
+ "updatedAt",
294
+ ]
295
+ ),
296
+ computed_fields=frozenset([]),
297
+ relationships={
298
+ "entity": RelationshipDef(
299
+ target_entity="entity", # Dynamic based on entityType
300
+ fetch_strategy="entity_method",
301
+ method_or_service="get_entity",
302
+ cardinality="one",
303
+ requires_n_plus_1=True,
304
+ ),
305
+ },
306
+ api_version="v2",
307
+ fetch_strategy=FetchStrategy.REQUIRES_PARENT,
308
+ required_filters=frozenset(["listId", "listName"]), # Either listId OR listName
309
+ parent_filter_field="listId",
310
+ parent_id_type="ListId",
311
+ parent_method_name="entries",
312
+ ),
313
+ "interactions": EntitySchema(
314
+ name="interactions",
315
+ service_attr="interactions",
316
+ id_field="id",
317
+ filterable_fields=frozenset(
318
+ [
319
+ "id",
320
+ "type",
321
+ "subject",
322
+ "createdAt",
323
+ "happenedAt",
324
+ ]
325
+ ),
326
+ computed_fields=frozenset([]),
327
+ relationships={
328
+ "persons": RelationshipDef(
329
+ target_entity="persons",
330
+ fetch_strategy="entity_method",
331
+ method_or_service="get_associated_person_ids",
332
+ requires_n_plus_1=True,
333
+ ),
334
+ },
335
+ api_version="v1",
336
+ fetch_strategy=FetchStrategy.RELATIONSHIP_ONLY,
337
+ ),
338
+ "notes": EntitySchema(
339
+ name="notes",
340
+ service_attr="notes",
341
+ id_field="id",
342
+ filterable_fields=frozenset(
343
+ [
344
+ "id",
345
+ "content",
346
+ "createdAt",
347
+ "creatorId",
348
+ ]
349
+ ),
350
+ computed_fields=frozenset([]),
351
+ relationships={},
352
+ api_version="v1",
353
+ fetch_strategy=FetchStrategy.RELATIONSHIP_ONLY,
354
+ ),
355
+ }
356
+
357
+
358
+ def get_entity_schema(entity_name: str) -> EntitySchema | None:
359
+ """Get schema for an entity type.
360
+
361
+ Args:
362
+ entity_name: Entity type name (e.g., "persons")
363
+
364
+ Returns:
365
+ EntitySchema or None if not found
366
+ """
367
+ return SCHEMA_REGISTRY.get(entity_name)
368
+
369
+
370
+ def get_relationship(entity_name: str, relationship_name: str) -> RelationshipDef | None:
371
+ """Get relationship definition.
372
+
373
+ Args:
374
+ entity_name: Source entity type (e.g., "persons")
375
+ relationship_name: Relationship name (e.g., "companies")
376
+
377
+ Returns:
378
+ RelationshipDef or None if not found
379
+ """
380
+ schema = SCHEMA_REGISTRY.get(entity_name)
381
+ if schema is None:
382
+ return None
383
+ return schema.relationships.get(relationship_name)
384
+
385
+
386
+ def is_valid_field_path(entity_name: str, path: str) -> bool:
387
+ """Check if a field path is valid for an entity.
388
+
389
+ Handles nested paths like "companies._count" or "fields.Status".
390
+
391
+ Args:
392
+ entity_name: Entity type name
393
+ path: Field path to validate
394
+
395
+ Returns:
396
+ True if the path is valid
397
+ """
398
+ schema = SCHEMA_REGISTRY.get(entity_name)
399
+ if schema is None:
400
+ return False
401
+
402
+ parts = path.split(".")
403
+
404
+ # Simple field
405
+ if len(parts) == 1:
406
+ field_name = parts[0]
407
+ return (
408
+ field_name in schema.filterable_fields
409
+ or field_name in schema.computed_fields
410
+ or field_name == schema.id_field
411
+ or field_name.startswith("fields.") # List entry fields
412
+ )
413
+
414
+ # Relationship path (e.g., "companies._count")
415
+ first_part = parts[0]
416
+ if first_part in schema.relationships:
417
+ remaining = ".".join(parts[1:])
418
+ # _count is always valid for relationships
419
+ if remaining == "_count":
420
+ return True
421
+ # Validate against target entity
422
+ rel = schema.relationships[first_part]
423
+ return is_valid_field_path(rel.target_entity, remaining)
424
+
425
+ # fields.* for list entries
426
+ return first_part == "fields"
427
+
428
+
429
+ def get_supported_entities() -> list[str]:
430
+ """Get list of all supported entity types."""
431
+ return list(SCHEMA_REGISTRY.keys())
432
+
433
+
434
+ def get_entity_relationships(entity_name: str) -> list[str]:
435
+ """Get list of relationship names for an entity."""
436
+ schema = SCHEMA_REGISTRY.get(entity_name)
437
+ if schema is None:
438
+ return []
439
+ return list(schema.relationships.keys())