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