init repo
Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
This commit is contained in:
commit
667713b575
11 changed files with 3961 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
ISSUES
|
||||
weekly.json
|
3221
Cargo.lock
generated
Normal file
3221
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "rte-france"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.91"
|
||||
chrono = "0.4.38"
|
||||
oauth2 = "4.4.2"
|
||||
polars = { version = "0.43.1", features = ["timezones"] }
|
||||
reqwest = { version = "0.12.8", features = ["blocking", "json"] }
|
||||
serde = "1.0.213"
|
||||
serde_json = "1.0.132"
|
26
examples/consumption.rs
Normal file
26
examples/consumption.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use rte_france::api::consumption::ConsumptionForecast;
|
||||
use rte_france::api::DateRange;
|
||||
use rte_france::RteApi;
|
||||
|
||||
fn main() {
|
||||
let mut rte_api = RteApi::from_env_values();
|
||||
rte_api.authenticate().expect("Failed to authenticate");
|
||||
|
||||
let consumption_forecast = ConsumptionForecast::new(&rte_api);
|
||||
|
||||
let in_1h = chrono::Utc::now() + chrono::Duration::hours(1);
|
||||
|
||||
let range = DateRange {
|
||||
start: in_1h,
|
||||
end: in_1h + chrono::Duration::hours(35),
|
||||
};
|
||||
println!("range: {:?}", range);
|
||||
// let data =
|
||||
// consumption_forecast.short_term(ShortTermForecastType::DayAfterTomorrow, Some(range));
|
||||
// println!("data: {:?}", data);
|
||||
// println!("{}", data.unwrap().as_polars_df().unwrap());
|
||||
|
||||
let weekly = consumption_forecast.weekly_forecast(None);
|
||||
println!("data: {:?}", weekly);
|
||||
println!("{}", weekly.unwrap().as_polars_df().unwrap());
|
||||
}
|
32
examples/generation.rs
Normal file
32
examples/generation.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use rte_france::api::generation::ForecastType;
|
||||
use rte_france::api::generation::GenerationForecast;
|
||||
use rte_france::api::generation::ProductionType;
|
||||
use rte_france::api::DateRange;
|
||||
use rte_france::RteApi;
|
||||
|
||||
fn main() {
|
||||
let mut rte_api = RteApi::from_env_values();
|
||||
rte_api.authenticate().expect("Failed to authenticate");
|
||||
|
||||
let gf = GenerationForecast::new(&rte_api);
|
||||
|
||||
let in_1h = chrono::Utc::now() + chrono::Duration::hours(1);
|
||||
|
||||
let range = DateRange {
|
||||
start: in_1h,
|
||||
end: in_1h + chrono::Duration::hours(23),
|
||||
};
|
||||
|
||||
let forecast = gf.short_term(
|
||||
Some(ProductionType::Solar),
|
||||
None, //Some(ForecastType::AfterAfterTomorrow),
|
||||
Some(range), //None,
|
||||
);
|
||||
for forecast in forecast.unwrap().forecasts {
|
||||
println!(
|
||||
"forecast: {:?} / {:?} / {:?}",
|
||||
forecast.ty, forecast.sub_type, forecast.production_type
|
||||
);
|
||||
println!("{}", forecast.as_polars_df().unwrap());
|
||||
}
|
||||
}
|
40
examples/static.rs
Normal file
40
examples/static.rs
Normal file
File diff suppressed because one or more lines are too long
13
examples/token.rs
Normal file
13
examples/token.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use rte_france::RteApi;
|
||||
|
||||
fn main() {
|
||||
let client_id = std::env::var("CLIENT_ID").expect("CLIENT_ID must be set");
|
||||
let client_secret = std::env::var("CLIENT_SECRET").expect("CLIENT_SECRET must be set");
|
||||
let mut rte_api = RteApi::new(client_id, client_secret);
|
||||
println!("rte_api: {:?}", rte_api);
|
||||
|
||||
rte_api.authenticate().expect("Failed to authenticate");
|
||||
|
||||
let token = rte_api.get_token();
|
||||
println!("token: {:?}", token);
|
||||
}
|
245
src/api/consumption.rs
Normal file
245
src/api/consumption.rs
Normal file
|
@ -0,0 +1,245 @@
|
|||
use std::fmt;
|
||||
|
||||
use crate::ApiClient;
|
||||
use anyhow::Ok;
|
||||
use polars::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use serde_json;
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
|
||||
use super::DateRange;
|
||||
|
||||
pub struct ConsumptionForecast<'a> {
|
||||
client: &'a dyn ApiClient,
|
||||
}
|
||||
|
||||
/// The type of forecast to retrieve
|
||||
pub enum ShortTermForecastType {
|
||||
/// Realised consumption, not a forecast then
|
||||
Realised,
|
||||
|
||||
/// Intra-day forecast
|
||||
Intraday,
|
||||
|
||||
/// Next day forecast
|
||||
Tomorrow,
|
||||
|
||||
/// Day after tomorrow forecast
|
||||
DayAfterTomorrow,
|
||||
}
|
||||
|
||||
impl fmt::Display for ShortTermForecastType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let ft = match self {
|
||||
ShortTermForecastType::Realised => "REALISED",
|
||||
ShortTermForecastType::Intraday => "ID",
|
||||
ShortTermForecastType::Tomorrow => "D-1",
|
||||
ShortTermForecastType::DayAfterTomorrow => "D-2",
|
||||
};
|
||||
write!(f, "{}", ft)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ShortTermResponse {
|
||||
pub short_term: Vec<ShortTerm>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ShortTerm {
|
||||
#[serde(rename = "type")]
|
||||
pub ty: String,
|
||||
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub values: Vec<ShortTermValue>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ShortTermValue {
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub updated_date: DateTime<Utc>,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct WeeklyForecastResponse {
|
||||
pub weekly_forecasts: Vec<WeeklyForecast>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct WeeklyForecast {
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub updated_date: DateTime<Utc>,
|
||||
pub peak: PeakForecast,
|
||||
pub values: Vec<WeeklyForecastValue>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PeakForecast {
|
||||
pub peak_hour: DateTime<Utc>,
|
||||
pub value: f64,
|
||||
pub temperature: f64,
|
||||
pub temperature_deviation: f64,
|
||||
}
|
||||
|
||||
// XXX maybe we could share it with ShortTermValue (except for update)
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct WeeklyForecastValue {
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
impl<'a> ConsumptionForecast<'a> {
|
||||
const SHORT_TERM_URL: &'static str = "/open_api/consumption/v1/short_term";
|
||||
const WEEKLY_URL: &'static str = "/open_api/consumption/v1/weekly_forecasts";
|
||||
|
||||
pub fn new(client: &'a dyn ApiClient) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Returns a short term forecast given the forecast type
|
||||
pub fn short_term(
|
||||
&self,
|
||||
forecast_type: ShortTermForecastType,
|
||||
date_range: Option<DateRange>,
|
||||
) -> Result<ShortTermResponse, anyhow::Error> {
|
||||
let mut qs: Vec<(String, String)> = vec![];
|
||||
|
||||
qs.push(("type".to_string(), forecast_type.to_string()));
|
||||
|
||||
if let Some(date_range) = date_range {
|
||||
qs.append(&mut date_range.to_query_string());
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.http_get(ConsumptionForecast::SHORT_TERM_URL, &qs);
|
||||
|
||||
let reply = response.unwrap();
|
||||
|
||||
let res = serde_json::from_str(&reply);
|
||||
if let Err(e) = res {
|
||||
eprintln!(
|
||||
"Error: parsing reply of {}?{:?} => '{:?}': {:?}",
|
||||
ConsumptionForecast::SHORT_TERM_URL,
|
||||
qs,
|
||||
reply,
|
||||
e
|
||||
);
|
||||
return Err(anyhow::Error::msg("Failed to parse response"));
|
||||
}
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
pub fn weekly_forecast(
|
||||
&self,
|
||||
date_range: Option<DateRange>,
|
||||
) -> Result<WeeklyForecastResponse, anyhow::Error> {
|
||||
let mut qs: Vec<(String, String)> = vec![];
|
||||
if let Some(date_range) = date_range {
|
||||
qs.append(&mut date_range.to_query_string());
|
||||
}
|
||||
|
||||
let response = self.client.http_get(ConsumptionForecast::WEEKLY_URL, &qs);
|
||||
|
||||
let reply = response.unwrap();
|
||||
|
||||
let res = serde_json::from_str(&reply);
|
||||
if let Err(e) = res {
|
||||
eprintln!(
|
||||
"Error: parsing reply of {}?{:?} => '{:?}': {:?}",
|
||||
ConsumptionForecast::WEEKLY_URL,
|
||||
qs,
|
||||
reply,
|
||||
e
|
||||
);
|
||||
return Err(anyhow::Error::msg("Failed to parse response"));
|
||||
}
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
// XXX trait
|
||||
impl ShortTermResponse {
|
||||
pub fn as_polars_df(&self) -> Result<polars::prelude::DataFrame, anyhow::Error> {
|
||||
let mut start_dates: Vec<NaiveDateTime> = vec![];
|
||||
let mut end_dates: Vec<NaiveDateTime> = vec![];
|
||||
let mut updated_dates: Vec<NaiveDateTime> = vec![];
|
||||
let mut values: Vec<f64> = vec![];
|
||||
|
||||
let short_term_response = &self.short_term[0];
|
||||
|
||||
for st in short_term_response.values.iter() {
|
||||
start_dates.push(st.start_date.naive_utc());
|
||||
end_dates.push(st.end_date.naive_utc());
|
||||
//if let Some(ud) = &st.updated_date {
|
||||
updated_dates.push(st.updated_date.naive_utc());
|
||||
// }
|
||||
values.push(st.value);
|
||||
}
|
||||
|
||||
let start_dates_series = Series::new("start_date".into(), start_dates);
|
||||
let end_dates_series = Series::new("end_date".into(), end_dates);
|
||||
let updated_dates_series = Series::new("updated_date".into(), updated_dates);
|
||||
let values_series = Series::new("value".into(), values);
|
||||
|
||||
let df = DataFrame::new(vec![
|
||||
start_dates_series,
|
||||
end_dates_series,
|
||||
updated_dates_series,
|
||||
values_series,
|
||||
])?;
|
||||
|
||||
Ok(df)
|
||||
}
|
||||
}
|
||||
|
||||
impl WeeklyForecastResponse {
|
||||
pub fn as_polars_df(&self) -> Result<polars::prelude::DataFrame, anyhow::Error> {
|
||||
let mut start_dates: Vec<NaiveDateTime> = vec![];
|
||||
let mut end_dates: Vec<NaiveDateTime> = vec![];
|
||||
let mut updated_dates: Vec<NaiveDateTime> = vec![];
|
||||
let mut peak_temperatures: Vec<f64> = vec![];
|
||||
let mut peak_temperature_deviations: Vec<f64> = vec![];
|
||||
let mut values: Vec<f64> = vec![];
|
||||
|
||||
for day_forecast in &self.weekly_forecasts {
|
||||
let temperature = day_forecast.peak.temperature;
|
||||
let temperature_deviation = day_forecast.peak.temperature_deviation;
|
||||
|
||||
for wf in day_forecast.values.iter() {
|
||||
start_dates.push(wf.start_date.naive_utc());
|
||||
end_dates.push(wf.end_date.naive_utc());
|
||||
updated_dates.push(day_forecast.updated_date.naive_utc());
|
||||
peak_temperatures.push(temperature);
|
||||
peak_temperature_deviations.push(temperature_deviation);
|
||||
values.push(wf.value);
|
||||
}
|
||||
}
|
||||
|
||||
let start_dates_series = Series::new("start_date".into(), start_dates);
|
||||
let end_dates_series = Series::new("end_date".into(), end_dates);
|
||||
let updated_dates_series = Series::new("updated_date".into(), updated_dates);
|
||||
let peak_temperatures_series = Series::new("peak_temperature".into(), peak_temperatures);
|
||||
let peak_temperature_deviations_series = Series::new(
|
||||
"peak_temperature_deviation".into(),
|
||||
peak_temperature_deviations,
|
||||
);
|
||||
|
||||
let df = DataFrame::new(vec![
|
||||
start_dates_series,
|
||||
end_dates_series,
|
||||
updated_dates_series,
|
||||
peak_temperatures_series,
|
||||
peak_temperature_deviations_series,
|
||||
Series::new("value".into(), values),
|
||||
])?;
|
||||
|
||||
Ok(df)
|
||||
}
|
||||
}
|
214
src/api/generation.rs
Normal file
214
src/api/generation.rs
Normal file
|
@ -0,0 +1,214 @@
|
|||
use core::fmt;
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use polars::prelude::*;
|
||||
use polars::{frame::DataFrame, series::Series};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ApiClient;
|
||||
|
||||
use super::DateRange;
|
||||
|
||||
pub struct GenerationForecast<'a> {
|
||||
client: &'a dyn ApiClient,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProductionType {
|
||||
/// Agrégée France
|
||||
AggregatedFrance,
|
||||
/// Eolien terrestre
|
||||
WindOnshore,
|
||||
/// Eolien en mer
|
||||
WindOffshore,
|
||||
/// Solaire
|
||||
Solar,
|
||||
/// Agrégée OA
|
||||
AggregatedCpc,
|
||||
/// Production potentielle des cogénérations MDSE (Mise à disposition du système électrique)
|
||||
Mdse,
|
||||
}
|
||||
|
||||
impl fmt::Display for ProductionType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let pt = match self {
|
||||
ProductionType::AggregatedFrance => "AGGREGATED_FRANCE",
|
||||
ProductionType::WindOnshore => "WIND_ONSHORE",
|
||||
ProductionType::WindOffshore => "WIND_OFFSHORE",
|
||||
ProductionType::Solar => "SOLAR",
|
||||
ProductionType::AggregatedCpc => "AGGREGATED_CPC",
|
||||
ProductionType::Mdse => "MDSE",
|
||||
};
|
||||
write!(f, "{}", pt)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ProductionTypeResponse {
|
||||
/// production des moyens programmables agrégée sur la France
|
||||
AggregatedProgrammableFrance,
|
||||
/// production des moyens dits "fatals" agrégée sur la France
|
||||
AggregatedNonProgrammableFrance,
|
||||
|
||||
WindOnshore,
|
||||
WindOffshore,
|
||||
Solar,
|
||||
AggregatedCpc,
|
||||
/// Installations bénéficiant d'un contrat d'achat indexé aux prix de marché Trading Region France
|
||||
#[serde(rename = "MDSETRF")]
|
||||
MdseTrf,
|
||||
/// Installations bénéficiant d'un contrat d'achat indexé sur le tarif réglementé de fourniture de gaz STS
|
||||
#[serde(rename = "MDSESTS")]
|
||||
MdseSts,
|
||||
}
|
||||
impl fmt::Display for ProductionTypeResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let pt = match self {
|
||||
ProductionTypeResponse::AggregatedProgrammableFrance => {
|
||||
"AGGREGATED_PROGRAMMABLE_FRANCE"
|
||||
}
|
||||
ProductionTypeResponse::AggregatedNonProgrammableFrance => {
|
||||
"AGGREGATED_NON_PROGRAMMABLE_FRANCE"
|
||||
}
|
||||
ProductionTypeResponse::WindOnshore => "WIND_ONSHORE",
|
||||
ProductionTypeResponse::WindOffshore => "WIND_OFFSHORE",
|
||||
ProductionTypeResponse::Solar => "SOLAR",
|
||||
ProductionTypeResponse::AggregatedCpc => "AGGREGATED_CPC",
|
||||
ProductionTypeResponse::MdseTrf => "MDSE_TRF",
|
||||
ProductionTypeResponse::MdseSts => "MDSE_STS",
|
||||
};
|
||||
write!(f, "{}", pt)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub enum ForecastType {
|
||||
Current,
|
||||
Intraday,
|
||||
Tomorrow,
|
||||
AfterTomorrow,
|
||||
AfterAfterTomorrow, // Lol, this is a dumb name
|
||||
}
|
||||
|
||||
impl fmt::Display for ForecastType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let ft = match self {
|
||||
ForecastType::Current => "CURRENT",
|
||||
ForecastType::Intraday => "ID",
|
||||
ForecastType::Tomorrow => "D-1",
|
||||
ForecastType::AfterTomorrow => "D-2",
|
||||
ForecastType::AfterAfterTomorrow => "D-3",
|
||||
};
|
||||
write!(f, "{}", ft)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ForecastResponse {
|
||||
pub forecasts: Vec<Forecast>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Forecast {
|
||||
#[serde(rename = "type")]
|
||||
pub ty: String, // XXX enum
|
||||
|
||||
pub sub_type: Option<String>, // XXX enum
|
||||
pub production_type: ProductionTypeResponse, // XXX enum
|
||||
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub values: Vec<ForecastValue>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ForecastValue {
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub updated_date: DateTime<Utc>,
|
||||
pub value: f64,
|
||||
pub load_factor: Option<f64>, // only for production_type: Wind*
|
||||
}
|
||||
|
||||
impl<'a> GenerationForecast<'a> {
|
||||
const URL: &'static str = "/open_api/generation_forecast/v2/forecasts";
|
||||
|
||||
pub fn new(client: &'a dyn ApiClient) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Returns a short term forecast given the forecast type
|
||||
pub fn short_term(
|
||||
&self,
|
||||
production_type: Option<ProductionType>,
|
||||
forecast_type: Option<ForecastType>,
|
||||
date_range: Option<DateRange>,
|
||||
) -> Result<ForecastResponse, anyhow::Error> {
|
||||
let mut qs: Vec<(String, String)> = vec![];
|
||||
|
||||
//qs.push(("type".to_string(), forecast_type.to_string()));
|
||||
|
||||
if let Some(production_type) = production_type {
|
||||
qs.push(("production_type".to_string(), production_type.to_string()));
|
||||
}
|
||||
if let Some(forecast_type) = forecast_type {
|
||||
qs.push(("type".to_string(), forecast_type.to_string()));
|
||||
}
|
||||
|
||||
if let Some(date_range) = date_range {
|
||||
qs.append(&mut date_range.to_query_string());
|
||||
}
|
||||
|
||||
let response = self.client.http_get(GenerationForecast::URL, &qs);
|
||||
|
||||
let reply = response.unwrap();
|
||||
|
||||
let res = serde_json::from_str(&reply);
|
||||
if let Err(e) = res {
|
||||
eprintln!(
|
||||
"Error: parsing reply of {}?{:?} => '{:?}': {:?}",
|
||||
GenerationForecast::URL,
|
||||
qs,
|
||||
reply,
|
||||
e
|
||||
);
|
||||
return Err(anyhow::Error::msg("Failed to parse response"));
|
||||
}
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Forecast {
|
||||
pub fn as_polars_df(&self) -> Result<polars::prelude::DataFrame, anyhow::Error> {
|
||||
let mut start_dates: Vec<NaiveDateTime> = vec![];
|
||||
let mut end_dates: Vec<NaiveDateTime> = vec![];
|
||||
let mut updated_dates: Vec<NaiveDateTime> = vec![];
|
||||
let mut values: Vec<f64> = vec![];
|
||||
let mut load_factors: Vec<Option<f64>> = vec![];
|
||||
|
||||
for fv in &self.values {
|
||||
start_dates.push(fv.start_date.naive_utc());
|
||||
end_dates.push(fv.end_date.naive_utc());
|
||||
updated_dates.push(fv.updated_date.naive_utc());
|
||||
values.push(fv.value);
|
||||
load_factors.push(fv.load_factor);
|
||||
}
|
||||
|
||||
let start_dates_series = Series::new("start_date".into(), start_dates);
|
||||
let end_dates_series = Series::new("end_date".into(), end_dates);
|
||||
let updated_dates_series = Series::new("updated_date".into(), updated_dates);
|
||||
let value_series = Series::new("value".into(), values);
|
||||
let lf_series = Series::new("load_factor".into(), load_factors);
|
||||
|
||||
let df = DataFrame::new(vec![
|
||||
start_dates_series,
|
||||
end_dates_series,
|
||||
updated_dates_series,
|
||||
value_series,
|
||||
lf_series,
|
||||
])?;
|
||||
|
||||
Ok(df)
|
||||
}
|
||||
}
|
31
src/api/mod.rs
Normal file
31
src/api/mod.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
|
||||
pub mod consumption;
|
||||
pub mod generation;
|
||||
|
||||
pub trait FormatToApiFmt {
|
||||
fn to_api_format(&self) -> String;
|
||||
}
|
||||
|
||||
impl FormatToApiFmt for DateTime<Utc> {
|
||||
fn to_api_format(&self) -> String {
|
||||
// Define the desired format for your API
|
||||
// You can adjust this format string to match the API's expected format
|
||||
self.format("%Y-%m-%dT%H:%M:%S+00:00").to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DateRange {
|
||||
pub start: DateTime<Utc>,
|
||||
pub end: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl DateRange {
|
||||
fn to_query_string(&self) -> Vec<(String, String)> {
|
||||
vec![
|
||||
("start_date".to_string(), self.start.to_api_format()),
|
||||
("end_date".to_string(), self.end.to_api_format()),
|
||||
]
|
||||
}
|
||||
}
|
123
src/lib.rs
Normal file
123
src/lib.rs
Normal file
|
@ -0,0 +1,123 @@
|
|||
use oauth2::reqwest::http_client;
|
||||
use oauth2::AccessToken;
|
||||
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, TokenResponse, TokenUrl};
|
||||
|
||||
pub mod api;
|
||||
//use api::generation::GenerationForecast;
|
||||
|
||||
const PRODUCTION_BASE_URL: &str = "https://digital.iservices.rte-france.com/";
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ApiException {
|
||||
/// Invalid cliend id or secret
|
||||
InvalidToken,
|
||||
/// Too many requests
|
||||
TooManyRequests,
|
||||
/// The application (api endpoint) has not be registered with the oauth application
|
||||
ApplicationNotRegistered,
|
||||
UnknownError,
|
||||
}
|
||||
|
||||
pub trait ApiClient {
|
||||
fn http_get(
|
||||
&self,
|
||||
path: &str,
|
||||
query_string: &[(String, String)],
|
||||
//) -> Result<reqwest::blocking::Response, reqwest::Error>;
|
||||
) -> Result<String, anyhow::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RteApi {
|
||||
client_id: ClientId,
|
||||
client_secret: ClientSecret,
|
||||
base_url: String,
|
||||
|
||||
token: Option<AccessToken>,
|
||||
}
|
||||
|
||||
impl RteApi {
|
||||
pub fn new(client_id: String, client_secret: String) -> Self {
|
||||
RteApi {
|
||||
client_id: ClientId::new(client_id),
|
||||
client_secret: ClientSecret::new(client_secret),
|
||||
base_url: PRODUCTION_BASE_URL.to_string(),
|
||||
token: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env_values() -> Self {
|
||||
let client_id = std::env::var("CLIENT_ID").expect("CLIENT_ID must be set");
|
||||
let client_secret = std::env::var("CLIENT_SECRET").expect("CLIENT_SECRET must be set");
|
||||
|
||||
RteApi::new(client_id, client_secret)
|
||||
}
|
||||
|
||||
pub fn with_base_url(mut self, base_url: String) -> Self {
|
||||
self.base_url = base_url;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn authenticate(&mut self) -> anyhow::Result<()> {
|
||||
let auth_url = format!("{}/oauth/authorize", self.base_url);
|
||||
let token_url = format!("{}/oauth/token", self.base_url);
|
||||
let client = BasicClient::new(
|
||||
self.client_id.clone(),
|
||||
Some(self.client_secret.clone()),
|
||||
AuthUrl::new(auth_url)?,
|
||||
Some(TokenUrl::new(token_url)?),
|
||||
);
|
||||
|
||||
let token_result = client.exchange_client_credentials().request(http_client)?;
|
||||
|
||||
self.token = Some(token_result.access_token().clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_token(&self) -> &String {
|
||||
self.token.as_ref().unwrap().secret()
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiClient for RteApi {
|
||||
fn http_get(
|
||||
&self,
|
||||
path: &str,
|
||||
query_string: &[(String, String)],
|
||||
) -> Result<String, anyhow::Error> {
|
||||
let url = format!("{}{}", self.base_url, path);
|
||||
let token = self.token.as_ref().unwrap().secret();
|
||||
|
||||
let http_client = reqwest::blocking::Client::new();
|
||||
|
||||
println!("url: {:?}", url);
|
||||
let response = http_client
|
||||
.get(&url)
|
||||
.query(&query_string)
|
||||
.bearer_auth(token)
|
||||
.send()?;
|
||||
|
||||
let status_code = response.status();
|
||||
|
||||
let body = response.text()?;
|
||||
println!("response: {:?}", body);
|
||||
if !status_code.is_success() {
|
||||
let status = match status_code.as_u16() {
|
||||
401 => ApiException::InvalidToken,
|
||||
429 => ApiException::TooManyRequests,
|
||||
403 => ApiException::ApplicationNotRegistered,
|
||||
_ => ApiException::UnknownError,
|
||||
};
|
||||
eprintln!(
|
||||
"Error HTTP {} ({:?}): {}",
|
||||
status_code.as_str(),
|
||||
status,
|
||||
body
|
||||
);
|
||||
return Err(anyhow::Error::msg(format!("Request failed: {}", 42)));
|
||||
}
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue