deepwrap 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.
@@ -0,0 +1,4 @@
1
+ from .session_manager import SessionManager
2
+ from .auth import Auth
3
+
4
+ __all__ = ["SessionManager", "Auth"]
deepwrap/core/auth.py ADDED
@@ -0,0 +1,123 @@
1
+ import time
2
+ import requests
3
+ import websocket
4
+
5
+ from typing import Optional
6
+ from deepwrap.config import Config
7
+ from deepwrap.utils.browser_process import BrowserProcess
8
+ from deepwrap.utils.port_finder import PortFinder
9
+ from deepwrap.utils.cdp_client import CDPClient
10
+ from deepwrap.utils.dev_tools_http import DevToolsHTTP
11
+ from deepwrap.utils.bearer_token_extractor import BearerTokenExtractor
12
+
13
+ class BrowserAuthProvider:
14
+ """
15
+ Handles browser-based authentication to obtain a Bearer token.
16
+ - Launches a Chromium-based browser with remote debugging enabled.
17
+ - Waits for the user to complete the login process.
18
+ - Listens for network requests in the browser to extract the Bearer token from the Authorization header.
19
+ - Cleans up the browser process and temporary user data directory after authentication is complete or if a timeout occurs.
20
+ """
21
+
22
+ def __init__(self, login_url: str, timeout: Optional[int] = None) -> None:
23
+ self.login_url = login_url
24
+ self.timeout = timeout
25
+
26
+ def get_bearer_token(self) -> Optional[str]:
27
+ """
28
+ Perform the browser-based authentication flow to obtain a Bearer token.
29
+
30
+ Returns:
31
+ The obtained Bearer token if authentication is successful, otherwise None.
32
+ """
33
+
34
+ port = PortFinder.find_free_port()
35
+
36
+ browser = BrowserProcess(self.login_url, port)
37
+ client = None
38
+
39
+ try:
40
+ browser.start()
41
+
42
+ DevToolsHTTP.wait_until_ready(port)
43
+ websocket_url = DevToolsHTTP.get_page_websocket_url(port)
44
+
45
+ client = CDPClient(websocket_url)
46
+ client.enable_network()
47
+
48
+ started_at = time.time()
49
+
50
+ while True:
51
+ if self.timeout is not None and time.time() - started_at > self.timeout:
52
+ return None
53
+
54
+ try:
55
+ message = client.recv()
56
+ except websocket.WebSocketTimeoutException:
57
+ continue
58
+
59
+ token = BearerTokenExtractor.from_cdp_message(message)
60
+
61
+ if token:
62
+ return token
63
+
64
+ finally:
65
+ if client:
66
+ client.close_browser()
67
+ client.close()
68
+
69
+ browser.terminate()
70
+
71
+
72
+ class Auth:
73
+ """
74
+ Handles authentication for DeepWrap interactions.
75
+
76
+ - Stores a requests.Session instance.
77
+ - Supports browser-based authentication through an installed Chromium browser.
78
+ - Captures Bearer tokens from authenticated browser requests.
79
+ - Applies Authorization header to the session.
80
+ """
81
+
82
+ def __init__(self) -> None:
83
+ self._is_authenticated = False
84
+ self.bearer_token: Optional[str] = None
85
+
86
+ def is_authed(self) -> bool:
87
+ """
88
+ Check if the session is currently authenticated.
89
+
90
+ Returns:
91
+ True if authenticated, False otherwise.
92
+ """
93
+
94
+ return self._is_authenticated
95
+
96
+ def browser(
97
+ self,
98
+ timeout: Optional[int] = None,
99
+ ) -> Optional[str]:
100
+ """
101
+ Perform browser-based authentication to obtain a Bearer token.
102
+
103
+ Args:
104
+ timeout: Optional timeout in seconds for the authentication process. If None, will wait indefinitely.
105
+
106
+ Returns:
107
+ The obtained Bearer token if authentication is successful, otherwise None.
108
+ """
109
+
110
+ provider = BrowserAuthProvider(
111
+ login_url = Config.login_url,
112
+ timeout = timeout,
113
+ )
114
+
115
+
116
+ token = provider.get_bearer_token()
117
+
118
+ if not token:
119
+ return None
120
+
121
+ self._is_authenticated = True
122
+
123
+ return token
@@ -0,0 +1,55 @@
1
+ import requests
2
+
3
+ from typing import Optional
4
+ from deepwrap.config import Config
5
+
6
+ class SessionManager:
7
+ """
8
+ Manages HTTP sessions with appropriate headers and cookies for DeepSeek interactions.
9
+ - Initializes a requests.Session with default headers from Config.
10
+ - Allows setting of authorization header using a bearer token.
11
+ - Supports adding cookies to the session for domain-specific interactions.
12
+ - Provides get and post methods that automatically include default timeouts.
13
+ """
14
+
15
+ def __init__(self, bearer_token: str, cookies: Optional[dict[str, str]] = None) -> None:
16
+ self.session = requests.Session()
17
+ self.session.headers.update(Config.headers)
18
+ self.session.headers["authorization"] = f"Bearer {bearer_token}"
19
+
20
+ if cookies:
21
+ for name, value in cookies.items():
22
+ self.session.cookies.set(name, value, domain = Config.base_domain)
23
+
24
+ def get(self, url: str, **kwargs) -> requests.Response:
25
+ """
26
+ Wrapper around requests.Session.get that applies default timeout and any additional kwargs.
27
+
28
+ Args:
29
+ url (str): The URL to send the GET request to.
30
+ **kwargs: Additional keyword arguments to pass to requests.Session.get.
31
+
32
+ Returns:
33
+ requests.Response: The response object resulting from the GET request.
34
+ """
35
+
36
+ kwargs.setdefault("timeout", Config.default_timeout)
37
+
38
+ return self.session.get(url, **kwargs)
39
+
40
+ def post(self, url: str, **kwargs) -> requests.Response:
41
+ """
42
+ Wrapper around requests.Session.post that applies default timeout and any additional kwargs.
43
+
44
+ Args:
45
+ url (str): The URL to send the POST request to.
46
+ **kwargs: Additional keyword arguments to pass to requests.Session.post.
47
+
48
+ Returns:
49
+ requests.Response: The response object resulting from the POST request.
50
+ """
51
+
52
+ if not kwargs.get("stream"):
53
+ kwargs.setdefault("timeout", Config.default_timeout)
54
+
55
+ return self.session.post(url, **kwargs)
@@ -0,0 +1 @@
1
+ from .cli import DeepWrapCLI
@@ -0,0 +1,373 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import threading
5
+ import uuid
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Dict, Generator, Literal, Optional
9
+
10
+ from fastapi import FastAPI, HTTPException, status
11
+ from fastapi.responses import StreamingResponse
12
+ from pydantic import BaseModel, Field, field_validator
13
+
14
+ from deepwrap import Client
15
+ from deepwrap.utils.config_store import ConfigStore
16
+
17
+
18
+ ModelName = Literal["expert", "default", "vision"]
19
+ StreamFormat = Literal["text", "sse"]
20
+
21
+
22
+ class ChatRequest(BaseModel):
23
+ message: str = Field(..., min_length=1)
24
+ model: ModelName = "expert"
25
+ token: Optional[str] = None
26
+ thinking: bool = True
27
+ search: bool = True
28
+ god_mode: bool = False
29
+ stream: bool = False
30
+ stream_format: StreamFormat = "text"
31
+ session_id: Optional[str] = None
32
+
33
+ @field_validator("message")
34
+ @classmethod
35
+ def normalize_message(cls, value: str) -> str:
36
+ value = value.strip()
37
+
38
+ if not value:
39
+ raise ValueError("Message cannot be empty.")
40
+
41
+ return value
42
+
43
+
44
+ class ChatResponse(BaseModel):
45
+ model: str
46
+ response: str
47
+ session_id: Optional[str] = None
48
+
49
+
50
+ class CreateSessionRequest(BaseModel):
51
+ model: ModelName = "expert"
52
+ token: Optional[str] = None
53
+ god_mode: bool = False
54
+
55
+
56
+ class CreateSessionResponse(BaseModel):
57
+ session_id: str
58
+ model: str
59
+ god_mode: bool
60
+
61
+
62
+ class DeleteSessionResponse(BaseModel):
63
+ ok: bool
64
+ session_id: str
65
+
66
+
67
+ class HealthResponse(BaseModel):
68
+ ok: bool
69
+ app: str
70
+ version: str
71
+ token_configured: bool
72
+ cached_clients: int
73
+ active_sessions: int
74
+
75
+
76
+ @dataclass
77
+ class SessionRecord:
78
+ chat: object
79
+ model: str
80
+ token_fingerprint: str
81
+ god_mode: bool
82
+ lock: threading.Lock = field(default_factory=threading.Lock)
83
+
84
+
85
+ class APIState:
86
+ def __init__(self) -> None:
87
+ self.store = ConfigStore()
88
+
89
+ self._clients: Dict[str, Client] = {}
90
+ self._sessions: Dict[str, SessionRecord] = {}
91
+
92
+ self._clients_lock = threading.Lock()
93
+ self._sessions_lock = threading.Lock()
94
+
95
+ @staticmethod
96
+ def fingerprint_token(token: str) -> str:
97
+ return hashlib.sha256(token.encode("utf-8")).hexdigest()
98
+
99
+ def resolve_token(self, explicit_token: Optional[str] = None) -> str:
100
+ if explicit_token:
101
+ return explicit_token.strip()
102
+
103
+ config = self.store.load()
104
+
105
+ if config.token:
106
+ return config.token.strip()
107
+
108
+ raise HTTPException(
109
+ status_code=status.HTTP_401_UNAUTHORIZED,
110
+ detail=(
111
+ "No token configured. Run `deepwrap auth --browser` first "
112
+ "or pass `token` in the request body."
113
+ ),
114
+ )
115
+
116
+ def get_client(self, token: str) -> Client:
117
+ fingerprint = self.fingerprint_token(token)
118
+
119
+ with self._clients_lock:
120
+ client = self._clients.get(fingerprint)
121
+
122
+ if client is not None:
123
+ return client
124
+
125
+ try:
126
+ client = Client(api_key=token)
127
+ except Exception as exc:
128
+ raise HTTPException(
129
+ status_code=status.HTTP_401_UNAUTHORIZED,
130
+ detail=f"Failed to initialize DeepWrap client: {exc}",
131
+ ) from exc
132
+
133
+ self._clients[fingerprint] = client
134
+
135
+ return client
136
+
137
+ def create_session(
138
+ self,
139
+ token: Optional[str],
140
+ model: str,
141
+ god_mode: bool,
142
+ ) -> CreateSessionResponse:
143
+ resolved_token = self.resolve_token(token)
144
+ fingerprint = self.fingerprint_token(resolved_token)
145
+ client = self.get_client(resolved_token)
146
+
147
+ try:
148
+ chat = client.chats.create_session(
149
+ model=model,
150
+ god_mode=god_mode,
151
+ )
152
+ except Exception as exc:
153
+ raise HTTPException(
154
+ status_code=status.HTTP_502_BAD_GATEWAY,
155
+ detail=f"Failed to create chat session: {exc}",
156
+ ) from exc
157
+
158
+ session_id = f"chat_{uuid.uuid4().hex}"
159
+
160
+ record = SessionRecord(
161
+ chat=chat,
162
+ model=model,
163
+ token_fingerprint=fingerprint,
164
+ god_mode=god_mode,
165
+ )
166
+
167
+ with self._sessions_lock:
168
+ self._sessions[session_id] = record
169
+
170
+ return CreateSessionResponse(
171
+ session_id=session_id,
172
+ model=model,
173
+ god_mode=god_mode,
174
+ )
175
+
176
+ def delete_session(self, session_id: str) -> DeleteSessionResponse:
177
+ with self._sessions_lock:
178
+ if session_id not in self._sessions:
179
+ raise HTTPException(
180
+ status_code=status.HTTP_404_NOT_FOUND,
181
+ detail=f"Unknown session_id: {session_id}",
182
+ )
183
+
184
+ self._sessions.pop(session_id)
185
+
186
+ return DeleteSessionResponse(
187
+ ok=True,
188
+ session_id=session_id,
189
+ )
190
+
191
+ def get_session(self, session_id: str) -> SessionRecord:
192
+ with self._sessions_lock:
193
+ record = self._sessions.get(session_id)
194
+
195
+ if record is None:
196
+ raise HTTPException(
197
+ status_code=status.HTTP_404_NOT_FOUND,
198
+ detail=f"Unknown session_id: {session_id}",
199
+ )
200
+
201
+ return record
202
+
203
+ def create_ephemeral_chat(
204
+ self,
205
+ token: Optional[str],
206
+ model: str,
207
+ god_mode: bool,
208
+ ):
209
+ resolved_token = self.resolve_token(token)
210
+ client = self.get_client(resolved_token)
211
+
212
+ try:
213
+ return client.chats.create_session(
214
+ model=model,
215
+ god_mode=god_mode,
216
+ )
217
+ except Exception as exc:
218
+ raise HTTPException(
219
+ status_code=status.HTTP_502_BAD_GATEWAY,
220
+ detail=f"Failed to create chat session: {exc}",
221
+ ) from exc
222
+
223
+ @property
224
+ def cached_clients_count(self) -> int:
225
+ with self._clients_lock:
226
+ return len(self._clients)
227
+
228
+ @property
229
+ def active_sessions_count(self) -> int:
230
+ with self._sessions_lock:
231
+ return len(self._sessions)
232
+
233
+
234
+ def format_sse_chunk(chunk: str) -> str:
235
+ chunk = chunk.replace("\r", "")
236
+
237
+ lines = chunk.split("\n")
238
+
239
+ return "".join(f"data: {line}\n" for line in lines) + "\n"
240
+
241
+
242
+ def create_app() -> FastAPI:
243
+ app = FastAPI(
244
+ title="DeepWrap API",
245
+ version="0.1.0",
246
+ description="Local HTTP API for DeepWrap.",
247
+ )
248
+
249
+ state = APIState()
250
+
251
+ @app.get("/health", response_model=HealthResponse)
252
+ def health() -> HealthResponse:
253
+ config = state.store.load()
254
+
255
+ return HealthResponse(
256
+ ok=True,
257
+ app="deepwrap",
258
+ version="0.1.0",
259
+ token_configured=bool(config.token),
260
+ cached_clients=state.cached_clients_count,
261
+ active_sessions=state.active_sessions_count,
262
+ )
263
+
264
+ @app.post("/sessions", response_model=CreateSessionResponse)
265
+ def create_session(request: CreateSessionRequest) -> CreateSessionResponse:
266
+ return state.create_session(
267
+ token=request.token,
268
+ model=request.model,
269
+ god_mode=request.god_mode,
270
+ )
271
+
272
+ @app.delete("/sessions/{session_id}", response_model=DeleteSessionResponse)
273
+ def delete_session(session_id: str) -> DeleteSessionResponse:
274
+ return state.delete_session(session_id)
275
+
276
+ @app.post("/chat")
277
+ def chat(request: ChatRequest):
278
+ if request.session_id:
279
+ record = state.get_session(request.session_id)
280
+
281
+ if record.model != request.model:
282
+ raise HTTPException(
283
+ status_code=status.HTTP_409_CONFLICT,
284
+ detail=(
285
+ f"Session model mismatch. Session uses '{record.model}', "
286
+ f"but request asked for '{request.model}'."
287
+ ),
288
+ )
289
+
290
+ chat_session = record.chat
291
+ session_lock = record.lock
292
+ response_session_id = request.session_id
293
+
294
+ else:
295
+ chat_session = state.create_ephemeral_chat(
296
+ token=request.token,
297
+ model=request.model,
298
+ god_mode=request.god_mode,
299
+ )
300
+
301
+ session_lock = threading.Lock()
302
+ response_session_id = None
303
+
304
+ if request.stream:
305
+ def stream_text() -> Generator[str, None, None]:
306
+ with session_lock:
307
+ try:
308
+ for chunk in chat_session.respond(
309
+ request.message,
310
+ thinking=request.thinking,
311
+ search=request.search,
312
+ stream=True,
313
+ ):
314
+ yield chunk
315
+
316
+ except Exception as exc:
317
+ yield f"\n[DeepWrap API error] {exc}\n"
318
+
319
+ def stream_sse() -> Generator[str, None, None]:
320
+ with session_lock:
321
+ try:
322
+ for chunk in chat_session.respond(
323
+ request.message,
324
+ thinking=request.thinking,
325
+ search=request.search,
326
+ stream=True,
327
+ ):
328
+ yield format_sse_chunk(chunk)
329
+
330
+ yield "event: done\ndata: [DONE]\n\n"
331
+
332
+ except Exception as exc:
333
+ yield f"event: error\ndata: {str(exc)}\n\n"
334
+
335
+ if request.stream_format == "sse":
336
+ return StreamingResponse(
337
+ stream_sse(),
338
+ media_type="text/event-stream",
339
+ headers={
340
+ "Cache-Control": "no-cache",
341
+ "X-Accel-Buffering": "no",
342
+ },
343
+ )
344
+
345
+ return StreamingResponse(
346
+ stream_text(),
347
+ media_type="text/plain; charset=utf-8",
348
+ )
349
+
350
+ with session_lock:
351
+ try:
352
+ response = chat_session.respond(
353
+ request.message,
354
+ thinking=request.thinking,
355
+ search=request.search,
356
+ stream=False,
357
+ )
358
+ except Exception as exc:
359
+ raise HTTPException(
360
+ status_code=status.HTTP_502_BAD_GATEWAY,
361
+ detail=f"Chat request failed: {exc}",
362
+ ) from exc
363
+
364
+ return ChatResponse(
365
+ model=request.model,
366
+ response=response,
367
+ session_id=response_session_id,
368
+ )
369
+
370
+ return app
371
+
372
+
373
+ app = create_app()