Skip to content

Commit 2cb7362

Browse files
committedNov 4, 2022··
Add DataClient struct, allowing users to query Grafana for data over gRPC
WIP, not even tested, feels like it might just work though? TODO: - [ ] Finish off `Frame::from_arrow` so it actually populates the chunks - [ ] Test it out Depends on grafana/grafana#55781.
1 parent ce2dc43 commit 2cb7362

File tree

8 files changed

+333
-56
lines changed

8 files changed

+333
-56
lines changed
 

‎crates/grafana-plugin-sdk/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ description = "SDK for building Grafana backend plugins."
1010

1111
[dependencies]
1212
arrow2 = { version = "0.14.0", features = ["io_ipc"] }
13+
bytes = "1.2.1"
1314
chrono = "0.4.19"
1415
futures-core = "0.3.17"
1516
futures-util = "0.3.17"
@@ -40,7 +41,7 @@ tracing-subscriber = { version = "0.3.1", features = [
4041

4142
[dev-dependencies]
4243
async-stream = "0.3.2"
43-
bytes = "1.1.0"
44+
bytes = "1.2.1"
4445
futures = "0.3.17"
4546
paste = "1.0.6"
4647
pretty_assertions = "1.0.0"

‎crates/grafana-plugin-sdk/examples/main.rs

+64-30
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use bytes::Bytes;
1010
use chrono::prelude::*;
1111
use futures_util::stream::FuturesOrdered;
1212
use http::Response;
13-
use serde::Deserialize;
13+
use serde::{Deserialize, Serialize};
1414
use thiserror::Error;
1515
use tokio_stream::StreamExt;
1616
use tracing::{debug, info};
@@ -21,28 +21,42 @@ use grafana_plugin_sdk::{
2121
prelude::*,
2222
};
2323

24-
#[derive(Clone, Debug, Default)]
25-
struct MyPluginService(Arc<AtomicUsize>);
24+
#[derive(Clone, Debug)]
25+
struct MyPluginService {
26+
counter: Arc<AtomicUsize>,
27+
data_client: backend::DataClient,
28+
}
2629

2730
impl MyPluginService {
2831
fn new() -> Self {
29-
Self(Arc::new(AtomicUsize::new(0)))
32+
Self {
33+
counter: Arc::new(AtomicUsize::new(0)),
34+
data_client: backend::DataClient::new("localhost:10000").expect("valid URL"),
35+
}
3036
}
3137
}
3238

3339
// Data service implementation.
3440

35-
#[derive(Debug, Deserialize)]
41+
#[derive(Clone, Debug, Deserialize, Serialize)]
3642
#[serde(rename_all = "camelCase")]
3743
struct Query {
3844
pub expression: String,
3945
pub other_user_input: u64,
4046
}
4147

48+
#[derive(Debug, Error)]
49+
enum SDKError {
50+
#[error("SDK error: {0}")]
51+
Data(data::Error),
52+
#[error("Query data error: {0}")]
53+
QueryData(backend::QueryDataError),
54+
}
55+
4256
#[derive(Debug, Error)]
4357
#[error("Error querying backend for query {ref_id}: {source}")]
4458
struct QueryError {
45-
source: data::Error,
59+
source: SDKError,
4660
ref_id: String,
4761
}
4862

@@ -58,40 +72,60 @@ impl backend::DataService for MyPluginService {
5872
type QueryError = QueryError;
5973
type Stream = backend::BoxDataResponseStream<Self::QueryError>;
6074
async fn query_data(&self, request: backend::QueryDataRequest<Self::Query>) -> Self::Stream {
75+
let client = self.data_client.clone();
76+
let transport_metadata = request.transport_metadata().clone();
6177
Box::pin(
6278
request
6379
.queries
6480
.into_iter()
65-
.map(|x: DataQuery<Self::Query>| async move {
81+
.map(|x: DataQuery<Self::Query>| {
6682
// We can see the user's query in `x.query`:
6783
debug!(
6884
expression = x.query.expression,
6985
other_user_input = x.query.other_user_input,
7086
"Got backend query",
7187
);
72-
// Here we create a single response Frame for each query.
73-
// Frames can be created from iterators of fields using [`IntoFrame`].
74-
Ok(backend::DataResponse::new(
75-
x.ref_id.clone(),
76-
vec![[
77-
// Fields can be created from iterators of a variety of
78-
// relevant datatypes.
79-
[
80-
Utc.ymd(2021, 1, 1).and_hms(12, 0, 0),
81-
Utc.ymd(2021, 1, 1).and_hms(12, 0, 1),
82-
Utc.ymd(2021, 1, 1).and_hms(12, 0, 2),
88+
89+
let ref_id = x.ref_id.clone();
90+
let transport_metadata = transport_metadata.clone();
91+
let mut client = client.clone();
92+
93+
async move {
94+
// We can proxy this request to a different datasource if we like.
95+
// Here we do that, but ignore the response.
96+
let proxied = client
97+
.query_data(vec![x.clone()], &transport_metadata)
98+
.await
99+
.map_err(|source| QueryError {
100+
source: SDKError::QueryData(source),
101+
ref_id: ref_id.clone(),
102+
})?;
103+
info!("Got proxied response: {:?}", proxied);
104+
105+
// Here we create a single response Frame for each query.
106+
// Frames can be created from iterators of fields using [`IntoFrame`].
107+
Ok(backend::DataResponse::new(
108+
ref_id.clone(),
109+
vec![[
110+
// Fields can be created from iterators of a variety of
111+
// relevant datatypes.
112+
[
113+
Utc.ymd(2021, 1, 1).and_hms(12, 0, 0),
114+
Utc.ymd(2021, 1, 1).and_hms(12, 0, 1),
115+
Utc.ymd(2021, 1, 1).and_hms(12, 0, 2),
116+
]
117+
.into_field("time"),
118+
[1_u32, 2, 3].into_field("x"),
119+
["a", "b", "c"].into_field("y"),
83120
]
84-
.into_field("time"),
85-
[1_u32, 2, 3].into_field("x"),
86-
["a", "b", "c"].into_field("y"),
87-
]
88-
.into_frame("foo")
89-
.check()
90-
.map_err(|source| QueryError {
91-
ref_id: x.ref_id,
92-
source,
93-
})?],
94-
))
121+
.into_frame("foo")
122+
.check()
123+
.map_err(|source| QueryError {
124+
source: SDKError::Data(source),
125+
ref_id,
126+
})?],
127+
))
128+
}
95129
})
96130
.collect::<FuturesOrdered<_>>(),
97131
)
@@ -195,7 +229,7 @@ impl backend::ResourceService for MyPluginService {
195229
&self,
196230
r: backend::CallResourceRequest,
197231
) -> Result<(Self::InitialResponse, Self::Stream), Self::Error> {
198-
let count = Arc::clone(&self.0);
232+
let count = Arc::clone(&self.counter);
199233
let response_and_stream = match r.request.uri().path() {
200234
// Just send back a single response.
201235
"/echo" => Ok((

‎crates/grafana-plugin-sdk/src/backend/data.rs

+166-19
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
//! SDK types and traits relevant to plugins that query data.
22
use std::{collections::HashMap, pin::Pin, time::Duration};
33

4+
use bytes::Bytes;
45
use futures_core::Stream;
56
use futures_util::StreamExt;
6-
use serde::de::DeserializeOwned;
7+
use serde::{de::DeserializeOwned, Serialize};
8+
use tonic::transport::Endpoint;
79

810
use crate::{
911
backend::{self, ConvertFromError, TimeRange},
1012
data, pluginv2,
1113
};
1214

15+
use super::ConvertToError;
16+
17+
/// Transport metadata sent alongside a query.
18+
///
19+
/// This should be passed into [`DataClient::query_data`] when forwarding
20+
/// a query to the same Grafana instance as the query originated.
21+
#[derive(Clone, Debug)]
22+
pub struct TransportMetadata(tonic::metadata::MetadataMap);
23+
1324
/// A request for data made by Grafana.
1425
///
1526
/// Details of the request source can be found in `plugin_context`,
1627
/// while the actual plugins themselves are in `queries`.
1728
#[derive(Debug)]
1829
#[non_exhaustive]
19-
pub struct QueryDataRequest<Q>
20-
where
21-
Q: DeserializeOwned,
22-
{
30+
pub struct QueryDataRequest<Q> {
2331
/// Details of the plugin instance from which the request originated.
2432
///
2533
/// If the request originates from a datasource instance, this will
@@ -34,38 +42,51 @@ where
3442
/// the query to the frontend; this should be included in the corresponding
3543
/// `DataResponse` for each query.
3644
pub queries: Vec<DataQuery<Q>>,
45+
46+
grpc_meta: TransportMetadata,
47+
}
48+
49+
impl<Q> QueryDataRequest<Q> {
50+
/// Get the transport metadata in this request.
51+
///
52+
/// This is required when using the [`DataClient`] to query
53+
/// other Grafana datasources.
54+
pub fn transport_metadata(&self) -> &TransportMetadata {
55+
&self.grpc_meta
56+
}
3757
}
3858

39-
impl<Q> TryFrom<pluginv2::QueryDataRequest> for QueryDataRequest<Q>
59+
impl<Q> TryFrom<tonic::Request<pluginv2::QueryDataRequest>> for QueryDataRequest<Q>
4060
where
4161
Q: DeserializeOwned,
4262
{
4363
type Error = ConvertFromError;
44-
fn try_from(other: pluginv2::QueryDataRequest) -> Result<Self, Self::Error> {
64+
fn try_from(other: tonic::Request<pluginv2::QueryDataRequest>) -> Result<Self, Self::Error> {
65+
// Clone is required until https://github.com/hyperium/tonic/pull/1118 is released.
66+
let grpc_meta = other.metadata().clone();
67+
let request = other.into_inner();
4568
Ok(Self {
46-
plugin_context: other
69+
plugin_context: request
4770
.plugin_context
4871
.ok_or(ConvertFromError::MissingPluginContext)
4972
.and_then(TryInto::try_into)?,
50-
headers: other.headers,
51-
queries: other
73+
headers: request.headers,
74+
queries: request
5275
.queries
5376
.into_iter()
5477
.map(DataQuery::try_from)
5578
.collect::<Result<Vec<_>, _>>()?,
79+
grpc_meta: TransportMetadata(grpc_meta),
5680
})
5781
}
5882
}
5983

6084
/// A query made by Grafana to the plugin as part of a [`QueryDataRequest`].
6185
///
6286
/// The `query` field contains any fields set by the plugin's UI.
63-
#[derive(Debug)]
6487
#[non_exhaustive]
65-
pub struct DataQuery<Q>
66-
where
67-
Q: DeserializeOwned,
68-
{
88+
#[derive(Clone, Debug)]
89+
pub struct DataQuery<Q> {
6990
/// The unique identifier of the query, set by the frontend call.
7091
///
7192
/// This should be included in the corresponding [`DataResponse`].
@@ -111,6 +132,46 @@ where
111132
}
112133
}
113134

135+
impl<Q> TryFrom<DataQuery<Q>> for pluginv2::DataQuery
136+
where
137+
Q: Serialize,
138+
{
139+
type Error = ConvertToError;
140+
141+
fn try_from(other: DataQuery<Q>) -> Result<Self, Self::Error> {
142+
Ok(Self {
143+
ref_id: other.ref_id,
144+
max_data_points: other.max_data_points,
145+
interval_ms: other.interval.as_millis() as i64,
146+
time_range: Some(other.time_range.into()),
147+
json: serde_json::to_vec(&other.query)
148+
.map_err(|err| ConvertToError::InvalidJson { err })?,
149+
query_type: other.query_type,
150+
})
151+
}
152+
}
153+
154+
#[derive(Debug)]
155+
enum DataResponseFrames {
156+
Deserialized(Vec<data::Frame>),
157+
Serialized(Result<Vec<Vec<u8>>, data::Error>),
158+
}
159+
160+
impl DataResponseFrames {
161+
fn into_serialized(self) -> Result<Vec<Vec<u8>>, data::Error> {
162+
match self {
163+
Self::Deserialized(frames) => to_arrow(
164+
frames
165+
.iter()
166+
.map(|x| x.check())
167+
.collect::<Result<Vec<_>, _>>()?,
168+
&None,
169+
),
170+
Self::Serialized(x) => x,
171+
}
172+
}
173+
}
174+
114175
/// The results from a [`DataQuery`].
115176
#[derive(Debug)]
116177
pub struct DataResponse {
@@ -121,7 +182,7 @@ pub struct DataResponse {
121182
ref_id: String,
122183

123184
/// The data returned from the query.
124-
frames: Result<Vec<Vec<u8>>, data::Error>,
185+
frames: DataResponseFrames,
125186
}
126187

127188
impl DataResponse {
@@ -130,7 +191,7 @@ impl DataResponse {
130191
pub fn new(ref_id: String, frames: Vec<data::CheckedFrame<'_>>) -> Self {
131192
Self {
132193
ref_id: ref_id.clone(),
133-
frames: to_arrow(frames, &Some(ref_id)),
194+
frames: DataResponseFrames::Serialized(to_arrow(frames, &Some(ref_id))),
134195
}
135196
}
136197
}
@@ -286,15 +347,15 @@ where
286347
let responses = DataService::query_data(
287348
self,
288349
request
289-
.into_inner()
350+
// .into_inner()
290351
.try_into()
291352
.map_err(ConvertFromError::into_tonic_status)?,
292353
)
293354
.await
294355
.map(|resp| match resp {
295356
Ok(x) => {
296357
let ref_id = x.ref_id;
297-
x.frames.map_or_else(
358+
x.frames.into_serialized().map_or_else(
298359
|e| {
299360
(
300361
ref_id.clone(),
@@ -336,3 +397,89 @@ where
336397
}))
337398
}
338399
}
400+
401+
/// A client for querying data from Grafana.
402+
///
403+
/// This can be used by plugins which need to query data from
404+
/// a different datasource of the same Grafana instance.
405+
#[derive(Debug, Clone)]
406+
pub struct DataClient<T = tonic::transport::Channel> {
407+
inner: pluginv2::data_client::DataClient<T>,
408+
}
409+
410+
impl DataClient<tonic::transport::Channel> {
411+
/// Create a new DataClient to connect to the given endpoint.
412+
///
413+
/// This constructor uses the default [`tonic::transport::Channel`] as the
414+
/// transport. Use [`DataClient::with_channel`] to provide your own channel.
415+
pub fn new(url: impl Into<Bytes>) -> Result<Self, tonic::transport::Error> {
416+
let endpoint = Endpoint::from_shared(url)?;
417+
let channel = endpoint.connect_lazy();
418+
Ok(Self {
419+
inner: pluginv2::data_client::DataClient::new(channel),
420+
})
421+
}
422+
}
423+
424+
/// Errors which can occur when querying data.
425+
#[derive(Clone, Debug, thiserror::Error)]
426+
#[error("Error querying data")]
427+
pub struct QueryDataError;
428+
429+
impl<T> DataClient<T> {
430+
/// Query for data from a Grafana datasource.
431+
pub async fn query_data<Q>(
432+
&mut self,
433+
queries: Vec<DataQuery<Q>>,
434+
upstream_metadata: &TransportMetadata,
435+
) -> Result<HashMap<String, DataResponse>, QueryDataError>
436+
where
437+
Q: Serialize,
438+
T: tonic::client::GrpcService<tonic::body::BoxBody> + Clone + Send + Sync + 'static,
439+
T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + Sync + 'static,
440+
<T::ResponseBody as tonic::codegen::Body>::Error: Into<tonic::codegen::StdError> + Send,
441+
{
442+
let queries: Vec<_> = queries
443+
.into_iter()
444+
// TODO: add enum variant
445+
.map(|q| q.try_into().map_err(|_| QueryDataError))
446+
.collect::<Result<_, _>>()?;
447+
let query_data_request = pluginv2::QueryDataRequest {
448+
plugin_context: None,
449+
headers: Default::default(),
450+
queries,
451+
};
452+
let mut request = tonic::Request::new(query_data_request);
453+
for kv in upstream_metadata.0.iter() {
454+
match kv {
455+
tonic::metadata::KeyAndValueRef::Ascii(k, v) => {
456+
request.metadata_mut().insert(k, v.clone());
457+
}
458+
tonic::metadata::KeyAndValueRef::Binary(k, v) => {
459+
request.metadata_mut().insert_bin(k, v.clone());
460+
}
461+
}
462+
}
463+
let responses = self
464+
.inner
465+
.query_data(request)
466+
.await
467+
// TODO: add enum variant
468+
.map_err(|_| QueryDataError)?
469+
.into_inner()
470+
.responses
471+
.into_iter()
472+
.map(|(k, v)| {
473+
let frames = v
474+
.frames
475+
.into_iter()
476+
.map(|f| data::Frame::from_arrow(f).map_err(|_| QueryDataError))
477+
.collect::<Result<Vec<_>, _>>()
478+
.map(DataResponseFrames::Deserialized)
479+
.unwrap();
480+
Ok((k.clone(), DataResponse { ref_id: k, frames }))
481+
})
482+
.collect::<Result<_, _>>()?;
483+
Ok(responses)
484+
}
485+
}

‎crates/grafana-plugin-sdk/src/backend/mod.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ mod stream;
159159
mod tracing_fmt;
160160

161161
pub use data::{
162-
BoxDataResponseStream, DataQuery, DataQueryError, DataResponse, DataService, QueryDataRequest,
162+
BoxDataResponseStream, DataClient, DataQuery, DataQueryError, DataResponse, DataService,
163+
QueryDataError, QueryDataRequest, TransportMetadata,
163164
};
164165
pub use diagnostics::{
165166
CheckHealthRequest, CheckHealthResponse, CollectMetricsRequest, CollectMetricsResponse,
@@ -701,6 +702,15 @@ impl From<pluginv2::TimeRange> for TimeRange {
701702
}
702703
}
703704

705+
impl From<TimeRange> for pluginv2::TimeRange {
706+
fn from(other: TimeRange) -> Self {
707+
Self {
708+
from_epoch_ms: other.from.timestamp_millis(),
709+
to_epoch_ms: other.to.timestamp_millis(),
710+
}
711+
}
712+
}
713+
704714
/// A role within Grafana.
705715
#[derive(Clone, Copy, Debug)]
706716
#[non_exhaustive]

‎crates/grafana-plugin-sdk/src/data/error.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ use arrow2::datatypes::DataType;
33
use itertools::Itertools;
44
use thiserror::Error;
55

6-
use super::frame::to_arrow;
6+
use super::frame::arrow;
77

88
/// Errors that can occur when interacting with the Grafana plugin SDK.
99
#[derive(Debug, Error)]
1010
#[non_exhaustive]
1111
pub enum Error {
1212
/// An error has occurred when serializing to Arrow IPC format.
1313
#[error("Arrow serialization error: {0}")]
14-
ArrowSerialization(#[from] to_arrow::Error),
14+
ArrowSerialization(#[from] arrow::Error),
1515

1616
/// There is a datatype mismatch in a field.
1717
///

‎crates/grafana-plugin-sdk/src/data/field.rs

+47-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{
55
};
66

77
use arrow2::{
8-
array::Array,
8+
array::{new_empty_array, Array},
99
datatypes::{DataType, Field as ArrowField, TimeUnit},
1010
};
1111
use serde::{Deserialize, Serialize};
@@ -70,6 +70,27 @@ impl Field {
7070
})
7171
}
7272

73+
pub(crate) fn from_arrow(f: ArrowField) -> Result<Self, serde_json::Error> {
74+
let data_type: TypeInfoType = f.data_type().try_into().unwrap();
75+
Ok(Self {
76+
name: f.name,
77+
labels: f
78+
.metadata
79+
.get("labels")
80+
.and_then(|c| serde_json::from_str(c).ok())
81+
.unwrap_or_default(),
82+
config: f
83+
.metadata
84+
.get("config")
85+
.and_then(|c| serde_json::from_str(c).ok()),
86+
values: data_type.empty_array(),
87+
type_info: TypeInfo {
88+
frame: data_type,
89+
nullable: Some(f.is_nullable),
90+
},
91+
})
92+
}
93+
7394
/// Return a new field with the given name.
7495
///
7596
/// # Example
@@ -476,6 +497,26 @@ impl TryFrom<&DataType> for TypeInfoType {
476497
}
477498
}
478499

500+
impl From<&TypeInfoType> for DataType {
501+
fn from(other: &TypeInfoType) -> Self {
502+
match other {
503+
TypeInfoType::Int8 => Self::Int8,
504+
TypeInfoType::Int16 => Self::Int16,
505+
TypeInfoType::Int32 => Self::Int32,
506+
TypeInfoType::Int64 => Self::Int64,
507+
TypeInfoType::UInt8 => Self::UInt8,
508+
TypeInfoType::UInt16 => Self::UInt16,
509+
TypeInfoType::UInt32 => Self::UInt32,
510+
TypeInfoType::UInt64 => Self::UInt64,
511+
TypeInfoType::Float32 => Self::Float32,
512+
TypeInfoType::Float64 => Self::Float64,
513+
TypeInfoType::String => Self::Utf8,
514+
TypeInfoType::Bool => Self::Boolean,
515+
TypeInfoType::Time => Self::Timestamp(TimeUnit::Nanosecond, None),
516+
}
517+
}
518+
}
519+
479520
impl From<TypeInfoType> for DataType {
480521
fn from(other: TypeInfoType) -> Self {
481522
match other {
@@ -515,6 +556,11 @@ impl TypeInfoType {
515556
Self::Time => SimpleType::Time,
516557
}
517558
}
559+
560+
#[must_use]
561+
pub(crate) fn empty_array(&self) -> Box<dyn Array> {
562+
new_empty_array(self.into())
563+
}
518564
}
519565

520566
/// The 'simple' type of this data.

‎crates/grafana-plugin-sdk/src/data/frame/to_arrow.rs ‎crates/grafana-plugin-sdk/src/data/frame/arrow.rs

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
//! Conversion of [`Frame`][crate::data::Frame]s to the Arrow IPC format.
22
use std::{collections::BTreeMap, sync::Arc};
33

4-
use arrow2::{chunk::Chunk, datatypes::Schema, io::ipc::write::FileWriter};
4+
use arrow2::{
5+
chunk::Chunk,
6+
datatypes::Schema,
7+
io::ipc::{
8+
read::{read_file_metadata, FileReader},
9+
write::FileWriter,
10+
},
11+
};
512
use thiserror::Error;
613

714
use crate::data::{field::Field, frame::CheckedFrame};
815

16+
use super::Frame;
17+
918
/// Errors occurring when serializing a [`Frame`] to the Arrow IPC format.
1019
#[derive(Debug, Error)]
1120
#[non_exhaustive]
@@ -83,6 +92,36 @@ impl CheckedFrame<'_> {
8392
}
8493
}
8594

95+
impl Frame {
96+
pub(crate) fn from_arrow(data: Vec<u8>) -> Result<Self, Error> {
97+
let mut cursor = std::io::Cursor::new(data);
98+
let metadata = read_file_metadata(&mut cursor).unwrap();
99+
let arrow_schema = metadata.schema.clone();
100+
let mut frame = Frame {
101+
name: arrow_schema.metadata.get("name").unwrap().to_string(),
102+
ref_id: arrow_schema.metadata.get("refId").map(Clone::clone),
103+
meta: arrow_schema
104+
.metadata
105+
.get("meta")
106+
.and_then(|m| serde_json::from_str(m).ok()),
107+
fields: vec![],
108+
};
109+
110+
let fields = arrow_schema
111+
.fields
112+
.into_iter()
113+
.map(Field::from_arrow)
114+
.collect::<Result<Vec<_>, _>>()
115+
.unwrap();
116+
let reader = FileReader::new(cursor, metadata, None, None);
117+
// TODO: are these chunks of columns? need to actually read them in...
118+
let chunks = reader.collect::<Result<Vec<_>, _>>().unwrap();
119+
120+
frame.fields = fields;
121+
Ok(frame)
122+
}
123+
}
124+
86125
#[cfg(test)]
87126
mod test {
88127
use crate::data;

‎crates/grafana-plugin-sdk/src/data/frame/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ use crate::{
1111
live::Channel,
1212
};
1313

14+
pub(crate) mod arrow;
1415
pub(self) mod de;
1516
pub(self) mod ser;
16-
pub(crate) mod to_arrow;
1717

1818
use ser::{SerializableField, SerializableFrame, SerializableFrameData, SerializableFrameSchema};
1919

0 commit comments

Comments
 (0)
Please sign in to comment.