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 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.