Skip to content
This repository was archived by the owner on May 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ See [docs/protocol_design.md](docs/protocol_design.md) for the protocol overview
## Resource transfers

The package also includes helpers for sending and receiving larger files over Reticulum links. Use
`LinkClient.send_resource()` to upload a file with progress reporting and
`LinkService.resource_received_callback()` to store incoming resources in a chosen directory.
`ResourceClient.send_resource()` to upload a file with progress reporting and
`ResourceService.resource_received_callback()` to store incoming resources in a chosen directory.

## Quick start

Expand Down
12 changes: 6 additions & 6 deletions reticulum_openapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Reticulum OpenAPI package."""

from .controller import Controller, APIException, handle_exceptions
from .model import BaseModel, dataclass_from_json, dataclass_to_json
from .controller import APIException
from .controller import Controller
from .controller import handle_exceptions
from .model import BaseModel
from .model import dataclass_from_json
from .model import dataclass_to_json
from .link_client import LinkClient
from .link_service import LinkService
from .service import LXMFService
from .status import StatusCode
from .link_client import LinkClient
from .link_service import LinkService

__all__ = [
"Controller",
Expand All @@ -20,6 +22,4 @@
"LinkService",
"LXMFService",
"StatusCode",
"LinkClient",
"LinkService",
]
18 changes: 8 additions & 10 deletions reticulum_openapi/link_client.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@

"""Utilities for sending files over Reticulum links."""

import os
from typing import Callable
"""Utilities for communicating over Reticulum links."""

import asyncio
import os
from dataclasses import asdict
from dataclasses import is_dataclass
from typing import Any
from typing import Callable
from typing import Optional

import RNS

from .model import dataclass_to_json


class LinkClient:
class ResourceClient:
"""Client helper for sending resources over an established link."""

def __init__(
Expand Down Expand Up @@ -66,8 +66,6 @@ def _wrapped_callback(resource: RNS.Resource) -> None:
progress_callback=progress_callback,
)
return resource

from .model import dataclass_to_json


class LinkClient:
Expand All @@ -86,6 +84,8 @@ def __init__(
self.reticulum = RNS.Reticulum(config_path)
self.identity = identity or RNS.Identity()
self._loop = asyncio.get_event_loop()
self.established = asyncio.Event()
self.closed = asyncio.Event()
remote_hash = bytes.fromhex(dest_hash)
remote_id = RNS.Identity.recall(remote_hash) or RNS.Identity.recall(
remote_hash, create=True
Expand All @@ -103,8 +103,6 @@ def __init__(
closed_callback=self._on_closed,
)
self.link.set_packet_callback(self._handle_packet)
self.established = asyncio.Event()
self.closed = asyncio.Event()
self.packet_queue: asyncio.Queue[bytes] = asyncio.Queue()

def _on_established(self, _link: RNS.Link) -> None:
Expand Down
14 changes: 6 additions & 8 deletions reticulum_openapi/link_service.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@

"""Helpers for handling incoming resources on Reticulum links."""

import asyncio
import os
import shutil
from typing import Callable

import asyncio
from typing import Any
from typing import Awaitable
from typing import Callable
from typing import Dict
from typing import Optional

import RNS


class LinkService:
class ResourceService:
"""Service utilities for receiving resources on a link."""

def __init__(
Expand Down Expand Up @@ -61,7 +59,9 @@ def resource_received_callback(self, resource: RNS.Resource) -> None:
self.on_download_complete(dest_path)
except Exception as exc:
RNS.log(f"Failed to store resource: {exc}")



class LinkService:
"""Service accepting incoming ``RNS.Link`` connections."""

def __init__(
Expand Down Expand Up @@ -139,5 +139,3 @@ async def stop(self) -> None:
for task in self._keepalive_tasks.values():
task.cancel()
self._keepalive_tasks.clear()


21 changes: 14 additions & 7 deletions tests/test_link_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ def identify(self, identity):
# helper used in tests
def respond(self, payload: bytes):
receipt = SimpleNamespace(response=payload)
if self._response_callback:
self._response_callback(receipt)
callback = getattr(self, "_response_callback", None)
if callback:
callback(receipt)


class FakeDestination:
Expand All @@ -58,6 +59,10 @@ class FakeIdentity:
def __init__(self):
pass

@staticmethod
def recall(_hash, create=False):
return FakeIdentity()


@pytest.mark.asyncio
async def test_send_serializes_dict(monkeypatch):
Expand All @@ -67,11 +72,12 @@ async def test_send_serializes_dict(monkeypatch):
monkeypatch.setattr(lc_module.RNS, "Link", FakeLink)

captured = {}
monkeypatch.setattr(
lc_module,
"dataclass_to_json",
lambda d: captured.setdefault("payload", d) or b"data",
)

def serializer(d):
captured.setdefault("payload", d)
return b"data"

monkeypatch.setattr(lc_module, "dataclass_to_json", serializer)

cli = lc_module.LinkClient("aa")
await cli.send({"k": "v"})
Expand All @@ -88,6 +94,7 @@ async def test_request_returns_response(monkeypatch):

cli = lc_module.LinkClient("aa")
task = asyncio.create_task(cli.request("/path", {"a": 1}))
await asyncio.sleep(0)
cli.link.respond(b"ok")
resp = await task
assert resp == b"ok"
Expand Down
8 changes: 4 additions & 4 deletions tests/test_link_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def completion(res):
def hook(res):
calls["hook"] = True

cli = link_client.LinkClient(fake_link, on_upload_complete=hook)
cli = link_client.ResourceClient(fake_link, on_upload_complete=hook)
cli.send_resource(
str(file_path), progress_callback=progress, completion_callback=completion
)
Expand All @@ -59,14 +59,14 @@ def raise_resource(*a, **k):
raise ValueError("boom")

monkeypatch.setattr(link_client.RNS, "Resource", raise_resource)
cli = link_client.LinkClient(object())
cli = link_client.ResourceClient(object())
with pytest.raises(ValueError):
cli.send_resource(str(file_path))


def test_resource_received_callback(tmp_path):
storage = tmp_path / "store"
service = link_service.LinkService(str(storage))
service = link_service.ResourceService(str(storage))

src_path = tmp_path / "incoming"
src_path.write_bytes(b"content")
Expand All @@ -87,7 +87,7 @@ def test_resource_received_callback_no_metadata(tmp_path):
def hook(path):
called["path"] = path

service = link_service.LinkService(str(storage), on_download_complete=hook)
service = link_service.ResourceService(str(storage), on_download_complete=hook)

src_path = tmp_path / "incoming"
src_path.write_bytes(b"data")
Expand Down