kpower-forecast 2026.2.1__tar.gz → 2026.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kpower_forecast-2026.2.2/.pre-commit-config.yaml +30 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/PKG-INFO +6 -2
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/pyproject.toml +20 -2
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/scripts/validate_version.py +8 -5
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/src/kpower_forecast/__init__.py +1 -1
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/src/kpower_forecast/core.py +66 -62
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/src/kpower_forecast/storage.py +3 -2
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/src/kpower_forecast/utils.py +3 -5
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/src/kpower_forecast/weather_client.py +30 -23
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/tests/test_core.py +56 -43
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/tests/test_utils.py +3 -2
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/uv.lock +180 -1
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/.github/pull_request_template.md +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/.github/workflows/ci.yml +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/.github/workflows/deploy.yml +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/.gitignore +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/.python-version +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/AGENTS.md +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/GEMINI.md +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/LICENSE +0 -0
- {kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/README.md +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: ruff-check
|
|
5
|
+
name: ruff check
|
|
6
|
+
entry: uv run ruff check --fix
|
|
7
|
+
language: system
|
|
8
|
+
types: [python]
|
|
9
|
+
require_serial: true
|
|
10
|
+
|
|
11
|
+
- id: ruff-format
|
|
12
|
+
name: ruff format
|
|
13
|
+
entry: uv run ruff format
|
|
14
|
+
language: system
|
|
15
|
+
types: [python]
|
|
16
|
+
require_serial: true
|
|
17
|
+
|
|
18
|
+
- id: mypy
|
|
19
|
+
name: mypy
|
|
20
|
+
entry: uv run mypy src
|
|
21
|
+
language: system
|
|
22
|
+
types: [python]
|
|
23
|
+
pass_filenames: false
|
|
24
|
+
|
|
25
|
+
- id: pytest
|
|
26
|
+
name: pytest
|
|
27
|
+
entry: uv run pytest
|
|
28
|
+
language: system
|
|
29
|
+
pass_filenames: false
|
|
30
|
+
always_run: true
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kpower-forecast
|
|
3
|
-
Version: 2026.2.
|
|
3
|
+
Version: 2026.2.2
|
|
4
4
|
Summary: Solar production and power consumption forecasting package.
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: "KSoft.TECH OSS" <oss@ksoft.tech>
|
|
6
6
|
License: AGPL-3.0
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Keywords: energy,forecast,prophet,solar,weather
|
|
@@ -23,8 +23,12 @@ Requires-Dist: pytz>=2024.1
|
|
|
23
23
|
Requires-Dist: requests>=2.31.0
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pandas-stubs; extra == 'dev'
|
|
27
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
26
28
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
27
29
|
Requires-Dist: ruff>=0.2.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: scipy; extra == 'dev'
|
|
31
|
+
Requires-Dist: scipy-stubs; extra == 'dev'
|
|
28
32
|
Requires-Dist: types-pytz; extra == 'dev'
|
|
29
33
|
Requires-Dist: types-requests; extra == 'dev'
|
|
30
34
|
Description-Content-Type: text/markdown
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kpower-forecast"
|
|
3
|
-
version = "2026.2.
|
|
3
|
+
version = "2026.2.2"
|
|
4
4
|
description = "Solar production and power consumption forecasting package."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
7
7
|
license = { text = "AGPL-3.0" }
|
|
8
8
|
authors = [
|
|
9
|
-
{ name = "
|
|
9
|
+
{ name = "KSoft.TECH OSS", email = "oss@ksoft.tech" }
|
|
10
10
|
]
|
|
11
11
|
keywords = ["solar", "energy", "forecast", "prophet", "weather"]
|
|
12
12
|
classifiers = [
|
|
@@ -35,8 +35,26 @@ dev = [
|
|
|
35
35
|
"mypy>=1.8.0",
|
|
36
36
|
"types-requests",
|
|
37
37
|
"types-pytz",
|
|
38
|
+
"pre-commit",
|
|
39
|
+
"scipy",
|
|
40
|
+
"pandas-stubs",
|
|
41
|
+
"scipy-stubs",
|
|
38
42
|
]
|
|
39
43
|
|
|
44
|
+
[tool.mypy]
|
|
45
|
+
python_version = "3.13"
|
|
46
|
+
warn_return_any = true
|
|
47
|
+
warn_unused_configs = true
|
|
48
|
+
ignore_missing_imports = true
|
|
49
|
+
|
|
50
|
+
[[tool.mypy.overrides]]
|
|
51
|
+
module = [
|
|
52
|
+
"prophet.*",
|
|
53
|
+
"pvlib.*",
|
|
54
|
+
"pysolar.*",
|
|
55
|
+
]
|
|
56
|
+
ignore_missing_imports = true
|
|
57
|
+
|
|
40
58
|
[build-system]
|
|
41
59
|
requires = ["hatchling", "hatch-vcs"]
|
|
42
60
|
build-backend = "hatchling.build"
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
# Regex for YYYY.MM.Patch (e.g., 2026.2.1)
|
|
6
6
|
VERSION_PATTERN = r"^\d{4}\.(?:[1-9]|1[0-2])\.\d+$"
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
def validate_version(file_path: str, pattern: str):
|
|
9
10
|
content = Path(file_path).read_text()
|
|
10
11
|
# Find version in pyproject.toml or __init__.py
|
|
@@ -12,25 +13,27 @@ def validate_version(file_path: str, pattern: str):
|
|
|
12
13
|
if not match:
|
|
13
14
|
print(f"❌ Could not find version in {file_path}")
|
|
14
15
|
return False
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
version = match.group(1)
|
|
17
18
|
if not re.match(VERSION_PATTERN, version):
|
|
18
19
|
print(f"❌ Invalid version format in {file_path}: '{version}'")
|
|
19
20
|
print(" Expected format: YYYY.MM.Patch (e.g., 2026.2.1)")
|
|
20
21
|
return False
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
print(f"✅ Version {version} in {file_path} is valid.")
|
|
23
24
|
return True
|
|
24
25
|
|
|
26
|
+
|
|
25
27
|
if __name__ == "__main__":
|
|
26
28
|
success = True
|
|
27
29
|
# Check pyproject.toml
|
|
28
30
|
if not validate_version("pyproject.toml", r'^version\s*=\s*"([^"]+)"'):
|
|
29
31
|
success = False
|
|
30
|
-
|
|
32
|
+
|
|
31
33
|
# Check __init__.py
|
|
32
|
-
|
|
34
|
+
pattern = r'^__version__\s*=\s*"([^"]+)"'
|
|
35
|
+
if not validate_version("src/kpower_forecast/__init__.py", pattern):
|
|
33
36
|
success = False
|
|
34
|
-
|
|
37
|
+
|
|
35
38
|
if not success:
|
|
36
39
|
sys.exit(1)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import List, Literal
|
|
2
|
+
from typing import List, Literal, cast
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from prophet import Prophet
|
|
@@ -12,14 +12,16 @@ from .weather_client import WeatherClient
|
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
class PredictionInterval(BaseModel):
|
|
16
17
|
timestamp: pd.Timestamp
|
|
17
18
|
expected_kwh: float
|
|
18
|
-
lower_bound_kwh: float
|
|
19
|
-
upper_bound_kwh: float
|
|
20
|
-
|
|
19
|
+
lower_bound_kwh: float # P10
|
|
20
|
+
upper_bound_kwh: float # P90
|
|
21
|
+
|
|
21
22
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
22
23
|
|
|
24
|
+
|
|
23
25
|
class KPowerConfig(BaseModel):
|
|
24
26
|
model_id: str
|
|
25
27
|
latitude: float = Field(..., ge=-90, le=90)
|
|
@@ -37,9 +39,10 @@ class KPowerConfig(BaseModel):
|
|
|
37
39
|
if v not in (15, 60):
|
|
38
40
|
raise ValueError("interval_minutes must be 15 or 60")
|
|
39
41
|
return v
|
|
40
|
-
|
|
42
|
+
|
|
41
43
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
42
44
|
|
|
45
|
+
|
|
43
46
|
class KPowerForecast:
|
|
44
47
|
def __init__(
|
|
45
48
|
self,
|
|
@@ -60,7 +63,7 @@ class KPowerForecast:
|
|
|
60
63
|
forecast_type=forecast_type,
|
|
61
64
|
heat_pump_mode=heat_pump_mode,
|
|
62
65
|
)
|
|
63
|
-
|
|
66
|
+
|
|
64
67
|
self.weather_client = WeatherClient(
|
|
65
68
|
lat=self.config.latitude, lon=self.config.longitude
|
|
66
69
|
)
|
|
@@ -72,25 +75,26 @@ class KPowerForecast:
|
|
|
72
75
|
"""
|
|
73
76
|
df = df.copy()
|
|
74
77
|
df["ds"] = pd.to_datetime(df["ds"], utc=True)
|
|
75
|
-
|
|
78
|
+
|
|
76
79
|
# 1. Physics: Clear Sky GHI
|
|
77
80
|
logger.info("Calculating physics-informed Clear Sky GHI...")
|
|
78
81
|
# Ensure index is DatetimeIndex for pvlib
|
|
79
82
|
temp_df = df.set_index("ds")
|
|
83
|
+
if not isinstance(temp_df.index, pd.DatetimeIndex):
|
|
84
|
+
raise ValueError("Index must be DatetimeIndex")
|
|
85
|
+
|
|
80
86
|
df["clear_sky_ghi"] = get_clear_sky_ghi(
|
|
81
87
|
self.config.latitude, self.config.longitude, temp_df.index
|
|
82
88
|
).values
|
|
83
|
-
|
|
89
|
+
|
|
84
90
|
# 2. Rolling Cloud Cover (3-hour window)
|
|
85
91
|
# 3 hours = 180 minutes. Window depends on interval_minutes.
|
|
86
92
|
window_size = 180 // self.config.interval_minutes
|
|
87
93
|
logger.info(f"Adding rolling cloud cover (window={window_size})...")
|
|
88
94
|
df["rolling_cloud_cover"] = (
|
|
89
|
-
df["cloud_cover"]
|
|
90
|
-
.rolling(window=window_size, min_periods=1)
|
|
91
|
-
.mean()
|
|
95
|
+
df["cloud_cover"].rolling(window=window_size, min_periods=1).mean()
|
|
92
96
|
)
|
|
93
|
-
|
|
97
|
+
|
|
94
98
|
return df
|
|
95
99
|
|
|
96
100
|
def train(self, history_df: pd.DataFrame, force: bool = False):
|
|
@@ -107,23 +111,23 @@ class KPowerForecast:
|
|
|
107
111
|
df = history_df.copy()
|
|
108
112
|
if "ds" not in df.columns or "y" not in df.columns:
|
|
109
113
|
raise ValueError("history_df must contain 'ds' and 'y' columns")
|
|
110
|
-
|
|
114
|
+
|
|
111
115
|
df["ds"] = pd.to_datetime(df["ds"], utc=True)
|
|
112
116
|
df = df.sort_values("ds")
|
|
113
|
-
|
|
117
|
+
|
|
114
118
|
start_date = df["ds"].min().date()
|
|
115
119
|
end_date = df["ds"].max().date()
|
|
116
|
-
|
|
120
|
+
|
|
117
121
|
weather_df = self.weather_client.fetch_historical(start_date, end_date)
|
|
118
122
|
weather_df = self.weather_client.resample_weather(
|
|
119
123
|
weather_df, self.config.interval_minutes
|
|
120
124
|
)
|
|
121
|
-
|
|
125
|
+
|
|
122
126
|
df = pd.merge(df, weather_df, on="ds", how="left")
|
|
123
|
-
|
|
127
|
+
|
|
124
128
|
weather_cols = ["temperature_2m", "cloud_cover", "shortwave_radiation"]
|
|
125
129
|
df[weather_cols] = df[weather_cols].interpolate(method="linear").bfill().ffill()
|
|
126
|
-
|
|
130
|
+
|
|
127
131
|
if df[weather_cols].isnull().any().any():
|
|
128
132
|
df = df.dropna(subset=weather_cols)
|
|
129
133
|
|
|
@@ -134,9 +138,9 @@ class KPowerForecast:
|
|
|
134
138
|
m = Prophet(
|
|
135
139
|
changepoint_prior_scale=self.config.changepoint_prior_scale,
|
|
136
140
|
seasonality_prior_scale=self.config.seasonality_prior_scale,
|
|
137
|
-
interval_width=0.8,
|
|
141
|
+
interval_width=0.8, # Used for P10/P90 (80% interval)
|
|
138
142
|
)
|
|
139
|
-
|
|
143
|
+
|
|
140
144
|
if self.config.forecast_type == "solar":
|
|
141
145
|
m.add_regressor("temperature_2m")
|
|
142
146
|
m.add_regressor("rolling_cloud_cover")
|
|
@@ -145,10 +149,10 @@ class KPowerForecast:
|
|
|
145
149
|
elif self.config.forecast_type == "consumption":
|
|
146
150
|
if self.config.heat_pump_mode:
|
|
147
151
|
m.add_regressor("temperature_2m")
|
|
148
|
-
|
|
152
|
+
|
|
149
153
|
logger.info(f"Training Prophet model for {self.config.forecast_type}...")
|
|
150
154
|
m.fit(df)
|
|
151
|
-
|
|
155
|
+
|
|
152
156
|
self.storage.save_model(m, self.config.model_id)
|
|
153
157
|
|
|
154
158
|
def tune_model(self, history_df: pd.DataFrame, days: int = 30):
|
|
@@ -156,11 +160,11 @@ class KPowerForecast:
|
|
|
156
160
|
Find optimal hyperparameters using cross-validation.
|
|
157
161
|
"""
|
|
158
162
|
logger.info(f"Tuning model hyperparameters using {days} days of history...")
|
|
159
|
-
|
|
163
|
+
|
|
160
164
|
# We need to prepare data first as cross_validation needs the regressors
|
|
161
165
|
# This is a bit complex as we need weather data for history_df
|
|
162
166
|
# For simplicity, we assume train() logic but without fitting.
|
|
163
|
-
|
|
167
|
+
|
|
164
168
|
# Prepare data (duplicated logic from train, could be refactored)
|
|
165
169
|
df = history_df.copy()
|
|
166
170
|
df["ds"] = pd.to_datetime(df["ds"], utc=True)
|
|
@@ -176,16 +180,16 @@ class KPowerForecast:
|
|
|
176
180
|
df = self._prepare_features(df.dropna(subset=weather_cols))
|
|
177
181
|
|
|
178
182
|
param_grid = {
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
"changepoint_prior_scale": [0.001, 0.05, 0.5],
|
|
184
|
+
"seasonality_prior_scale": [0.01, 1.0, 10.0],
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
# Simplified tuning loop
|
|
184
188
|
best_params = {}
|
|
185
|
-
min_rmse = float(
|
|
189
|
+
min_rmse = float("inf")
|
|
186
190
|
|
|
187
|
-
for cps in param_grid[
|
|
188
|
-
for sps in param_grid[
|
|
191
|
+
for cps in param_grid["changepoint_prior_scale"]:
|
|
192
|
+
for sps in param_grid["seasonality_prior_scale"]:
|
|
189
193
|
m = Prophet(changepoint_prior_scale=cps, seasonality_prior_scale=sps)
|
|
190
194
|
if self.config.forecast_type == "solar":
|
|
191
195
|
m.add_regressor("temperature_2m")
|
|
@@ -197,24 +201,24 @@ class KPowerForecast:
|
|
|
197
201
|
and self.config.heat_pump_mode
|
|
198
202
|
):
|
|
199
203
|
m.add_regressor("temperature_2m")
|
|
200
|
-
|
|
204
|
+
|
|
201
205
|
m.fit(df)
|
|
202
|
-
|
|
206
|
+
|
|
203
207
|
# Cross-validation
|
|
204
208
|
# initial should be at least 3x horizon
|
|
205
209
|
df_cv = cross_validation(
|
|
206
|
-
m, initial=f
|
|
210
|
+
m, initial=f"{days // 2} days", period="5 days", horizon="5 days"
|
|
207
211
|
)
|
|
208
212
|
df_p = performance_metrics(df_cv, rolling_window=1)
|
|
209
|
-
rmse = df_p[
|
|
213
|
+
rmse = df_p["rmse"].values[0]
|
|
210
214
|
|
|
211
215
|
if rmse < min_rmse:
|
|
212
216
|
min_rmse = rmse
|
|
213
|
-
best_params = {
|
|
217
|
+
best_params = {"cps": cps, "sps": sps}
|
|
214
218
|
|
|
215
219
|
logger.info(f"Best params found: {best_params} with RMSE {min_rmse}")
|
|
216
|
-
self.config.changepoint_prior_scale = best_params[
|
|
217
|
-
self.config.seasonality_prior_scale = best_params[
|
|
220
|
+
self.config.changepoint_prior_scale = best_params["cps"]
|
|
221
|
+
self.config.seasonality_prior_scale = best_params["sps"]
|
|
218
222
|
|
|
219
223
|
def predict(self, days: int = 7) -> pd.DataFrame:
|
|
220
224
|
"""
|
|
@@ -225,25 +229,25 @@ class KPowerForecast:
|
|
|
225
229
|
raise RuntimeError(
|
|
226
230
|
f"Model {self.config.model_id} not found. Please run train() first."
|
|
227
231
|
)
|
|
228
|
-
|
|
232
|
+
|
|
229
233
|
weather_forecast = self.weather_client.fetch_forecast(days=days)
|
|
230
234
|
weather_forecast = self.weather_client.resample_weather(
|
|
231
235
|
weather_forecast, self.config.interval_minutes
|
|
232
236
|
)
|
|
233
|
-
|
|
237
|
+
|
|
234
238
|
future = pd.DataFrame({"ds": weather_forecast["ds"]})
|
|
235
239
|
future = pd.merge(future, weather_forecast, on="ds", how="left")
|
|
236
|
-
|
|
240
|
+
|
|
237
241
|
weather_cols = ["temperature_2m", "cloud_cover", "shortwave_radiation"]
|
|
238
242
|
future[weather_cols] = (
|
|
239
243
|
future[weather_cols].interpolate(method="linear").bfill().ffill()
|
|
240
244
|
)
|
|
241
|
-
|
|
245
|
+
|
|
242
246
|
# Feature Engineering
|
|
243
247
|
future = self._prepare_features(future)
|
|
244
|
-
|
|
248
|
+
|
|
245
249
|
forecast = m.predict(future)
|
|
246
|
-
|
|
250
|
+
|
|
247
251
|
# Night Mask & Clipping
|
|
248
252
|
if self.config.forecast_type == "solar":
|
|
249
253
|
logger.info("Applying night mask for solar forecast...")
|
|
@@ -251,26 +255,28 @@ class KPowerForecast:
|
|
|
251
255
|
self.config.latitude, self.config.longitude, forecast["ds"]
|
|
252
256
|
)
|
|
253
257
|
forecast.loc[elevations < 0, ["yhat", "yhat_lower", "yhat_upper"]] = 0
|
|
254
|
-
|
|
258
|
+
|
|
255
259
|
for col in ["yhat", "yhat_lower", "yhat_upper"]:
|
|
256
260
|
forecast[col] = forecast[col].clip(lower=0)
|
|
257
|
-
|
|
258
|
-
return forecast
|
|
261
|
+
|
|
262
|
+
return cast(pd.DataFrame, forecast)
|
|
259
263
|
|
|
260
264
|
def get_prediction_intervals(self, days: int = 7) -> List[PredictionInterval]:
|
|
261
265
|
"""
|
|
262
266
|
Returns prediction intervals for EMS.
|
|
263
267
|
"""
|
|
264
268
|
forecast = self.predict(days=days)
|
|
265
|
-
|
|
269
|
+
|
|
266
270
|
intervals = []
|
|
267
271
|
for _, row in forecast.iterrows():
|
|
268
|
-
intervals.append(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
intervals.append(
|
|
273
|
+
PredictionInterval(
|
|
274
|
+
timestamp=row["ds"],
|
|
275
|
+
expected_kwh=row["yhat"],
|
|
276
|
+
lower_bound_kwh=row["yhat_lower"],
|
|
277
|
+
upper_bound_kwh=row["yhat_upper"],
|
|
278
|
+
)
|
|
279
|
+
)
|
|
274
280
|
return intervals
|
|
275
281
|
|
|
276
282
|
def get_surplus_probability(
|
|
@@ -278,21 +284,19 @@ class KPowerForecast:
|
|
|
278
284
|
) -> pd.DataFrame:
|
|
279
285
|
"""
|
|
280
286
|
Returns probability of exceeding threshold_kwh.
|
|
281
|
-
Prophet doesn't provide direct probabilities, but we can estimate
|
|
282
|
-
from the uncertainty interval (yhat_upper - yhat_lower)
|
|
287
|
+
Prophet doesn't provide direct probabilities, but we can estimate
|
|
288
|
+
from the uncertainty interval (yhat_upper - yhat_lower)
|
|
283
289
|
assuming normal distribution.
|
|
284
290
|
"""
|
|
285
291
|
forecast = self.predict(days=days)
|
|
286
|
-
|
|
292
|
+
|
|
287
293
|
# Estimate sigma from 80% interval (approx 1.28 * sigma)
|
|
288
294
|
sigma = (forecast["yhat_upper"] - forecast["yhat_lower"]) / (2 * 1.28)
|
|
289
|
-
sigma = sigma.replace(0, 1e-9)
|
|
290
|
-
|
|
295
|
+
sigma = sigma.replace(0, 1e-9) # Avoid div by zero
|
|
296
|
+
|
|
291
297
|
from scipy.stats import norm
|
|
298
|
+
|
|
292
299
|
z_score = (threshold_kwh - forecast["yhat"]) / sigma
|
|
293
300
|
prob_exceed = 1 - norm.cdf(z_score)
|
|
294
|
-
|
|
295
|
-
return pd.DataFrame({
|
|
296
|
-
"ds": forecast["ds"],
|
|
297
|
-
"surplus_prob": prob_exceed
|
|
298
|
-
})
|
|
301
|
+
|
|
302
|
+
return pd.DataFrame({"ds": forecast["ds"], "surplus_prob": prob_exceed})
|
|
@@ -8,6 +8,7 @@ from prophet.serialize import model_from_json, model_to_json
|
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger(__name__)
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
class ModelStorage:
|
|
12
13
|
def __init__(self, storage_path: str):
|
|
13
14
|
self.storage_path = Path(storage_path)
|
|
@@ -50,8 +51,8 @@ class ModelStorage:
|
|
|
50
51
|
return model_from_json(json.load(f))
|
|
51
52
|
except Exception as e:
|
|
52
53
|
logger.error(f"Failed to load model {model_id}: {e}")
|
|
53
|
-
# If load fails, we might want to return None to trigger retraining,
|
|
54
|
-
# or raise to alert the user.
|
|
54
|
+
# If load fails, we might want to return None to trigger retraining,
|
|
55
|
+
# or raise to alert the user.
|
|
55
56
|
# Given "production-grade", maybe explicit failure is safer
|
|
56
57
|
# than silent fallback?
|
|
57
58
|
# But the prompt says "If a model exists, load it...
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
-
from typing import List, Union
|
|
2
|
+
from typing import List, Union, cast
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import pandas as pd
|
|
@@ -37,9 +37,7 @@ def calculate_solar_elevation(
|
|
|
37
37
|
return np.array(elevations)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def get_clear_sky_ghi(
|
|
41
|
-
lat: float, lon: float, times: pd.DatetimeIndex
|
|
42
|
-
) -> pd.Series:
|
|
40
|
+
def get_clear_sky_ghi(lat: float, lon: float, times: pd.DatetimeIndex) -> pd.Series:
|
|
43
41
|
"""
|
|
44
42
|
Calculate Theoretical Clear Sky GHI (Global Horizontal Irradiance)
|
|
45
43
|
using pvlib.
|
|
@@ -56,4 +54,4 @@ def get_clear_sky_ghi(
|
|
|
56
54
|
# get_clearsky returns GHI, DNI, DHI. We only need GHI.
|
|
57
55
|
# Ineichen is the default model.
|
|
58
56
|
clearsky = location.get_clearsky(times)
|
|
59
|
-
return clearsky["ghi"]
|
|
57
|
+
return cast(pd.Series, clearsky["ghi"])
|
|
@@ -8,6 +8,7 @@ from pydantic import BaseModel
|
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger(__name__)
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
class WeatherConfig(BaseModel):
|
|
12
13
|
base_url: str = "https://api.open-meteo.com/v1/forecast"
|
|
13
14
|
# Historical API is different, usually:
|
|
@@ -19,27 +20,28 @@ class WeatherConfig(BaseModel):
|
|
|
19
20
|
# For simplicity, let's allow passing the base_url.
|
|
20
21
|
archive_url: str = "https://archive-api.open-meteo.com/v1/archive"
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
class WeatherClient:
|
|
23
25
|
def __init__(self, lat: float, lon: float, config: Optional[WeatherConfig] = None):
|
|
24
26
|
self.lat = lat
|
|
25
27
|
self.lon = lon
|
|
26
28
|
self.config = config or WeatherConfig()
|
|
27
|
-
|
|
29
|
+
|
|
28
30
|
def fetch_historical(
|
|
29
31
|
self, start_date: datetime.date, end_date: datetime.date
|
|
30
32
|
) -> pd.DataFrame:
|
|
31
33
|
"""
|
|
32
34
|
Fetch historical weather data for training.
|
|
33
35
|
"""
|
|
34
|
-
params = {
|
|
36
|
+
params: dict[str, str | float | list[str]] = {
|
|
35
37
|
"latitude": self.lat,
|
|
36
38
|
"longitude": self.lon,
|
|
37
39
|
"start_date": start_date.isoformat(),
|
|
38
40
|
"end_date": end_date.isoformat(),
|
|
39
41
|
"hourly": ["temperature_2m", "cloud_cover", "shortwave_radiation"],
|
|
40
|
-
"timezone": "UTC"
|
|
42
|
+
"timezone": "UTC",
|
|
41
43
|
}
|
|
42
|
-
|
|
44
|
+
|
|
43
45
|
try:
|
|
44
46
|
logger.info(f"Fetching historical weather from {self.config.archive_url}")
|
|
45
47
|
response = requests.get(self.config.archive_url, params=params, timeout=10)
|
|
@@ -54,14 +56,14 @@ class WeatherClient:
|
|
|
54
56
|
"""
|
|
55
57
|
Fetch weather forecast for prediction.
|
|
56
58
|
"""
|
|
57
|
-
params = {
|
|
59
|
+
params: dict[str, str | float | int | list[str]] = {
|
|
58
60
|
"latitude": self.lat,
|
|
59
61
|
"longitude": self.lon,
|
|
60
62
|
"hourly": ["temperature_2m", "cloud_cover", "shortwave_radiation"],
|
|
61
63
|
"forecast_days": days,
|
|
62
|
-
"timezone": "UTC"
|
|
64
|
+
"timezone": "UTC",
|
|
63
65
|
}
|
|
64
|
-
|
|
66
|
+
|
|
65
67
|
try:
|
|
66
68
|
logger.info(f"Fetching forecast weather from {self.config.base_url}")
|
|
67
69
|
response = requests.get(self.config.base_url, params=params, timeout=10)
|
|
@@ -76,18 +78,20 @@ class WeatherClient:
|
|
|
76
78
|
hourly = data.get("hourly", {})
|
|
77
79
|
if not hourly:
|
|
78
80
|
raise ValueError("No hourly data in response")
|
|
79
|
-
|
|
80
|
-
df = pd.DataFrame(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
|
|
82
|
+
df = pd.DataFrame(
|
|
83
|
+
{
|
|
84
|
+
"ds": pd.to_datetime(hourly["time"], utc=True),
|
|
85
|
+
"temperature_2m": hourly["temperature_2m"],
|
|
86
|
+
"cloud_cover": hourly["cloud_cover"],
|
|
87
|
+
"shortwave_radiation": hourly["shortwave_radiation"],
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Open-Meteo returns nulls sometimes, fill or drop?
|
|
88
92
|
# Linear interpolation is usually safe for weather gaps
|
|
89
|
-
df = df.interpolate(method=
|
|
90
|
-
|
|
93
|
+
df = df.interpolate(method="linear").bfill().ffill()
|
|
94
|
+
|
|
91
95
|
return df
|
|
92
96
|
|
|
93
97
|
def resample_weather(self, df: pd.DataFrame, interval_minutes: int) -> pd.DataFrame:
|
|
@@ -97,19 +101,22 @@ class WeatherClient:
|
|
|
97
101
|
"""
|
|
98
102
|
if df.empty:
|
|
99
103
|
return df
|
|
100
|
-
|
|
104
|
+
|
|
101
105
|
df = df.set_index("ds").sort_index()
|
|
102
|
-
|
|
106
|
+
|
|
103
107
|
# Check if we need resampling
|
|
108
|
+
if not isinstance(df.index, pd.DatetimeIndex):
|
|
109
|
+
raise ValueError("Index must be DatetimeIndex")
|
|
110
|
+
|
|
104
111
|
current_freq = pd.infer_freq(df.index)
|
|
105
112
|
target_freq = f"{interval_minutes}min"
|
|
106
|
-
|
|
113
|
+
|
|
107
114
|
if current_freq == target_freq:
|
|
108
115
|
return df.reset_index()
|
|
109
|
-
|
|
116
|
+
|
|
110
117
|
# Resample and interpolate
|
|
111
118
|
# Cubic is good for temperature/radiation curves
|
|
112
119
|
df_resampled = df.resample(target_freq).interpolate(method="cubic")
|
|
113
|
-
|
|
120
|
+
|
|
114
121
|
# Reset index to get 'ds' column back
|
|
115
122
|
return df_resampled.reset_index()
|
|
@@ -10,91 +10,104 @@ from kpower_forecast.core import KPowerForecast
|
|
|
10
10
|
def mock_weather_client():
|
|
11
11
|
with patch("kpower_forecast.core.WeatherClient") as mock:
|
|
12
12
|
client_instance = mock.return_value
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
# Mock fetch_historical
|
|
15
|
-
client_instance.fetch_historical.return_value = pd.DataFrame(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
client_instance.fetch_historical.return_value = pd.DataFrame(
|
|
16
|
+
{
|
|
17
|
+
"ds": pd.date_range("2024-01-01", periods=24, freq="h", tz="UTC"),
|
|
18
|
+
"temperature_2m": [20] * 24,
|
|
19
|
+
"cloud_cover": [0] * 24,
|
|
20
|
+
"shortwave_radiation": [500] * 24,
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
22
24
|
# Mock resample
|
|
23
25
|
def side_effect_resample(df, interval):
|
|
24
26
|
# Simple pass through for mock
|
|
25
27
|
return df
|
|
28
|
+
|
|
26
29
|
client_instance.resample_weather.side_effect = side_effect_resample
|
|
27
|
-
|
|
30
|
+
|
|
28
31
|
# Mock fetch_forecast
|
|
29
|
-
client_instance.fetch_forecast.return_value = pd.DataFrame(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
client_instance.fetch_forecast.return_value = pd.DataFrame(
|
|
33
|
+
{
|
|
34
|
+
"ds": pd.date_range("2024-01-02", periods=24, freq="h", tz="UTC"),
|
|
35
|
+
"temperature_2m": [20] * 24,
|
|
36
|
+
"cloud_cover": [0] * 24,
|
|
37
|
+
"shortwave_radiation": [500] * 24,
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
36
41
|
yield client_instance
|
|
37
42
|
|
|
43
|
+
|
|
38
44
|
@pytest.fixture
|
|
39
45
|
def mock_storage():
|
|
40
46
|
with patch("kpower_forecast.core.ModelStorage") as mock:
|
|
41
47
|
yield mock.return_value
|
|
42
48
|
|
|
49
|
+
|
|
43
50
|
@pytest.fixture
|
|
44
51
|
def mock_prophet():
|
|
45
52
|
with patch("kpower_forecast.core.Prophet") as mock:
|
|
46
53
|
model_instance = mock.return_value
|
|
47
54
|
# Mock predict return
|
|
48
|
-
model_instance.predict.return_value = pd.DataFrame(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
model_instance.predict.return_value = pd.DataFrame(
|
|
56
|
+
{
|
|
57
|
+
"ds": pd.date_range("2024-01-02", periods=24, freq="h", tz="UTC"),
|
|
58
|
+
"yhat": [100] * 24,
|
|
59
|
+
}
|
|
60
|
+
)
|
|
52
61
|
yield model_instance
|
|
53
62
|
|
|
63
|
+
|
|
54
64
|
def test_train(mock_weather_client, mock_storage, mock_prophet):
|
|
55
65
|
kp = KPowerForecast("test_model", 0, 0)
|
|
56
|
-
|
|
57
|
-
history = pd.DataFrame(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
|
|
67
|
+
history = pd.DataFrame(
|
|
68
|
+
{
|
|
69
|
+
"ds": pd.date_range("2024-01-01", periods=24, freq="h", tz="UTC"),
|
|
70
|
+
"y": [100] * 24,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
mock_storage.load_model.return_value = None # No existing model
|
|
75
|
+
|
|
64
76
|
kp.train(history)
|
|
65
|
-
|
|
77
|
+
|
|
66
78
|
assert mock_weather_client.fetch_historical.called
|
|
67
79
|
assert mock_prophet.fit.called
|
|
68
80
|
assert mock_storage.save_model.called
|
|
69
81
|
|
|
82
|
+
|
|
70
83
|
def test_predict(mock_weather_client, mock_storage, mock_prophet):
|
|
71
84
|
kp = KPowerForecast("test_model", 0, 0)
|
|
72
|
-
|
|
85
|
+
|
|
73
86
|
mock_storage.load_model.return_value = mock_prophet
|
|
74
|
-
|
|
87
|
+
|
|
75
88
|
forecast = kp.predict(days=1)
|
|
76
|
-
|
|
89
|
+
|
|
77
90
|
assert mock_storage.load_model.called
|
|
78
91
|
assert mock_weather_client.fetch_forecast.called
|
|
79
92
|
assert not forecast.empty
|
|
80
93
|
assert "yhat" in forecast.columns
|
|
81
94
|
|
|
95
|
+
|
|
82
96
|
def test_consumption_forecast(mock_weather_client, mock_storage, mock_prophet):
|
|
83
97
|
kp = KPowerForecast(
|
|
84
|
-
"test_consumption",
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
"test_consumption", 0, 0, forecast_type="consumption", heat_pump_mode=True
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
history = pd.DataFrame(
|
|
102
|
+
{
|
|
103
|
+
"ds": pd.date_range("2024-01-01", periods=24, freq="h", tz="UTC"),
|
|
104
|
+
"y": [200] * 24,
|
|
105
|
+
}
|
|
88
106
|
)
|
|
89
|
-
|
|
90
|
-
history = pd.DataFrame({
|
|
91
|
-
"ds": pd.date_range("2024-01-01", periods=24, freq="h", tz="UTC"),
|
|
92
|
-
"y": [200]*24
|
|
93
|
-
})
|
|
94
|
-
|
|
107
|
+
|
|
95
108
|
mock_storage.load_model.return_value = None
|
|
96
109
|
kp.train(history)
|
|
97
|
-
|
|
110
|
+
|
|
98
111
|
# Verify add_regressor was called for temperature
|
|
99
112
|
mock_prophet.add_regressor.assert_called_with("temperature_2m")
|
|
100
113
|
assert mock_prophet.fit.called
|
|
@@ -6,16 +6,17 @@ from kpower_forecast.utils import calculate_solar_elevation
|
|
|
6
6
|
def test_calculate_solar_elevation():
|
|
7
7
|
# Test location: Equator, Prime Meridian
|
|
8
8
|
lat, lon = 0.0, 0.0
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
# Test time: Noon at Equinox (approx) -> Sun should be high (90 deg ideally)
|
|
11
11
|
# March 21st, 12:00 UTC
|
|
12
12
|
times = [datetime.datetime(2024, 3, 21, 12, 0, tzinfo=datetime.timezone.utc)]
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
elevations = calculate_solar_elevation(lat, lon, times)
|
|
15
15
|
assert len(elevations) == 1
|
|
16
16
|
# Should be close to 90 degrees (zenith)
|
|
17
17
|
assert 85 < elevations[0] < 95
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
def test_night_mask():
|
|
20
21
|
# Test midnight
|
|
21
22
|
lat, lon = 0.0, 0.0
|
|
@@ -28,6 +28,15 @@ wheels = [
|
|
|
28
28
|
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
+
[[package]]
|
|
32
|
+
name = "cfgv"
|
|
33
|
+
version = "3.5.0"
|
|
34
|
+
source = { registry = "https://pypi.org/simple" }
|
|
35
|
+
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
|
|
36
|
+
wheels = [
|
|
37
|
+
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
|
38
|
+
]
|
|
39
|
+
|
|
31
40
|
[[package]]
|
|
32
41
|
name = "charset-normalizer"
|
|
33
42
|
version = "3.4.4"
|
|
@@ -157,6 +166,24 @@ wheels = [
|
|
|
157
166
|
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
|
|
158
167
|
]
|
|
159
168
|
|
|
169
|
+
[[package]]
|
|
170
|
+
name = "distlib"
|
|
171
|
+
version = "0.4.0"
|
|
172
|
+
source = { registry = "https://pypi.org/simple" }
|
|
173
|
+
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
|
|
174
|
+
wheels = [
|
|
175
|
+
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
[[package]]
|
|
179
|
+
name = "filelock"
|
|
180
|
+
version = "3.20.3"
|
|
181
|
+
source = { registry = "https://pypi.org/simple" }
|
|
182
|
+
sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
|
|
183
|
+
wheels = [
|
|
184
|
+
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
|
|
185
|
+
]
|
|
186
|
+
|
|
160
187
|
[[package]]
|
|
161
188
|
name = "fonttools"
|
|
162
189
|
version = "4.61.1"
|
|
@@ -229,6 +256,15 @@ wheels = [
|
|
|
229
256
|
{ url = "https://files.pythonhosted.org/packages/9b/3d/e3d23cb51f6353931436df4e9d1d4873311d0ed50d273ef1531bdd074958/holidays-0.90-py3-none-any.whl", hash = "sha256:8ed92ea72e2db5ef00f024c37b03641085699809f42fe5ba03b040be6740f72d", size = 1353734, upload-time = "2026-02-02T18:48:16.138Z" },
|
|
230
257
|
]
|
|
231
258
|
|
|
259
|
+
[[package]]
|
|
260
|
+
name = "identify"
|
|
261
|
+
version = "2.6.16"
|
|
262
|
+
source = { registry = "https://pypi.org/simple" }
|
|
263
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" }
|
|
264
|
+
wheels = [
|
|
265
|
+
{ url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" },
|
|
266
|
+
]
|
|
267
|
+
|
|
232
268
|
[[package]]
|
|
233
269
|
name = "idna"
|
|
234
270
|
version = "3.11"
|
|
@@ -317,7 +353,7 @@ wheels = [
|
|
|
317
353
|
|
|
318
354
|
[[package]]
|
|
319
355
|
name = "kpower-forecast"
|
|
320
|
-
version = "2026.2.
|
|
356
|
+
version = "2026.2.2"
|
|
321
357
|
source = { editable = "." }
|
|
322
358
|
dependencies = [
|
|
323
359
|
{ name = "cmdstanpy" },
|
|
@@ -334,8 +370,12 @@ dependencies = [
|
|
|
334
370
|
[package.optional-dependencies]
|
|
335
371
|
dev = [
|
|
336
372
|
{ name = "mypy" },
|
|
373
|
+
{ name = "pandas-stubs" },
|
|
374
|
+
{ name = "pre-commit" },
|
|
337
375
|
{ name = "pytest" },
|
|
338
376
|
{ name = "ruff" },
|
|
377
|
+
{ name = "scipy" },
|
|
378
|
+
{ name = "scipy-stubs" },
|
|
339
379
|
{ name = "types-pytz" },
|
|
340
380
|
{ name = "types-requests" },
|
|
341
381
|
]
|
|
@@ -346,6 +386,8 @@ requires-dist = [
|
|
|
346
386
|
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" },
|
|
347
387
|
{ name = "numpy", specifier = ">=1.26.0" },
|
|
348
388
|
{ name = "pandas", specifier = ">=2.2.0" },
|
|
389
|
+
{ name = "pandas-stubs", marker = "extra == 'dev'" },
|
|
390
|
+
{ name = "pre-commit", marker = "extra == 'dev'" },
|
|
349
391
|
{ name = "prophet", specifier = ">=1.1.5" },
|
|
350
392
|
{ name = "pvlib", specifier = ">=0.11.0" },
|
|
351
393
|
{ name = "pydantic", specifier = ">=2.6.0" },
|
|
@@ -354,6 +396,8 @@ requires-dist = [
|
|
|
354
396
|
{ name = "pytz", specifier = ">=2024.1" },
|
|
355
397
|
{ name = "requests", specifier = ">=2.31.0" },
|
|
356
398
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.2.0" },
|
|
399
|
+
{ name = "scipy", marker = "extra == 'dev'" },
|
|
400
|
+
{ name = "scipy-stubs", marker = "extra == 'dev'" },
|
|
357
401
|
{ name = "types-pytz", marker = "extra == 'dev'" },
|
|
358
402
|
{ name = "types-requests", marker = "extra == 'dev'" },
|
|
359
403
|
]
|
|
@@ -483,6 +527,15 @@ wheels = [
|
|
|
483
527
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
|
484
528
|
]
|
|
485
529
|
|
|
530
|
+
[[package]]
|
|
531
|
+
name = "nodeenv"
|
|
532
|
+
version = "1.10.0"
|
|
533
|
+
source = { registry = "https://pypi.org/simple" }
|
|
534
|
+
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
|
|
535
|
+
wheels = [
|
|
536
|
+
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
|
|
537
|
+
]
|
|
538
|
+
|
|
486
539
|
[[package]]
|
|
487
540
|
name = "numpy"
|
|
488
541
|
version = "2.4.2"
|
|
@@ -533,6 +586,33 @@ wheels = [
|
|
|
533
586
|
{ url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" },
|
|
534
587
|
]
|
|
535
588
|
|
|
589
|
+
[[package]]
|
|
590
|
+
name = "numpy-typing-compat"
|
|
591
|
+
version = "20251206.2.4"
|
|
592
|
+
source = { registry = "https://pypi.org/simple" }
|
|
593
|
+
dependencies = [
|
|
594
|
+
{ name = "numpy" },
|
|
595
|
+
]
|
|
596
|
+
sdist = { url = "https://files.pythonhosted.org/packages/42/5f/29fd5f29b0a5d96e2def96ecba3112fc330ecd16e8c97c2b332563c5e201/numpy_typing_compat-20251206.2.4.tar.gz", hash = "sha256:59882d23aaff054a2536da80564012cdce33487657be4d79c5925bb8705fcabc", size = 5011, upload-time = "2025-12-06T20:02:04.942Z" }
|
|
597
|
+
wheels = [
|
|
598
|
+
{ url = "https://files.pythonhosted.org/packages/63/7c/5c2892e6bc0628a2ccf4e938e1e2db22794657ccb374672d66e20d73839e/numpy_typing_compat-20251206.2.4-py3-none-any.whl", hash = "sha256:a82e723bd20efaa4cf2886709d4264c144f1f2b609bda83d1545113b7e47a5b5", size = 6300, upload-time = "2025-12-06T20:01:57.578Z" },
|
|
599
|
+
]
|
|
600
|
+
|
|
601
|
+
[[package]]
|
|
602
|
+
name = "optype"
|
|
603
|
+
version = "0.15.0"
|
|
604
|
+
source = { registry = "https://pypi.org/simple" }
|
|
605
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d7/93/6b9e43138ce36fbad134bd1a50460a7bbda61105b5a964e4cf773fe4d845/optype-0.15.0.tar.gz", hash = "sha256:457d6ca9e7da19967ec16d42bdf94e240b33b5d70a56fbbf5b427e5ea39cf41e", size = 99978, upload-time = "2025-12-08T12:32:41.422Z" }
|
|
606
|
+
wheels = [
|
|
607
|
+
{ url = "https://files.pythonhosted.org/packages/07/8b/93f6c496fc5da062fd7e7c4745b5a8dd09b7b576c626075844fe97951a7d/optype-0.15.0-py3-none-any.whl", hash = "sha256:caba40ece9ea39b499fa76c036a82e0d452a432dd4dd3e8e0d30892be2e8c76c", size = 88716, upload-time = "2025-12-08T12:32:39.669Z" },
|
|
608
|
+
]
|
|
609
|
+
|
|
610
|
+
[package.optional-dependencies]
|
|
611
|
+
numpy = [
|
|
612
|
+
{ name = "numpy" },
|
|
613
|
+
{ name = "numpy-typing-compat" },
|
|
614
|
+
]
|
|
615
|
+
|
|
536
616
|
[[package]]
|
|
537
617
|
name = "packaging"
|
|
538
618
|
version = "26.0"
|
|
@@ -586,6 +666,18 @@ wheels = [
|
|
|
586
666
|
{ url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" },
|
|
587
667
|
]
|
|
588
668
|
|
|
669
|
+
[[package]]
|
|
670
|
+
name = "pandas-stubs"
|
|
671
|
+
version = "3.0.0.260204"
|
|
672
|
+
source = { registry = "https://pypi.org/simple" }
|
|
673
|
+
dependencies = [
|
|
674
|
+
{ name = "numpy" },
|
|
675
|
+
]
|
|
676
|
+
sdist = { url = "https://files.pythonhosted.org/packages/27/1d/297ff2c7ea50a768a2247621d6451abb2a07c0e9be7ca6d36ebe371658e5/pandas_stubs-3.0.0.260204.tar.gz", hash = "sha256:bf9294b76352effcffa9cb85edf0bed1339a7ec0c30b8e1ac3d66b4228f1fbc3", size = 109383, upload-time = "2026-02-04T15:17:17.247Z" }
|
|
677
|
+
wheels = [
|
|
678
|
+
{ url = "https://files.pythonhosted.org/packages/7c/2f/f91e4eee21585ff548e83358332d5632ee49f6b2dcd96cb5dca4e0468951/pandas_stubs-3.0.0.260204-py3-none-any.whl", hash = "sha256:5ab9e4d55a6e2752e9720828564af40d48c4f709e6a2c69b743014a6fcb6c241", size = 168540, upload-time = "2026-02-04T15:17:15.615Z" },
|
|
679
|
+
]
|
|
680
|
+
|
|
589
681
|
[[package]]
|
|
590
682
|
name = "pathspec"
|
|
591
683
|
version = "1.0.4"
|
|
@@ -653,6 +745,15 @@ wheels = [
|
|
|
653
745
|
{ url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" },
|
|
654
746
|
]
|
|
655
747
|
|
|
748
|
+
[[package]]
|
|
749
|
+
name = "platformdirs"
|
|
750
|
+
version = "4.5.1"
|
|
751
|
+
source = { registry = "https://pypi.org/simple" }
|
|
752
|
+
sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
|
|
753
|
+
wheels = [
|
|
754
|
+
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
|
|
755
|
+
]
|
|
756
|
+
|
|
656
757
|
[[package]]
|
|
657
758
|
name = "pluggy"
|
|
658
759
|
version = "1.6.0"
|
|
@@ -662,6 +763,22 @@ wheels = [
|
|
|
662
763
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
663
764
|
]
|
|
664
765
|
|
|
766
|
+
[[package]]
|
|
767
|
+
name = "pre-commit"
|
|
768
|
+
version = "4.5.1"
|
|
769
|
+
source = { registry = "https://pypi.org/simple" }
|
|
770
|
+
dependencies = [
|
|
771
|
+
{ name = "cfgv" },
|
|
772
|
+
{ name = "identify" },
|
|
773
|
+
{ name = "nodeenv" },
|
|
774
|
+
{ name = "pyyaml" },
|
|
775
|
+
{ name = "virtualenv" },
|
|
776
|
+
]
|
|
777
|
+
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
|
|
778
|
+
wheels = [
|
|
779
|
+
{ url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
|
|
780
|
+
]
|
|
781
|
+
|
|
665
782
|
[[package]]
|
|
666
783
|
name = "prophet"
|
|
667
784
|
version = "1.3.0"
|
|
@@ -836,6 +953,42 @@ wheels = [
|
|
|
836
953
|
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
|
837
954
|
]
|
|
838
955
|
|
|
956
|
+
[[package]]
|
|
957
|
+
name = "pyyaml"
|
|
958
|
+
version = "6.0.3"
|
|
959
|
+
source = { registry = "https://pypi.org/simple" }
|
|
960
|
+
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
|
961
|
+
wheels = [
|
|
962
|
+
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
|
963
|
+
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
|
964
|
+
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
|
965
|
+
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
|
966
|
+
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
|
967
|
+
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
|
968
|
+
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
|
969
|
+
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
|
970
|
+
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
|
971
|
+
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
|
972
|
+
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
|
973
|
+
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
|
974
|
+
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
|
975
|
+
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
|
976
|
+
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
|
977
|
+
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
|
978
|
+
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
|
979
|
+
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
|
980
|
+
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
|
981
|
+
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
|
982
|
+
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
|
983
|
+
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
|
984
|
+
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
|
985
|
+
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
|
986
|
+
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
|
987
|
+
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
|
988
|
+
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
|
989
|
+
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
|
990
|
+
]
|
|
991
|
+
|
|
839
992
|
[[package]]
|
|
840
993
|
name = "requests"
|
|
841
994
|
version = "2.32.5"
|
|
@@ -927,6 +1080,18 @@ wheels = [
|
|
|
927
1080
|
{ url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" },
|
|
928
1081
|
]
|
|
929
1082
|
|
|
1083
|
+
[[package]]
|
|
1084
|
+
name = "scipy-stubs"
|
|
1085
|
+
version = "1.17.0.2"
|
|
1086
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1087
|
+
dependencies = [
|
|
1088
|
+
{ name = "optype", extra = ["numpy"] },
|
|
1089
|
+
]
|
|
1090
|
+
sdist = { url = "https://files.pythonhosted.org/packages/40/fe/5fa7da49821ea94d60629ae71277fa8d7e16eb20602f720062b6c30a644c/scipy_stubs-1.17.0.2.tar.gz", hash = "sha256:3981bd7fa4c189a8493307afadaee1a830d9a0de8e3ae2f4603f192b6260ef2a", size = 379897, upload-time = "2026-01-22T19:17:08Z" }
|
|
1091
|
+
wheels = [
|
|
1092
|
+
{ url = "https://files.pythonhosted.org/packages/51/e3/20233497e4a27956e7392c3f7879e6ee7f767f268079f24f4b089b70f563/scipy_stubs-1.17.0.2-py3-none-any.whl", hash = "sha256:99d1aa75b7d72a7ee36a68d18bcf1149f62ab577bbd1236c65c471b3b465d824", size = 586137, upload-time = "2026-01-22T19:17:05.802Z" },
|
|
1093
|
+
]
|
|
1094
|
+
|
|
930
1095
|
[[package]]
|
|
931
1096
|
name = "six"
|
|
932
1097
|
version = "1.17.0"
|
|
@@ -1019,3 +1184,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6
|
|
|
1019
1184
|
wheels = [
|
|
1020
1185
|
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
|
1021
1186
|
]
|
|
1187
|
+
|
|
1188
|
+
[[package]]
|
|
1189
|
+
name = "virtualenv"
|
|
1190
|
+
version = "20.36.1"
|
|
1191
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1192
|
+
dependencies = [
|
|
1193
|
+
{ name = "distlib" },
|
|
1194
|
+
{ name = "filelock" },
|
|
1195
|
+
{ name = "platformdirs" },
|
|
1196
|
+
]
|
|
1197
|
+
sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
|
|
1198
|
+
wheels = [
|
|
1199
|
+
{ url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
|
|
1200
|
+
]
|
|
File without changes
|
{kpower_forecast-2026.2.1 → kpower_forecast-2026.2.2}/.github/ISSUE_TEMPLATE/feature_request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|