11from __future__ import annotations
22
33import os
4- from typing import Any
4+ from typing import Any , NoReturn
55
66import httpx
77
8+ from ._exceptions import (
9+ APIConnectionError ,
10+ APIError ,
11+ APITimeoutError ,
12+ AuthenticationError ,
13+ NotFoundError ,
14+ PermissionDeniedError ,
15+ RateLimitError ,
16+ )
17+ from ._streaming import Stream
818from ._version import __version__
19+ from .resources .chat import Chat
20+ from .resources .connectors import Connectors
21+ from .resources .playbooks import Playbooks
22+ from .resources .sandbox import Sandbox
923
10- DEFAULT_BASE_URL = "https://api .textql.com"
24+ DEFAULT_BASE_URL = "https://app .textql.com"
1125DEFAULT_TIMEOUT = 60.0
1226
1327
1428class TextQL :
15- """Synchronous client for the TextQL Platform API.
29+ """Synchronous client for the TextQL v2 Platform API."""
1630
17- Resource clients (`chat`, `playbooks`, `sandbox`, `connectors`) are stubs
18- in v0.1.0 — they will be filled in as the v2 SDK takes shape.
19- """
31+ chat : Chat
32+ connectors : Connectors
33+ playbooks : Playbooks
34+ sandbox : Sandbox
2035
2136 def __init__ (
2237 self ,
@@ -28,26 +43,88 @@ def __init__(
2843 ) -> None :
2944 key = api_key or os .environ .get ("TEXTQL_API_KEY" )
3045 if not key :
31- raise ValueError (
32- "No API key provided. Pass api_key=... or set TEXTQL_API_KEY in the environment."
33- )
34-
46+ raise ValueError ("No API key provided. Pass api_key=... or set TEXTQL_API_KEY." )
3547 self .api_key = key
36- resolved_base = base_url or os .environ .get ("TEXTQL_BASE_URL" ) or DEFAULT_BASE_URL
37- self .base_url = resolved_base .rstrip ("/" )
48+
49+ resolved = base_url or os .environ .get ("TEXTQL_BASE_URL" ) or DEFAULT_BASE_URL
50+ resolved = resolved .rstrip ("/" )
51+ if not resolved .startswith (("http://" , "https://" )):
52+ resolved = f"https://{ resolved } "
53+ self .base_url = resolved
54+
3855 self ._http = http_client or httpx .Client (
3956 base_url = self .base_url ,
4057 timeout = timeout ,
4158 headers = self ._default_headers (),
4259 )
4360
61+ self .chat = Chat (self )
62+ self .connectors = Connectors (self )
63+ self .playbooks = Playbooks (self )
64+ self .sandbox = Sandbox (self )
65+
4466 def _default_headers (self ) -> dict [str , str ]:
4567 return {
4668 "Authorization" : f"Bearer { self .api_key } " ,
4769 "User-Agent" : f"textql-python/{ __version__ } " ,
4870 "Accept" : "application/json" ,
4971 }
5072
73+ def _request (self , method : str , path : str , ** kwargs : Any ) -> Any :
74+ try :
75+ resp = self ._http .request (method , path , ** kwargs )
76+ except httpx .TimeoutException as e :
77+ raise APITimeoutError (str (e )) from e
78+ except httpx .ConnectError as e :
79+ raise APIConnectionError (str (e )) from e
80+
81+ if resp .status_code >= 400 :
82+ self ._raise_for_status (resp )
83+
84+ if not resp .content :
85+ return None
86+ return resp .json ()
87+
88+ def _stream_request (self , method : str , path : str , ** kwargs : Any ) -> Stream :
89+ try :
90+ req = self ._http .build_request (method , path , ** kwargs )
91+ resp = self ._http .send (req , stream = True )
92+ except httpx .TimeoutException as e :
93+ raise APITimeoutError (str (e )) from e
94+ except httpx .ConnectError as e :
95+ raise APIConnectionError (str (e )) from e
96+
97+ if resp .status_code >= 400 :
98+ resp .read ()
99+ resp .close ()
100+ self ._raise_for_status (resp )
101+
102+ return Stream (resp )
103+
104+ def _raise_for_status (self , response : httpx .Response ) -> NoReturn :
105+ message = response .text
106+ request_id : str | None = None
107+ try :
108+ body = response .json ()
109+ if isinstance (body , dict ):
110+ error = body .get ("error" )
111+ if isinstance (error , dict ):
112+ message = error .get ("message" , message )
113+ request_id = error .get ("request" )
114+ except Exception :
115+ pass
116+
117+ status = response .status_code
118+ if status == 401 :
119+ raise AuthenticationError (message , status_code = status , request_id = request_id )
120+ if status == 403 :
121+ raise PermissionDeniedError (message , status_code = status , request_id = request_id )
122+ if status == 404 :
123+ raise NotFoundError (message , status_code = status , request_id = request_id )
124+ if status == 429 :
125+ raise RateLimitError (message , status_code = status , request_id = request_id )
126+ raise APIError (message , status_code = status , request_id = request_id )
127+
51128 def close (self ) -> None :
52129 self ._http .close ()
53130
0 commit comments