bevor-sdk 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.
- bevor_sdk/__init__.py +6 -0
- bevor_sdk/_async/__init__.py +0 -0
- bevor_sdk/_async/client.py +97 -0
- bevor_sdk/_async/graph.py +76 -0
- bevor_sdk/_async/resources/__init__.py +24 -0
- bevor_sdk/_async/resources/_modules/__init__.py +4 -0
- bevor_sdk/_async/resources/_modules/events.py +88 -0
- bevor_sdk/_async/resources/_modules/pagination.py +50 -0
- bevor_sdk/_async/resources/chat/__init__.py +3 -0
- bevor_sdk/_async/resources/chat/chats.py +253 -0
- bevor_sdk/_async/resources/graph/__init__.py +5 -0
- bevor_sdk/_async/resources/graph/codes.py +365 -0
- bevor_sdk/_async/resources/graph/files.py +109 -0
- bevor_sdk/_async/resources/graph/nodes.py +207 -0
- bevor_sdk/_async/resources/manage/__init__.py +4 -0
- bevor_sdk/_async/resources/manage/projects.py +34 -0
- bevor_sdk/_async/resources/manage/users.py +103 -0
- bevor_sdk/_async/resources/security/__init__.py +13 -0
- bevor_sdk/_async/resources/security/analyses.py +697 -0
- bevor_sdk/_async/resources/security/findings.py +287 -0
- bevor_sdk/_async/resources/security/summary.py +107 -0
- bevor_sdk/_async/resources/security/transitions.py +104 -0
- bevor_sdk/_async/resources/security/versions.py +124 -0
- bevor_sdk/_shared/__init__.py +7 -0
- bevor_sdk/_shared/call_node.py +360 -0
- bevor_sdk/_shared/client.py +119 -0
- bevor_sdk/_shared/cursor.py +168 -0
- bevor_sdk/_shared/events.py +50 -0
- bevor_sdk/_shared/graph.py +335 -0
- bevor_sdk/_sync/__init__.py +0 -0
- bevor_sdk/_sync/client.py +97 -0
- bevor_sdk/_sync/graph.py +75 -0
- bevor_sdk/_sync/resources/__init__.py +24 -0
- bevor_sdk/_sync/resources/_modules/__init__.py +4 -0
- bevor_sdk/_sync/resources/_modules/events.py +88 -0
- bevor_sdk/_sync/resources/_modules/pagination.py +50 -0
- bevor_sdk/_sync/resources/chat/__init__.py +3 -0
- bevor_sdk/_sync/resources/chat/chats.py +253 -0
- bevor_sdk/_sync/resources/graph/__init__.py +5 -0
- bevor_sdk/_sync/resources/graph/codes.py +365 -0
- bevor_sdk/_sync/resources/graph/files.py +109 -0
- bevor_sdk/_sync/resources/graph/nodes.py +207 -0
- bevor_sdk/_sync/resources/manage/__init__.py +4 -0
- bevor_sdk/_sync/resources/manage/me.py +32 -0
- bevor_sdk/_sync/resources/manage/projects.py +34 -0
- bevor_sdk/_sync/resources/manage/users.py +103 -0
- bevor_sdk/_sync/resources/security/__init__.py +13 -0
- bevor_sdk/_sync/resources/security/analyses.py +683 -0
- bevor_sdk/_sync/resources/security/findings.py +287 -0
- bevor_sdk/_sync/resources/security/summary.py +107 -0
- bevor_sdk/_sync/resources/security/transitions.py +104 -0
- bevor_sdk/_sync/resources/security/versions.py +124 -0
- bevor_sdk/exceptions/__init__.py +173 -0
- bevor_sdk/helpers/__init__.py +0 -0
- bevor_sdk/helpers/call_chain/__init__.py +0 -0
- bevor_sdk/helpers/call_chain/enrich_impl.py +108 -0
- bevor_sdk/helpers/converted_schema/__init__.py +0 -0
- bevor_sdk/helpers/converted_schema/sarif.py +2443 -0
- bevor_sdk/helpers/converters/__init__.py +0 -0
- bevor_sdk/helpers/converters/sarif.py +303 -0
- bevor_sdk/helpers/json_schema/sarif.json +3315 -0
- bevor_sdk/helpers/source_fingerprint.py +100 -0
- bevor_sdk/helpers/static_analysis.py +119 -0
- bevor_sdk/types/__init__.py +3 -0
- bevor_sdk/types/_shared.py +14 -0
- bevor_sdk/types/dataclasses/__init__.py +17 -0
- bevor_sdk/types/dataclasses/graph.py +161 -0
- bevor_sdk/types/literals/__init__.py +4 -0
- bevor_sdk/types/literals/chat.py +5 -0
- bevor_sdk/types/literals/graph.py +61 -0
- bevor_sdk/types/literals/manage.py +20 -0
- bevor_sdk/types/literals/node_types.py +294 -0
- bevor_sdk/types/literals/security.py +38 -0
- bevor_sdk/types/literals/static_analysis.py +112 -0
- bevor_sdk/types/requests/__init__.py +3 -0
- bevor_sdk/types/requests/_shared.py +14 -0
- bevor_sdk/types/requests/chat/__init__.py +19 -0
- bevor_sdk/types/requests/graph/__init__.py +20 -0
- bevor_sdk/types/requests/graph/code.py +42 -0
- bevor_sdk/types/requests/graph/snapshot.py +30 -0
- bevor_sdk/types/requests/manage/__init__.py +8 -0
- bevor_sdk/types/requests/manage/projects.py +28 -0
- bevor_sdk/types/requests/security/__init__.py +41 -0
- bevor_sdk/types/requests/security/analyses.py +66 -0
- bevor_sdk/types/requests/security/findings.py +92 -0
- bevor_sdk/types/requests/security/transitions.py +19 -0
- bevor_sdk/types/requests/security/versions.py +24 -0
- bevor_sdk/types/responses/__init__.py +17 -0
- bevor_sdk/types/responses/_shared.py +61 -0
- bevor_sdk/types/responses/chat/__init__.py +45 -0
- bevor_sdk/types/responses/events.py +26 -0
- bevor_sdk/types/responses/graph/__init__.py +29 -0
- bevor_sdk/types/responses/graph/code.py +35 -0
- bevor_sdk/types/responses/graph/snapshot.py +88 -0
- bevor_sdk/types/responses/manage/__init__.py +36 -0
- bevor_sdk/types/responses/manage/projects.py +65 -0
- bevor_sdk/types/responses/manage/users.py +37 -0
- bevor_sdk/types/responses/manage/workspaces.py +73 -0
- bevor_sdk/types/responses/security/__init__.py +52 -0
- bevor_sdk/types/responses/security/analysis.py +108 -0
- bevor_sdk/types/responses/security/base.py +8 -0
- bevor_sdk/types/responses/security/findings.py +52 -0
- bevor_sdk/types/responses/security/summary.py +44 -0
- bevor_sdk/types/responses/security/transitions.py +23 -0
- bevor_sdk-0.1.0.dist-info/METADATA +9 -0
- bevor_sdk-0.1.0.dist-info/RECORD +107 -0
- bevor_sdk-0.1.0.dist-info/WHEEL +4 -0
bevor_sdk/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from bevor_sdk._async.client import BevorClient as AsyncBevorClient
|
|
2
|
+
from bevor_sdk._async.graph import Graph as AsyncGraph
|
|
3
|
+
from bevor_sdk._sync.client import BevorClient
|
|
4
|
+
from bevor_sdk._sync.graph import Graph
|
|
5
|
+
|
|
6
|
+
__all__ = ["BevorClient", "AsyncBevorClient", "Graph", "AsyncGraph"]
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import Any, Dict, Mapping, Self
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from bevor_sdk._shared import _BaseClient
|
|
6
|
+
|
|
7
|
+
from .resources import (
|
|
8
|
+
AnalysisResources,
|
|
9
|
+
AnalysisSummaryResources,
|
|
10
|
+
AnalysisVersionsResources,
|
|
11
|
+
ChatResources,
|
|
12
|
+
CodesResources,
|
|
13
|
+
FindingResources,
|
|
14
|
+
ProjectResources,
|
|
15
|
+
TransitionResources,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT = httpx.Timeout(120.0, connect=30.0)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BevorClient(_BaseClient):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
api_key: str,
|
|
25
|
+
*,
|
|
26
|
+
base_url: str | None = None,
|
|
27
|
+
default_headers: Mapping[str, str] | None = None,
|
|
28
|
+
timeout: httpx.Timeout | None = None,
|
|
29
|
+
):
|
|
30
|
+
super().__init__(
|
|
31
|
+
api_key=api_key, base_url=base_url, default_headers=default_headers, timeout=timeout
|
|
32
|
+
)
|
|
33
|
+
self._http: httpx.AsyncClient | None = None
|
|
34
|
+
|
|
35
|
+
self.codes = CodesResources(self)
|
|
36
|
+
self.chats = ChatResources(self)
|
|
37
|
+
self.analyses = AnalysisResources(self)
|
|
38
|
+
self.transitions = TransitionResources(self)
|
|
39
|
+
self.analysis_versions = AnalysisVersionsResources(self)
|
|
40
|
+
self.summary = AnalysisSummaryResources(self)
|
|
41
|
+
self.findings = FindingResources(self)
|
|
42
|
+
self.projects = ProjectResources(self)
|
|
43
|
+
|
|
44
|
+
def _transport(self, client: httpx.AsyncClient | None) -> httpx.AsyncClient:
|
|
45
|
+
if client is not None:
|
|
46
|
+
return client
|
|
47
|
+
if self._http is None:
|
|
48
|
+
self._http = httpx.AsyncClient(timeout=self._timeout)
|
|
49
|
+
return self._http
|
|
50
|
+
|
|
51
|
+
async def _aclose(self) -> None:
|
|
52
|
+
if self._closed:
|
|
53
|
+
return
|
|
54
|
+
self._closed = True
|
|
55
|
+
if self._http is not None:
|
|
56
|
+
await self._http.aclose()
|
|
57
|
+
self._http = None
|
|
58
|
+
|
|
59
|
+
async def __aenter__(self) -> Self:
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
63
|
+
await self._aclose()
|
|
64
|
+
|
|
65
|
+
async def request_json(
|
|
66
|
+
self,
|
|
67
|
+
method: str,
|
|
68
|
+
path: str,
|
|
69
|
+
*,
|
|
70
|
+
params: Mapping[str, Any] | None = None,
|
|
71
|
+
json: Any = None,
|
|
72
|
+
content: httpx._types.RequestContent | None = None,
|
|
73
|
+
data: httpx._types.RequestData | None = None,
|
|
74
|
+
files: httpx._types.RequestFiles | None = None,
|
|
75
|
+
client: httpx.AsyncClient | None = None,
|
|
76
|
+
headers: Dict[str, str] | None = None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
self._ensure_open()
|
|
79
|
+
url = self._url(path)
|
|
80
|
+
http = self._transport(client)
|
|
81
|
+
payload: Any = json
|
|
82
|
+
if isinstance(json, dict):
|
|
83
|
+
payload = self._prune_json_body(json)
|
|
84
|
+
resp = await http.request(
|
|
85
|
+
method,
|
|
86
|
+
url,
|
|
87
|
+
params=self._prune_query_params(params),
|
|
88
|
+
json=payload,
|
|
89
|
+
content=content,
|
|
90
|
+
data=data,
|
|
91
|
+
files=files,
|
|
92
|
+
headers=self._merge_headers(headers),
|
|
93
|
+
)
|
|
94
|
+
has_content = self._handle_http_error(resp)
|
|
95
|
+
if not has_content:
|
|
96
|
+
return None
|
|
97
|
+
return resp.json()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from bevor_sdk import AsyncBevorClient
|
|
5
|
+
from bevor_sdk._shared import _BaseGraph
|
|
6
|
+
from bevor_sdk.types import ID
|
|
7
|
+
from bevor_sdk.types.responses.graph import GraphSnapshotEdge, GraphSnapshotFile, GraphSnapshotNode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Graph(_BaseGraph):
|
|
11
|
+
"""
|
|
12
|
+
Language-agnostic graph view for one code version, backed by HTTP only.
|
|
13
|
+
|
|
14
|
+
Call :meth:`build` after construction (or use :meth:`from_snapshot` for offline snapshots).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, client: AsyncBevorClient, code_id: ID) -> None:
|
|
18
|
+
super().__init__(code_id=code_id)
|
|
19
|
+
self._client: AsyncBevorClient = client
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_snapshot(
|
|
23
|
+
cls,
|
|
24
|
+
client: AsyncBevorClient,
|
|
25
|
+
code_id: ID,
|
|
26
|
+
nodes: List[GraphSnapshotNode],
|
|
27
|
+
edges: List[GraphSnapshotEdge],
|
|
28
|
+
files: List[GraphSnapshotFile],
|
|
29
|
+
) -> "Graph":
|
|
30
|
+
core = cls(client=client, code_id=code_id)
|
|
31
|
+
core._ingest_snapshot(nodes=nodes, edges=edges, files=files)
|
|
32
|
+
return core
|
|
33
|
+
|
|
34
|
+
async def build(self) -> None:
|
|
35
|
+
"""Fetch snapshot via client (nodes + edges + files)."""
|
|
36
|
+
if self._is_built:
|
|
37
|
+
return
|
|
38
|
+
node_res, edge_res, file_res = await asyncio.gather(
|
|
39
|
+
self._client.codes.nodes.list(self.code_id),
|
|
40
|
+
self._client.codes.edges(self.code_id),
|
|
41
|
+
self._client.codes.files.list(self.code_id),
|
|
42
|
+
)
|
|
43
|
+
self._ingest_snapshot(nodes=node_res.results, edges=edge_res.results, files=file_res.results)
|
|
44
|
+
self._is_built = True
|
|
45
|
+
|
|
46
|
+
async def get_file_content(self, file_id: ID) -> str:
|
|
47
|
+
if file_id in self._lazy_file_text:
|
|
48
|
+
return self._lazy_file_text[file_id]
|
|
49
|
+
|
|
50
|
+
text = await self._client.codes.files.content(file_id=file_id, code_id=self.code_id)
|
|
51
|
+
self._lazy_file_text[file_id] = text
|
|
52
|
+
return text
|
|
53
|
+
|
|
54
|
+
async def reconstruct_chunk(self, node_id: ID, *, with_docstring: bool = False) -> str:
|
|
55
|
+
self._ensure_built()
|
|
56
|
+
node = self.get_node(node_id)
|
|
57
|
+
text = await self.get_file_content(node.file_id)
|
|
58
|
+
source_bytes = text.encode("utf-8")
|
|
59
|
+
|
|
60
|
+
docstring_use = None
|
|
61
|
+
if with_docstring and node.docstring:
|
|
62
|
+
if "@inheritdoc" not in node.docstring:
|
|
63
|
+
docstring_use = node.docstring
|
|
64
|
+
else:
|
|
65
|
+
natspecs = self.get_edges_for_node(
|
|
66
|
+
node_id, direction="out", edge_type="has_ref_comment"
|
|
67
|
+
)
|
|
68
|
+
if natspecs:
|
|
69
|
+
natspec = natspecs[0]
|
|
70
|
+
assert natspec.dst_id
|
|
71
|
+
natspec_node = self.get_node(natspec.dst_id)
|
|
72
|
+
docstring_use = natspec_node.docstring
|
|
73
|
+
else:
|
|
74
|
+
docstring_use = node.docstring
|
|
75
|
+
|
|
76
|
+
return Graph.reconstruct(node=node, source=source_bytes, docstring_use=docstring_use)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .chat import ChatResources
|
|
2
|
+
from .graph import CodesResources, FileResources, NodeResources
|
|
3
|
+
from .manage import ProjectResources, ProjectUserResources
|
|
4
|
+
from .security import (
|
|
5
|
+
AnalysisResources,
|
|
6
|
+
AnalysisSummaryResources,
|
|
7
|
+
AnalysisVersionsResources,
|
|
8
|
+
FindingResources,
|
|
9
|
+
TransitionResources,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ProjectResources",
|
|
14
|
+
"AnalysisResources",
|
|
15
|
+
"FindingResources",
|
|
16
|
+
"ChatResources",
|
|
17
|
+
"CodesResources",
|
|
18
|
+
"AnalysisVersionsResources",
|
|
19
|
+
"FileResources",
|
|
20
|
+
"NodeResources",
|
|
21
|
+
"AnalysisSummaryResources",
|
|
22
|
+
"TransitionResources",
|
|
23
|
+
"ProjectUserResources",
|
|
24
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, AsyncIterator, Awaitable, Callable, Generic, TypeVar
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from bevor_sdk._shared import SSEEvent, _BaseSSEParser
|
|
7
|
+
from bevor_sdk.exceptions import ServerException
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from bevor_sdk._async.client import BevorClient
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T", bound=BaseModel)
|
|
13
|
+
EventT = TypeVar("EventT", bound=BaseModel)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SSEParser(_BaseSSEParser):
|
|
17
|
+
async def events(self, lines: AsyncIterator[str]) -> AsyncIterator[SSEEvent]:
|
|
18
|
+
async for line in lines:
|
|
19
|
+
event = self.feed(line)
|
|
20
|
+
if event is not None:
|
|
21
|
+
yield event
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EventResource(Generic[T, EventT]):
|
|
25
|
+
"""generic wrapper for streaming async responses"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
data: T,
|
|
30
|
+
refresh_data: Callable[[], Awaitable[T]],
|
|
31
|
+
is_completed: Callable[[T], Awaitable[bool]],
|
|
32
|
+
handle_event: Callable[[SSEEvent], Awaitable[EventT | None]],
|
|
33
|
+
*,
|
|
34
|
+
_client: "BevorClient",
|
|
35
|
+
_timeout: httpx.Timeout | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._data = data
|
|
38
|
+
self._refresh_data = refresh_data
|
|
39
|
+
self._is_completed = is_completed
|
|
40
|
+
self._handle_event = handle_event
|
|
41
|
+
self._timeout = _timeout or httpx.Timeout(300.0, connect=30.0)
|
|
42
|
+
self._client = _client
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def data(self) -> T:
|
|
46
|
+
return self._data
|
|
47
|
+
|
|
48
|
+
async def _get_token(self, client: httpx.AsyncClient | None = None) -> str:
|
|
49
|
+
data = await self._client.request_json(
|
|
50
|
+
"POST",
|
|
51
|
+
"/v1/events/auth",
|
|
52
|
+
client=client,
|
|
53
|
+
)
|
|
54
|
+
token = data.get("token")
|
|
55
|
+
if not token:
|
|
56
|
+
raise ServerException()
|
|
57
|
+
return token
|
|
58
|
+
|
|
59
|
+
async def stream_completion(
|
|
60
|
+
self, client: httpx.AsyncClient | None = None
|
|
61
|
+
) -> AsyncIterator[EventT]:
|
|
62
|
+
if await self._is_completed(self._data):
|
|
63
|
+
return
|
|
64
|
+
self._client._ensure_open()
|
|
65
|
+
token = await self._get_token(client)
|
|
66
|
+
|
|
67
|
+
url = self._client._url("/v1/events/handler")
|
|
68
|
+
http = self._client._transport(client)
|
|
69
|
+
|
|
70
|
+
async with http.stream(
|
|
71
|
+
"GET",
|
|
72
|
+
url,
|
|
73
|
+
params={"token": token},
|
|
74
|
+
headers=self._client._default_headers,
|
|
75
|
+
timeout=self._timeout,
|
|
76
|
+
) as response:
|
|
77
|
+
response.raise_for_status()
|
|
78
|
+
parser = SSEParser()
|
|
79
|
+
async for event in parser.events(response.aiter_lines()):
|
|
80
|
+
if relevant_event := await self._handle_event(event):
|
|
81
|
+
yield relevant_event
|
|
82
|
+
self._data = await self._refresh_data()
|
|
83
|
+
if await self._is_completed(self._data):
|
|
84
|
+
await response.aclose()
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["EventResource"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Awaitable, Callable, Generic, List, TypeVar
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from bevor_sdk.types.responses._shared import Pagination
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T", bound=BaseModel)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PaginatedResource(Generic[T]):
|
|
11
|
+
"""generic wrapper for incrementally building paginated data"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
data: Pagination[T],
|
|
16
|
+
*,
|
|
17
|
+
fetch_page: Callable[[int], Awaitable[Pagination[T]]],
|
|
18
|
+
) -> None:
|
|
19
|
+
self._results = list(data.results)
|
|
20
|
+
self._current = data
|
|
21
|
+
self._fetch_page = fetch_page
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def results(self) -> List[T]:
|
|
25
|
+
return self._results
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def current(self) -> Pagination[T]:
|
|
29
|
+
return self._current
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def has_more(self) -> bool:
|
|
33
|
+
return self._current.more
|
|
34
|
+
|
|
35
|
+
async def fetch_next(self) -> None:
|
|
36
|
+
if not self.has_more:
|
|
37
|
+
raise ValueError("No more pages to fetch. check .has_more")
|
|
38
|
+
nxt = await self._fetch_page(self._current.page + 1)
|
|
39
|
+
self._results.extend(nxt.results)
|
|
40
|
+
self._current = nxt
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
## Expanded Types
|
|
45
|
+
|
|
46
|
+
Certain resources support a `with_pagination` or `with_streaming` argument. If `True` is provided, then the responses extend the standard
|
|
47
|
+
API responses for added functionality.
|
|
48
|
+
|
|
49
|
+
`with_pagination` will return a `PaginatedResource` type
|
|
50
|
+
"""
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, AsyncGenerator, Dict, Literal, overload
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from bevor_sdk._async.resources._modules import PaginatedResource
|
|
8
|
+
from bevor_sdk.types import ID
|
|
9
|
+
from bevor_sdk.types.requests.chat import ChatMessageBodyDict, ChatsQueryParams
|
|
10
|
+
from bevor_sdk.types.responses import Pagination, ResultsResponse
|
|
11
|
+
from bevor_sdk.types.responses.chat import (
|
|
12
|
+
ChatMessageSchema,
|
|
13
|
+
ChatSchema,
|
|
14
|
+
StreamedMessageType,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from bevor_sdk._async.client import BevorClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ChatResources:
|
|
22
|
+
def __init__(self, client: BevorClient) -> None:
|
|
23
|
+
self._client = client
|
|
24
|
+
|
|
25
|
+
def __call__(self, chat_id: ID) -> BoundChatResources:
|
|
26
|
+
return BoundChatResources(self, chat_id)
|
|
27
|
+
|
|
28
|
+
async def create_for_analysis(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
version_id: ID,
|
|
32
|
+
client: httpx.AsyncClient | None = None,
|
|
33
|
+
headers: Dict[str, str] | None = None,
|
|
34
|
+
) -> ChatSchema:
|
|
35
|
+
"""POST ``/v1/chats``."""
|
|
36
|
+
data = await self._client.request_json(
|
|
37
|
+
"POST",
|
|
38
|
+
"/v1/chats",
|
|
39
|
+
json={"version_id": version_id, "scope": "analysis"},
|
|
40
|
+
client=client,
|
|
41
|
+
headers=headers,
|
|
42
|
+
)
|
|
43
|
+
return ChatSchema.model_validate(data)
|
|
44
|
+
|
|
45
|
+
async def create_for_code(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
code_id: ID,
|
|
49
|
+
client: httpx.AsyncClient | None = None,
|
|
50
|
+
headers: Dict[str, str] | None = None,
|
|
51
|
+
) -> ChatSchema:
|
|
52
|
+
"""POST ``/v1/chats``."""
|
|
53
|
+
data = await self._client.request_json(
|
|
54
|
+
"POST",
|
|
55
|
+
"/v1/chats",
|
|
56
|
+
json={"code_id": code_id, "scope": "code"},
|
|
57
|
+
client=client,
|
|
58
|
+
headers=headers,
|
|
59
|
+
)
|
|
60
|
+
return ChatSchema.model_validate(data)
|
|
61
|
+
|
|
62
|
+
@overload
|
|
63
|
+
async def list(
|
|
64
|
+
self,
|
|
65
|
+
options: ChatsQueryParams | None = None,
|
|
66
|
+
*,
|
|
67
|
+
with_pagination: Literal[True],
|
|
68
|
+
client: httpx.AsyncClient | None = None,
|
|
69
|
+
headers: Dict[str, str] | None = None,
|
|
70
|
+
) -> PaginatedResource[ChatSchema]: ...
|
|
71
|
+
@overload
|
|
72
|
+
async def list(
|
|
73
|
+
self,
|
|
74
|
+
options: ChatsQueryParams | None = None,
|
|
75
|
+
*,
|
|
76
|
+
with_pagination: Literal[False] = False,
|
|
77
|
+
client: httpx.AsyncClient | None = None,
|
|
78
|
+
headers: Dict[str, str] | None = None,
|
|
79
|
+
) -> Pagination[ChatSchema]: ...
|
|
80
|
+
async def list(
|
|
81
|
+
self,
|
|
82
|
+
options: ChatsQueryParams | None = None,
|
|
83
|
+
*,
|
|
84
|
+
with_pagination: bool = False,
|
|
85
|
+
client: httpx.AsyncClient | None = None,
|
|
86
|
+
headers: Dict[str, str] | None = None,
|
|
87
|
+
) -> PaginatedResource[ChatSchema] | Pagination[ChatSchema]:
|
|
88
|
+
"""GET ``/v1/chats``."""
|
|
89
|
+
opts = options or ChatsQueryParams()
|
|
90
|
+
|
|
91
|
+
async def fetch_page(p: int) -> Pagination[ChatSchema]:
|
|
92
|
+
opts["page"] = p
|
|
93
|
+
data = await self._client.request_json(
|
|
94
|
+
"GET",
|
|
95
|
+
"/v1/chats",
|
|
96
|
+
params=opts,
|
|
97
|
+
client=client,
|
|
98
|
+
headers=headers,
|
|
99
|
+
)
|
|
100
|
+
return Pagination[ChatSchema].model_validate(data)
|
|
101
|
+
|
|
102
|
+
first = await fetch_page(opts["page"] or 0 if "page" in opts else 0)
|
|
103
|
+
if not with_pagination:
|
|
104
|
+
return first
|
|
105
|
+
|
|
106
|
+
return PaginatedResource[ChatSchema](first, fetch_page=fetch_page)
|
|
107
|
+
|
|
108
|
+
async def get(
|
|
109
|
+
self,
|
|
110
|
+
chat_id: ID,
|
|
111
|
+
*,
|
|
112
|
+
client: httpx.AsyncClient | None = None,
|
|
113
|
+
headers: Dict[str, str] | None = None,
|
|
114
|
+
) -> ChatSchema:
|
|
115
|
+
"""GET ``/v1/chats/{chat_id}``."""
|
|
116
|
+
data = await self._client.request_json(
|
|
117
|
+
"GET", f"/v1/chats/{chat_id}", client=client, headers=headers
|
|
118
|
+
)
|
|
119
|
+
return ChatSchema.model_validate(data)
|
|
120
|
+
|
|
121
|
+
async def messages(
|
|
122
|
+
self,
|
|
123
|
+
chat_id: ID,
|
|
124
|
+
*,
|
|
125
|
+
client: httpx.AsyncClient | None = None,
|
|
126
|
+
headers: Dict[str, str] | None = None,
|
|
127
|
+
) -> ResultsResponse[ChatMessageSchema]:
|
|
128
|
+
"""GET ``/v1/chats/{chat_id}/messages``."""
|
|
129
|
+
data = await self._client.request_json(
|
|
130
|
+
"GET", f"/v1/chats/{chat_id}/messages", client=client, headers=headers
|
|
131
|
+
)
|
|
132
|
+
return ResultsResponse[ChatMessageSchema].model_validate(data)
|
|
133
|
+
|
|
134
|
+
async def sync(
|
|
135
|
+
self,
|
|
136
|
+
chat_id: ID,
|
|
137
|
+
body: ChatMessageBodyDict,
|
|
138
|
+
*,
|
|
139
|
+
client: httpx.AsyncClient | None = None,
|
|
140
|
+
headers: Dict[str, str] | None = None,
|
|
141
|
+
) -> ChatMessageSchema:
|
|
142
|
+
"""POST ``/v1/chats/{chat_id}/sync``."""
|
|
143
|
+
data = await self._client.request_json(
|
|
144
|
+
"POST",
|
|
145
|
+
f"/v1/chats/{chat_id}/sync",
|
|
146
|
+
json=body,
|
|
147
|
+
client=client,
|
|
148
|
+
headers=headers,
|
|
149
|
+
)
|
|
150
|
+
return ChatMessageSchema.model_validate(data)
|
|
151
|
+
|
|
152
|
+
async def session_token(
|
|
153
|
+
self,
|
|
154
|
+
chat_id: ID,
|
|
155
|
+
*,
|
|
156
|
+
client: httpx.AsyncClient | None = None,
|
|
157
|
+
headers: Dict[str, str] | None = None,
|
|
158
|
+
) -> str:
|
|
159
|
+
data = await self._client.request_json(
|
|
160
|
+
"POST",
|
|
161
|
+
f"/v1/chats/{chat_id}/session-token",
|
|
162
|
+
client=client,
|
|
163
|
+
headers=headers,
|
|
164
|
+
)
|
|
165
|
+
assert isinstance(data, dict) and "token" in data
|
|
166
|
+
return str(data["token"])
|
|
167
|
+
|
|
168
|
+
async def stream(
|
|
169
|
+
self,
|
|
170
|
+
chat_id: ID,
|
|
171
|
+
session_token: str,
|
|
172
|
+
body: ChatMessageBodyDict,
|
|
173
|
+
*,
|
|
174
|
+
client: httpx.AsyncClient | None = None,
|
|
175
|
+
headers: Dict[str, str] | None = None,
|
|
176
|
+
) -> AsyncGenerator[StreamedMessageType]:
|
|
177
|
+
"""POST ``/v1/chats/{chat_id}/stream``. Use session_token() for initial request"""
|
|
178
|
+
self._client._ensure_open()
|
|
179
|
+
|
|
180
|
+
url = self._client._url(f"/v1/chats/{chat_id}/stream")
|
|
181
|
+
http = self._client._transport(client)
|
|
182
|
+
async with http.stream(
|
|
183
|
+
"POST",
|
|
184
|
+
url,
|
|
185
|
+
json=self._client._prune_json_body({**body, "token": session_token}),
|
|
186
|
+
headers=self._client._merge_headers(headers),
|
|
187
|
+
) as resp:
|
|
188
|
+
resp.raise_for_status()
|
|
189
|
+
async for line in resp.aiter_lines():
|
|
190
|
+
if not line:
|
|
191
|
+
continue
|
|
192
|
+
try:
|
|
193
|
+
yield StreamedMessageType.model_validate_json(line)
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class BoundChatResources:
|
|
199
|
+
__slots__ = ("_parent", "_chat_id")
|
|
200
|
+
|
|
201
|
+
def __init__(self, parent: ChatResources, chat_id: ID) -> None:
|
|
202
|
+
self._parent = parent
|
|
203
|
+
self._chat_id = chat_id
|
|
204
|
+
|
|
205
|
+
async def get(
|
|
206
|
+
self,
|
|
207
|
+
*,
|
|
208
|
+
client: httpx.AsyncClient | None = None,
|
|
209
|
+
headers: Dict[str, str] | None = None,
|
|
210
|
+
) -> ChatSchema:
|
|
211
|
+
"""GET ``/v1/chats/{chat_id}``."""
|
|
212
|
+
return await self._parent.get(self._chat_id, client=client, headers=headers)
|
|
213
|
+
|
|
214
|
+
async def messages(
|
|
215
|
+
self,
|
|
216
|
+
*,
|
|
217
|
+
client: httpx.AsyncClient | None = None,
|
|
218
|
+
headers: Dict[str, str] | None = None,
|
|
219
|
+
) -> ResultsResponse[ChatMessageSchema]:
|
|
220
|
+
"""GET ``/v1/chats/{chat_id}/messages``."""
|
|
221
|
+
return await self._parent.messages(self._chat_id, client=client, headers=headers)
|
|
222
|
+
|
|
223
|
+
async def sync(
|
|
224
|
+
self,
|
|
225
|
+
body: ChatMessageBodyDict,
|
|
226
|
+
*,
|
|
227
|
+
client: httpx.AsyncClient | None = None,
|
|
228
|
+
headers: Dict[str, str] | None = None,
|
|
229
|
+
) -> ChatMessageSchema:
|
|
230
|
+
"""POST ``/v1/chats/{chat_id}/sync``."""
|
|
231
|
+
return await self._parent.sync(
|
|
232
|
+
self._chat_id,
|
|
233
|
+
body,
|
|
234
|
+
client=client,
|
|
235
|
+
headers=headers,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def stream(
|
|
239
|
+
self,
|
|
240
|
+
session_token: str,
|
|
241
|
+
body: ChatMessageBodyDict,
|
|
242
|
+
*,
|
|
243
|
+
client: httpx.AsyncClient | None = None,
|
|
244
|
+
headers: Dict[str, str] | None = None,
|
|
245
|
+
) -> AsyncGenerator[StreamedMessageType]:
|
|
246
|
+
"""POST ``/v1/chats/{chat_id}/stream``. Use session_token() for initial request"""
|
|
247
|
+
return self._parent.stream(
|
|
248
|
+
self._chat_id,
|
|
249
|
+
session_token,
|
|
250
|
+
body,
|
|
251
|
+
client=client,
|
|
252
|
+
headers=headers,
|
|
253
|
+
)
|