File size: 6,546 Bytes
dd07930
76ca3e9
dd07930
 
76ca3e9
 
53b7d1b
 
76ca3e9
 
1d7545f
76ca3e9
49a61bb
76ca3e9
dd07930
 
 
76ca3e9
49a61bb
76ca3e9
 
49a61bb
76ca3e9
 
dd07930
49a61bb
 
 
 
 
 
dd07930
53b7d1b
 
 
 
 
26b4d36
362d092
 
26b4d36
 
 
 
 
 
76ca3e9
dd07930
 
 
 
 
 
 
 
 
 
 
 
 
 
76ca3e9
 
 
 
 
 
 
 
 
 
 
dd07930
 
 
 
 
 
76ca3e9
 
 
 
 
 
 
 
dd07930
 
 
 
 
53b7d1b
1d7545f
53b7d1b
 
1d7545f
 
 
 
dd07930
 
 
49a61bb
dd07930
 
 
 
362d092
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd07930
 
362d092
 
 
 
 
 
 
 
 
 
dd07930
 
 
 
 
 
 
76ca3e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd07930
 
76ca3e9
 
dd07930
76ca3e9
dd07930
76ca3e9
dd07930
49a61bb
 
 
 
dd07930
49a61bb
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# server.py
from dataclasses import dataclass, asdict
import secrets
import logging
import asyncio
import json
import yaml
from pathlib import Path
from typing import Tuple

from quart import Quart, websocket, request, send_from_directory, redirect
from quart_schema import QuartSchema, validate_request, validate_response
from quart_cors import cors
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware

from broker import SessionBroker, SessionDoesNotExist, ClientRequest, ClientResponse, ClientError

# Configuraci贸n
VERSION = "1.0.0"  # Versi贸n de la API
TIMEOUT: int = 40
LOG_LEVEL: int = logging.DEBUG
TRUSTED_HOSTS: list[str] = ["127.0.0.1", "10.16.38.136", "10.16.3.13", "10.16.13.73"]

# Create app
app = Quart(__name__)
app = cors(app, 
    allow_origin=["https://*.hf.space", "https://*.huggingface.co"],
    allow_methods=["GET", "POST", "OPTIONS"],
    allow_headers=["Content-Type"],
    max_age=3600
)

# Cargar OpenAPI spec
OPENAPI_PATH = Path(__file__).parent / "openapi.yaml"
with open(OPENAPI_PATH) as f:
    openapi_spec = yaml.safe_load(f)

# Configurar Quart Schema con la especificaci贸n OpenAPI
schema = QuartSchema(app)
schema.update_openapi(openapi_spec)

app.asgi_app = ProxyHeadersMiddleware(app.asgi_app, trusted_hosts=TRUSTED_HOSTS)
app.logger.setLevel(LOG_LEVEL)

broker = SessionBroker()

# Modelos de datos
@dataclass
class Status:
    status: str
    version: str

@dataclass
class Session:
    session_id: str

@dataclass
class Command:
    session_id: str
    command: str

@dataclass
class Read:
    session_id: str
    path: str

@dataclass
class Write:
    session_id: str
    path: str
    content: str

@dataclass
class CommandResponse:
    return_code: int
    stdout: str
    stderr: str

@dataclass
class ReadResponse:
    content: str

@dataclass
class WriteResponse:
    size: int

@dataclass
class ErrorResponse:
    error: str

# Rutas API
@app.route('/')
async def root():
    return await send_from_directory('static', 'swagger.html')

@app.route('/docs')
async def docs():
    return redirect('/')

@app.get("/status")
@validate_response(Status)
async def status() -> Status:
    return Status(status="OK", version=VERSION)

@app.websocket('/session')
async def session_handler():
    try:
        session_id = secrets.token_hex()
        app.logger.info(f"{websocket.remote_addr} - NEW SESSION - {session_id}")
        await websocket.send_as(Session(session_id=session_id), Session)

        task = None
        try:
            task = asyncio.ensure_future(_receive(session_id))
            async for request in broker.subscribe(session_id):
                app.logger.info(f"{websocket.remote_addr} - REQUEST - {session_id} - {json.dumps(asdict(request))}")
                await websocket.send_as(request, ClientRequest)
        except websockets.exceptions.ConnectionClosed:
            app.logger.warning(f"{websocket.remote_addr} - CONNECTION CLOSED - {session_id}")
        except Exception as e:
            app.logger.error(f"{websocket.remote_addr} - ERROR - {session_id} - {str(e)}")
            raise
        finally:
            if task is not None:
                task.cancel()
                try:
                    await task
                except asyncio.CancelledError:
                    pass
    except Exception as e:
        app.logger.error(f"Error in session handler: {str(e)}")
        raise

async def _receive(session_id: str) -> None:
    try:
        while True:
            response = await websocket.receive_as(ClientResponse)
            app.logger.info(f"{websocket.remote_addr} - RESPONSE - {session_id} - {json.dumps(asdict(response))}")
            await broker.receive_response(session_id, response)
    except websockets.exceptions.ConnectionClosed:
        app.logger.warning(f"{websocket.remote_addr} - RECEIVE CONNECTION CLOSED - {session_id}")
    except Exception as e:
        app.logger.error(f"{websocket.remote_addr} - RECEIVE ERROR - {session_id} - {str(e)}")
        raise

@app.post('/command')
@validate_request(Command)
@validate_response(CommandResponse, 200)
@validate_response(ErrorResponse, 500)
async def command(data: Command) -> Tuple[CommandResponse | ErrorResponse, int]:
    try:
        response = CommandResponse(**await broker.send_request(
            data.session_id,
            {'action': 'command', 'command': data.command},
            timeout=TIMEOUT))
        return response, 200
    except SessionDoesNotExist:
        app.logger.warning(f"{request.remote_addr} - INVALID SESSION ID - {repr(data.session_id)}")
        return ErrorResponse('Session does not exist.'), 500
    except ClientError as e:
        return ErrorResponse(e.message), 500
    except asyncio.TimeoutError:
        return ErrorResponse('Timeout when waiting for client.'), 500

@app.post('/read')
@validate_request(Read)
@validate_response(ReadResponse, 200)
@validate_response(ErrorResponse, 500)
async def read(data: Read) -> Tuple[ReadResponse | ErrorResponse, int]:
    try:
        response = ReadResponse(**await broker.send_request(
            data.session_id,
            {'action': 'read', 'path': data.path},
            timeout=TIMEOUT))
        return response, 200
    except SessionDoesNotExist:
        app.logger.warning(f"{request.remote_addr} - INVALID SESSION ID - {repr(data.session_id)}")
        return ErrorResponse('Session does not exist.'), 500
    except ClientError as e:
        return ErrorResponse(e.message), 500
    except asyncio.TimeoutError:
        return ErrorResponse('Timeout when waiting for client.'), 500

@app.post('/write')
@validate_request(Write)
@validate_response(WriteResponse, 200)
@validate_response(ErrorResponse, 500)
async def write(data: Write) -> Tuple[WriteResponse | ErrorResponse, int]:
    try:
        response = WriteResponse(**await broker.send_request(
            data.session_id,
            {'action': 'write', 'path': data.path, 'content': data.content},
            timeout=TIMEOUT))
        return response, 200
    except SessionDoesNotExist:
        app.logger.warning(f"{request.remote_addr} - INVALID SESSION ID - {repr(data.session_id)}")
        return ErrorResponse('Session does not exist.'), 500
    except ClientError as e:
        return ErrorResponse(e.message), 500
    except asyncio.TimeoutError:
        return ErrorResponse('Timeout when waiting for client.'), 500

@app.route("/health")
async def health_check():
    return {"status": "healthy"}

def run():
    app.run(host='0.0.0.0', port=7860)

if __name__ == "__main__":
    run()