init repo

Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
This commit is contained in:
Frank Villaro-Dixon 2024-10-27 19:44:07 +01:00
commit 667713b575
11 changed files with 3961 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
ISSUES
weekly.json

3221
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

13
examples/token.rs Normal file
View 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
View 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
View 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
View 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
View 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)
}
}