This commit is contained in:
Elijah McMorris 2023-03-14 23:40:18 +00:00 committed by NexVeridian@gmail.com
commit 3a34f6c1a3
12 changed files with 705 additions and 0 deletions

89
src/main.rs Normal file
View file

@ -0,0 +1,89 @@
use aide::{
axum::{
routing::{get, get_with},
ApiRouter, IntoApiResponse,
},
openapi::{Info, OpenApi},
redoc::Redoc,
transform::TransformOperation,
};
use axum::{error_handling::HandleErrorLayer, http::StatusCode, BoxError, Extension, Json};
use std::{net::SocketAddr, time::Duration};
use tower::{buffer::BufferLayer, limit::RateLimitLayer, ServiceBuilder};
mod routes;
async fn serve_api(Extension(api): Extension<OpenApi>) -> impl IntoApiResponse {
return Json(api);
}
fn description_date<'t>(op: TransformOperation<'t>) -> TransformOperation<'t> {
op.parameter_untyped("start", |p| {
p.description("Start date range - Inclusive >= - ISO 8601")
})
.parameter_untyped("end", |p| {
p.description("End date range - Inclusive <= - ISO 8601")
})
}
#[tokio::main]
async fn main() {
let rate_limit = |req_per_sec: u64| {
ServiceBuilder::new()
.layer(HandleErrorLayer::new(|err: BoxError| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled error: {}", err),
)
}))
.layer(BufferLayer::new(1024))
.layer(RateLimitLayer::new(req_per_sec, Duration::from_secs(1)))
};
let app = ApiRouter::new()
.route("/", Redoc::new("/api.json").axum_route())
.api_route(
"/arkvc_holdings",
get_with(routes::arkvc_holdings, |mut o| {
o = o.id("ARKVC Holdings");
description_date(o)
}),
)
.layer(rate_limit(5))
.api_route(
"/ark_holdings",
get_with(routes::ark_holdings, |mut o| {
o = o.id("ARK* ETF Holdings");
description_date(o)
}),
)
.layer(rate_limit(20))
.route("/api.json", get(serve_api));
let mut api = OpenApi {
info: Info {
summary: Some(
"A REST API for ARK Invest holdings data, writen in rust using [axum](https://github.com/tokio-rs/axum),
Redoc/Swagger through [Aide](https://github.com/tamasfe/aide),
and parquet using [polars](https://github.com/pola-rs/polars)\n\nNot affiliated with Ark Invest
".to_owned(),
),
description: Some(
"[Github](https://github.com/NexVeridian/ark-invest-api-rust)\n\n[Contact Info](https://NexVeridian.com/About)".to_owned(),
),
..Info::default()
},
..OpenApi::default()
};
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(
app.finish_api(&mut api)
.layer(Extension(api))
.into_make_service(),
)
.await
.unwrap();
}

29
src/routes.rs Normal file
View file

@ -0,0 +1,29 @@
use aide::axum::IntoApiResponse;
use axum::extract::Query;
mod polars_utils;
pub async fn arkvc_holdings(date_range: Query<polars_utils::DateRange>) -> impl IntoApiResponse {
let df = polars_utils::get_parquet("ARKVC".to_owned()).await.unwrap();
let filter_df = polars_utils::filter_date_range(df, date_range)
.await
.unwrap();
return axum::Json(polars_utils::to_json(filter_df).await.unwrap());
}
pub async fn ark_holdings(
ticker: Query<polars_utils::Ticker>,
date_range: Query<polars_utils::DateRange>,
) -> impl IntoApiResponse {
let df = polars_utils::get_parquet(ticker.ticker.to_string())
.await
.unwrap();
let filter_df = polars_utils::filter_date_range(df, date_range)
.await
.unwrap();
return axum::Json(polars_utils::to_json(filter_df).await.unwrap());
}

View file

@ -0,0 +1,71 @@
use axum::extract::Query;
use chrono::NaiveDate;
use polars::prelude::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::error::Error;
use std::fs::File;
pub async fn get_parquet(file: String) -> Result<DataFrame, Box<dyn Error>> {
let mut file = File::open(format!("data/parquet/{}.parquet", file))?;
let df = ParquetReader::new(&mut file).finish()?;
Ok(df)
}
pub async fn to_json(mut df: DataFrame) -> Result<Value, Box<dyn Error>> {
let mut buffer = Vec::new();
JsonWriter::new(&mut buffer)
.with_json_format(JsonFormat::Json)
.finish(&mut df)?;
let json_string = String::from_utf8(buffer)?;
let json: Value = serde_json::from_str(&json_string)?;
Ok(json)
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct DateRange {
start: Option<NaiveDate>,
end: Option<NaiveDate>,
}
pub async fn filter_date_range(
df: DataFrame,
date_range: Query<DateRange>,
) -> Result<DataFrame, Box<dyn Error>> {
if date_range.start.or(date_range.end) == None {
return Ok(df);
}
let mask = df["date"]
.date()?
.as_date_iter()
.map(|x| {
let date = x.unwrap();
match (date_range.start, date_range.end) {
(Some(start), Some(end)) => return date >= start && date <= end,
(Some(start), _) => return date >= start,
(_, Some(end)) => return date <= end,
_ => return false,
}
})
.collect();
let filter_df = df.filter(&mask)?;
Ok(filter_df)
}
#[derive(Serialize, Deserialize, JsonSchema, strum_macros::Display)]
pub enum TypeTicker {
ARKF,
ARKG,
ARKK,
ARKQ,
ARKW,
ARKX,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct Ticker {
pub ticker: TypeTicker,
}