darkpyonix 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.
darkpyonix/__init__.py ADDED
File without changes
darkpyonix/__main__.py ADDED
File without changes
File without changes
File without changes
@@ -0,0 +1,432 @@
1
+ import asyncio
2
+ import socket
3
+ import array
4
+ import os
5
+ import platform
6
+ import json
7
+ import uuid
8
+ from typing import Dict, List, Optional, AsyncGenerator
9
+ from fastapi import FastAPI, Request, HTTPException
10
+ from fastapi.responses import StreamingResponse
11
+ from contextlib import asynccontextmanager
12
+ #import uvloop # Linux/macOS에서 성능 향상
13
+
14
+
15
+ class UnixFDManager:
16
+ def __init__(self):
17
+ self.unix_sockets: Dict[str, socket.socket] = {}
18
+
19
+ def send_fd(self, sock: socket.socket, fd: int, msg: bytes = b"FD"):
20
+ """Unix Domain Socket을 통해 FD 전달"""
21
+ try:
22
+ return sock.sendmsg(
23
+ [msg],
24
+ [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", [fd]))]
25
+ )
26
+ except OSError as e:
27
+ raise RuntimeError(f"Failed to send FD {fd}: {e}")
28
+
29
+ def recv_fd(self, sock: socket.socket, maxfds: int = 1) -> tuple[bytes, List[int]]:
30
+ """Unix Domain Socket에서 FD 수신"""
31
+ fds = array.array("i")
32
+ msg, ancdata, flags, addr = sock.recvmsg(
33
+ 1024,
34
+ socket.CMSG_LEN(maxfds * fds.itemsize)
35
+ )
36
+
37
+ received_fds = []
38
+ for cmsg_level, cmsg_type, cmsg_data in ancdata:
39
+ if (cmsg_level == socket.SOL_SOCKET and
40
+ cmsg_type == socket.SCM_RIGHTS):
41
+ fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
42
+ received_fds.extend(fds.tolist())
43
+
44
+ return msg, received_fds
45
+
46
+ # SSE 연결 관리
47
+ class SSEConnection:
48
+ def __init__(self, client_socket: socket.socket, client_addr: tuple):
49
+ self.client_socket = client_socket
50
+ self.client_addr = client_addr
51
+ self.connection_id = str(uuid.uuid4())
52
+ self.is_active = True
53
+
54
+ def close(self):
55
+ """SSE 연결 종료"""
56
+ self.is_active = False
57
+ if self.client_socket:
58
+ try:
59
+ self.client_socket.close()
60
+ except:
61
+ pass
62
+
63
+ # 커널 매니저 클래스
64
+ class KernelManager:
65
+ def __init__(self):
66
+ self.kernels: Dict[str, dict] = {} # kernel_id -> kernel_info
67
+ self.sse_connections: Dict[str, SSEConnection] = {}
68
+ self.kernel_sockets: Dict[str, socket.socket] = {}
69
+
70
+ # 플랫폼별 FD 매니저 초기화
71
+ if platform.system() == "Windows":
72
+ self.fd_manager = WindowsFDManager()
73
+ else:
74
+ self.fd_manager = UnixFDManager()
75
+
76
+ async def create_kernel(self, kernel_id: str) -> dict:
77
+ """새 커널 프로세스 생성"""
78
+ # Unix Domain Socket 쌍 생성 (커널 매니저 <-> 커널)
79
+ if platform.system() != "Windows":
80
+ sock_pair = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
81
+ manager_sock, kernel_sock = sock_pair
82
+
83
+ # 커널 프로세스 시작
84
+ kernel_process = await asyncio.create_subprocess_exec(
85
+ "python", "kernel_process.py",
86
+ "--kernel-id", kernel_id,
87
+ "--socket-fd", str(kernel_sock.fileno()),
88
+ pass_fds=[kernel_sock.fileno()] # FD를 자식 프로세스에 전달
89
+ )
90
+
91
+ # 소켓 저장
92
+ self.kernel_sockets[kernel_id] = manager_sock
93
+ kernel_sock.close() # 부모에서는 더 이상 필요 없음
94
+
95
+ else:
96
+ # Windows Named Pipe 구현
97
+ pipe_name = f"kernel_{kernel_id}"
98
+ # Windows 구현...
99
+ pass
100
+
101
+ kernel_info = {
102
+ "process": kernel_process,
103
+ "status": "running",
104
+ "created_at": asyncio.get_event_loop().time()
105
+ }
106
+ self.kernels[kernel_id] = kernel_info
107
+ return kernel_info
108
+
109
+ async def pass_http_socket_to_kernel(self, kernel_id: str, request: Request):
110
+ """HTTP 연결의 소켓을 커널에 전달"""
111
+ if platform.system() != "Windows":
112
+ # FastAPI/Uvicorn에서 원시 소켓 추출
113
+ transport = request.scope.get("transport")
114
+ if not transport:
115
+ raise ValueError("No transport found in request scope")
116
+
117
+ # Uvicorn transport에서 소켓 추출
118
+ client_socket = transport.get_extra_info('socket')
119
+ if not client_socket:
120
+ raise ValueError("Could not extract socket from transport")
121
+
122
+ kernel_sock = self.kernel_sockets.get(kernel_id)
123
+ if not kernel_sock:
124
+ raise ValueError(f"Kernel {kernel_id} not found")
125
+
126
+ # 클라이언트 소켓 FD를 커널에 전달
127
+ client_fd = client_socket.fileno()
128
+ self.fd_manager.send_fd(kernel_sock, client_fd)
129
+
130
+ # 클라이언트 정보도 함께 전송
131
+ client_info = {
132
+ "connection_type": "sse",
133
+ "client_addr": request.client.host if request.client else "unknown",
134
+ "user_agent": request.headers.get("user-agent", ""),
135
+ "connection_id": str(uuid.uuid4())
136
+ }
137
+ await self._send_json_to_kernel(kernel_sock, client_info)
138
+
139
+ return client_info["connection_id"]
140
+
141
+ else:
142
+ # Windows: DuplicateHandle 구현
143
+ pass
144
+
145
+ async def _send_json_to_kernel(self, kernel_sock: socket.socket, data: dict):
146
+ """커널에 JSON 데이터 전송"""
147
+ message = json.dumps(data).encode() + b'\n'
148
+ kernel_sock.send(message)
149
+
150
+ async def execute_code(self, kernel_id: str, code: str, request: Request) -> AsyncGenerator[str, None]:
151
+ """코드 실행 요청을 커널에 전달하고 SSE 스트림 생성"""
152
+ # HTTP 소켓을 커널에 전달
153
+ connection_id = await self.pass_http_socket_to_kernel(kernel_id, request)
154
+
155
+ # 실행 요청을 커널에 전송
156
+ execute_request = {
157
+ "msg_type": "execute_request",
158
+ "connection_id": connection_id,
159
+ "content": {
160
+ "code": code,
161
+ "silent": False
162
+ }
163
+ }
164
+
165
+ kernel_sock = self.kernel_sockets.get(kernel_id)
166
+ if kernel_sock:
167
+ await self._send_json_to_kernel(kernel_sock, execute_request)
168
+
169
+ # 이제 커널이 SSE 응답을 직접 클라이언트로 전송
170
+ # 매니저는 연결이 활성화되어 있는지만 확인
171
+ yield f"data: {json.dumps({'status': 'executing', 'connection_id': connection_id})}\n\n"
172
+
173
+ # 커널이 직접 응답을 보내므로 여기서는 연결 상태만 모니터링
174
+ while True:
175
+ await asyncio.sleep(0.1)
176
+ # 실제로는 커널이 직접 SSE 응답을 보냄
177
+ break
178
+
179
+ # FastAPI 애플리케이션
180
+ class JupyterKernelAPI:
181
+ def __init__(self):
182
+ self.app = FastAPI(title="Jupyter Kernel Manager with SSE FD Passing")
183
+ self.kernel_manager = KernelManager()
184
+ self.setup_routes()
185
+
186
+ def setup_routes(self):
187
+ @self.app.post("/kernels")
188
+ async def create_kernel():
189
+ """새 커널 생성"""
190
+ kernel_id = str(uuid.uuid4())
191
+ kernel_info = await self.kernel_manager.create_kernel(kernel_id)
192
+ return {"kernel_id": kernel_id, "status": kernel_info["status"]}
193
+
194
+ @self.app.get("/kernels")
195
+ async def list_kernels():
196
+ """커널 목록 조회"""
197
+ return {
198
+ "kernels": [
199
+ {"kernel_id": kid, "status": info["status"]}
200
+ for kid, info in self.kernel_manager.kernels.items()
201
+ ]
202
+ }
203
+
204
+ @self.app.delete("/kernels/{kernel_id}")
205
+ async def delete_kernel(kernel_id: str):
206
+ """커널 삭제"""
207
+ if kernel_id not in self.kernel_manager.kernels:
208
+ raise HTTPException(status_code=404, detail="Kernel not found")
209
+
210
+ kernel_info = self.kernel_manager.kernels[kernel_id]
211
+ if kernel_info["process"]:
212
+ kernel_info["process"].terminate()
213
+ await kernel_info["process"].wait()
214
+
215
+ del self.kernel_manager.kernels[kernel_id]
216
+ return {"status": "deleted"}
217
+
218
+ @self.app.post("/kernels/{kernel_id}/execute")
219
+ async def execute_code_endpoint(kernel_id: str, request: Request):
220
+ """코드 실행 (SSE 응답)"""
221
+ if kernel_id not in self.kernel_manager.kernels:
222
+ raise HTTPException(status_code=404, detail="Kernel not found")
223
+
224
+ # 요청 본문에서 코드 추출
225
+ body = await request.body()
226
+ try:
227
+ data = json.loads(body)
228
+ code = data.get("code", "")
229
+ except json.JSONDecodeError:
230
+ raise HTTPException(status_code=400, detail="Invalid JSON")
231
+
232
+ # SSE 스트림 생성 및 FD 전달
233
+ async def sse_generator():
234
+ async for chunk in self.kernel_manager.execute_code(kernel_id, code, request):
235
+ yield chunk
236
+
237
+ return StreamingResponse(
238
+ sse_generator(),
239
+ media_type="text/event-stream",
240
+ headers={
241
+ "Cache-Control": "no-cache",
242
+ "Connection": "keep-alive",
243
+ "Access-Control-Allow-Origin": "*",
244
+ "Access-Control-Allow-Headers": "Cache-Control"
245
+ }
246
+ )
247
+
248
+ # 커널 프로세스 측 코드 (kernel_process.py)
249
+ KERNEL_PROCESS_CODE = '''
250
+ import asyncio
251
+ import socket
252
+ import json
253
+ import sys
254
+ import array
255
+ import time
256
+ import traceback
257
+
258
+ class SSEKernelProcess:
259
+ def __init__(self, kernel_id: str, manager_socket_fd: int):
260
+ self.kernel_id = kernel_id
261
+ # 매니저로부터 받은 소켓 FD 복원
262
+ self.manager_socket = socket.fromfd(
263
+ manager_socket_fd,
264
+ socket.AF_UNIX,
265
+ socket.SOCK_STREAM
266
+ )
267
+ self.client_connections = {}
268
+
269
+ async def run(self):
270
+ """커널 메인 루프"""
271
+ print(f"Kernel {self.kernel_id} started")
272
+
273
+ while True:
274
+ try:
275
+ # 매니저로부터 클라이언트 FD 수신 대기
276
+ msg, fds = await self.receive_fd_from_manager()
277
+
278
+ if fds:
279
+ # 새 클라이언트 소켓 처리
280
+ for fd in fds:
281
+ client_socket = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
282
+ asyncio.create_task(self.handle_client_sse(client_socket, msg))
283
+
284
+ except Exception as e:
285
+ print(f"Kernel {self.kernel_id} error: {e}")
286
+ traceback.print_exc()
287
+ break
288
+
289
+ async def receive_fd_from_manager(self):
290
+ """매니저로부터 FD 수신"""
291
+ loop = asyncio.get_event_loop()
292
+
293
+ # 논블로킹으로 메시지 수신
294
+ try:
295
+ ready = await asyncio.wait_for(
296
+ loop.sock_recv(self.manager_socket, 1024),
297
+ timeout=1.0
298
+ )
299
+ except asyncio.TimeoutError:
300
+ return None, []
301
+
302
+ if not ready:
303
+ raise ConnectionError("Manager socket closed")
304
+
305
+ # SCM_RIGHTS로 FD 수신
306
+ fds = array.array("i")
307
+ msg, ancdata, flags, addr = self.manager_socket.recvmsg(
308
+ 1024, socket.CMSG_LEN(1 * fds.itemsize)
309
+ )
310
+
311
+ received_fds = []
312
+ for cmsg_level, cmsg_type, cmsg_data in ancdata:
313
+ if (cmsg_level == socket.SOL_SOCKET and
314
+ cmsg_type == socket.SCM_RIGHTS):
315
+ fds.frombytes(cmsg_data)
316
+ received_fds.extend(fds.tolist())
317
+
318
+ # JSON 메시지 파싱
319
+ try:
320
+ client_info = json.loads(msg.decode().strip())
321
+ except json.JSONDecodeError:
322
+ client_info = {}
323
+
324
+ return client_info, received_fds
325
+
326
+ async def handle_client_sse(self, client_socket: socket.socket, client_info: dict):
327
+ """SSE 클라이언트와 직접 통신"""
328
+ connection_id = client_info.get("connection_id", "unknown")
329
+ print(f"Kernel {self.kernel_id} handling SSE client {connection_id} directly")
330
+
331
+ loop = asyncio.get_event_loop()
332
+
333
+ try:
334
+ # HTTP 요청 읽기 (이미 전달된 연결이므로 요청 데이터가 있을 수 있음)
335
+
336
+ # SSE 응답 헤더 전송
337
+ sse_headers = (
338
+ "HTTP/1.1 200 OK\\r\\n"
339
+ "Content-Type: text/event-stream\\r\\n"
340
+ "Cache-Control: no-cache\\r\\n"
341
+ "Connection: keep-alive\\r\\n"
342
+ "Access-Control-Allow-Origin: *\\r\\n"
343
+ "\\r\\n"
344
+ ).encode()
345
+
346
+ await loop.sock_sendall(client_socket, sse_headers)
347
+
348
+ # 실행 시작 이벤트
349
+ start_event = f"data: {json.dumps({'status': 'started', 'kernel_id': self.kernel_id})}\\n\\n"
350
+ await loop.sock_sendall(client_socket, start_event.encode())
351
+
352
+ # 실제 코드 실행 시뮬레이션
353
+ await self.execute_python_code(client_socket, client_info)
354
+
355
+ except Exception as e:
356
+ print(f"SSE Client {connection_id} error: {e}")
357
+ traceback.print_exc()
358
+ finally:
359
+ client_socket.close()
360
+
361
+ async def execute_python_code(self, client_socket: socket.socket, client_info: dict):
362
+ """Python 코드 실행 및 SSE 스트리밍"""
363
+ loop = asyncio.get_event_loop()
364
+
365
+ # 실행 중 이벤트
366
+ executing_event = f"data: {json.dumps({'status': 'executing', 'timestamp': time.time()})}\\n\\n"
367
+ await loop.sock_sendall(client_socket, executing_event.encode())
368
+
369
+ # 코드 실행 시뮬레이션 (실제로는 IPython 커널 로직)
370
+ await asyncio.sleep(0.1) # 실행 지연 시뮬레이션
371
+
372
+ # 결과 스트리밍
373
+ results = [
374
+ {"type": "stdout", "text": "Hello from kernel!"},
375
+ {"type": "execute_result", "data": {"text/plain": "4"}},
376
+ ]
377
+
378
+ for result in results:
379
+ result_event = f"data: {json.dumps(result)}\\n\\n"
380
+ await loop.sock_sendall(client_socket, result_event.encode())
381
+ await asyncio.sleep(0.05) # 스트리밍 효과
382
+
383
+ # 완료 이벤트
384
+ complete_event = f"data: {json.dumps({'status': 'complete', 'execution_count': 1})}\\n\\n"
385
+ await loop.sock_sendall(client_socket, complete_event.encode())
386
+
387
+ if __name__ == "__main__":
388
+ import argparse
389
+ parser = argparse.ArgumentParser()
390
+ parser.add_argument("--kernel-id", required=True)
391
+ parser.add_argument("--socket-fd", type=int, required=True)
392
+ args = parser.parse_args()
393
+
394
+ kernel = SSEKernelProcess(args.kernel_id, args.socket_fd)
395
+ asyncio.run(kernel.run())
396
+ '''
397
+
398
+ # 실행을 위한 설정
399
+ def setup_uvloop():
400
+ """성능 향상을 위한 uvloop 설정 (Linux/macOS)"""
401
+ if platform.system() != "Windows":
402
+ try:
403
+ import uvloop
404
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
405
+ except ImportError:
406
+ print("uvloop not available, using default event loop")
407
+
408
+ async def main():
409
+ """메인 실행 함수"""
410
+ setup_uvloop()
411
+
412
+ # kernel_process.py 파일 생성
413
+ with open("kernel_process.py", "w") as f:
414
+ f.write(KERNEL_PROCESS_CODE)
415
+
416
+ # FastAPI 앱 실행
417
+ kernel_api = JupyterKernelAPI()
418
+
419
+ import uvicorn
420
+ config = uvicorn.Config(
421
+ kernel_api.app,
422
+ host="0.0.0.0",
423
+ port=8000,
424
+ #loop="uvloop" if platform.system() != "Windows" else "asyncio",
425
+ access_log=False # 성능을 위해 비활성화
426
+ )
427
+ server = uvicorn.Server(config)
428
+ await server.serve()
429
+
430
+
431
+ if __name__ == '__main__':
432
+ asyncio.run(main())
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.1
2
+ Name: darkpyonix
3
+ Version: 0.1.0
4
+ Summary: AI-optimized Python kernel with VS Code integration
5
+ Author-Email: Your Name <your.email@example.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 DarkPyonix
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.9
29
+ Requires-Dist: fastapi>=0.116.1
30
+ Requires-Dist: uvicorn>=0.35.0
31
+ Provides-Extra: build
32
+ Requires-Dist: pdm>=2.25.5; extra == "build"
33
+ Description-Content-Type: text/markdown
34
+
35
+ # darkpyonix
36
+ DarkPyonix Kernel for AI/ML Development
@@ -0,0 +1,10 @@
1
+ darkpyonix-0.1.0.dist-info/METADATA,sha256=EZf524iC9dchPenMh6xm_iAaWupMkn8m909fXbqJ8kQ,1677
2
+ darkpyonix-0.1.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ darkpyonix-0.1.0.dist-info/entry_points.txt,sha256=lVTVlrduYQOg2AvQRATpfG1UsMS7GHGLNo6yChSrkRI,72
4
+ darkpyonix-0.1.0.dist-info/licenses/LICENSE,sha256=rptdrmBr1zwKQDp0xJZZVuz6AaLTc1yoxdrDuZrZi1Y,1088
5
+ darkpyonix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ darkpyonix/__main__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ darkpyonix/kernel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ darkpyonix/kernel/__main__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ darkpyonix/manager/app.py,sha256=esyqXlopWKCI7IJUuXrrQrH7hHj0rRYXhva6AFC0HMc,16351
10
+ darkpyonix-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.4.5)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ darkpyonix = darkpyonix.__main__:main
3
+
4
+ [gui_scripts]
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 DarkPyonix
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.