Skip to content

Commit

Permalink
BRAYNS-653 Add object endpoints (#1275)
Browse files Browse the repository at this point in the history
  • Loading branch information
Adrien4193 authored Jul 31, 2024
1 parent 8e6149b commit a010de5
Show file tree
Hide file tree
Showing 45 changed files with 1,528 additions and 363 deletions.
1 change: 1 addition & 0 deletions python/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.pytest_cache
.ruff_cache
.vscode
.env
venv
__pycache__
*.pyc
Expand Down
90 changes: 89 additions & 1 deletion python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,107 @@ You can install this package from [PyPI](https://pypi.org/):
pip install brayns
```

Or from source:
The package can also be installed from source:

```bash
git clone https://github.com/BlueBrain/Brayns.git
cd Brayns/python
pip install .
```

To install packages that are only required for development:

```bash
pip install -r requirements-dev.txt
```

## Usage
--------

TODO

## Tests

To run the tests, use

```bash
pytest tests
```

## Lint

To format and lint use

```bash
ruff format brayns
ruff check brayns
mypy brayns
```

## VSCode integration

From the current directory (Brayns/python):

1. Create a venv

```bash
python3.11 -m venv venv
source venv/bin/activate
```

2. Install requirements for development:

```bash
pip install -r requirements.txt
pip install -r requirements-dev.txt
```

3. For integration testing, create a `.env` file:

```bash
BRAYNS_HOST=localhost
BRAYNS_PORT=5000
BRAYNS_EXECUTABLE=path/to/braynsService
LD_LIBRARY_PATH=path/to/additional/libs
```

Note: integration testing can be disable using the pytest --without-integration flag.

4. Create a .vscode folder and create a `launch.json` inside to use to debug tests:

```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Debug Tests",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"purpose": [
"debug-test"
],
"console": "integratedTerminal",
"justMyCode": false
}
]
}
```

5. In the same folder, create a `settings.json` to configure pytest:

```json
{
"python.analysis.typeCheckingMode": "basic",
"python.testing.pytestArgs": [
"tests"
],
"python.envFile": "${workspaceFolder}/.env",
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
```

## Documentation
-----------------

Expand Down
52 changes: 44 additions & 8 deletions python/brayns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,64 @@
"""
Brayns Python package.
This package provides a high level API to interact with an instance of Brayns
instance through websockets.
The low level JSON-RPC API is also available using the instance directly.
This package provides an API to interact with Brayns service.
"""

from .version import VERSION
from .network.connection import Connection, connect
from .network.json_rpc import JsonRpcError, JsonRpcErrorResponse, JsonRpcId, JsonRpcRequest, JsonRpcResponse
from .api.core.service import (
Endpoint,
Task,
TaskInfo,
TaskOperation,
Version,
cancel_task,
get_endpoint,
get_methods,
get_task,
get_task_result,
get_tasks,
get_version,
stop_service,
)
from .network.connection import Connection, FutureResponse, Request, Response, connect
from .network.json_rpc import (
JsonRpcError,
JsonRpcErrorResponse,
JsonRpcId,
JsonRpcRequest,
JsonRpcResponse,
JsonRpcSuccessResponse,
)
from .network.websocket import ServiceUnavailable, WebSocketError
from .version import VERSION

__version__ = VERSION
"""Version tag of brayns Python package (major.minor.patch)."""

__all__ = [
"Connection",
"cancel_task",
"connect",
"Connection",
"Endpoint",
"FutureResponse",
"get_endpoint",
"get_methods",
"get_task_result",
"get_task",
"get_tasks",
"get_version",
"JsonRpcError",
"JsonRpcErrorResponse",
"JsonRpcId",
"JsonRpcRequest",
"JsonRpcResponse",
"JsonRpcSuccessResponse",
"Request",
"Response",
"ServiceUnavailable",
"stop_service",
"Task",
"TaskInfo",
"TaskOperation",
"Version",
"WebSocketError",
]
23 changes: 0 additions & 23 deletions python/tests/test_json_rpc.py → python/brayns/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,3 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from brayns.network.json_rpc import JsonRpcRequest, compose_request


def test_compose_request() -> None:
request = JsonRpcRequest(0, "test", 123)
data = compose_request(request)
text = """{"jsonrpc":"2.0","id":0,"method":"test","params":123}"""
assert data == text

request.binary = b"123"

data = compose_request(request)

assert len(data) == len(text) + 4 + 3

assert isinstance(data, bytes)

size = int.from_bytes(data[:4], byteorder="little", signed=False)

assert size == len(text)
assert data[4 : size + 4].decode() == text
assert data[size + 4 :] == request.binary
19 changes: 19 additions & 0 deletions python/brayns/api/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) 2015-2024 EPFL/Blue Brain Project
# All rights reserved. Do not distribute without permission.
#
# Responsible Author: [email protected]
#
# This file is part of Brayns <https://github.com/BlueBrain/Brayns>
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License version 3.0 as published
# by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
162 changes: 162 additions & 0 deletions python/brayns/api/core/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Copyright (c) 2015-2024 EPFL/Blue Brain Project
# All rights reserved. Do not distribute without permission.
#
# Responsible Author: [email protected]
#
# This file is part of Brayns <https://github.com/BlueBrain/Brayns>
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License version 3.0 as published
# by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic, TypeVar

from brayns.network.connection import Connection, Response
from brayns.network.json_rpc import get


@dataclass
class Version:
major: int
minor: int
patch: int
pre_release: int
tag: str


async def get_version(connection: Connection) -> Version:
result = await connection.get_result("get-version")

return Version(
major=get(result, "major", int),
minor=get(result, "minor", int),
patch=get(result, "patch", int),
pre_release=get(result, "pre_release", int),
tag=get(result, "tag", str),
)


async def get_methods(connection: Connection) -> list[str]:
result = await connection.get_result("get-methods")

return get(result, "methods", list[str])


@dataclass
class Endpoint:
method: str
description: str
params_schema: dict[str, Any]
result_schema: dict[str, Any]
asynchronous: bool


async def get_endpoint(connection: Connection, method: str) -> Endpoint:
result = await connection.get_result("get-schema", {"method": method})

return Endpoint(
method=get(result, "method", str),
description=get(result, "description", str),
params_schema=get(result, "params", dict[str, Any]),
result_schema=get(result, "result", dict[str, Any]),
asynchronous=get(result, "async", bool),
)


@dataclass
class TaskOperation:
description: str
index: int
completion: float


@dataclass
class TaskInfo:
id: int
operation_count: int
current_operation: TaskOperation

@property
def done(self) -> bool:
index = self.current_operation.index
completion = self.current_operation.completion
return index == self.operation_count - 1 and completion == 1.0


T = TypeVar("T")


def deserialize_task(message: dict[str, Any]) -> TaskInfo:
operation = get(message, "current_operation", dict[str, Any])

return TaskInfo(
id=get(message, "id", int),
operation_count=get(message, "operation_count", int),
current_operation=TaskOperation(
description=get(operation, "description", str),
index=get(operation, "index", int),
completion=get(operation, "completion", float),
),
)


async def get_tasks(connection: Connection) -> list[TaskInfo]:
result = await connection.get_result("get-tasks")

tasks: list[dict[str, Any]] = get(result, "tasks", list[dict[str, Any]])

return [deserialize_task(task) for task in tasks]


async def get_task(connection: Connection, task_id: int) -> TaskInfo:
result = await connection.get_result("get-task", {"task_id": task_id})

return deserialize_task(result)


async def cancel_task(connection: Connection, task_id: int) -> None:
await connection.get_result("cancel-task", {"task_id": task_id})


async def get_task_result(connection: Connection, task_id: int) -> Response:
return await connection.request("get-task-result", {"task_id": task_id})


class Task(Generic[T]):
def __init__(self, connection: Connection, id: int, parser: Callable[[Response], T]) -> None:
self._connection = connection
self._id = id
self._parser = parser

@property
def id(self) -> int:
return self._id

async def get_status(self) -> TaskInfo:
return await get_task(self._connection, self._id)

async def is_done(self) -> bool:
status = await self.get_status()
return status.done

async def cancel(self) -> None:
await cancel_task(self._connection, self._id)

async def wait(self) -> T:
result = await get_task_result(self._connection, self._id)
return self._parser(result)


async def stop_service(connection: Connection) -> None:
await connection.get_result("stop")
Loading

0 comments on commit a010de5

Please sign in to comment.