infratex 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.
- infratex/__init__.py +35 -0
- infratex/_client.py +84 -0
- infratex/_http.py +184 -0
- infratex/_types.py +195 -0
- infratex/resources/__init__.py +17 -0
- infratex/resources/account.py +22 -0
- infratex/resources/billing.py +22 -0
- infratex/resources/collections.py +47 -0
- infratex/resources/conversations.py +52 -0
- infratex/resources/documents.py +123 -0
- infratex/resources/responses.py +69 -0
- infratex/resources/searches.py +54 -0
- infratex-0.1.0.dist-info/METADATA +198 -0
- infratex-0.1.0.dist-info/RECORD +16 -0
- infratex-0.1.0.dist-info/WHEEL +4 -0
- infratex-0.1.0.dist-info/licenses/LICENSE +21 -0
infratex/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Infratex Python SDK."""
|
|
2
|
+
|
|
3
|
+
from ._client import Infratex
|
|
4
|
+
from ._http import InfratexError
|
|
5
|
+
from ._types import (
|
|
6
|
+
Account,
|
|
7
|
+
BillingSummary,
|
|
8
|
+
Collection,
|
|
9
|
+
Conversation,
|
|
10
|
+
Document,
|
|
11
|
+
DocumentList,
|
|
12
|
+
Index,
|
|
13
|
+
SearchResponse,
|
|
14
|
+
SearchResult,
|
|
15
|
+
StreamEvent,
|
|
16
|
+
UploadedDocument,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Infratex",
|
|
21
|
+
"InfratexError",
|
|
22
|
+
"Account",
|
|
23
|
+
"BillingSummary",
|
|
24
|
+
"Collection",
|
|
25
|
+
"Conversation",
|
|
26
|
+
"Document",
|
|
27
|
+
"DocumentList",
|
|
28
|
+
"Index",
|
|
29
|
+
"SearchResponse",
|
|
30
|
+
"SearchResult",
|
|
31
|
+
"StreamEvent",
|
|
32
|
+
"UploadedDocument",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
infratex/_client.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Main Infratex client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ._http import HTTPClient
|
|
11
|
+
from .resources.documents import Documents
|
|
12
|
+
from .resources.searches import Searches
|
|
13
|
+
from .resources.responses import Responses
|
|
14
|
+
from .resources.collections import Collections
|
|
15
|
+
from .resources.conversations import Conversations
|
|
16
|
+
from .resources.account import AccountResource
|
|
17
|
+
from .resources.billing import Billing
|
|
18
|
+
|
|
19
|
+
__all__ = ["Infratex"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Infratex:
|
|
23
|
+
"""Infratex API client.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
api_key:
|
|
28
|
+
Your Infratex API key (``infratex_sk_...``). Falls back to the
|
|
29
|
+
``INFRATEX_API_KEY`` environment variable when not provided.
|
|
30
|
+
base_url:
|
|
31
|
+
Override the API base URL (default ``https://api.infratex.io``).
|
|
32
|
+
timeout:
|
|
33
|
+
Request timeout in seconds (default 300).
|
|
34
|
+
httpx_client:
|
|
35
|
+
Optionally provide your own ``httpx.Client`` instance.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
documents: Documents
|
|
39
|
+
searches: Searches
|
|
40
|
+
responses: Responses
|
|
41
|
+
collections: Collections
|
|
42
|
+
conversations: Conversations
|
|
43
|
+
account: AccountResource
|
|
44
|
+
billing: Billing
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_key: Optional[str] = None,
|
|
49
|
+
*,
|
|
50
|
+
base_url: Optional[str] = None,
|
|
51
|
+
timeout: Optional[float] = None,
|
|
52
|
+
httpx_client: Optional[httpx.Client] = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
resolved_key = api_key or os.environ.get("INFRATEX_API_KEY", "")
|
|
55
|
+
if not resolved_key:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"An API key is required. Pass api_key= or set the INFRATEX_API_KEY "
|
|
58
|
+
"environment variable."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self._http = HTTPClient(
|
|
62
|
+
api_key=resolved_key,
|
|
63
|
+
base_url=base_url,
|
|
64
|
+
timeout=timeout,
|
|
65
|
+
httpx_client=httpx_client,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.documents = Documents(self._http)
|
|
69
|
+
self.searches = Searches(self._http)
|
|
70
|
+
self.responses = Responses(self._http)
|
|
71
|
+
self.collections = Collections(self._http)
|
|
72
|
+
self.conversations = Conversations(self._http)
|
|
73
|
+
self.account = AccountResource(self._http)
|
|
74
|
+
self.billing = Billing(self._http)
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
"""Close the underlying HTTP connection pool."""
|
|
78
|
+
self._http.close()
|
|
79
|
+
|
|
80
|
+
def __enter__(self) -> Infratex:
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def __exit__(self, *args: object) -> None:
|
|
84
|
+
self.close()
|
infratex/_http.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Low-level HTTP transport for the Infratex SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, Iterator, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
__all__ = ["HTTPClient", "InfratexError"]
|
|
11
|
+
|
|
12
|
+
_DEFAULT_BASE_URL = "https://api.infratex.io"
|
|
13
|
+
_DEFAULT_TIMEOUT = 300.0 # seconds
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InfratexError(Exception):
|
|
17
|
+
"""Raised when the Infratex API returns an error response."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
message: str,
|
|
22
|
+
*,
|
|
23
|
+
status_code: int = 0,
|
|
24
|
+
code: Optional[str] = None,
|
|
25
|
+
details: Optional[Dict[str, Any]] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.code = code
|
|
30
|
+
self.details = details
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return "InfratexError(status_code={!r}, code={!r}, message={!r})".format(
|
|
34
|
+
self.status_code, self.code, str(self)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HTTPClient:
|
|
39
|
+
"""Thin wrapper around ``httpx.Client`` with auth and error handling."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
api_key: str,
|
|
45
|
+
base_url: Optional[str] = None,
|
|
46
|
+
timeout: Optional[float] = None,
|
|
47
|
+
httpx_client: Optional[httpx.Client] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._api_key = api_key
|
|
50
|
+
self._base_url = (base_url or _DEFAULT_BASE_URL).rstrip("/")
|
|
51
|
+
|
|
52
|
+
if httpx_client is not None:
|
|
53
|
+
self._client = httpx_client
|
|
54
|
+
else:
|
|
55
|
+
self._client = httpx.Client(
|
|
56
|
+
base_url=self._base_url,
|
|
57
|
+
timeout=timeout or _DEFAULT_TIMEOUT,
|
|
58
|
+
headers={
|
|
59
|
+
"Authorization": "Bearer {}".format(api_key),
|
|
60
|
+
"User-Agent": "infratex-python/0.1.0",
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# --------------------------------------------------------------------- #
|
|
65
|
+
# Public helpers
|
|
66
|
+
# --------------------------------------------------------------------- #
|
|
67
|
+
|
|
68
|
+
def request(
|
|
69
|
+
self,
|
|
70
|
+
method: str,
|
|
71
|
+
path: str,
|
|
72
|
+
*,
|
|
73
|
+
json_body: Optional[Dict[str, Any]] = None,
|
|
74
|
+
data: Optional[Dict[str, Any]] = None,
|
|
75
|
+
files: Optional[Any] = None,
|
|
76
|
+
params: Optional[Dict[str, Any]] = None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
"""Send an HTTP request and return the parsed JSON body."""
|
|
79
|
+
# Strip None values from params
|
|
80
|
+
if params:
|
|
81
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
82
|
+
|
|
83
|
+
response = self._client.request(
|
|
84
|
+
method,
|
|
85
|
+
path,
|
|
86
|
+
json=json_body,
|
|
87
|
+
data=data,
|
|
88
|
+
files=files,
|
|
89
|
+
params=params or None,
|
|
90
|
+
)
|
|
91
|
+
return self._handle_response(response)
|
|
92
|
+
|
|
93
|
+
def request_text(
|
|
94
|
+
self,
|
|
95
|
+
method: str,
|
|
96
|
+
path: str,
|
|
97
|
+
*,
|
|
98
|
+
params: Optional[Dict[str, Any]] = None,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Send an HTTP request and return the raw text body."""
|
|
101
|
+
if params:
|
|
102
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
103
|
+
|
|
104
|
+
response = self._client.request(method, path, params=params or None)
|
|
105
|
+
|
|
106
|
+
if response.status_code >= 400:
|
|
107
|
+
self._raise_for_status(response)
|
|
108
|
+
|
|
109
|
+
return response.text
|
|
110
|
+
|
|
111
|
+
def request_no_content(
|
|
112
|
+
self,
|
|
113
|
+
method: str,
|
|
114
|
+
path: str,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Send an HTTP request that returns 204 No Content."""
|
|
117
|
+
response = self._client.request(method, path)
|
|
118
|
+
if response.status_code >= 400:
|
|
119
|
+
self._raise_for_status(response)
|
|
120
|
+
|
|
121
|
+
def stream_sse(
|
|
122
|
+
self,
|
|
123
|
+
method: str,
|
|
124
|
+
path: str,
|
|
125
|
+
*,
|
|
126
|
+
json_body: Optional[Dict[str, Any]] = None,
|
|
127
|
+
) -> Iterator[Dict[str, Any]]:
|
|
128
|
+
"""Send an HTTP request and iterate over SSE ``data:`` lines."""
|
|
129
|
+
with self._client.stream(method, path, json=json_body) as response:
|
|
130
|
+
if response.status_code >= 400:
|
|
131
|
+
# Read the full body for the error message
|
|
132
|
+
response.read()
|
|
133
|
+
self._raise_for_status(response)
|
|
134
|
+
|
|
135
|
+
for line in response.iter_lines():
|
|
136
|
+
if not line.startswith("data: "):
|
|
137
|
+
continue
|
|
138
|
+
payload = line[len("data: "):]
|
|
139
|
+
try:
|
|
140
|
+
yield json.loads(payload)
|
|
141
|
+
except json.JSONDecodeError:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
def close(self) -> None:
|
|
145
|
+
self._client.close()
|
|
146
|
+
|
|
147
|
+
# --------------------------------------------------------------------- #
|
|
148
|
+
# Internal
|
|
149
|
+
# --------------------------------------------------------------------- #
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _handle_response(response: httpx.Response) -> Any:
|
|
153
|
+
if response.status_code >= 400:
|
|
154
|
+
HTTPClient._raise_for_status(response)
|
|
155
|
+
|
|
156
|
+
# 204 No Content
|
|
157
|
+
if response.status_code == 204 or not response.content:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
return response.json()
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
164
|
+
status_code = response.status_code
|
|
165
|
+
code = None
|
|
166
|
+
message = "API request failed"
|
|
167
|
+
details = None
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
body = response.json()
|
|
171
|
+
if isinstance(body, dict):
|
|
172
|
+
# The API may return {"detail": "..."} or {"code": "...", "message": "..."}
|
|
173
|
+
message = body.get("message") or body.get("detail") or message
|
|
174
|
+
code = body.get("code")
|
|
175
|
+
details = body.get("details")
|
|
176
|
+
except Exception:
|
|
177
|
+
message = response.text or message
|
|
178
|
+
|
|
179
|
+
raise InfratexError(
|
|
180
|
+
message,
|
|
181
|
+
status_code=status_code,
|
|
182
|
+
code=code,
|
|
183
|
+
details=details,
|
|
184
|
+
)
|
infratex/_types.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Typed response objects returned by the Infratex SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _Base:
|
|
9
|
+
"""Lightweight base that populates attributes from a dict."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, data: Dict[str, Any]) -> None:
|
|
12
|
+
self._data = data
|
|
13
|
+
for key, value in data.items():
|
|
14
|
+
setattr(self, key, value)
|
|
15
|
+
|
|
16
|
+
def __repr__(self) -> str:
|
|
17
|
+
name = type(self).__name__
|
|
18
|
+
fields = ", ".join(
|
|
19
|
+
"{}={!r}".format(k, v)
|
|
20
|
+
for k, v in self._data.items()
|
|
21
|
+
if not k.startswith("_")
|
|
22
|
+
)
|
|
23
|
+
return "{}({})".format(name, fields)
|
|
24
|
+
|
|
25
|
+
def __getitem__(self, key: str) -> Any:
|
|
26
|
+
return self._data[key]
|
|
27
|
+
|
|
28
|
+
def __contains__(self, key: str) -> bool:
|
|
29
|
+
return key in self._data
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
32
|
+
return dict(self._data)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Documents
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
class Document(_Base):
|
|
39
|
+
id: str
|
|
40
|
+
filename: str
|
|
41
|
+
status: str
|
|
42
|
+
method: str
|
|
43
|
+
pipeline: Optional[str]
|
|
44
|
+
page_count: Optional[int]
|
|
45
|
+
processing_time_ms: Optional[int]
|
|
46
|
+
error_message: Optional[str]
|
|
47
|
+
markdown_size_bytes: Optional[int]
|
|
48
|
+
chunk_count: Optional[int]
|
|
49
|
+
index_method: Optional[str]
|
|
50
|
+
collection_id: Optional[str]
|
|
51
|
+
markdown: Optional[str]
|
|
52
|
+
upload_time: Optional[str]
|
|
53
|
+
extraction_pages: Optional[List[Dict[str, Any]]]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UploadedDocument(_Base):
|
|
57
|
+
id: str
|
|
58
|
+
status: str
|
|
59
|
+
method: str
|
|
60
|
+
filename: str
|
|
61
|
+
pipeline: Optional[str]
|
|
62
|
+
page_count: Optional[int]
|
|
63
|
+
markdown: Optional[str]
|
|
64
|
+
extraction_ms: int
|
|
65
|
+
collection_id: Optional[str]
|
|
66
|
+
extraction_pages: Optional[List[Dict[str, Any]]]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DocumentList(_Base):
|
|
70
|
+
documents: List[Document]
|
|
71
|
+
total: int
|
|
72
|
+
|
|
73
|
+
def __init__(self, data: Dict[str, Any]) -> None:
|
|
74
|
+
super().__init__(data)
|
|
75
|
+
self.documents = [Document(d) for d in data.get("documents", [])]
|
|
76
|
+
|
|
77
|
+
def __iter__(self): # type: ignore
|
|
78
|
+
return iter(self.documents)
|
|
79
|
+
|
|
80
|
+
def __len__(self) -> int:
|
|
81
|
+
return len(self.documents)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Indexes
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
class Index(_Base):
|
|
88
|
+
document_id: str
|
|
89
|
+
filename: str
|
|
90
|
+
method: str
|
|
91
|
+
status: str
|
|
92
|
+
node_count: int
|
|
93
|
+
chunk_count: int
|
|
94
|
+
has_ast: bool
|
|
95
|
+
has_description: bool
|
|
96
|
+
processing_ms: int
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Search
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
class SearchResult(_Base):
|
|
103
|
+
document_id: str
|
|
104
|
+
document_name: Optional[str]
|
|
105
|
+
score: float
|
|
106
|
+
content: str
|
|
107
|
+
title: str
|
|
108
|
+
summary: str
|
|
109
|
+
node_id: Optional[str]
|
|
110
|
+
chunk_index: Optional[int]
|
|
111
|
+
metadata: Optional[Dict[str, Any]]
|
|
112
|
+
source: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class SearchResponse(_Base):
|
|
116
|
+
method: str
|
|
117
|
+
query: str
|
|
118
|
+
results: List[SearchResult]
|
|
119
|
+
|
|
120
|
+
def __init__(self, data: Dict[str, Any]) -> None:
|
|
121
|
+
super().__init__(data)
|
|
122
|
+
self.results = [SearchResult(r) for r in data.get("results", [])]
|
|
123
|
+
|
|
124
|
+
def __iter__(self): # type: ignore
|
|
125
|
+
return iter(self.results)
|
|
126
|
+
|
|
127
|
+
def __len__(self) -> int:
|
|
128
|
+
return len(self.results)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Responses (SSE streaming)
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
class StreamEvent:
|
|
135
|
+
"""A single event from the SSE response stream."""
|
|
136
|
+
|
|
137
|
+
__slots__ = ("type", "content")
|
|
138
|
+
|
|
139
|
+
def __init__(self, type: str, content: Any) -> None:
|
|
140
|
+
self.type = type
|
|
141
|
+
self.content = content
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
return "StreamEvent(type={!r}, content={!r})".format(self.type, self.content)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# Collections
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
class Collection(_Base):
|
|
151
|
+
id: str
|
|
152
|
+
name: str
|
|
153
|
+
created_at: str
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Conversations
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
class Conversation(_Base):
|
|
160
|
+
id: str
|
|
161
|
+
title: str
|
|
162
|
+
created_at: str
|
|
163
|
+
updated_at: Optional[str]
|
|
164
|
+
messages: Optional[List[Dict[str, Any]]]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Account & Billing
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
class Tenant(_Base):
|
|
171
|
+
id: str
|
|
172
|
+
name: str
|
|
173
|
+
email: Optional[str]
|
|
174
|
+
credit_balance_micros: int
|
|
175
|
+
is_admin: bool
|
|
176
|
+
is_frozen: bool
|
|
177
|
+
created_at: str
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class Account(_Base):
|
|
181
|
+
tenant: Tenant
|
|
182
|
+
|
|
183
|
+
def __init__(self, data: Dict[str, Any]) -> None:
|
|
184
|
+
super().__init__(data)
|
|
185
|
+
self.tenant = Tenant(data.get("tenant", {}))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class BillingSummary(_Base):
|
|
189
|
+
balance_micros: int
|
|
190
|
+
recent_transactions: List[Dict[str, Any]]
|
|
191
|
+
recent_credit_transactions: List[Dict[str, Any]]
|
|
192
|
+
recent_usage: List[Dict[str, Any]]
|
|
193
|
+
spend_by_service: List[Dict[str, Any]]
|
|
194
|
+
daily_spend: List[Dict[str, Any]]
|
|
195
|
+
totals: Dict[str, Any]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .documents import Documents
|
|
2
|
+
from .searches import Searches
|
|
3
|
+
from .responses import Responses
|
|
4
|
+
from .collections import Collections
|
|
5
|
+
from .conversations import Conversations
|
|
6
|
+
from .account import AccountResource
|
|
7
|
+
from .billing import Billing
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Documents",
|
|
11
|
+
"Searches",
|
|
12
|
+
"Responses",
|
|
13
|
+
"Collections",
|
|
14
|
+
"Conversations",
|
|
15
|
+
"AccountResource",
|
|
16
|
+
"Billing",
|
|
17
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Account resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .._types import Account as AccountModel
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .._http import HTTPClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AccountResource:
|
|
14
|
+
"""Interact with the ``/api/v1/account`` endpoint."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
def get(self) -> AccountModel:
|
|
20
|
+
"""Retrieve the current account details."""
|
|
21
|
+
resp = self._client.request("GET", "/api/v1/account")
|
|
22
|
+
return AccountModel(resp)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Billing resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .._types import BillingSummary
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .._http import HTTPClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Billing:
|
|
14
|
+
"""Interact with the ``/api/v1/billing`` endpoint."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
def get(self) -> BillingSummary:
|
|
20
|
+
"""Retrieve the billing summary for the current account."""
|
|
21
|
+
resp = self._client.request("GET", "/api/v1/billing")
|
|
22
|
+
return BillingSummary(resp)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Collections resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, List
|
|
6
|
+
|
|
7
|
+
from .._types import Collection
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .._http import HTTPClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Collections:
|
|
14
|
+
"""Interact with the ``/api/v1/collections`` endpoints."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
def create(self, *, name: str) -> Collection:
|
|
20
|
+
"""Create a new collection."""
|
|
21
|
+
resp = self._client.request(
|
|
22
|
+
"POST", "/api/v1/collections", json_body={"name": name}
|
|
23
|
+
)
|
|
24
|
+
return Collection(resp)
|
|
25
|
+
|
|
26
|
+
def list(self) -> List[Collection]:
|
|
27
|
+
"""List all collections."""
|
|
28
|
+
resp = self._client.request("GET", "/api/v1/collections")
|
|
29
|
+
return [Collection(c) for c in resp]
|
|
30
|
+
|
|
31
|
+
def get(self, collection_id: str) -> Collection:
|
|
32
|
+
"""Retrieve a single collection by ID."""
|
|
33
|
+
resp = self._client.request("GET", "/api/v1/collections/{}".format(collection_id))
|
|
34
|
+
return Collection(resp)
|
|
35
|
+
|
|
36
|
+
def update(self, collection_id: str, *, name: str) -> Collection:
|
|
37
|
+
"""Rename a collection."""
|
|
38
|
+
resp = self._client.request(
|
|
39
|
+
"PATCH",
|
|
40
|
+
"/api/v1/collections/{}".format(collection_id),
|
|
41
|
+
json_body={"name": name},
|
|
42
|
+
)
|
|
43
|
+
return Collection(resp)
|
|
44
|
+
|
|
45
|
+
def delete(self, collection_id: str) -> None:
|
|
46
|
+
"""Delete a collection."""
|
|
47
|
+
self._client.request_no_content("DELETE", "/api/v1/collections/{}".format(collection_id))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Conversations resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
6
|
+
|
|
7
|
+
from .._types import Conversation
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .._http import HTTPClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Conversations:
|
|
14
|
+
"""Interact with the ``/api/v1/conversations`` endpoints."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
def create(self, *, title: str = "New Chat") -> Conversation:
|
|
20
|
+
"""Create a new conversation thread."""
|
|
21
|
+
resp = self._client.request(
|
|
22
|
+
"POST", "/api/v1/conversations", json_body={"title": title}
|
|
23
|
+
)
|
|
24
|
+
return Conversation(resp)
|
|
25
|
+
|
|
26
|
+
def list(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
limit: int = 50,
|
|
30
|
+
offset: int = 0,
|
|
31
|
+
) -> List[Conversation]:
|
|
32
|
+
"""List conversations.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
limit:
|
|
37
|
+
Maximum number of conversations to return (1--100).
|
|
38
|
+
offset:
|
|
39
|
+
Pagination offset.
|
|
40
|
+
"""
|
|
41
|
+
params = {"limit": limit, "offset": offset}
|
|
42
|
+
resp = self._client.request("GET", "/api/v1/conversations", params=params)
|
|
43
|
+
return [Conversation(c) for c in resp]
|
|
44
|
+
|
|
45
|
+
def get(self, conversation_id: str) -> Conversation:
|
|
46
|
+
"""Retrieve a conversation and its messages."""
|
|
47
|
+
resp = self._client.request("GET", "/api/v1/conversations/{}".format(conversation_id))
|
|
48
|
+
return Conversation(resp)
|
|
49
|
+
|
|
50
|
+
def delete(self, conversation_id: str) -> None:
|
|
51
|
+
"""Delete a conversation."""
|
|
52
|
+
self._client.request_no_content("DELETE", "/api/v1/conversations/{}".format(conversation_id))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Documents resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from .._types import DocumentList, Document, Index, UploadedDocument
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .._http import HTTPClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Documents:
|
|
15
|
+
"""Interact with the ``/api/v1/documents`` endpoints."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
18
|
+
self._client = client
|
|
19
|
+
|
|
20
|
+
def upload(
|
|
21
|
+
self,
|
|
22
|
+
file_path: str,
|
|
23
|
+
*,
|
|
24
|
+
method: Optional[str] = None,
|
|
25
|
+
pipeline: Optional[str] = None,
|
|
26
|
+
collection_id: Optional[str] = None,
|
|
27
|
+
) -> UploadedDocument:
|
|
28
|
+
"""Upload and parse a PDF document.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
file_path:
|
|
33
|
+
Local path to the PDF file.
|
|
34
|
+
method:
|
|
35
|
+
Parsing method (``"standard"``, ``"cost-efficient"``, ``"experimental"``).
|
|
36
|
+
pipeline:
|
|
37
|
+
Optional sub-pipeline (``"traditional"``, ``"math"``).
|
|
38
|
+
collection_id:
|
|
39
|
+
Optional collection to assign the document to.
|
|
40
|
+
"""
|
|
41
|
+
filename = os.path.basename(file_path)
|
|
42
|
+
with open(file_path, "rb") as f:
|
|
43
|
+
files = {"file": (filename, f, "application/pdf")}
|
|
44
|
+
data: Dict[str, Any] = {}
|
|
45
|
+
if method is not None:
|
|
46
|
+
data["method"] = method
|
|
47
|
+
if pipeline is not None:
|
|
48
|
+
data["pipeline"] = pipeline
|
|
49
|
+
if collection_id is not None:
|
|
50
|
+
data["collection_id"] = collection_id
|
|
51
|
+
|
|
52
|
+
resp = self._client.request(
|
|
53
|
+
"POST",
|
|
54
|
+
"/api/v1/documents",
|
|
55
|
+
data=data,
|
|
56
|
+
files=files,
|
|
57
|
+
)
|
|
58
|
+
return UploadedDocument(resp)
|
|
59
|
+
|
|
60
|
+
def list(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
limit: int = 50,
|
|
64
|
+
offset: int = 0,
|
|
65
|
+
collection_id: Optional[str] = None,
|
|
66
|
+
status: Optional[str] = None,
|
|
67
|
+
) -> DocumentList:
|
|
68
|
+
"""List documents.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
limit:
|
|
73
|
+
Maximum number of documents to return.
|
|
74
|
+
offset:
|
|
75
|
+
Pagination offset.
|
|
76
|
+
collection_id:
|
|
77
|
+
Filter by collection.
|
|
78
|
+
status:
|
|
79
|
+
Filter by document status.
|
|
80
|
+
"""
|
|
81
|
+
params: Dict[str, Any] = {
|
|
82
|
+
"limit": limit,
|
|
83
|
+
"offset": offset,
|
|
84
|
+
"collection_id": collection_id,
|
|
85
|
+
"status": status,
|
|
86
|
+
}
|
|
87
|
+
resp = self._client.request("GET", "/api/v1/documents", params=params)
|
|
88
|
+
return DocumentList(resp)
|
|
89
|
+
|
|
90
|
+
def get(self, document_id: str) -> Document:
|
|
91
|
+
"""Retrieve a single document by ID."""
|
|
92
|
+
resp = self._client.request("GET", "/api/v1/documents/{}".format(document_id))
|
|
93
|
+
return Document(resp)
|
|
94
|
+
|
|
95
|
+
def markdown(self, document_id: str) -> str:
|
|
96
|
+
"""Download the parsed markdown for a document."""
|
|
97
|
+
return self._client.request_text("GET", "/api/v1/documents/{}/markdown".format(document_id))
|
|
98
|
+
|
|
99
|
+
def delete(self, document_id: str) -> None:
|
|
100
|
+
"""Delete a document."""
|
|
101
|
+
self._client.request_no_content("DELETE", "/api/v1/documents/{}".format(document_id))
|
|
102
|
+
|
|
103
|
+
def index(
|
|
104
|
+
self,
|
|
105
|
+
document_id: str,
|
|
106
|
+
*,
|
|
107
|
+
method: str = "vector",
|
|
108
|
+
) -> Index:
|
|
109
|
+
"""Create a search index for a document.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
document_id:
|
|
114
|
+
The document to index.
|
|
115
|
+
method:
|
|
116
|
+
Indexing strategy: ``"vector"`` or ``"hybrid"``.
|
|
117
|
+
"""
|
|
118
|
+
resp = self._client.request(
|
|
119
|
+
"POST",
|
|
120
|
+
"/api/v1/documents/{}/indexes".format(document_id),
|
|
121
|
+
json_body={"method": method},
|
|
122
|
+
)
|
|
123
|
+
return Index(resp)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Responses resource (streaming AI answers)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional
|
|
6
|
+
|
|
7
|
+
from .._types import StreamEvent
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .._http import HTTPClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Responses:
|
|
14
|
+
"""Interact with the ``/api/v1/responses`` endpoint."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
def create(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
message: str,
|
|
23
|
+
method: str = "vector",
|
|
24
|
+
limit: int = 5,
|
|
25
|
+
document_ids: Optional[List[str]] = None,
|
|
26
|
+
collection_id: Optional[str] = None,
|
|
27
|
+
conversation_id: Optional[str] = None,
|
|
28
|
+
) -> Iterator[StreamEvent]:
|
|
29
|
+
"""Generate a streamed AI response grounded in your documents.
|
|
30
|
+
|
|
31
|
+
Yields :class:`StreamEvent` objects with ``type`` and ``content``.
|
|
32
|
+
Event types:
|
|
33
|
+
|
|
34
|
+
- ``"sources"`` -- retrieved source chunks (list of dicts)
|
|
35
|
+
- ``"text"`` -- a chunk of generated text
|
|
36
|
+
- ``"done"`` -- stream complete
|
|
37
|
+
- ``"error"`` -- an error occurred
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
message:
|
|
42
|
+
The user message / question.
|
|
43
|
+
method:
|
|
44
|
+
``"vector"`` or ``"hybrid"``.
|
|
45
|
+
limit:
|
|
46
|
+
How many source chunks to retrieve (1--20).
|
|
47
|
+
document_ids:
|
|
48
|
+
Restrict to these document IDs.
|
|
49
|
+
collection_id:
|
|
50
|
+
Restrict to this collection.
|
|
51
|
+
conversation_id:
|
|
52
|
+
Continue an existing conversation thread.
|
|
53
|
+
"""
|
|
54
|
+
body: Dict[str, Any] = {
|
|
55
|
+
"message": message,
|
|
56
|
+
"method": method,
|
|
57
|
+
"limit": limit,
|
|
58
|
+
}
|
|
59
|
+
if document_ids is not None:
|
|
60
|
+
body["document_ids"] = document_ids
|
|
61
|
+
if collection_id is not None:
|
|
62
|
+
body["collection_id"] = collection_id
|
|
63
|
+
if conversation_id is not None:
|
|
64
|
+
body["conversation_id"] = conversation_id
|
|
65
|
+
|
|
66
|
+
for event_data in self._client.stream_sse("POST", "/api/v1/responses", json_body=body):
|
|
67
|
+
event_type = event_data.get("type", "unknown")
|
|
68
|
+
content = event_data.get("content")
|
|
69
|
+
yield StreamEvent(type=event_type, content=content)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Searches resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from .._types import SearchResponse
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .._http import HTTPClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Searches:
|
|
14
|
+
"""Interact with the ``/api/v1/searches`` endpoint."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
def create(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
query: str,
|
|
23
|
+
method: str = "vector",
|
|
24
|
+
limit: int = 5,
|
|
25
|
+
document_ids: Optional[List[str]] = None,
|
|
26
|
+
collection_id: Optional[str] = None,
|
|
27
|
+
) -> SearchResponse:
|
|
28
|
+
"""Run a semantic or hybrid search across indexed documents.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
query:
|
|
33
|
+
The natural-language search query.
|
|
34
|
+
method:
|
|
35
|
+
``"vector"`` or ``"hybrid"``.
|
|
36
|
+
limit:
|
|
37
|
+
Maximum number of results to return (1--50).
|
|
38
|
+
document_ids:
|
|
39
|
+
Restrict search to these document IDs.
|
|
40
|
+
collection_id:
|
|
41
|
+
Restrict search to this collection.
|
|
42
|
+
"""
|
|
43
|
+
body: Dict[str, Any] = {
|
|
44
|
+
"query": query,
|
|
45
|
+
"method": method,
|
|
46
|
+
"limit": limit,
|
|
47
|
+
}
|
|
48
|
+
if document_ids is not None:
|
|
49
|
+
body["document_ids"] = document_ids
|
|
50
|
+
if collection_id is not None:
|
|
51
|
+
body["collection_id"] = collection_id
|
|
52
|
+
|
|
53
|
+
resp = self._client.request("POST", "/api/v1/searches", json_body=body)
|
|
54
|
+
return SearchResponse(resp)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: infratex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Infratex document intelligence API
|
|
5
|
+
Project-URL: Homepage, https://infratex.io
|
|
6
|
+
Project-URL: Documentation, https://docs.infratex.io
|
|
7
|
+
Project-URL: Repository, https://github.com/infratex/infratex-python
|
|
8
|
+
Author-email: Infratex <support@infratex.io>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: document,infratex,parsing,pdf,rag
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Requires-Dist: httpx>=0.24.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Infratex Python SDK
|
|
28
|
+
|
|
29
|
+
Official Python client for the [Infratex](https://infratex.io) document intelligence API. Parse PDFs, build search indexes, and generate AI-powered answers grounded in your documents.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install infratex
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from infratex import Infratex
|
|
41
|
+
|
|
42
|
+
client = Infratex(api_key="infratex_sk_...")
|
|
43
|
+
|
|
44
|
+
# Upload and parse a PDF
|
|
45
|
+
doc = client.documents.upload("report.pdf")
|
|
46
|
+
print(doc.id, doc.status, doc.page_count)
|
|
47
|
+
|
|
48
|
+
# Index for search
|
|
49
|
+
index = client.documents.index(doc.id, method="vector")
|
|
50
|
+
|
|
51
|
+
# Search
|
|
52
|
+
results = client.searches.create(query="revenue growth", document_ids=[doc.id])
|
|
53
|
+
for r in results:
|
|
54
|
+
print(r.score, r.content[:100])
|
|
55
|
+
|
|
56
|
+
# AI response (streamed)
|
|
57
|
+
for event in client.responses.create(message="Summarize the key findings", document_ids=[doc.id]):
|
|
58
|
+
if event.type == "text":
|
|
59
|
+
print(event.content, end="")
|
|
60
|
+
elif event.type == "sources":
|
|
61
|
+
print("Sources:", event.content)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Authentication
|
|
65
|
+
|
|
66
|
+
Pass your API key directly or set the `INFRATEX_API_KEY` environment variable:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Explicit
|
|
70
|
+
client = Infratex(api_key="infratex_sk_...")
|
|
71
|
+
|
|
72
|
+
# From environment
|
|
73
|
+
import os
|
|
74
|
+
os.environ["INFRATEX_API_KEY"] = "infratex_sk_..."
|
|
75
|
+
client = Infratex()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Resources
|
|
79
|
+
|
|
80
|
+
### Documents
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# Upload
|
|
84
|
+
doc = client.documents.upload("report.pdf")
|
|
85
|
+
doc = client.documents.upload("report.pdf", method="standard", collection_id="col-id")
|
|
86
|
+
|
|
87
|
+
# List
|
|
88
|
+
docs = client.documents.list(limit=50, offset=0, collection_id="col-id")
|
|
89
|
+
print(docs.total)
|
|
90
|
+
for d in docs:
|
|
91
|
+
print(d.filename)
|
|
92
|
+
|
|
93
|
+
# Get
|
|
94
|
+
doc = client.documents.get("doc-id")
|
|
95
|
+
|
|
96
|
+
# Download markdown
|
|
97
|
+
md = client.documents.markdown("doc-id")
|
|
98
|
+
|
|
99
|
+
# Delete
|
|
100
|
+
client.documents.delete("doc-id")
|
|
101
|
+
|
|
102
|
+
# Index
|
|
103
|
+
index = client.documents.index("doc-id", method="hybrid")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Searches
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
results = client.searches.create(
|
|
110
|
+
query="What is the EBITDA?",
|
|
111
|
+
method="vector",
|
|
112
|
+
limit=5,
|
|
113
|
+
document_ids=["doc-id"],
|
|
114
|
+
collection_id="col-id",
|
|
115
|
+
)
|
|
116
|
+
for r in results:
|
|
117
|
+
print(r.score, r.content[:200])
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Responses (streaming)
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
for event in client.responses.create(
|
|
124
|
+
message="Summarize the report",
|
|
125
|
+
method="hybrid",
|
|
126
|
+
limit=5,
|
|
127
|
+
document_ids=["doc-id"],
|
|
128
|
+
conversation_id="conv-id",
|
|
129
|
+
):
|
|
130
|
+
if event.type == "text":
|
|
131
|
+
print(event.content, end="")
|
|
132
|
+
elif event.type == "sources":
|
|
133
|
+
print("Sources:", event.content)
|
|
134
|
+
elif event.type == "done":
|
|
135
|
+
print("\n--- Done ---")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Collections
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
col = client.collections.create(name="Q3 Reports")
|
|
142
|
+
cols = client.collections.list()
|
|
143
|
+
col = client.collections.get("col-id")
|
|
144
|
+
client.collections.update("col-id", name="Q4 Reports")
|
|
145
|
+
client.collections.delete("col-id")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Conversations
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
conv = client.conversations.create(title="Analysis")
|
|
152
|
+
convs = client.conversations.list()
|
|
153
|
+
conv = client.conversations.get("conv-id") # includes messages
|
|
154
|
+
client.conversations.delete("conv-id")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Account & Billing
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
account = client.account.get()
|
|
161
|
+
print(account.tenant["email"])
|
|
162
|
+
|
|
163
|
+
billing = client.billing.get()
|
|
164
|
+
print(billing.balance_micros)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Error handling
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from infratex import Infratex, InfratexError
|
|
171
|
+
|
|
172
|
+
client = Infratex(api_key="infratex_sk_...")
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
doc = client.documents.get("nonexistent-id")
|
|
176
|
+
except InfratexError as e:
|
|
177
|
+
print(e.status_code) # 404
|
|
178
|
+
print(e.code) # error code from the API
|
|
179
|
+
print(str(e)) # human-readable message
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Configuration
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
client = Infratex(
|
|
186
|
+
api_key="infratex_sk_...",
|
|
187
|
+
base_url="https://api.infratex.io", # custom base URL
|
|
188
|
+
timeout=60.0, # request timeout in seconds
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Use as a context manager
|
|
192
|
+
with Infratex(api_key="infratex_sk_...") as client:
|
|
193
|
+
doc = client.documents.upload("report.pdf")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
infratex/__init__.py,sha256=jLHxlOqqsJGyWF88PqIpdaY1hpC5oFheomBS2fh8KOk,586
|
|
2
|
+
infratex/_client.py,sha256=wnuoineJfpE8ZRYiokYBe6pTYg8RtSuvMMRO_HRA5W4,2400
|
|
3
|
+
infratex/_http.py,sha256=zr17M7ohGfdx51uxX4iFD5k7OQt60Qi2yt8DlDQfcnw,5565
|
|
4
|
+
infratex/_types.py,sha256=J8R6dIrHyt3dpCsi_gMKq1i3_5Zr7Y7RXbIQyc4Gcfs,5279
|
|
5
|
+
infratex/resources/__init__.py,sha256=Ff6uglCd2qjiEWc092Yza3JInXNUVJBsVEAxkug4KUk,384
|
|
6
|
+
infratex/resources/account.py,sha256=mQpT-NqkvNzUXlMgtTcVSobncqK_t602vOtiIdC3tzA,544
|
|
7
|
+
infratex/resources/billing.py,sha256=89oup5A15BpK5p-tdY9M4Q4S1Ym3VUrccVh-RrWN3r8,547
|
|
8
|
+
infratex/resources/collections.py,sha256=6rPo-huN-zirNT2jwXvWlHIOQ0U6dm9gStdxnqP58P8,1498
|
|
9
|
+
infratex/resources/conversations.py,sha256=h8ct7zKkD8z7-9_lMcJZtruS_6smYdImsM59wzWnAG8,1588
|
|
10
|
+
infratex/resources/documents.py,sha256=g5Y3rBRR3Sj4fWsTgTdJSxJcor-U383-pIxmNNr6iy8,3650
|
|
11
|
+
infratex/resources/responses.py,sha256=VZgWTPhTFLViEaqokG4BphuFmPSMJ1WaV38QVKGGyCQ,2184
|
|
12
|
+
infratex/resources/searches.py,sha256=HFWkAropTlp_8AgN4N5W8YipRj3snpdBadUYGpV_2nA,1486
|
|
13
|
+
infratex-0.1.0.dist-info/METADATA,sha256=aXO6Qfq6VWuJcGPKyFP2VhexzZukK8TRQeattZuN5h0,4793
|
|
14
|
+
infratex-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
infratex-0.1.0.dist-info/licenses/LICENSE,sha256=3rHxvsFpZj0vZKIrYtFcpPyqP2UcbfJE395E9NKFVDk,1065
|
|
16
|
+
infratex-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Infratex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|