Skip to content

Commit eda144d

Browse files
authored
Merge pull request #16 from codegreen-framework/main
0.0.3 version
2 parents 8672d52 + a76287e commit eda144d

24 files changed

+1056
-174
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,4 @@ poetry.lock
177177

178178
codegreen_core/tools/test.py
179179
codegreen_core/data/test.py
180+
tests/test_notebook.ipynb

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
This repository contains the main functionality of the codegreen project. The complete documentation including installation and usage are available on the [documentation website](https://codegreen-framework.github.io/codegreen-core/).
44

5+
# Development
6+
7+
## Installation
8+
- `git clone`
9+
- install poetry
10+
- install in editable mode : `poetry install`
11+
12+
## Github workflows
13+
Changes in the repo also triggers github actions
14+
515
## Development workflow
616
- the `release` branch contains the latest stable version of the released python package
717
- the `main` branch contains stable, tested code ready to be released.

codegreen_core/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
from .utilities.config import Config
2-
3-
Config.load_config()
1+
from . import utilities
2+
from . import data
3+
from . import tools
4+
from . import models
5+
utilities.config.Config.load_config()

codegreen_core/data/entsoe.py

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,23 @@ def _refine_data(options, data1):
117117
data1.sort_index(inplace=True)
118118
return {"data": data1, "refine_logs": refine_logs}
119119

120+
def _convert_local_to_utc(dte):
121+
# datetime obj is converted from local time zone to utc
122+
local_timezone = datetime.now().astimezone().tzinfo
123+
return pd.Timestamp(dte,tz=local_timezone).tz_convert('UTC')
120124

121125
def _entsoe_get_actual_generation(options={"country": "", "start": "", "end": ""}):
122126
"""Fetches the aggregated actual generation per production type data (16.1.B&C) for the given country within the given start and end date
123127
params: options = {country (2 letter country code),start,end} . Both the dates are in the YYYYMMDDhhmm format and the local time zone
124128
returns : {"data":pd.DataFrame, "duration":duration (in min) of the time series data, "refine_logs":"notes on refinements made" }
125129
"""
130+
utc_start = _convert_local_to_utc(options["start"])
131+
utc_end = _convert_local_to_utc(options["end"])
126132
client1 = entsoePandas(api_key=_get_API_token())
127133
data1 = client1.query_generation(
128134
options["country"],
129-
start=pd.Timestamp(options["start"], tz="UTC"),
130-
end=pd.Timestamp(options["end"], tz="UTC"),
135+
start = utc_start ,
136+
end = utc_end ,
131137
psr_type=None,
132138
)
133139
# drop columns with actual consumption values (we want actual aggregated generation values)
@@ -159,8 +165,8 @@ def _entsoe_get_total_forecast(options={"country": "", "start": "", "end": ""}):
159165
client = entsoePandas(api_key=_get_API_token())
160166
data = client.query_generation_forecast(
161167
options["country"],
162-
start=pd.Timestamp(options["start"], tz="UTC"),
163-
end=pd.Timestamp(options["end"], tz="UTC"),
168+
start=_convert_local_to_utc(options["start"]) ,
169+
end=_convert_local_to_utc(options["end"])
164170
)
165171
# if the data is a series instead of a dataframe, it will be converted to a dataframe
166172
if isinstance(data, pd.Series):
@@ -188,8 +194,8 @@ def _entsoe_get_wind_solar_forecast(options={"country": "", "start": "", "end":
188194
client = entsoePandas(api_key=_get_API_token())
189195
data = client.query_wind_and_solar_forecast(
190196
options["country"],
191-
start=pd.Timestamp(options["start"], tz="UTC"),
192-
end=pd.Timestamp(options["end"], tz="UTC"),
197+
start=_convert_local_to_utc(options["start"]) ,
198+
end=_convert_local_to_utc(options["end"])
193199
)
194200
durationMin = (data.index[1] - data.index[0]).total_seconds() / 60
195201
# refining the data
@@ -225,6 +231,10 @@ def _convert_to_60min_interval(rawData):
225231
# determining how many rows need to be combined to get data in 60 min format.
226232
groupingFactor = int(60 / duration)
227233
oldData = rawData["data"]
234+
# check if there is enough data to convert to 60 min
235+
if (len(oldData) < groupingFactor):
236+
raise ValueError("Data cannot be converted into 60 min interval since there is inadequate number of rows in the data")
237+
228238
oldData["startTimeUTC"] = pd.to_datetime(oldData["startTimeUTC"])
229239
start_time = oldData["startTimeUTC"].min()
230240
end_time = oldData["startTimeUTC"].max()
@@ -246,9 +256,19 @@ def _convert_to_60min_interval(rawData):
246256

247257

248258
def _convert_date_to_entsoe_format(dt: datetime):
259+
""" rounds the date to nearest hour """
249260
return dt.replace(minute=0, second=0, microsecond=0).strftime("%Y%m%d%H%M")
250261

251262

263+
def _format_energy_data(df):
264+
start_time_column = df.pop("startTimeUTC")
265+
df.insert(0, "startTime", start_time_column)
266+
local_timezone = datetime.now().astimezone().tzinfo
267+
df["startTime"] = pd.to_datetime(df["startTime"], format="%Y%m%d%H%M").dt.tz_localize("UTC").dt.tz_convert(local_timezone)
268+
df.insert(1, "startTimeUTC", start_time_column)
269+
return df
270+
271+
252272
# the main methods
253273

254274

@@ -260,6 +280,7 @@ def get_actual_production_percentage(country, start, end, interval60=False) -> d
260280
:param str country: The 2 alphabet country code.
261281
:param datetime start: The start date for data retrieval. A Datetime object. Note that this date will be rounded to the nearest hour.
262282
:param datetime end: The end date for data retrieval. A datetime object. This date is also rounded to the nearest hour.
283+
:param boolean interval60: To convert the data into 60 min time interval. False by default
263284
:return: A DataFrame containing the hourly energy production mix and percentage of energy generated from renewable and non renewable sources.
264285
:return: A dictionary containing:
265286
- `error`: A string with an error message, empty if no errors.
@@ -269,12 +290,32 @@ def get_actual_production_percentage(country, start, end, interval60=False) -> d
269290
:rtype: dict
270291
"""
271292
try:
293+
if not isinstance(country, str):
294+
raise ValueError("Invalid country")
295+
if not isinstance(start, datetime):
296+
raise ValueError("Invalid start date")
297+
if not isinstance(end, datetime):
298+
raise ValueError("Invalid end date")
299+
300+
if start > datetime.now():
301+
raise ValueError("Invalid start date. Generation data is only available for the past and not the future. Use the forecast API instead")
302+
303+
if start > end :
304+
raise ValueError("Invalid date range. End date must be greater than the start date")
305+
306+
# if end date is in the future and the start date is in the past , only data till the available moment will be returned.
307+
if end > datetime.now():
308+
raise ValueError("Invalid end date. Generation data is only available for the past and not the future. Use the forecast API instead")
309+
# this is not allowed because the entsoe-py returns error if it's greater than the present
310+
#warnings.warn("End date is in the future. Will fetch data only till the present")
311+
272312
options = {
273313
"country": country,
274-
"start": start,
275-
"end": end,
314+
"start": start.replace(minute=0,second=0),
315+
"end": end.replace(second=0,minute=0),
276316
"interval60": interval60,
277317
}
318+
# print(options)
278319
# get actual generation data per production type and convert it into 60 min interval if required
279320
totalRaw = _entsoe_get_actual_generation(options)
280321
total = totalRaw["data"]
@@ -327,18 +368,18 @@ def get_actual_production_percentage(country, start, end, interval60=False) -> d
327368
table[fieldName] = table[fieldName].astype(int)
328369

329370
return {
330-
"data": table,
371+
"data": _format_energy_data(table),
331372
"data_available": True,
332-
"time_interval": totalRaw["duration"],
373+
"time_interval": duration,
333374
}
334375
except Exception as e:
335-
print(e)
376+
# print(e)
336377
print(traceback.format_exc())
337378
return {
338379
"data": None,
339380
"data_available": False,
340-
"error": Exception,
341-
"time_interval": totalRaw["duration"],
381+
"error": e,
382+
"time_interval": 0,
342383
}
343384

344385

@@ -364,6 +405,13 @@ def get_forecast_percent_renewable(
364405
"""
365406
try:
366407
# print(country,start,end)
408+
if not isinstance(country, str):
409+
raise ValueError("Invalid country")
410+
if not isinstance(start, datetime):
411+
raise ValueError("Invalid start date")
412+
if not isinstance(end, datetime):
413+
raise ValueError("Invalid end date")
414+
367415
start = _convert_date_to_entsoe_format(start)
368416
end = _convert_date_to_entsoe_format(end)
369417
options = {"country": country, "start": start, "end": end}
@@ -390,7 +438,7 @@ def get_forecast_percent_renewable(
390438
windsolar["startTimeUTC"], format="%Y%m%d%H%M"
391439
)
392440
windsolar["posix_timestamp"] = windsolar["startTimeUTC"].astype(int) // 10**9
393-
return {"data": windsolar, "data_available": True, "time_interval": 60}
441+
return {"data": _format_energy_data(windsolar), "data_available": True, "time_interval": 60}
394442
except Exception as e:
395443
print(e)
396444
print(traceback.format_exc())

codegreen_core/data/main.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
from ..utilities.message import Message, CodegreenDataError
55
from ..utilities import metadata as meta
6-
from . import entsoe as et
6+
from ..utilities.config import Config
77

8+
from . import entsoe as et
9+
from . import offline as off
810

9-
def energy(country, start_time, end_time, type="generation", interval60=True) -> dict:
11+
def energy(country, start_time, end_time, type="generation") -> dict:
1012
"""
1113
Returns hourly time series of energy production mix for a specified country and time range.
1214
@@ -19,8 +21,9 @@ def energy(country, start_time, end_time, type="generation", interval60=True) ->
1921
========================== ========== ================================================================
2022
Column type Description
2123
========================== ========== ================================================================
22-
startTimeUTC datetime Start date in UTC (60 min interval)
23-
Biomass float64
24+
startTimeUTC object Start date in UTC (format YYYYMMDDhhmm)
25+
startTime datetime Start time in local timezone
26+
Biomass float64
2427
Fossil Hard coal float64
2528
Geothermal float64
2629
....more energy sources float64
@@ -47,11 +50,13 @@ def energy(country, start_time, end_time, type="generation", interval60=True) ->
4750
:param datetime start_time: The start date for data retrieval. A Datetime object. Note that this date will be rounded to the nearest hour.
4851
:param datetime end_time: The end date for data retrieval. A datetime object. This date is also rounded to the nearest hour.
4952
:param str type: The type of data to retrieve; either 'generation' or 'forecast'. Defaults to 'generation'.
53+
:param boolean interval60: To fix the time interval of data to 60 minutes. True by default. Only applicable for generation data
54+
5055
:return: A dictionary containing:
5156
- `error`: A string with an error message, empty if no errors.
5257
- `data_available`: A boolean indicating if data was successfully retrieved.
5358
- `data`: A pandas DataFrame containing the energy data if available, empty DataFrame if not.
54-
- `time_interval` : the time interval of the DataFrame
59+
- `time_interval` : the time interval of the DataFrame
5560
:rtype: dict
5661
"""
5762
if not isinstance(country, str):
@@ -70,11 +75,27 @@ def energy(country, start_time, end_time, type="generation", interval60=True) ->
7075
e_source = meta.get_country_energy_source(country)
7176
if e_source == "ENTSOE":
7277
if type == "generation":
73-
return et.get_actual_production_percentage(
74-
country, start_time, end_time, interval60
75-
)
78+
"""
79+
let local_found= false
80+
see if caching is enabled, if yes, first check in the cache
81+
if not,
82+
check if offline data is enabled
83+
if yes, check is data is available locally
84+
if no, go online
85+
"""
86+
offline_data = off.get_offline_data(country,start_time,end_time)
87+
if offline_data["available"] is True and offline_data["partial"] is False and offline_data["data"] is not None:
88+
# todo fix this if partial get remaining data and merge instead of fetching the complete data
89+
return {"data":offline_data["data"],"data_available":True,"error":"None","time_interval":60,"source":offline_data["source"]}
90+
else:
91+
energy_data = et.get_actual_production_percentage(country, start_time, end_time, interval60=True)
92+
energy_data["data"] = energy_data["data"]
93+
energy_data["source"] = "public_data"
94+
return energy_data
7695
elif type == "forecast":
77-
return et.get_forecast_percent_renewable(country, start_time, end_time)
96+
energy_data = et.get_forecast_percent_renewable(country, start_time, end_time)
97+
energy_data["data"] = energy_data["data"]
98+
return energy_data
7899
else:
79100
raise CodegreenDataError(Message.NO_ENERGY_SOURCE)
80101
return None

0 commit comments

Comments
 (0)