Skip to content

Commit 638dfc6

Browse files
committed
Merge branch 'rc' into sy-4331-move-most-channel-functionality-to-service-layer
# Conflicts: # aspen/internal/kv/config.go # aspen/internal/kv/version.go # core/cmd/start/start.go # core/pkg/api/arc/arc_suite_test.go # core/pkg/distribution/channel/counter.go # core/pkg/distribution/channel/create_test.go # core/pkg/distribution/channel/delete_test.go # core/pkg/distribution/channel/limit_test.go # core/pkg/distribution/channel/pb/services.pb.go # core/pkg/distribution/channel/pb/services.proto # core/pkg/distribution/channel/pb/services_grpc.pb.go # core/pkg/distribution/channel/rename_test.go # core/pkg/distribution/channel/service.go # core/pkg/distribution/channel/service_test.go # core/pkg/distribution/channel/transport.go # core/pkg/distribution/framer/codec/codec_test.go # core/pkg/distribution/framer/deleter/deleter_test.go # core/pkg/distribution/framer/deleter/lease_proxy.go # core/pkg/distribution/framer/iterator/iterator_test.go # core/pkg/distribution/framer/iterator/service.go # core/pkg/distribution/framer/relay/relay_test.go # core/pkg/distribution/framer/relay/transport.go # core/pkg/distribution/framer/writer/service.go # core/pkg/distribution/framer/writer/transport.go # core/pkg/distribution/framer/writer/writer_test.go # core/pkg/distribution/layer.go # core/pkg/distribution/mock/cluster.go # core/pkg/distribution/mock/cluster_test.go # core/pkg/distribution/mock/node.go # core/pkg/distribution/mock/node_test.go # core/pkg/distribution/mock/static_host_provider.go # core/pkg/distribution/mock/static_host_provider_test.go # core/pkg/distribution/proxy/proxy.go # core/pkg/distribution/proxy/proxy_test.go # core/pkg/distribution/transport/grpc/channel/channel_suite_test.go # core/pkg/distribution/transport/grpc/channel/transport.go # core/pkg/distribution/transport/grpc/channel/transport_test.go # core/pkg/distribution/transport/grpc/framer/framer_suite_test.go # core/pkg/distribution/transport/grpc/framer/transport.go # core/pkg/distribution/transport/grpc/framer/transport_test.go # core/pkg/distribution/transport/grpc/grpc_suite_test.go # core/pkg/service/actions/actions_suite_test.go # core/pkg/service/actions/signals_test.go # core/pkg/service/arc/arc_suite_test.go # core/pkg/service/arc/rename_test.go # core/pkg/service/arc/runtime/dependencies_test.go # core/pkg/service/arc/service_test.go # core/pkg/service/arc/task/task_test.go # core/pkg/service/channel/calculation/compiler/compile_test.go # core/pkg/service/channel/calculation/graph/graph_test.go # core/pkg/service/channel/channel_suite_test.go # core/pkg/service/channel/channel_test.go # core/pkg/service/channel/ontology_test.go # core/pkg/service/channel/resolver_test.go # core/pkg/service/channel/retrieve_test.go # core/pkg/service/channel/signals/signals_suite_test.go # core/pkg/service/channel/signals/signals_test.go # core/pkg/service/driver/driver_suite_test.go # core/pkg/service/framer/calculation/calculator/bench_test.go # core/pkg/service/framer/calculation/calculator/calculator_test.go # core/pkg/service/framer/calculation/graph/graph_test.go # core/pkg/service/framer/calculation/service_test.go # core/pkg/service/framer/iterator/bench_test.go # core/pkg/service/framer/iterator/iterator_test.go # core/pkg/service/framer/streamer/bench_test.go # core/pkg/service/framer/streamer/streamer_test.go # core/pkg/service/log/service_test.go # core/pkg/service/metrics/metrics_suite_test.go # core/pkg/service/node/ontology_test.go # core/pkg/service/ontology/signals/signals_suite_test.go # core/pkg/service/ontology/signals/signals_test.go # core/pkg/service/panel/panel_suite_test.go # core/pkg/service/panel/signals_test.go # core/pkg/service/ranger/alias/alias_test.go # core/pkg/service/signals/publisher_test.go # core/pkg/service/signals/signals_suite_test.go # core/pkg/transport/grpc/framer/framer_suite_test.go # core/pkg/transport/grpc/grpc_suite_test.go # core/pkg/transport/http/framer/framer_suite_test.go # core/pkg/transport/http/http_suite_test.go # core/pkg/transport/transport_suite_test.go # x/go/kv/counter_test.go # x/go/kv/tx_test.go
2 parents d656c6c + 332e4af commit 638dfc6

298 files changed

Lines changed: 7925 additions & 3526 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.freighter.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ jobs:
112112
packages: write
113113
with:
114114
module: freighter/integration
115-
tag: ghcr.io/synnaxlabs/freighter-go-integration:latest
115+
tag: ghcr.io/synnaxlabs/freighter-go-integration:${{ github.sha }}
116116

117117
py:
118118
name: Test - Python
@@ -123,7 +123,7 @@ jobs:
123123
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
124124
services:
125125
integration:
126-
image: ghcr.io/synnaxlabs/freighter-go-integration:latest
126+
image: ghcr.io/synnaxlabs/freighter-go-integration:${{ github.sha }}
127127
credentials:
128128
username: ${{ github.actor }}
129129
password: ${{ secrets.GITHUB_TOKEN }}
@@ -152,7 +152,7 @@ jobs:
152152
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
153153
services:
154154
integration:
155-
image: ghcr.io/synnaxlabs/freighter-go-integration:latest
155+
image: ghcr.io/synnaxlabs/freighter-go-integration:${{ github.sha }}
156156
credentials:
157157
username: ${{ github.actor }}
158158
password: ${{ secrets.GITHUB_TOKEN }}

aspen/internal/kv/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func (cfg Config) Override(other Config) Config {
8181

8282
// Validate implements config.Config.
8383
func (cfg Config) Validate() error {
84-
v := validate.New("aspen.kv")
84+
v := validate.New("aspen.kv.db")
8585
validate.NotNil(v, "cluster", cfg.Cluster)
8686
validate.NotNil(v, "tx_transport_client", cfg.BatchTransportClient)
8787
validate.NotNil(v, "tx_transport_server", cfg.BatchTransportServer)

aspen/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func mergeDefaultOptions(o *options) {
186186
// provide their own transport. Otherwise the eagerly-created pool would
187187
// leak: nothing would reference it and nothing would close it.
188188
if o.transport.Transport == nil {
189-
pool := fgrpc.NewPool("", grpc.WithTransportCredentials(insecure.NewCredentials()))
189+
pool := fgrpc.OpenPool("", grpc.WithTransportCredentials(insecure.NewCredentials()))
190190
o.transport.ownedPool = pool
191191
o.transport.Transport = grpct.New(pool)
192192
}

client/py/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ markers = [
8484
"framer: mark test as a framer test",
8585
"http: mark test as an http test",
8686
"group: mark test as a group test",
87+
"imex: mark test as an imex test",
8788
"frame_codec: mark test as a frame codec test",
8889
"iterator: mark test as a reader test",
8990
"internal: mark test as an internal test",

client/py/synnax/framer/codec.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
import struct
1313

1414
import synnax.channel.payload as channel
15-
from freighter import JSONCodec
16-
from freighter.codec import Codec as FreighterCodec
1715
from synnax.exceptions import ValidationError
1816
from synnax.framer.frame import Frame, FramePayload
1917
from synnax.telem import Alignment, DataType, Series, TimeRange
18+
from x.codec import Codec as XCodec
19+
from x.codec import JSONCodec
2020

2121
ZERO_ALIGNMENTS_FLAG_POS = 5
2222
EQUAL_ALIGNMENTS_FLAG_POS = 4
@@ -334,13 +334,12 @@ def decode_series(key: channel.Key) -> bool:
334334

335335
LOW_PERF_SPECIAL_CHAR = 254
336336
HIGH_PERF_SPECIAL_CHAR = 255
337-
CONTENT_TYPE = "application/vnd.synnax.frame"
338337

339338

340-
class WSFramerCodec(FreighterCodec):
339+
class WSFramerCodec(XCodec):
341340
def __init__(self, codec: Codec) -> None:
342341
self.codec = codec
343342
self.lower_perf_codec = JSONCodec()
344343

345344
def content_type(self) -> str:
346-
return CONTENT_TYPE
345+
return "application/vnd.synnax.frame"

client/py/synnax/imex/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright 2026 Synnax Labs, Inc.
2+
#
3+
# Use of this software is governed by the Business Source License included in the file
4+
# licenses/BSL.txt.
5+
#
6+
# As of the Change Date specified in that file, in accordance with the Business Source
7+
# License, use of this software will be governed by the Apache License, Version 2.0,
8+
# included in the file licenses/APL.txt.
9+
10+
from synnax.imex.client import Client
11+
12+
__all__ = [
13+
"Client",
14+
]

client/py/synnax/imex/client.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2026 Synnax Labs, Inc.
2+
#
3+
# Use of this software is governed by the Business Source License included in the file
4+
# licenses/BSL.txt.
5+
#
6+
# As of the Change Date specified in that file, in accordance with the Business Source
7+
# License, use of this software will be governed by the Apache License, Version 2.0,
8+
# included in the file licenses/APL.txt.
9+
10+
11+
from freighter import FileTransport
12+
from synnax import ontology
13+
from x.fs import FilePath
14+
15+
16+
class Client:
17+
"""Imports and exports resources from and to the Core.
18+
19+
Each call moves exactly one envelope, streamed to or from disk: ``import_`` streams
20+
a file from disk, and ``export`` streams the response straight into a destination
21+
file.
22+
"""
23+
24+
_file_transport: FileTransport
25+
26+
def __init__(self, file_transport: FileTransport) -> None:
27+
self._file_transport = file_transport
28+
29+
def import_(self, source: FilePath) -> ontology.ID:
30+
"""Imports the resource at source and returns its new ontology ID.
31+
32+
:param source: a file path streamed from disk.
33+
:returns: the new resource's ontology ID.
34+
"""
35+
return self._file_transport.upload("/imex/import", source, ontology.ID)
36+
37+
def export(self, id: ontology.ID, dest: FilePath) -> None:
38+
"""Exports the resource identified by id, streaming it into dest.
39+
40+
The response body is streamed straight into dest — the on-disk format is driven
41+
by the destination's extension.
42+
43+
:param id: the ontology ID of the resource to export.
44+
:param dest: a file path to stream the response body into.
45+
"""
46+
self._file_transport.download("/imex/export", id, dest)

client/py/synnax/synnax.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
device,
2323
framer,
2424
group,
25+
imex,
2526
ontology,
2627
project,
2728
rack,
@@ -75,6 +76,7 @@ class Synnax(framer.Client):
7576
arcs: arc.Client
7677
groups: group.Client
7778
views: view.Client
79+
imex: imex.Client
7880
projects: project.Client
7981

8082
_transport: Transport
@@ -167,6 +169,7 @@ def __init__(
167169
self.racks = rack.Client(client=self._transport.unary)
168170
self.devices = device.Client(client=self._transport.unary)
169171
self.views = view.Client(client=self._transport.unary)
172+
self.imex = imex.Client(file_transport=self._transport.file_transport)
170173
self.projects = project.Client(client=self._transport.unary)
171174
self.tasks = task.Client(
172175
client=self._transport.unary,

client/py/synnax/transport.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,24 @@
1414
URL,
1515
AsyncMiddleware,
1616
AsyncWebsocketClient,
17+
FileTransport,
1718
HTTPClient,
18-
JSONCodec,
1919
Middleware,
2020
UnaryClient,
2121
WebsocketClient,
2222
async_instrumentation_middleware,
2323
instrumentation_middleware,
2424
)
2525
from synnax.telem import Size, TimeSpan
26+
from x.codec import JSONCodec
2627

2728

2829
class Transport:
2930
url: URL
3031
stream: WebsocketClient
3132
stream_async: AsyncWebsocketClient
3233
unary: UnaryClient
34+
file_transport: FileTransport
3335
secure: bool
3436

3537
def __init__(
@@ -53,18 +55,20 @@ def __init__(
5355
"close_timeout": read_timeout.seconds,
5456
}
5557
self.stream = WebsocketClient(**ws_args)
56-
# We need to update these here because the websocket client doesn't support
57-
# the same arguments as the async websocket client.
58+
# We need to update these here because the WebSocket client doesn't support the
59+
# same arguments as the async WebSocket client.
5860
ws_args["ping_interval"] = keep_alive.seconds
5961
ws_args["ping_timeout"] = 180
6062
self.stream_async = AsyncWebsocketClient(**ws_args)
61-
self.unary = HTTPClient(
63+
http = HTTPClient(
6264
url=self.url,
6365
codec=codec,
6466
secure=secure,
6567
timeout=Timeout(connect=open_timeout.seconds, read=read_timeout.seconds),
6668
retries=Retry(total=max_retries),
6769
)
70+
self.unary = http
71+
self.file_transport = http
6872
self.use(instrumentation_middleware(instrumentation))
6973
self.use_async(async_instrumentation_middleware(instrumentation))
7074

client/py/tests/test_imex.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Copyright 2026 Synnax Labs, Inc.
2+
#
3+
# Use of this software is governed by the Business Source License included in the file
4+
# licenses/BSL.txt.
5+
#
6+
# As of the Change Date specified in that file, in accordance with the Business Source
7+
# License, use of this software will be governed by the Apache License, Version 2.0,
8+
# included in the file licenses/APL.txt.
9+
10+
11+
import json
12+
import uuid
13+
from pathlib import Path
14+
15+
import pytest
16+
17+
import synnax as sy
18+
19+
20+
def _log_envelope_json(name: str) -> str:
21+
"""A minimal valid v1 log envelope, matching core's testdata/import_v1.json."""
22+
return json.dumps(
23+
{
24+
"version": 1,
25+
"type": "log",
26+
"name": name,
27+
"channels": [
28+
{
29+
"channel": 1,
30+
"color": "red",
31+
"notation": "scientific",
32+
"precision": 2,
33+
"alias": "temp",
34+
}
35+
],
36+
"remote_created": False,
37+
"timestamp_precision": 1,
38+
"show_channel_names": True,
39+
"show_receipt_timestamp": False,
40+
}
41+
)
42+
43+
44+
@pytest.mark.imex
45+
class TestImex:
46+
"""Round-trip imex against a live Synnax Core.
47+
48+
Uses the ``log`` resource type.
49+
"""
50+
51+
def test_import_valid(self, client: sy.Synnax, tmp_path: Path) -> None:
52+
"""Path source → streamed upload."""
53+
name = f"imex-path-{uuid.uuid4()}"
54+
path = tmp_path / "in.json"
55+
path.write_text(_log_envelope_json(name))
56+
id = client.imex.import_(path)
57+
assert id.type == "log"
58+
assert uuid.UUID(id.key)
59+
60+
def test_import_invalid(self, client: sy.Synnax, tmp_path: Path) -> None:
61+
"""An envelope with an unrecognized type is rejected."""
62+
path = tmp_path / "in.json"
63+
path.write_text(
64+
json.dumps({"version": 1, "type": "not_a_real_type", "name": "bad"})
65+
)
66+
with pytest.raises(sy.ValidationError):
67+
client.imex.import_(path)
68+
69+
def test_export(self, client: sy.Synnax, tmp_path: Path) -> None:
70+
"""Path dest → streamed download; on-disk content parses back."""
71+
name = f"imex-export-path-{uuid.uuid4()}"
72+
src = tmp_path / "in.json"
73+
src.write_text(_log_envelope_json(name))
74+
id = client.imex.import_(src)
75+
out = tmp_path / "log.json"
76+
client.imex.export(id, out)
77+
parsed = json.loads(out.read_bytes())
78+
assert parsed["name"] == name and parsed["type"] == "log"
79+
80+
def test_export_nonexistent(self, client: sy.Synnax, tmp_path: Path) -> None:
81+
"""Exporting a resource that does not exist raises NotFoundError."""
82+
id = sy.ontology.ID(type="log", key=str(uuid.uuid4()))
83+
out = tmp_path / "log.json"
84+
with pytest.raises(sy.NotFoundError):
85+
client.imex.export(id, out)

0 commit comments

Comments
 (0)