raysdk-py 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.
raysdk_py/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .api import AsyncRaySearch as AsyncRaySearch
2
+ from .api import RaySearch as RaySearch
3
+
4
+ __all__ = ["AsyncRaySearch", "RaySearch"]
raysdk_py/api.py ADDED
@@ -0,0 +1,607 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.metadata
4
+ import os
5
+ from collections.abc import Mapping
6
+ from typing import TYPE_CHECKING, Any, TypeVar
7
+
8
+ import httpx
9
+ from pydantic import BaseModel, ValidationError
10
+
11
+ from .utils import normalize_base_url
12
+
13
+ if TYPE_CHECKING:
14
+ from .monitors import AsyncMonitorsClient, MonitorsClient
15
+ from .monitors.types import (
16
+ AnswerResponse,
17
+ FetchResponse,
18
+ HealthResponse,
19
+ SearchFetchRequest,
20
+ SearchResponse,
21
+ )
22
+ from .research import AsyncResearchClient, ResearchClient
23
+
24
+ DEFAULT_BASE_URL = "http://localhost:8000"
25
+ DEFAULT_TIMEOUT = 30.0
26
+
27
+ ModelT = TypeVar("ModelT", bound=BaseModel)
28
+
29
+
30
+ def _get_package_version() -> str:
31
+ try:
32
+ return importlib.metadata.version("raysdk-py")
33
+ except importlib.metadata.PackageNotFoundError:
34
+ return "0.1.0"
35
+
36
+
37
+ DEFAULT_USER_AGENT = f"raysdk-py/{_get_package_version()}"
38
+
39
+
40
+ class RaySDKError(Exception):
41
+ """Base exception for the SDK."""
42
+
43
+
44
+ class APIConnectionError(RaySDKError):
45
+ """Raised when the service cannot be reached."""
46
+
47
+
48
+ class APITimeoutError(RaySDKError):
49
+ """Raised when a request exceeds the configured timeout."""
50
+
51
+
52
+ class APIStatusError(RaySDKError):
53
+ """Raised when the API returns a non-success status code."""
54
+
55
+ def __init__(
56
+ self,
57
+ *,
58
+ status_code: int,
59
+ detail: str,
60
+ url: str,
61
+ payload: object | None = None,
62
+ ) -> None:
63
+ self.status_code = status_code
64
+ self.detail = detail
65
+ self.url = url
66
+ self.payload = payload
67
+ super().__init__(f"API request failed with status {status_code}: {detail}")
68
+
69
+
70
+ class APIResponseValidationError(RaySDKError):
71
+ """Raised when an API response does not match the expected schema."""
72
+
73
+ def __init__(
74
+ self,
75
+ *,
76
+ model_name: str,
77
+ payload: object,
78
+ cause: ValidationError,
79
+ ) -> None:
80
+ self.model_name = model_name
81
+ self.payload = payload
82
+ self.cause = cause
83
+ super().__init__(f"failed to validate response as {model_name}: {cause}")
84
+
85
+
86
+ def _resolve_api_key(api_key: str | None) -> str:
87
+ token = api_key or os.environ.get("RAYSEARCH_API_KEY")
88
+ if token is None or not token.strip():
89
+ raise ValueError(
90
+ "API key must be provided as an argument or in RAYSEARCH_API_KEY."
91
+ )
92
+ return token
93
+
94
+
95
+ def _merge_headers(
96
+ *,
97
+ api_key: str,
98
+ default_headers: Mapping[str, str] | None,
99
+ user_agent: str,
100
+ ) -> dict[str, str]:
101
+ headers: dict[str, str] = {
102
+ "Accept": "application/json",
103
+ "Authorization": f"Bearer {api_key}",
104
+ "User-Agent": user_agent,
105
+ }
106
+ if default_headers:
107
+ headers.update(default_headers)
108
+ return headers
109
+
110
+
111
+ def _extract_error_detail(response: httpx.Response) -> tuple[str, object | None]:
112
+ try:
113
+ payload = response.json()
114
+ except ValueError:
115
+ text = response.text.strip()
116
+ return text or response.reason_phrase, None
117
+
118
+ if isinstance(payload, dict):
119
+ detail = payload.get("detail")
120
+ if isinstance(detail, str) and detail.strip():
121
+ return detail, payload
122
+ return response.reason_phrase, payload
123
+
124
+
125
+ def validate_response_model(model_type: type[ModelT], payload: object) -> ModelT:
126
+ try:
127
+ return model_type.model_validate(payload)
128
+ except ValidationError as exc:
129
+ raise APIResponseValidationError(
130
+ model_name=model_type.__name__,
131
+ payload=payload,
132
+ cause=exc,
133
+ ) from exc
134
+
135
+
136
+ def serialize_request_model(
137
+ model_type: type[ModelT],
138
+ payload: ModelT | Mapping[str, Any],
139
+ ) -> dict[str, Any]:
140
+ model = (
141
+ payload
142
+ if isinstance(payload, model_type)
143
+ else model_type.model_validate(payload)
144
+ )
145
+ return model.model_dump(mode="json", exclude_none=True)
146
+
147
+
148
+ def _raise_non_json_error(response: httpx.Response, exc: ValueError) -> None:
149
+ try:
150
+ raise ValidationError.from_exception_data(
151
+ title="JSON",
152
+ line_errors=[],
153
+ )
154
+ except ValidationError as validation_error:
155
+ raise APIResponseValidationError(
156
+ model_name="JSON",
157
+ payload=response.text,
158
+ cause=validation_error,
159
+ ) from exc
160
+
161
+
162
+ class SyncAPIClient:
163
+ def __init__(
164
+ self,
165
+ *,
166
+ base_url: str = DEFAULT_BASE_URL,
167
+ api_key: str | None = None,
168
+ timeout: float = DEFAULT_TIMEOUT,
169
+ client: httpx.Client | None = None,
170
+ default_headers: Mapping[str, str] | None = None,
171
+ user_agent: str = DEFAULT_USER_AGENT,
172
+ ) -> None:
173
+ resolved_api_key = _resolve_api_key(api_key)
174
+ self.base_url = normalize_base_url(base_url)
175
+ self.timeout = timeout
176
+ self.headers = _merge_headers(
177
+ api_key=resolved_api_key,
178
+ default_headers=default_headers,
179
+ user_agent=user_agent,
180
+ )
181
+ self._owns_client = client is None
182
+ self._client = client or httpx.Client(timeout=timeout)
183
+
184
+ def close(self) -> None:
185
+ if self._owns_client:
186
+ self._client.close()
187
+
188
+ def __enter__(self) -> SyncAPIClient:
189
+ return self
190
+
191
+ def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
192
+ del exc_type, exc, tb
193
+ self.close()
194
+
195
+ def _url_for(self, path: str) -> str:
196
+ return f"{self.base_url}{path}"
197
+
198
+ def request(
199
+ self,
200
+ endpoint: str,
201
+ data: Mapping[str, Any] | str | None = None,
202
+ method: str = "POST",
203
+ params: Mapping[str, str | int] | None = None,
204
+ headers: Mapping[str, str] | None = None,
205
+ ) -> object:
206
+ url = self._url_for(endpoint)
207
+ request_headers = dict(self.headers)
208
+ if headers:
209
+ request_headers.update(headers)
210
+
211
+ try:
212
+ if isinstance(data, str):
213
+ response = self._client.request(
214
+ method,
215
+ url,
216
+ headers=request_headers,
217
+ params=params,
218
+ content=data,
219
+ )
220
+ elif data is not None:
221
+ response = self._client.request(
222
+ method,
223
+ url,
224
+ headers=request_headers,
225
+ params=params,
226
+ json=data,
227
+ )
228
+ else:
229
+ response = self._client.request(
230
+ method,
231
+ url,
232
+ headers=request_headers,
233
+ params=params,
234
+ )
235
+ except httpx.TimeoutException as exc:
236
+ raise APITimeoutError(f"request timed out for {url}") from exc
237
+ except httpx.HTTPError as exc:
238
+ raise APIConnectionError(f"request failed for {url}") from exc
239
+
240
+ if response.is_error:
241
+ detail, payload = _extract_error_detail(response)
242
+ raise APIStatusError(
243
+ status_code=response.status_code,
244
+ detail=detail,
245
+ url=str(response.request.url),
246
+ payload=payload,
247
+ )
248
+
249
+ try:
250
+ return response.json()
251
+ except ValueError as exc:
252
+ _raise_non_json_error(response, exc)
253
+
254
+ def request_model(
255
+ self,
256
+ model_type: type[ModelT],
257
+ endpoint: str,
258
+ *,
259
+ data: Mapping[str, Any] | str | None = None,
260
+ method: str = "POST",
261
+ params: Mapping[str, str | int] | None = None,
262
+ headers: Mapping[str, str] | None = None,
263
+ ) -> ModelT:
264
+ payload = self.request(
265
+ endpoint,
266
+ data=data,
267
+ method=method,
268
+ params=params,
269
+ headers=headers,
270
+ )
271
+ return validate_response_model(model_type, payload)
272
+
273
+
274
+ class AsyncAPIClient:
275
+ def __init__(
276
+ self,
277
+ *,
278
+ base_url: str = DEFAULT_BASE_URL,
279
+ api_key: str | None = None,
280
+ timeout: float = DEFAULT_TIMEOUT,
281
+ client: httpx.AsyncClient | None = None,
282
+ default_headers: Mapping[str, str] | None = None,
283
+ user_agent: str = DEFAULT_USER_AGENT,
284
+ ) -> None:
285
+ resolved_api_key = _resolve_api_key(api_key)
286
+ self.base_url = normalize_base_url(base_url)
287
+ self.timeout = timeout
288
+ self.headers = _merge_headers(
289
+ api_key=resolved_api_key,
290
+ default_headers=default_headers,
291
+ user_agent=user_agent,
292
+ )
293
+ self._owns_client = client is None
294
+ self._client = client or httpx.AsyncClient(timeout=timeout)
295
+
296
+ async def aclose(self) -> None:
297
+ if self._owns_client:
298
+ await self._client.aclose()
299
+
300
+ async def __aenter__(self) -> AsyncAPIClient:
301
+ return self
302
+
303
+ async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
304
+ del exc_type, exc, tb
305
+ await self.aclose()
306
+
307
+ def _url_for(self, path: str) -> str:
308
+ return f"{self.base_url}{path}"
309
+
310
+ async def async_request(
311
+ self,
312
+ endpoint: str,
313
+ data: Mapping[str, Any] | str | None = None,
314
+ method: str = "POST",
315
+ params: Mapping[str, str | int] | None = None,
316
+ headers: Mapping[str, str] | None = None,
317
+ ) -> object:
318
+ url = self._url_for(endpoint)
319
+ request_headers = dict(self.headers)
320
+ if headers:
321
+ request_headers.update(headers)
322
+
323
+ try:
324
+ if isinstance(data, str):
325
+ response = await self._client.request(
326
+ method,
327
+ url,
328
+ headers=request_headers,
329
+ params=params,
330
+ content=data,
331
+ )
332
+ elif data is not None:
333
+ response = await self._client.request(
334
+ method,
335
+ url,
336
+ headers=request_headers,
337
+ params=params,
338
+ json=data,
339
+ )
340
+ else:
341
+ response = await self._client.request(
342
+ method,
343
+ url,
344
+ headers=request_headers,
345
+ params=params,
346
+ )
347
+ except httpx.TimeoutException as exc:
348
+ raise APITimeoutError(f"request timed out for {url}") from exc
349
+ except httpx.HTTPError as exc:
350
+ raise APIConnectionError(f"request failed for {url}") from exc
351
+
352
+ if response.is_error:
353
+ detail, payload = _extract_error_detail(response)
354
+ raise APIStatusError(
355
+ status_code=response.status_code,
356
+ detail=detail,
357
+ url=str(response.request.url),
358
+ payload=payload,
359
+ )
360
+
361
+ try:
362
+ return response.json()
363
+ except ValueError as exc:
364
+ _raise_non_json_error(response, exc)
365
+
366
+ async def request_model(
367
+ self,
368
+ model_type: type[ModelT],
369
+ endpoint: str,
370
+ *,
371
+ data: Mapping[str, Any] | str | None = None,
372
+ method: str = "POST",
373
+ params: Mapping[str, str | int] | None = None,
374
+ headers: Mapping[str, str] | None = None,
375
+ ) -> ModelT:
376
+ payload = await self.async_request(
377
+ endpoint,
378
+ data=data,
379
+ method=method,
380
+ params=params,
381
+ headers=headers,
382
+ )
383
+ return validate_response_model(model_type, payload)
384
+
385
+
386
+ class RaySearch(SyncAPIClient):
387
+ """Top-level synchronous client for the RaySearch API."""
388
+
389
+ monitors: MonitorsClient
390
+ research: ResearchClient
391
+
392
+ def __init__(
393
+ self,
394
+ api_key: str | None = None,
395
+ base_url: str = DEFAULT_BASE_URL,
396
+ timeout: float = DEFAULT_TIMEOUT,
397
+ user_agent: str | None = None,
398
+ client: httpx.Client | None = None,
399
+ default_headers: Mapping[str, str] | None = None,
400
+ ) -> None:
401
+ super().__init__(
402
+ base_url=base_url,
403
+ api_key=api_key,
404
+ timeout=timeout,
405
+ client=client,
406
+ default_headers=default_headers,
407
+ user_agent=user_agent or DEFAULT_USER_AGENT,
408
+ )
409
+
410
+ from .monitors import MonitorsClient
411
+ from .research import ResearchClient
412
+
413
+ self.monitors = MonitorsClient(self)
414
+ self.research = ResearchClient(self)
415
+
416
+ def healthz(self) -> HealthResponse:
417
+ return self.monitors.healthz()
418
+
419
+ def search(
420
+ self,
421
+ query: str,
422
+ *,
423
+ user_location: str,
424
+ additional_queries: list[str] | None = None,
425
+ mode: str = "auto",
426
+ max_results: int | None = None,
427
+ start_published_date: str | None = None,
428
+ end_published_date: str | None = None,
429
+ include_domains: list[str] | None = None,
430
+ exclude_domains: list[str] | None = None,
431
+ include_text: list[str] | None = None,
432
+ exclude_text: list[str] | None = None,
433
+ moderation: bool = True,
434
+ fetchs: SearchFetchRequest | dict[str, Any],
435
+ ) -> SearchResponse:
436
+ return self.monitors.search(
437
+ query,
438
+ user_location=user_location,
439
+ additional_queries=additional_queries,
440
+ mode=mode,
441
+ max_results=max_results,
442
+ start_published_date=start_published_date,
443
+ end_published_date=end_published_date,
444
+ include_domains=include_domains,
445
+ exclude_domains=exclude_domains,
446
+ include_text=include_text,
447
+ exclude_text=exclude_text,
448
+ moderation=moderation,
449
+ fetchs=fetchs,
450
+ )
451
+
452
+ def fetch(
453
+ self,
454
+ urls: str | list[str],
455
+ *,
456
+ crawl_mode: str = "fallback",
457
+ crawl_timeout: float | None = None,
458
+ content: bool | dict[str, Any] = False,
459
+ abstracts: bool | dict[str, Any] = False,
460
+ subpages: dict[str, Any] | None = None,
461
+ overview: bool | dict[str, Any] = False,
462
+ others: dict[str, Any] | None = None,
463
+ ) -> FetchResponse:
464
+ return self.monitors.fetch(
465
+ urls,
466
+ crawl_mode=crawl_mode,
467
+ crawl_timeout=crawl_timeout,
468
+ content=content,
469
+ abstracts=abstracts,
470
+ subpages=subpages,
471
+ overview=overview,
472
+ others=others,
473
+ )
474
+
475
+ def answer(
476
+ self,
477
+ query: str,
478
+ *,
479
+ json_schema: dict[str, Any] | None = None,
480
+ content: bool = False,
481
+ ) -> AnswerResponse:
482
+ return self.monitors.answer(
483
+ query,
484
+ json_schema=json_schema,
485
+ content=content,
486
+ )
487
+
488
+
489
+ class AsyncRaySearch(AsyncAPIClient):
490
+ """Top-level asynchronous client for the RaySearch API."""
491
+
492
+ monitors: AsyncMonitorsClient
493
+ research: AsyncResearchClient
494
+
495
+ def __init__(
496
+ self,
497
+ api_key: str | None = None,
498
+ base_url: str = DEFAULT_BASE_URL,
499
+ timeout: float = DEFAULT_TIMEOUT,
500
+ user_agent: str | None = None,
501
+ client: httpx.AsyncClient | None = None,
502
+ default_headers: Mapping[str, str] | None = None,
503
+ ) -> None:
504
+ super().__init__(
505
+ base_url=base_url,
506
+ api_key=api_key,
507
+ timeout=timeout,
508
+ client=client,
509
+ default_headers=default_headers,
510
+ user_agent=user_agent or DEFAULT_USER_AGENT,
511
+ )
512
+
513
+ from .monitors import AsyncMonitorsClient
514
+ from .research import AsyncResearchClient
515
+
516
+ self.monitors = AsyncMonitorsClient(self)
517
+ self.research = AsyncResearchClient(self)
518
+
519
+ async def healthz(self) -> HealthResponse:
520
+ return await self.monitors.healthz()
521
+
522
+ async def search(
523
+ self,
524
+ query: str,
525
+ *,
526
+ user_location: str,
527
+ additional_queries: list[str] | None = None,
528
+ mode: str = "auto",
529
+ max_results: int | None = None,
530
+ start_published_date: str | None = None,
531
+ end_published_date: str | None = None,
532
+ include_domains: list[str] | None = None,
533
+ exclude_domains: list[str] | None = None,
534
+ include_text: list[str] | None = None,
535
+ exclude_text: list[str] | None = None,
536
+ moderation: bool = True,
537
+ fetchs: SearchFetchRequest | dict[str, Any],
538
+ ) -> SearchResponse:
539
+ return await self.monitors.search(
540
+ query,
541
+ user_location=user_location,
542
+ additional_queries=additional_queries,
543
+ mode=mode,
544
+ max_results=max_results,
545
+ start_published_date=start_published_date,
546
+ end_published_date=end_published_date,
547
+ include_domains=include_domains,
548
+ exclude_domains=exclude_domains,
549
+ include_text=include_text,
550
+ exclude_text=exclude_text,
551
+ moderation=moderation,
552
+ fetchs=fetchs,
553
+ )
554
+
555
+ async def fetch(
556
+ self,
557
+ urls: str | list[str],
558
+ *,
559
+ crawl_mode: str = "fallback",
560
+ crawl_timeout: float | None = None,
561
+ content: bool | dict[str, Any] = False,
562
+ abstracts: bool | dict[str, Any] = False,
563
+ subpages: dict[str, Any] | None = None,
564
+ overview: bool | dict[str, Any] = False,
565
+ others: dict[str, Any] | None = None,
566
+ ) -> FetchResponse:
567
+ return await self.monitors.fetch(
568
+ urls,
569
+ crawl_mode=crawl_mode,
570
+ crawl_timeout=crawl_timeout,
571
+ content=content,
572
+ abstracts=abstracts,
573
+ subpages=subpages,
574
+ overview=overview,
575
+ others=others,
576
+ )
577
+
578
+ async def answer(
579
+ self,
580
+ query: str,
581
+ *,
582
+ json_schema: dict[str, Any] | None = None,
583
+ content: bool = False,
584
+ ) -> AnswerResponse:
585
+ return await self.monitors.answer(
586
+ query,
587
+ json_schema=json_schema,
588
+ content=content,
589
+ )
590
+
591
+
592
+ __all__ = [
593
+ "APIConnectionError",
594
+ "APIResponseValidationError",
595
+ "APIStatusError",
596
+ "APITimeoutError",
597
+ "AsyncAPIClient",
598
+ "AsyncRaySearch",
599
+ "DEFAULT_BASE_URL",
600
+ "DEFAULT_TIMEOUT",
601
+ "DEFAULT_USER_AGENT",
602
+ "RaySDKError",
603
+ "RaySearch",
604
+ "SyncAPIClient",
605
+ "serialize_request_model",
606
+ "validate_response_model",
607
+ ]
@@ -0,0 +1,59 @@
1
+ """Monitor-style API client modules for RaySearch."""
2
+
3
+ from .async_client import AsyncMonitorsClient
4
+ from .client import MonitorsClient
5
+ from .types import (
6
+ AnswerCitation,
7
+ AnswerRequest,
8
+ AnswerResponse,
9
+ CrawlMode,
10
+ FetchAbstractsRequest,
11
+ FetchContentDetail,
12
+ FetchContentRequest,
13
+ FetchContentTag,
14
+ FetchErrorTag,
15
+ FetchOthersRequest,
16
+ FetchOthersResult,
17
+ FetchOverviewRequest,
18
+ FetchRequest,
19
+ FetchResponse,
20
+ FetchResultItem,
21
+ FetchStatusError,
22
+ FetchStatusItem,
23
+ FetchSubpagesRequest,
24
+ FetchSubpagesResult,
25
+ HealthResponse,
26
+ SearchFetchRequest,
27
+ SearchMode,
28
+ SearchRequest,
29
+ SearchResponse,
30
+ )
31
+
32
+ __all__ = [
33
+ "AnswerCitation",
34
+ "AnswerRequest",
35
+ "AnswerResponse",
36
+ "AsyncMonitorsClient",
37
+ "CrawlMode",
38
+ "FetchAbstractsRequest",
39
+ "FetchContentDetail",
40
+ "FetchContentRequest",
41
+ "FetchContentTag",
42
+ "FetchErrorTag",
43
+ "FetchOthersRequest",
44
+ "FetchOthersResult",
45
+ "FetchOverviewRequest",
46
+ "FetchRequest",
47
+ "FetchResponse",
48
+ "FetchResultItem",
49
+ "FetchStatusError",
50
+ "FetchStatusItem",
51
+ "FetchSubpagesRequest",
52
+ "FetchSubpagesResult",
53
+ "HealthResponse",
54
+ "MonitorsClient",
55
+ "SearchFetchRequest",
56
+ "SearchMode",
57
+ "SearchRequest",
58
+ "SearchResponse",
59
+ ]
@@ -0,0 +1,33 @@
1
+ """Base async client classes for the RaySearch monitor-style endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from ..api import AsyncRaySearch
10
+
11
+
12
+ class AsyncMonitorsBaseClient:
13
+ """Base client for asynchronous monitor-style API operations."""
14
+
15
+ def __init__(self, client: AsyncRaySearch) -> None:
16
+ self._client = client
17
+
18
+ async def request(
19
+ self,
20
+ endpoint: str,
21
+ method: str = "POST",
22
+ data: Mapping[str, Any] | str | None = None,
23
+ params: dict[str, str | int] | None = None,
24
+ ) -> object:
25
+ return await self._client.async_request(
26
+ endpoint,
27
+ data=data,
28
+ method=method,
29
+ params=params,
30
+ )
31
+
32
+
33
+ __all__ = ["AsyncMonitorsBaseClient"]