aboutsummaryrefslogtreecommitdiff
path: root/kennel
diff options
context:
space:
mode:
Diffstat (limited to 'kennel')
-rw-r--r--kennel/app.py13
-rw-r--r--kennel/config.py20
-rw-r--r--kennel/engine/__init__.py0
-rw-r--r--kennel/engine/components/__init__.py0
-rw-r--r--kennel/engine/components/component.py16
-rw-r--r--kennel/engine/components/controllable.py13
-rw-r--r--kennel/engine/components/position.py14
-rw-r--r--kennel/engine/entities/__init__.py0
-rw-r--r--kennel/engine/entities/cat.py33
-rw-r--r--kennel/engine/entities/entity.py60
-rw-r--r--kennel/engine/entities/laser.py13
-rw-r--r--kennel/engine/game.py53
-rw-r--r--kennel/engine/systems/__init__.py0
-rw-r--r--kennel/engine/systems/network.py88
-rw-r--r--kennel/engine/systems/system.py31
-rw-r--r--kennel/kennel.py81
-rw-r--r--kennel/kennelcats.py45
-rw-r--r--kennel/main.py145
-rw-r--r--kennel/middleware.py30
19 files changed, 613 insertions, 42 deletions
diff --git a/kennel/app.py b/kennel/app.py
new file mode 100644
index 0000000..98a0448
--- /dev/null
+++ b/kennel/app.py
@@ -0,0 +1,13 @@
+import structlog
+from fastapi import FastAPI
+from fastapi.templating import Jinja2Templates
+
+app = FastAPI(
+ servers = [
+ {"url": "https://kennel.hatecomputers.club", "description": "prod"}
+ ]
+)
+
+logger = structlog.get_logger()
+
+templates = Jinja2Templates(directory="templates")
diff --git a/kennel/config.py b/kennel/config.py
new file mode 100644
index 0000000..8cec699
--- /dev/null
+++ b/kennel/config.py
@@ -0,0 +1,20 @@
+from dotenv import load_dotenv
+import os
+
+load_dotenv()
+
+
+class Config:
+ SIMULATION_WIDTH = int(os.getenv("SIMULATION_WIDTH", 1000))
+ SIMULATION_HEIGHT = int(os.getenv("SIMULATION_HEIGHT", 1000))
+
+ HATECOMPUTERS_ENDPOINT = os.getenv(
+ "HATECOMPUTERS_ENDPOINT", "https://hatecomputers.club"
+ )
+
+ MIN_TIME_STEP = float(os.getenv("MIN_TIME_STEP", 0.05))
+
+ COOKIE_MAX_AGE = int(os.getenv("COOKIE_MAX_AGE", 60 * 60 * 24 * 7)) # 1 week
+
+
+config = Config()
diff --git a/kennel/engine/__init__.py b/kennel/engine/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/kennel/engine/__init__.py
diff --git a/kennel/engine/components/__init__.py b/kennel/engine/components/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/kennel/engine/components/__init__.py
diff --git a/kennel/engine/components/component.py b/kennel/engine/components/component.py
new file mode 100644
index 0000000..c8ef2d0
--- /dev/null
+++ b/kennel/engine/components/component.py
@@ -0,0 +1,16 @@
+from enum import Enum
+from abc import abstractmethod
+
+
+class ComponentType(str, Enum):
+ POSITION = "POSITION"
+ CONTROLLABLE = "CONTROLLABLE"
+
+
+class Component:
+ def __init__(self, component_type: ComponentType):
+ self.component_type = component_type
+
+ @abstractmethod
+ def dict(self) -> dict:
+ pass
diff --git a/kennel/engine/components/controllable.py b/kennel/engine/components/controllable.py
new file mode 100644
index 0000000..49b0557
--- /dev/null
+++ b/kennel/engine/components/controllable.py
@@ -0,0 +1,13 @@
+from .component import Component, ComponentType
+
+
+class Controllable(Component):
+ def __init__(self, by: str):
+ super().__init__(ComponentType.CONTROLLABLE)
+ self.by = by
+
+ def __repr__(self) -> str:
+ return f"Controllable(by={self.by})"
+
+ def dict(self) -> dict:
+ return {"by": self.by}
diff --git a/kennel/engine/components/position.py b/kennel/engine/components/position.py
new file mode 100644
index 0000000..2a4da4a
--- /dev/null
+++ b/kennel/engine/components/position.py
@@ -0,0 +1,14 @@
+from .component import Component, ComponentType
+
+
+class Position(Component):
+ def __init__(self, x: float, y: float):
+ super().__init__(ComponentType.POSITION)
+ self.x = x
+ self.y = y
+
+ def __repr__(self) -> str:
+ return f"Position(x={self.x}, y={self.y})"
+
+ def dict(self) -> dict:
+ return {"x": self.x, "y": self.y}
diff --git a/kennel/engine/entities/__init__.py b/kennel/engine/entities/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/kennel/engine/entities/__init__.py
diff --git a/kennel/engine/entities/cat.py b/kennel/engine/entities/cat.py
new file mode 100644
index 0000000..02fdb33
--- /dev/null
+++ b/kennel/engine/entities/cat.py
@@ -0,0 +1,33 @@
+#
+# # IDLE, FROLICKING, EEPY, ALERT, CHASING_CURSOR, CHASING_CAT, SCRATCHING, ITCHY
+# state_stochastic_matrix = [
+# # IDLE
+# [0.5, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0],
+# # FROLICKING
+# [0.1, 0.5, 0.1, 0.1, 0.1, 0.05, 0.05, 0],
+# # EEPY
+# [0.1, 0.1, 0.5, 0.1, 0.1, 0.05, 0.05, 0],
+# # ALERT
+# [0.1, 0.1, 0.1, 0.5, 0.1, 0.05, 0.05, 0],
+# # CHASING_CURSOR
+# [0.1, 0.1, 0.1, 0.1, 0.5, 0.05, 0.05, 0],
+# # CHASING_CAT
+# [0.1, 0.1, 0.1, 0.1, 0.1, 0.5, 0.05, 0],
+# # SCRATCHING
+# [0.1, 0.1, 0.1, 0.1, 0.1, 0.05, 0.5, 0],
+# # ITCHY
+# [0, 0, 0, 0, 0, 0, 0, 1],
+# ]
+#
+#
+# class CatState(Enum):
+# IDLE = 0
+# FROLICKING = 1
+# EEPY = 2
+# ALERT = 3
+# CHASING_CURSOR = 4
+# CHASING_CAT = 5
+# SCRATCHING = 6
+# ITCHY = 7
+#
+#
diff --git a/kennel/engine/entities/entity.py b/kennel/engine/entities/entity.py
new file mode 100644
index 0000000..0ca8666
--- /dev/null
+++ b/kennel/engine/entities/entity.py
@@ -0,0 +1,60 @@
+from enum import Enum
+from kennel.engine.components.component import Component, ComponentType
+from typing import List, Optional
+
+
+class EntityType(str, Enum):
+ LASER = "LASER"
+
+
+class Entity:
+ def __init__(
+ self, entity_type: EntityType, id: str, component_list: List[Component]
+ ):
+ self.entity_type = entity_type
+ self.id = id
+ self.components = {}
+ for component in component_list:
+ self.add_component(component)
+
+ def get_component(self, component_type: ComponentType) -> Optional[Component]:
+ return self.components[component_type]
+
+ def add_component(self, component: Component) -> None:
+ self.components[component.component_type] = component
+
+
+class EntityManager:
+ def __init__(self):
+ self.entities = {}
+ self.component_entities = {}
+
+ def update(self) -> None:
+ self.component_entities = {}
+ for entity in self.entities.values():
+ for component in entity.components.values():
+ if component.component_type not in self.component_entities:
+ self.component_entities[component.component_type] = set()
+ self.component_entities[component.component_type].add(entity)
+
+ def get_entities_with_component(
+ self, component_type: ComponentType
+ ) -> List[Entity]:
+ return self.component_entities.get(component_type, [])
+
+ def add_entity(self, entity: Entity) -> None:
+ self.entities[entity.id] = entity
+ for component in entity.components.values():
+ if component.component_type not in self.component_entities:
+ self.component_entities[component.component_type] = set()
+ self.component_entities[component.component_type].add(entity)
+
+ def remove_entity(self, entity_id: str) -> None:
+ if entity_id in self.entities:
+ entity = self.entities[entity_id]
+ for component in entity.components.values():
+ if component.component_type in self.component_entities:
+ self.component_entities[component.component_type].remove(entity)
+
+ def get_entity(self, entity_id: str) -> Optional[Entity]:
+ return self.entities.get(entity_id)
diff --git a/kennel/engine/entities/laser.py b/kennel/engine/entities/laser.py
new file mode 100644
index 0000000..ead72a6
--- /dev/null
+++ b/kennel/engine/entities/laser.py
@@ -0,0 +1,13 @@
+from .entity import Entity, EntityType
+from kennel.engine.components.position import Position
+from kennel.engine.components.controllable import Controllable
+
+
+class Laser(Entity):
+ def __init__(self, id: str, controllable_by: str):
+ components = [
+ Position(0, 0),
+ Controllable(controllable_by),
+ ]
+
+ super().__init__(EntityType.LASER, id, components)
diff --git a/kennel/engine/game.py b/kennel/engine/game.py
new file mode 100644
index 0000000..2236306
--- /dev/null
+++ b/kennel/engine/game.py
@@ -0,0 +1,53 @@
+from kennel.engine.entities.entity import EntityManager
+from kennel.engine.systems.system import SystemManager
+from kennel.app import logger
+from typing import List, Optional
+import time
+import asyncio
+
+
+class Game:
+ def __init__(
+ self,
+ entity_manager: EntityManager,
+ system_manager: SystemManager,
+ min_time_step: float,
+ ):
+ self.entity_manager = entity_manager
+ self.system_manager = system_manager
+ self.min_time_step = min_time_step
+
+ self.last_time = time.time()
+ self.running = False
+
+ async def update(self, delta_time: float) -> None:
+ self.entity_manager.update()
+ await self.system_manager.update(self.entity_manager, delta_time)
+
+ async def run(self) -> None:
+ logger.info("Game started")
+ self.running = True
+ self.last_time = time.time()
+ steps_since_log = 0
+
+ while self.running:
+ current_time = time.time()
+ if (int(current_time) - int(self.last_time)) == 1 and int(
+ current_time
+ ) % 10 == 0:
+ logger.info(f"Game loop: {steps_since_log} steps since last log time")
+ steps_since_log = 0
+
+ delta_time = current_time - self.last_time
+ self.last_time = current_time
+
+ await self.update(delta_time)
+ await asyncio.sleep(
+ max(self.min_time_step - (time.time() - current_time), 0)
+ )
+
+ steps_since_log += 1
+
+ def stop(self) -> None:
+ self.running = False
+ logger.info("Game stopped")
diff --git a/kennel/engine/systems/__init__.py b/kennel/engine/systems/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/kennel/engine/systems/__init__.py
diff --git a/kennel/engine/systems/network.py b/kennel/engine/systems/network.py
new file mode 100644
index 0000000..7df8088
--- /dev/null
+++ b/kennel/engine/systems/network.py
@@ -0,0 +1,88 @@
+from enum import Enum
+from kennel.engine.entities.entity import EntityManager
+from .system import System, SystemType
+from abc import abstractmethod
+import asyncio
+
+
+class EventType(str, Enum):
+ ENTITY_BORN = "ENTITY_BORN"
+ ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE"
+ ENTITY_DEATH = "ENTITY_DEATH"
+
+
+class Event:
+ def __init__(self, event_type: EventType, data: dict):
+ self.event_type = event_type
+ self.data = data
+
+ def __str__(self):
+ return f"Event({self.event_type}, {self.data})"
+
+ def dict(self):
+ return {"event_type": self.event_type, "data": self.data}
+
+ @staticmethod
+ def from_dict(data: dict):
+ return Event(EventType(data["event_type"]), data["data"])
+
+
+class Publishable:
+ @abstractmethod
+ async def publish(self, event: Event):
+ pass
+
+
+class ClientEventTransformer:
+ @abstractmethod
+ def apply(self, event: Event) -> Event:
+ pass
+
+
+class EventProcessor:
+ @abstractmethod
+ def accept(self, entity_manager: EntityManager, event: Event) -> None:
+ pass
+
+
+class NetworkSystem(System):
+ def __init__(
+ self,
+ event_processor: EventProcessor,
+ client_event_transformer: ClientEventTransformer,
+ ):
+ super().__init__(SystemType.NETWORK)
+ self.event_processor = event_processor
+ self.client_event_transformer = client_event_transformer
+
+ self.events = []
+ self.clients = {}
+
+ async def update(self, entity_manager: EntityManager, delta_time: float) -> None:
+ for event in self.events:
+ self.event_processor.accept(entity_manager, event)
+ client_events = [
+ self.client_event_transformer.apply(entity_manager, event)
+ for event in self.events
+ ]
+ await asyncio.gather(
+ *[
+ client.publish(event)
+ for client in self.clients.values()
+ for event in client_events
+ ]
+ )
+ self.events = []
+
+ def client_event(self, client_id: str, event: Event) -> None:
+ event.data["client_id"] = client_id
+ self.events.append(event)
+
+ def add_event(self, event: Event) -> None:
+ self.events.append(event)
+
+ def add_client(self, client_id: str, client: Publishable) -> None:
+ self.clients[client_id] = client
+
+ def remove_client(self, client_id: str) -> None:
+ del self.clients[client_id]
diff --git a/kennel/engine/systems/system.py b/kennel/engine/systems/system.py
new file mode 100644
index 0000000..0e51f3a
--- /dev/null
+++ b/kennel/engine/systems/system.py
@@ -0,0 +1,31 @@
+from enum import Enum
+from kennel.engine.entities.entity import EntityManager
+from abc import abstractmethod
+
+
+class SystemType(str, Enum):
+ NETWORK = "NETWORK"
+
+
+class System:
+ def __init__(self, system_type: SystemType):
+ self.system_type = system_type
+
+ @abstractmethod
+ async def update(self, entity_manager: EntityManager, delta_time: float):
+ pass
+
+
+class SystemManager:
+ def __init__(self):
+ self.systems = {}
+
+ def add_system(self, system: System) -> None:
+ self.systems[system.system_type] = system
+
+ def get_system(self, system_type: SystemType) -> System:
+ return self.systems.get(system_type)
+
+ async def update(self, entity_manager: EntityManager, delta_time: float) -> None:
+ for system in self.systems.values():
+ await system.update(entity_manager, delta_time)
diff --git a/kennel/kennel.py b/kennel/kennel.py
new file mode 100644
index 0000000..0b38261
--- /dev/null
+++ b/kennel/kennel.py
@@ -0,0 +1,81 @@
+from kennel.engine.game import Game
+from kennel.engine.components.component import ComponentType
+from kennel.engine.entities.entity import EntityManager, Entity
+from kennel.engine.entities.laser import Laser
+from kennel.engine.systems.system import SystemManager
+from kennel.engine.systems.network import (
+ NetworkSystem,
+ EventProcessor,
+ ClientEventTransformer,
+ Event,
+ EventType,
+)
+from kennel.config import config
+from .app import logger
+from typing import List
+import uuid
+
+entity_manager = EntityManager()
+system_manager = SystemManager()
+
+
+class KennelEventProcessor(EventProcessor):
+ def accept(self, entity_manager: EntityManager, event: Event) -> None:
+ if event.event_type == EventType.ENTITY_POSITION_UPDATE:
+ self._process_entity_position_update(entity_manager, event)
+
+ def _process_entity_position_update(
+ self, entity_manager: EntityManager, event: Event
+ ) -> None:
+ entity = entity_manager.get_entity(event.data["entity_id"])
+ if entity is None:
+ logger.error(f"Entity {event.data['entity_id']} does not exist")
+ return
+ if event.data["client_id"] is None:
+ logger.error("Client ID is required for position updates")
+ return
+ controllable = entity.get_component(ComponentType.CONTROLLABLE)
+ if controllable is None:
+ logger.error(f"Entity {entity} is not controllable")
+ return
+ if controllable.by != event.data["client_id"]:
+ logger.error(
+ f"Entity {entity} is not controllable by client {event.data['client_id']}"
+ )
+ return
+ position = entity.get_component(ComponentType.POSITION)
+ if position is None:
+ logger.error(f"Entity {entity} has no position")
+ return
+ entity.add_component(position)
+
+
+class KennelClientEventTransformer(ClientEventTransformer):
+ def apply(self, entity_manager: EntityManager, event: Event) -> Event:
+ if event.event_type == EventType.ENTITY_BORN:
+ id = event.data["id"]
+ entity = entity_manager.get_entity(id)
+ if entity is None:
+ logger.error(f"Entity {id} does not exist")
+ return event
+ for component_type in entity.components:
+ if component_type == ComponentType.CONTROLLABLE:
+ # Do not send controllable components to clients
+ continue
+ component = entity.get_component(component_type)
+ if component is not None:
+ event.data[component_type] = component.dict()
+
+ return event
+
+
+system_manager.add_system(
+ NetworkSystem(EventProcessor(), KennelClientEventTransformer())
+)
+
+kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP)
+
+
+def create_session_controllable_entities(session: str) -> List[Entity]:
+ laser = Laser(uuid.uuid4().hex, session)
+ return [laser]
diff --git a/kennel/kennelcats.py b/kennel/kennelcats.py
new file mode 100644
index 0000000..340b07e
--- /dev/null
+++ b/kennel/kennelcats.py
@@ -0,0 +1,45 @@
+from datetime import datetime
+from typing import List
+import requests
+
+
+class KennelCat:
+ def __init__(
+ self,
+ id: str,
+ user_id: str,
+ name: str,
+ link: str,
+ description: str,
+ spritesheet: str,
+ created_at: datetime,
+ ):
+ self.id = id
+ self.user_id = user_id
+ self.name = name
+ self.link = link
+ self.description = description
+ self.spritesheet = spritesheet
+ self.created_at = created_at
+
+ @staticmethod
+ def from_dict(dict: dict) -> "KennelCat":
+ return KennelCat(
+ id=dict["id"],
+ user_id=dict["user_id"],
+ name=dict["name"],
+ link=dict["link"],
+ description=dict["description"],
+ spritesheet=dict["spritesheet"],
+ created_at=datetime.fromisoformat(dict["created_at"]),
+ )
+
+
+class KennelCatService:
+ def __init__(self, endpoint: str):
+ self.endpoint = endpoint
+
+ def get_kennel(self) -> List[KennelCat]:
+ response = requests.get(f"{self.endpoint}/kennel")
+ response.raise_for_status()
+ return [KennelCat.from_dict(cat) for cat in response.json()]
diff --git a/kennel/main.py b/kennel/main.py
index 183fb64..122e40a 100644
--- a/kennel/main.py
+++ b/kennel/main.py
@@ -1,57 +1,118 @@
+from fastapi import (
+ FastAPI,
+ Request,
+ Response,
+ WebSocket,
+ WebSocketException,
+ status,
+ Cookie,
+ Depends,
+)
+from fastapi.staticfiles import StaticFiles
+from kennel.engine.systems.system import SystemType
+from kennel.engine.systems.network import Event, Publishable, EventType
+from typing import Annotated, Optional
+from .kennel import (
+ kennel,
+ system_manager,
+ entity_manager,
+ create_session_controllable_entities,
+)
+from .app import app, templates, logger
+from .kennelcats import KennelCatService
+from .middleware import logger_middleware
+from .config import config
+import asyncio
import uuid
-import structlog
-from fastapi import FastAPI, Request, Response
-from fastapi.staticfiles import StaticFiles
-from fastapi.templating import Jinja2Templates
+app.mount("/static", StaticFiles(directory="static"), name="static")
-app = FastAPI(
- servers = [
- {"url": "https://kennel.hatecomputers.club", "description": "prod"}
- ]
-)
-logger = structlog.get_logger()
+loop = asyncio.get_event_loop()
-@app.middleware("http")
-async def logger_middleware(request: Request, call_next):
- structlog.contextvars.clear_contextvars()
- structlog.contextvars.bind_contextvars(
- path=request.url.path,
- method=request.method,
- client_host=request.client.host,
- request_id=str(uuid.uuid4()),
- )
- response = await call_next(request)
+@app.on_event("startup")
+async def startup_event():
+ logger.info("Starting Kennel...")
+ loop.create_task(kennel.run())
- structlog.contextvars.bind_contextvars(
- status_code=response.status_code,
- )
- # Exclude /healthcheck endpoint from producing logs
- if request.url.path != "/healthcheck":
- if 400 <= response.status_code < 500:
- logger.warn("Client error")
- elif response.status_code >= 500:
- logger.error("Server error")
- else:
- logger.info("OK")
+@app.on_event("shutdown")
+async def shutdown_event():
+ logger.info("Stopping Kennel...")
+ kennel.stop()
+ loop.stop()
+ logger.info("Kennel stopped")
- return response
-templates = Jinja2Templates(directory="templates")
+@app.get("/")
+async def index(request: Request):
+ return templates.TemplateResponse(request=request, name="index.html")
-@app.get("/healthcheck")
-async def healthcheck():
- return Response("hello")
+@app.get("/assign")
+async def assign(
+ response: Response,
+ session: Annotated[Optional[str], Cookie()] = None,
+):
+ if session is None:
+ session = str(uuid.uuid4().hex)
+ response.set_cookie(key="session", value=session, max_age=config.COOKIE_MAX_AGE)
+ return {"session": session}
-@app.get("/")
-async def read_main(request: Request):
- return templates.TemplateResponse(
- request=request, name="index.html"
- )
+async def get_cookie_or_token(
+ websocket: WebSocket,
+ session: Annotated[str | None, Cookie()] = None,
+):
+ if session is None:
+ raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)
+ return session
+
-app.mount("/static", StaticFiles(directory = "static"), name = "static")
+class WebSocketClient(Publishable):
+ def __init__(self, websocket: WebSocket):
+ self.websocket = websocket
+ async def publish(self, event: Event):
+ await self.websocket.send_json(event.dict())
+
+
+@app.websocket("/ws")
+async def websocket_endpoint(
+ websocket: WebSocket,
+ session: Annotated[str, Depends(get_cookie_or_token)],
+):
+ await websocket.accept()
+ logger.info(f"Websocket connection established for session {session}")
+
+ session_entities = create_session_controllable_entities(session)
+ try:
+ network_system = system_manager.get_system(SystemType.NETWORK)
+ if network_system is None:
+ raise "Network system not found"
+
+ for entity in session_entities:
+ logger.info(f"Adding entity {entity.id} for session {session}")
+ entity_manager.add_entity(entity)
+ network_system.add_event(Event(EventType.ENTITY_BORN, {"id": entity.id}))
+
+ network_system.add_client(session, WebSocketClient(websocket))
+ while True:
+ message = await websocket.receive_json()
+ network_system.client_event(session, Event.from_dict(message))
+ except Exception as e:
+ logger.error(f"WebSocket exception {e}")
+ finally:
+ logger.info("Websocket connection closed")
+ for entity in session_entities:
+ logger.info(f"Removing entity {entity.id} for session {session}")
+ entity_manager.remove_entity(entity.id)
+ network_system.add_event(Event(EventType.ENTITY_DEATH, {"id": entity.id}))
+
+ network_system.remove_client(session)
+ await websocket.close()
+
+
+@app.get("/healthcheck")
+async def healthcheck():
+ return Response("healthy")
diff --git a/kennel/middleware.py b/kennel/middleware.py
new file mode 100644
index 0000000..dddf63b
--- /dev/null
+++ b/kennel/middleware.py
@@ -0,0 +1,30 @@
+import uuid
+import structlog
+from fastapi import Request, Response
+from .app import app, logger
+
+@app.middleware("http")
+async def logger_middleware(request: Request, call_next):
+ structlog.contextvars.clear_contextvars()
+ structlog.contextvars.bind_contextvars(
+ path=request.url.path,
+ method=request.method,
+ client_host=request.client.host,
+ request_id=str(uuid.uuid4()),
+ )
+ response = await call_next(request)
+
+ structlog.contextvars.bind_contextvars(
+ status_code=response.status_code,
+ )
+
+ # Exclude /healthcheck endpoint from producing logs
+ if request.url.path != "/healthcheck":
+ if 400 <= response.status_code < 500:
+ logger.warn("Client error")
+ elif response.status_code >= 500:
+ logger.error("Server error")
+ else:
+ logger.info("OK")
+
+ return response