#!/usr/bin/env python3 # Изменено: 2026-03-11 # HTTP-обёртка для serverless функций на Python 3.11. # Загружает модуль из SLESS_ENTRYPOINT или handler.py по умолчанию. # Формат SLESS_ENTRYPOINT: "module_name.func_name" (например: handler.handle) # Почему importlib: нужно загружать модуль из фиксированного пути вне sys.path. import sys import os import json import importlib.util from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs PORT = 8080 # Разбираем SLESS_ENTRYPOINT="module.func" → файл и имя функции. # Fallback: handler.py + handle — для обратной совместимости. _entrypoint = os.environ.get("SLESS_ENTRYPOINT", "handler.handle") _dot_idx = _entrypoint.rfind(".") _module_name = _entrypoint[:_dot_idx] if _dot_idx >= 0 else _entrypoint _func_name = _entrypoint[_dot_idx + 1:] if _dot_idx >= 0 else "handle" HANDLER_PATH = f"/app/function/{_module_name}.py" def load_handler(): # Загружаем модуль пользователя динамически — путь известен только в runtime. spec = importlib.util.spec_from_file_location(_module_name, HANDLER_PATH) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) if not hasattr(module, _func_name): raise AttributeError(f"{HANDLER_PATH} must define a '{_func_name}(event)' function") return getattr(module, _func_name) _handle = load_handler() class FunctionHandler(BaseHTTPRequestHandler): def log_message(self, format, *args): # Логи в stderr чтобы не мешать stdout sys.stderr.write("%s - [%s] %s\n" % ( self.address_string(), self.log_date_time_string(), format % args, )) def _parse_request_meta(self, event): # Добавляем _path, _query, _method в event — позволяет роутить внутри функции. # Например: /fn/ns/notes/add?title=x → event._path='/add', event._query={'title':'x'} # Зарезервированные поля, не конфликтуют с бизнес-полями. parsed = urlparse(self.path) query = {k: v[0] if len(v) == 1 else v for k, v in parse_qs(parsed.query).items()} if isinstance(event, dict): event['_path'] = parsed.path event['_query'] = query event['_method'] = self.command return event def do_GET(self): if self.path == "/health": self._respond(200, {"status": "ok"}) else: event = self._parse_request_meta({}) self._respond(200, _handle(event)) def _handle_with_body(self): # Общий обработчик для методов с телом (POST, PUT, PATCH, DELETE и др.) # Читаем тело только если Content-Length > 0, иначе передаём пустой event. length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) if length > 0 else b"" try: event = json.loads(body) if body else {} except json.JSONDecodeError: # Если тело не JSON — передаём как строку, не ломаем вызов event = {"body": body.decode("utf-8", errors="replace")} event = self._parse_request_meta(event) self._respond(200, _handle(event)) def do_POST(self): self._handle_with_body() # PUT, DELETE, PATCH — передаём в функцию как обычные вызовы. # event['_method'] позволяет функции роутить по методу внутри. do_PUT = _handle_with_body do_DELETE = _handle_with_body do_PATCH = _handle_with_body def do_HEAD(self): # HEAD: те же заголовки что и GET, но без тела (HTTP-стандарт). if self.path == "/health": result = {"status": "ok"} else: result = _handle(self._parse_request_meta({})) body = json.dumps(result).encode("utf-8") self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() # Тело не отправляем — семантика HEAD def send_error(self, code, message=None, explain=None): # Переопределяем дефолтный HTML-ответ на JSON — пользователь всегда получает JSON. body = json.dumps({"error": message or f"HTTP {code}", "code": code}).encode("utf-8") self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _respond(self, status, data): body = json.dumps(data).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) if __name__ == "__main__": server = HTTPServer(("0.0.0.0", PORT), FunctionHandler) print(f"sless runtime (python3.11) listening on :{PORT}", flush=True) server.serve_forever()