struai 0.0.1__py3-none-any.whl → 0.1.0__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.
- struai/__init__.py +108 -2
- struai/_base.py +346 -0
- struai/_client.py +111 -0
- struai/_exceptions.py +103 -0
- struai/_version.py +2 -0
- struai/models/__init__.py +60 -0
- struai/models/common.py +36 -0
- struai/models/drawings.py +81 -0
- struai/models/entities.py +37 -0
- struai/models/projects.py +83 -0
- struai/models/search.py +70 -0
- struai/py.typed +0 -0
- struai/resources/__init__.py +5 -0
- struai/resources/drawings.py +122 -0
- struai/resources/projects.py +628 -0
- struai-0.1.0.dist-info/METADATA +148 -0
- struai-0.1.0.dist-info/RECORD +18 -0
- struai-0.0.1.dist-info/METADATA +0 -17
- struai-0.0.1.dist-info/RECORD +0 -4
- {struai-0.0.1.dist-info → struai-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"""Tier 2: Projects, Sheets, Search API."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import time
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, BinaryIO, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
from .._exceptions import JobFailedError, TimeoutError
|
|
9
|
+
from ..models.entities import Entity, Fact
|
|
10
|
+
from ..models.projects import JobStatus, Project, Sheet, SheetResult
|
|
11
|
+
from ..models.search import QueryResponse, SearchResponse
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .._base import AsyncBaseClient, BaseClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# Job handles for async sheet ingestion
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Job:
|
|
23
|
+
"""Handle for an async sheet ingestion job (sync)."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, client: "BaseClient", project_id: str, job_id: str):
|
|
26
|
+
self._client = client
|
|
27
|
+
self._project_id = project_id
|
|
28
|
+
self._job_id = job_id
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def id(self) -> str:
|
|
32
|
+
return self._job_id
|
|
33
|
+
|
|
34
|
+
def status(self) -> JobStatus:
|
|
35
|
+
"""Check current job status."""
|
|
36
|
+
return self._client.get(
|
|
37
|
+
f"/projects/{self._project_id}/jobs/{self._job_id}",
|
|
38
|
+
cast_to=JobStatus,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def wait(
|
|
42
|
+
self,
|
|
43
|
+
timeout: float = 120,
|
|
44
|
+
poll_interval: float = 2,
|
|
45
|
+
) -> SheetResult:
|
|
46
|
+
"""Wait for job completion.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
timeout: Maximum seconds to wait
|
|
50
|
+
poll_interval: Seconds between status checks
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
SheetResult on success
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
TimeoutError: If job doesn't complete in time
|
|
57
|
+
JobFailedError: If job fails
|
|
58
|
+
"""
|
|
59
|
+
start = time.time()
|
|
60
|
+
while time.time() - start < timeout:
|
|
61
|
+
status = self.status()
|
|
62
|
+
|
|
63
|
+
if status.is_complete:
|
|
64
|
+
return status.result # type: ignore
|
|
65
|
+
|
|
66
|
+
if status.is_failed:
|
|
67
|
+
raise JobFailedError(
|
|
68
|
+
f"Job {self._job_id} failed: {status.error}",
|
|
69
|
+
job_id=self._job_id,
|
|
70
|
+
error=status.error or "Unknown error",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
time.sleep(poll_interval)
|
|
74
|
+
|
|
75
|
+
raise TimeoutError(f"Job {self._job_id} did not complete within {timeout}s")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AsyncJob:
|
|
79
|
+
"""Handle for an async sheet ingestion job (async)."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, client: "AsyncBaseClient", project_id: str, job_id: str):
|
|
82
|
+
self._client = client
|
|
83
|
+
self._project_id = project_id
|
|
84
|
+
self._job_id = job_id
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def id(self) -> str:
|
|
88
|
+
return self._job_id
|
|
89
|
+
|
|
90
|
+
async def status(self) -> JobStatus:
|
|
91
|
+
"""Check current job status."""
|
|
92
|
+
return await self._client.get(
|
|
93
|
+
f"/projects/{self._project_id}/jobs/{self._job_id}",
|
|
94
|
+
cast_to=JobStatus,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async def wait(
|
|
98
|
+
self,
|
|
99
|
+
timeout: float = 120,
|
|
100
|
+
poll_interval: float = 2,
|
|
101
|
+
) -> SheetResult:
|
|
102
|
+
"""Wait for job completion (async)."""
|
|
103
|
+
start = time.time()
|
|
104
|
+
while time.time() - start < timeout:
|
|
105
|
+
status = await self.status()
|
|
106
|
+
|
|
107
|
+
if status.is_complete:
|
|
108
|
+
return status.result # type: ignore
|
|
109
|
+
|
|
110
|
+
if status.is_failed:
|
|
111
|
+
raise JobFailedError(
|
|
112
|
+
f"Job {self._job_id} failed: {status.error}",
|
|
113
|
+
job_id=self._job_id,
|
|
114
|
+
error=status.error or "Unknown error",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
await asyncio.sleep(poll_interval)
|
|
118
|
+
|
|
119
|
+
raise TimeoutError(f"Job {self._job_id} did not complete within {timeout}s")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# =============================================================================
|
|
123
|
+
# Sheets resource
|
|
124
|
+
# =============================================================================
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Sheets:
|
|
128
|
+
"""Sheet ingestion API (sync)."""
|
|
129
|
+
|
|
130
|
+
def __init__(self, client: "BaseClient", project_id: str):
|
|
131
|
+
self._client = client
|
|
132
|
+
self._project_id = project_id
|
|
133
|
+
|
|
134
|
+
def add(
|
|
135
|
+
self,
|
|
136
|
+
file: Union[str, Path, bytes, BinaryIO],
|
|
137
|
+
page: int,
|
|
138
|
+
webhook_url: Optional[str] = None,
|
|
139
|
+
) -> Job:
|
|
140
|
+
"""Add a sheet to the project (async job).
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
file: PDF file
|
|
144
|
+
page: Page number (1-indexed)
|
|
145
|
+
webhook_url: Optional callback URL when complete
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Job handle for polling/waiting
|
|
149
|
+
"""
|
|
150
|
+
files = self._prepare_file(file)
|
|
151
|
+
data: Dict[str, str] = {"page": str(page)}
|
|
152
|
+
if webhook_url:
|
|
153
|
+
data["webhook_url"] = webhook_url
|
|
154
|
+
|
|
155
|
+
response = self._client.post(
|
|
156
|
+
f"/projects/{self._project_id}/sheets",
|
|
157
|
+
files=files,
|
|
158
|
+
data=data,
|
|
159
|
+
)
|
|
160
|
+
return Job(self._client, self._project_id, response["job_id"])
|
|
161
|
+
|
|
162
|
+
def list(self, limit: int = 100) -> List[Sheet]:
|
|
163
|
+
"""List all sheets in project."""
|
|
164
|
+
response = self._client.get(
|
|
165
|
+
f"/projects/{self._project_id}/sheets",
|
|
166
|
+
params={"limit": limit},
|
|
167
|
+
)
|
|
168
|
+
return [Sheet.model_validate(s) for s in response["sheets"]]
|
|
169
|
+
|
|
170
|
+
def get(self, sheet_id: str) -> Sheet:
|
|
171
|
+
"""Get a sheet by ID."""
|
|
172
|
+
return self._client.get(
|
|
173
|
+
f"/projects/{self._project_id}/sheets/{sheet_id}",
|
|
174
|
+
cast_to=Sheet,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def delete(self, sheet_id: str) -> None:
|
|
178
|
+
"""Remove a sheet from the project."""
|
|
179
|
+
self._client.delete(f"/projects/{self._project_id}/sheets/{sheet_id}")
|
|
180
|
+
|
|
181
|
+
def _prepare_file(self, file: Union[str, Path, bytes, BinaryIO]) -> dict:
|
|
182
|
+
if isinstance(file, (str, Path)):
|
|
183
|
+
path = Path(file)
|
|
184
|
+
return {"file": (path.name, open(path, "rb"), "application/pdf")}
|
|
185
|
+
elif isinstance(file, bytes):
|
|
186
|
+
return {"file": ("document.pdf", file, "application/pdf")}
|
|
187
|
+
else:
|
|
188
|
+
name = getattr(file, "name", "document.pdf")
|
|
189
|
+
if hasattr(name, "split"):
|
|
190
|
+
name = Path(name).name
|
|
191
|
+
return {"file": (name, file, "application/pdf")}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class AsyncSheets:
|
|
195
|
+
"""Sheet ingestion API (async)."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, client: "AsyncBaseClient", project_id: str):
|
|
198
|
+
self._client = client
|
|
199
|
+
self._project_id = project_id
|
|
200
|
+
|
|
201
|
+
async def add(
|
|
202
|
+
self,
|
|
203
|
+
file: Union[str, Path, bytes, BinaryIO],
|
|
204
|
+
page: int,
|
|
205
|
+
webhook_url: Optional[str] = None,
|
|
206
|
+
) -> AsyncJob:
|
|
207
|
+
"""Add a sheet to the project (async job)."""
|
|
208
|
+
files = self._prepare_file(file)
|
|
209
|
+
data: Dict[str, str] = {"page": str(page)}
|
|
210
|
+
if webhook_url:
|
|
211
|
+
data["webhook_url"] = webhook_url
|
|
212
|
+
|
|
213
|
+
response = await self._client.post(
|
|
214
|
+
f"/projects/{self._project_id}/sheets",
|
|
215
|
+
files=files,
|
|
216
|
+
data=data,
|
|
217
|
+
)
|
|
218
|
+
return AsyncJob(self._client, self._project_id, response["job_id"])
|
|
219
|
+
|
|
220
|
+
async def list(self, limit: int = 100) -> List[Sheet]:
|
|
221
|
+
"""List all sheets in project."""
|
|
222
|
+
response = await self._client.get(
|
|
223
|
+
f"/projects/{self._project_id}/sheets",
|
|
224
|
+
params={"limit": limit},
|
|
225
|
+
)
|
|
226
|
+
return [Sheet.model_validate(s) for s in response["sheets"]]
|
|
227
|
+
|
|
228
|
+
async def get(self, sheet_id: str) -> Sheet:
|
|
229
|
+
"""Get a sheet by ID."""
|
|
230
|
+
return await self._client.get(
|
|
231
|
+
f"/projects/{self._project_id}/sheets/{sheet_id}",
|
|
232
|
+
cast_to=Sheet,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
async def delete(self, sheet_id: str) -> None:
|
|
236
|
+
"""Remove a sheet from the project."""
|
|
237
|
+
await self._client.delete(f"/projects/{self._project_id}/sheets/{sheet_id}")
|
|
238
|
+
|
|
239
|
+
def _prepare_file(self, file: Union[str, Path, bytes, BinaryIO]) -> dict:
|
|
240
|
+
if isinstance(file, (str, Path)):
|
|
241
|
+
path = Path(file)
|
|
242
|
+
return {"file": (path.name, open(path, "rb"), "application/pdf")}
|
|
243
|
+
elif isinstance(file, bytes):
|
|
244
|
+
return {"file": ("document.pdf", file, "application/pdf")}
|
|
245
|
+
else:
|
|
246
|
+
name = getattr(file, "name", "document.pdf")
|
|
247
|
+
if hasattr(name, "split"):
|
|
248
|
+
name = Path(name).name
|
|
249
|
+
return {"file": (name, file, "application/pdf")}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# =============================================================================
|
|
253
|
+
# Entities resource
|
|
254
|
+
# =============================================================================
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class Entities:
|
|
258
|
+
"""Entity retrieval API (sync)."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, client: "BaseClient", project_id: str):
|
|
261
|
+
self._client = client
|
|
262
|
+
self._project_id = project_id
|
|
263
|
+
|
|
264
|
+
def list(
|
|
265
|
+
self,
|
|
266
|
+
sheet_id: Optional[str] = None,
|
|
267
|
+
type: Optional[str] = None,
|
|
268
|
+
limit: int = 100,
|
|
269
|
+
) -> List[Entity]:
|
|
270
|
+
"""List entities in project."""
|
|
271
|
+
params: Dict[str, Union[str, int]] = {"limit": limit}
|
|
272
|
+
if sheet_id:
|
|
273
|
+
params["sheet_id"] = sheet_id
|
|
274
|
+
if type:
|
|
275
|
+
params["type"] = type
|
|
276
|
+
|
|
277
|
+
response = self._client.get(
|
|
278
|
+
f"/projects/{self._project_id}/entities",
|
|
279
|
+
params=params,
|
|
280
|
+
)
|
|
281
|
+
return [Entity.model_validate(e) for e in response["entities"]]
|
|
282
|
+
|
|
283
|
+
def get(self, entity_id: str) -> Entity:
|
|
284
|
+
"""Get entity with all relationships."""
|
|
285
|
+
return self._client.get(
|
|
286
|
+
f"/projects/{self._project_id}/entities/{entity_id}",
|
|
287
|
+
cast_to=Entity,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class AsyncEntities:
|
|
292
|
+
"""Entity retrieval API (async)."""
|
|
293
|
+
|
|
294
|
+
def __init__(self, client: "AsyncBaseClient", project_id: str):
|
|
295
|
+
self._client = client
|
|
296
|
+
self._project_id = project_id
|
|
297
|
+
|
|
298
|
+
async def list(
|
|
299
|
+
self,
|
|
300
|
+
sheet_id: Optional[str] = None,
|
|
301
|
+
type: Optional[str] = None,
|
|
302
|
+
limit: int = 100,
|
|
303
|
+
) -> List[Entity]:
|
|
304
|
+
"""List entities in project."""
|
|
305
|
+
params: Dict[str, Union[str, int]] = {"limit": limit}
|
|
306
|
+
if sheet_id:
|
|
307
|
+
params["sheet_id"] = sheet_id
|
|
308
|
+
if type:
|
|
309
|
+
params["type"] = type
|
|
310
|
+
|
|
311
|
+
response = await self._client.get(
|
|
312
|
+
f"/projects/{self._project_id}/entities",
|
|
313
|
+
params=params,
|
|
314
|
+
)
|
|
315
|
+
return [Entity.model_validate(e) for e in response["entities"]]
|
|
316
|
+
|
|
317
|
+
async def get(self, entity_id: str) -> Entity:
|
|
318
|
+
"""Get entity with all relationships."""
|
|
319
|
+
return await self._client.get(
|
|
320
|
+
f"/projects/{self._project_id}/entities/{entity_id}",
|
|
321
|
+
cast_to=Entity,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# =============================================================================
|
|
326
|
+
# Relationships resource
|
|
327
|
+
# =============================================================================
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class Relationships:
|
|
331
|
+
"""Relationship retrieval API (sync)."""
|
|
332
|
+
|
|
333
|
+
def __init__(self, client: "BaseClient", project_id: str):
|
|
334
|
+
self._client = client
|
|
335
|
+
self._project_id = project_id
|
|
336
|
+
|
|
337
|
+
def list(
|
|
338
|
+
self,
|
|
339
|
+
source_id: Optional[str] = None,
|
|
340
|
+
target_id: Optional[str] = None,
|
|
341
|
+
type: Optional[str] = None,
|
|
342
|
+
limit: int = 100,
|
|
343
|
+
) -> List[Fact]:
|
|
344
|
+
"""List relationships in project."""
|
|
345
|
+
params: Dict[str, Union[str, int]] = {"limit": limit}
|
|
346
|
+
if source_id:
|
|
347
|
+
params["source_id"] = source_id
|
|
348
|
+
if target_id:
|
|
349
|
+
params["target_id"] = target_id
|
|
350
|
+
if type:
|
|
351
|
+
params["type"] = type
|
|
352
|
+
|
|
353
|
+
response = self._client.get(
|
|
354
|
+
f"/projects/{self._project_id}/relationships",
|
|
355
|
+
params=params,
|
|
356
|
+
)
|
|
357
|
+
return [Fact.model_validate(r) for r in response["relationships"]]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class AsyncRelationships:
|
|
361
|
+
"""Relationship retrieval API (async)."""
|
|
362
|
+
|
|
363
|
+
def __init__(self, client: "AsyncBaseClient", project_id: str):
|
|
364
|
+
self._client = client
|
|
365
|
+
self._project_id = project_id
|
|
366
|
+
|
|
367
|
+
async def list(
|
|
368
|
+
self,
|
|
369
|
+
source_id: Optional[str] = None,
|
|
370
|
+
target_id: Optional[str] = None,
|
|
371
|
+
type: Optional[str] = None,
|
|
372
|
+
limit: int = 100,
|
|
373
|
+
) -> List[Fact]:
|
|
374
|
+
"""List relationships in project."""
|
|
375
|
+
params: Dict[str, Union[str, int]] = {"limit": limit}
|
|
376
|
+
if source_id:
|
|
377
|
+
params["source_id"] = source_id
|
|
378
|
+
if target_id:
|
|
379
|
+
params["target_id"] = target_id
|
|
380
|
+
if type:
|
|
381
|
+
params["type"] = type
|
|
382
|
+
|
|
383
|
+
response = await self._client.get(
|
|
384
|
+
f"/projects/{self._project_id}/relationships",
|
|
385
|
+
params=params,
|
|
386
|
+
)
|
|
387
|
+
return [Fact.model_validate(r) for r in response["relationships"]]
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# =============================================================================
|
|
391
|
+
# ProjectInstance - bound to a specific project
|
|
392
|
+
# =============================================================================
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class ProjectInstance:
|
|
396
|
+
"""A project with access to nested resources (sync)."""
|
|
397
|
+
|
|
398
|
+
def __init__(self, client: "BaseClient", project: Project):
|
|
399
|
+
self._client = client
|
|
400
|
+
self._project = project
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def id(self) -> str:
|
|
404
|
+
return self._project.id
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def name(self) -> str:
|
|
408
|
+
return self._project.name
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def description(self) -> Optional[str]:
|
|
412
|
+
return self._project.description
|
|
413
|
+
|
|
414
|
+
@cached_property
|
|
415
|
+
def sheets(self) -> Sheets:
|
|
416
|
+
"""Sheet ingestion API."""
|
|
417
|
+
return Sheets(self._client, self.id)
|
|
418
|
+
|
|
419
|
+
@cached_property
|
|
420
|
+
def entities(self) -> Entities:
|
|
421
|
+
"""Entity retrieval API."""
|
|
422
|
+
return Entities(self._client, self.id)
|
|
423
|
+
|
|
424
|
+
@cached_property
|
|
425
|
+
def relationships(self) -> Relationships:
|
|
426
|
+
"""Relationship retrieval API."""
|
|
427
|
+
return Relationships(self._client, self.id)
|
|
428
|
+
|
|
429
|
+
def search(
|
|
430
|
+
self,
|
|
431
|
+
query: str,
|
|
432
|
+
limit: int = 10,
|
|
433
|
+
filters: Optional[Dict] = None,
|
|
434
|
+
include_graph_context: bool = True,
|
|
435
|
+
) -> SearchResponse:
|
|
436
|
+
"""Semantic search across project.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
query: Search query text
|
|
440
|
+
limit: Max results to return
|
|
441
|
+
filters: Optional filters (sheet_id, entity_type)
|
|
442
|
+
include_graph_context: Include related entities
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
SearchResponse with results
|
|
446
|
+
"""
|
|
447
|
+
body: Dict = {
|
|
448
|
+
"query": query,
|
|
449
|
+
"limit": limit,
|
|
450
|
+
"include_graph_context": include_graph_context,
|
|
451
|
+
}
|
|
452
|
+
if filters:
|
|
453
|
+
body["filters"] = filters
|
|
454
|
+
|
|
455
|
+
return self._client.post(
|
|
456
|
+
f"/projects/{self.id}/search",
|
|
457
|
+
json=body,
|
|
458
|
+
cast_to=SearchResponse,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def query(self, question: str) -> QueryResponse:
|
|
462
|
+
"""Ask a natural language question.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
question: Question in natural language
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
QueryResponse with answer and sources
|
|
469
|
+
"""
|
|
470
|
+
return self._client.post(
|
|
471
|
+
f"/projects/{self.id}/query",
|
|
472
|
+
json={"question": question},
|
|
473
|
+
cast_to=QueryResponse,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
def delete(self) -> None:
|
|
477
|
+
"""Delete this project and all its data."""
|
|
478
|
+
self._client.delete(f"/projects/{self.id}")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class AsyncProjectInstance:
|
|
482
|
+
"""A project with access to nested resources (async)."""
|
|
483
|
+
|
|
484
|
+
def __init__(self, client: "AsyncBaseClient", project: Project):
|
|
485
|
+
self._client = client
|
|
486
|
+
self._project = project
|
|
487
|
+
|
|
488
|
+
@property
|
|
489
|
+
def id(self) -> str:
|
|
490
|
+
return self._project.id
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def name(self) -> str:
|
|
494
|
+
return self._project.name
|
|
495
|
+
|
|
496
|
+
@property
|
|
497
|
+
def description(self) -> Optional[str]:
|
|
498
|
+
return self._project.description
|
|
499
|
+
|
|
500
|
+
@cached_property
|
|
501
|
+
def sheets(self) -> AsyncSheets:
|
|
502
|
+
"""Sheet ingestion API."""
|
|
503
|
+
return AsyncSheets(self._client, self.id)
|
|
504
|
+
|
|
505
|
+
@cached_property
|
|
506
|
+
def entities(self) -> AsyncEntities:
|
|
507
|
+
"""Entity retrieval API."""
|
|
508
|
+
return AsyncEntities(self._client, self.id)
|
|
509
|
+
|
|
510
|
+
@cached_property
|
|
511
|
+
def relationships(self) -> AsyncRelationships:
|
|
512
|
+
"""Relationship retrieval API."""
|
|
513
|
+
return AsyncRelationships(self._client, self.id)
|
|
514
|
+
|
|
515
|
+
async def search(
|
|
516
|
+
self,
|
|
517
|
+
query: str,
|
|
518
|
+
limit: int = 10,
|
|
519
|
+
filters: Optional[Dict] = None,
|
|
520
|
+
include_graph_context: bool = True,
|
|
521
|
+
) -> SearchResponse:
|
|
522
|
+
"""Semantic search across project (async)."""
|
|
523
|
+
body: Dict = {
|
|
524
|
+
"query": query,
|
|
525
|
+
"limit": limit,
|
|
526
|
+
"include_graph_context": include_graph_context,
|
|
527
|
+
}
|
|
528
|
+
if filters:
|
|
529
|
+
body["filters"] = filters
|
|
530
|
+
|
|
531
|
+
return await self._client.post(
|
|
532
|
+
f"/projects/{self.id}/search",
|
|
533
|
+
json=body,
|
|
534
|
+
cast_to=SearchResponse,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
async def query(self, question: str) -> QueryResponse:
|
|
538
|
+
"""Ask a natural language question (async)."""
|
|
539
|
+
return await self._client.post(
|
|
540
|
+
f"/projects/{self.id}/query",
|
|
541
|
+
json={"question": question},
|
|
542
|
+
cast_to=QueryResponse,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
async def delete(self) -> None:
|
|
546
|
+
"""Delete this project and all its data."""
|
|
547
|
+
await self._client.delete(f"/projects/{self.id}")
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# =============================================================================
|
|
551
|
+
# Projects resource (top-level)
|
|
552
|
+
# =============================================================================
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class Projects:
|
|
556
|
+
"""Tier 2: Project management API (sync)."""
|
|
557
|
+
|
|
558
|
+
def __init__(self, client: "BaseClient"):
|
|
559
|
+
self._client = client
|
|
560
|
+
|
|
561
|
+
def create(
|
|
562
|
+
self,
|
|
563
|
+
name: str,
|
|
564
|
+
description: Optional[str] = None,
|
|
565
|
+
) -> ProjectInstance:
|
|
566
|
+
"""Create a new project.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
name: Project name
|
|
570
|
+
description: Optional description
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
ProjectInstance with access to sheets, search, etc.
|
|
574
|
+
"""
|
|
575
|
+
data = self._client.post(
|
|
576
|
+
"/projects",
|
|
577
|
+
json={"name": name, "description": description},
|
|
578
|
+
cast_to=Project,
|
|
579
|
+
)
|
|
580
|
+
return ProjectInstance(self._client, data)
|
|
581
|
+
|
|
582
|
+
def list(self, limit: int = 100) -> List[Project]:
|
|
583
|
+
"""List all projects."""
|
|
584
|
+
response = self._client.get("/projects", params={"limit": limit})
|
|
585
|
+
return [Project.model_validate(p) for p in response["projects"]]
|
|
586
|
+
|
|
587
|
+
def get(self, project_id: str) -> ProjectInstance:
|
|
588
|
+
"""Get a project by ID."""
|
|
589
|
+
data = self._client.get(f"/projects/{project_id}", cast_to=Project)
|
|
590
|
+
return ProjectInstance(self._client, data)
|
|
591
|
+
|
|
592
|
+
def delete(self, project_id: str) -> None:
|
|
593
|
+
"""Delete a project and all its data."""
|
|
594
|
+
self._client.delete(f"/projects/{project_id}")
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class AsyncProjects:
|
|
598
|
+
"""Tier 2: Project management API (async)."""
|
|
599
|
+
|
|
600
|
+
def __init__(self, client: "AsyncBaseClient"):
|
|
601
|
+
self._client = client
|
|
602
|
+
|
|
603
|
+
async def create(
|
|
604
|
+
self,
|
|
605
|
+
name: str,
|
|
606
|
+
description: Optional[str] = None,
|
|
607
|
+
) -> AsyncProjectInstance:
|
|
608
|
+
"""Create a new project (async)."""
|
|
609
|
+
data = await self._client.post(
|
|
610
|
+
"/projects",
|
|
611
|
+
json={"name": name, "description": description},
|
|
612
|
+
cast_to=Project,
|
|
613
|
+
)
|
|
614
|
+
return AsyncProjectInstance(self._client, data)
|
|
615
|
+
|
|
616
|
+
async def list(self, limit: int = 100) -> List[Project]:
|
|
617
|
+
"""List all projects (async)."""
|
|
618
|
+
response = await self._client.get("/projects", params={"limit": limit})
|
|
619
|
+
return [Project.model_validate(p) for p in response["projects"]]
|
|
620
|
+
|
|
621
|
+
async def get(self, project_id: str) -> AsyncProjectInstance:
|
|
622
|
+
"""Get a project by ID (async)."""
|
|
623
|
+
data = await self._client.get(f"/projects/{project_id}", cast_to=Project)
|
|
624
|
+
return AsyncProjectInstance(self._client, data)
|
|
625
|
+
|
|
626
|
+
async def delete(self, project_id: str) -> None:
|
|
627
|
+
"""Delete a project and all its data (async)."""
|
|
628
|
+
await self._client.delete(f"/projects/{project_id}")
|