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
|
@@ -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())
|