site-calc-investment 1.2.2__py3-none-any.whl → 1.2.3__py3-none-any.whl

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.
@@ -3,7 +3,7 @@
3
3
  Python client for long-term capacity planning and investment ROI analysis.
4
4
  """
5
5
 
6
- __version__ = "1.2.2"
6
+ __version__ = "1.2.3"
7
7
 
8
8
  from site_calc_investment.analysis import (
9
9
  aggregate_annual,
@@ -2,6 +2,12 @@
2
2
 
3
3
  import os
4
4
  from dataclasses import dataclass
5
+ from typing import Optional
6
+
7
+
8
+ def get_data_dir() -> Optional[str]:
9
+ """Get the configured data directory from INVESTMENT_DATA_DIR, or None."""
10
+ return os.environ.get("INVESTMENT_DATA_DIR") or None
5
11
 
6
12
 
7
13
  @dataclass(frozen=True)
@@ -169,3 +169,73 @@ def _find_first_numeric_column(headers: list[str], file_path: str) -> int:
169
169
  if hint in h_lower:
170
170
  return i
171
171
  return 0
172
+
173
+
174
+ def _resolve_save_path(file_path: str, data_dir: Optional[str] = None) -> str:
175
+ """Resolve a file path for saving, applying data_dir for relative paths.
176
+
177
+ :param file_path: Filename or path (relative or absolute).
178
+ :param data_dir: Base directory for relative paths (or None for cwd).
179
+ :returns: Absolute path string.
180
+ :raises ValueError: If the extension is present but not '.csv'.
181
+ """
182
+ _, ext = os.path.splitext(file_path)
183
+ if ext and ext.lower() != ".csv":
184
+ raise ValueError(f"Only .csv files are supported, got '{ext}'. Use a .csv extension or omit the extension.")
185
+ if not ext:
186
+ file_path = file_path + ".csv"
187
+
188
+ if os.path.isabs(file_path):
189
+ return file_path
190
+
191
+ base = data_dir if data_dir else os.getcwd()
192
+ return os.path.abspath(os.path.join(base, file_path))
193
+
194
+
195
+ def save_csv(
196
+ file_path: str,
197
+ columns: dict[str, list[float]],
198
+ data_dir: Optional[str] = None,
199
+ overwrite: bool = False,
200
+ ) -> str:
201
+ """Save column data as a CSV file.
202
+
203
+ :param file_path: Filename or path. Relative paths resolve against data_dir (or cwd).
204
+ Extension '.csv' is appended if missing.
205
+ :param columns: Named columns of numeric data. All must have the same length.
206
+ :param data_dir: Base directory for relative paths.
207
+ :param overwrite: Allow overwriting an existing file (default: False).
208
+ :returns: Absolute path to the saved file.
209
+ :raises ValueError: If columns are empty, have no rows, or have mismatched lengths.
210
+ :raises FileExistsError: If file exists and overwrite is False.
211
+ """
212
+ if not columns:
213
+ raise ValueError("columns must not be empty -- provide at least one named column.")
214
+
215
+ lengths = {name: len(vals) for name, vals in columns.items()}
216
+ unique_lengths = set(lengths.values())
217
+
218
+ if unique_lengths == {0}:
219
+ raise ValueError("All columns have 0 rows -- provide at least one row of data.")
220
+ if len(unique_lengths) > 1:
221
+ raise ValueError(f"All columns must have the same length, got: {lengths}")
222
+
223
+ resolved = _resolve_save_path(file_path, data_dir)
224
+
225
+ if not overwrite and os.path.exists(resolved):
226
+ raise FileExistsError(f"File already exists: {resolved}. Set overwrite=True to replace it.")
227
+
228
+ parent = os.path.dirname(resolved)
229
+ if parent:
230
+ os.makedirs(parent, exist_ok=True)
231
+
232
+ col_names = list(columns.keys())
233
+ row_count = len(next(iter(columns.values())))
234
+
235
+ with open(resolved, "w", encoding="utf-8", newline="") as f:
236
+ writer = csv.writer(f)
237
+ writer.writerow(col_names)
238
+ for i in range(row_count):
239
+ writer.writerow([columns[name][i] for name in col_names])
240
+
241
+ return resolved
@@ -5,14 +5,21 @@ from typing import Any, Literal, Optional, cast
5
5
  from fastmcp import FastMCP
6
6
 
7
7
  from site_calc_investment.api.client import InvestmentClient
8
- from site_calc_investment.mcp.config import Config
8
+ from site_calc_investment.mcp.config import Config, get_data_dir
9
+ from site_calc_investment.mcp.data_loaders import save_csv
9
10
  from site_calc_investment.mcp.scenario import ScenarioStore
10
11
 
11
12
  mcp = FastMCP(
12
13
  "site-calc-investment",
13
- instructions="Investment planning optimization tools for energy systems. "
14
- "Build scenarios with batteries, CHP, PV, and market connections, "
15
- "then submit for optimization to find optimal dispatch and ROI.",
14
+ instructions=(
15
+ "Investment planning optimization tools for energy systems. "
16
+ "Build scenarios with batteries, CHP, PV, and market connections, "
17
+ "then submit for optimization to find optimal dispatch and ROI.\n\n"
18
+ "IMPORTANT: Use save_data_file to write generated data (price arrays, "
19
+ "demand profiles) to the local filesystem BEFORE referencing them in "
20
+ "add_device. This MCP server runs locally and has filesystem access -- "
21
+ "you do not need to ask the user to save files manually."
22
+ ),
16
23
  )
17
24
 
18
25
  _store = ScenarioStore()
@@ -360,6 +367,52 @@ def list_jobs() -> list[dict[str, Any]]:
360
367
  return result
361
368
 
362
369
 
370
+ # --- Data File Tools ---
371
+
372
+
373
+ def save_data_file(
374
+ file_path: str,
375
+ columns: dict[str, list[float]],
376
+ overwrite: bool = False,
377
+ ) -> dict[str, Any]:
378
+ """Save generated data to a CSV file on the local filesystem.
379
+
380
+ This tool exists because you (the LLM) cannot write files directly --
381
+ but this MCP server runs on the user's local machine and CAN.
382
+ Use it to persist generated data arrays (prices, demand profiles, etc.)
383
+ so they can be referenced in add_device via {"file": "<path>", "column": "<name>"}.
384
+
385
+ Typical workflow:
386
+ 1. Generate price/demand data as arrays
387
+ 2. Call save_data_file to write them as CSV
388
+ 3. Use the returned file_path in add_device properties
389
+
390
+ :param file_path: Filename or path (e.g., "prices_2025.csv").
391
+ Relative paths resolve against INVESTMENT_DATA_DIR env var (or cwd).
392
+ Extension '.csv' is appended if missing.
393
+ :param columns: Named columns of numeric data.
394
+ Example: {"hour": [0, 1, 2, ...], "price_eur_mwh": [30.5, 42.1, ...]}.
395
+ All columns must have the same length.
396
+ :param overwrite: Allow overwriting an existing file (default: False).
397
+ :returns: Dict with file_path (absolute), column names, and row count.
398
+ """
399
+ data_dir = get_data_dir()
400
+ saved_path = save_csv(
401
+ file_path=file_path,
402
+ columns=columns,
403
+ data_dir=data_dir,
404
+ overwrite=overwrite,
405
+ )
406
+ col_names = list(columns.keys())
407
+ row_count = len(next(iter(columns.values())))
408
+ return {
409
+ "file_path": saved_path,
410
+ "columns": col_names,
411
+ "rows": row_count,
412
+ "message": f"Saved {row_count} rows to {saved_path}",
413
+ }
414
+
415
+
363
416
  # --- Helper Tools ---
364
417
 
365
418
 
@@ -644,11 +697,11 @@ def get_device_schema(device_type: str) -> dict[str, Any]:
644
697
  "description": "Maximum demand profile (MW, not MWh!)",
645
698
  },
646
699
  "min_demand_profile": {
647
- "type": "float | list[float]",
700
+ "type": "float | list[float] | {file: str}",
648
701
  "required": False,
649
702
  "default": 0,
650
703
  "unit": "MW",
651
- "description": "Minimum demand profile or constant",
704
+ "description": "Minimum demand profile or constant. Supports file loading.",
652
705
  },
653
706
  },
654
707
  "supports_schedule": False,
@@ -664,11 +717,11 @@ def get_device_schema(device_type: str) -> dict[str, Any]:
664
717
  "description": "Maximum heat demand profile (MW)",
665
718
  },
666
719
  "min_demand_profile": {
667
- "type": "float | list[float]",
720
+ "type": "float | list[float] | {file: str}",
668
721
  "required": False,
669
722
  "default": 0,
670
723
  "unit": "MW",
671
- "description": "Minimum heat demand profile or constant",
724
+ "description": "Minimum heat demand profile or constant. Supports file loading.",
672
725
  },
673
726
  },
674
727
  "supports_schedule": False,
@@ -702,3 +755,4 @@ mcp.tool()(get_job_result)
702
755
  mcp.tool()(cancel_job)
703
756
  mcp.tool()(list_jobs)
704
757
  mcp.tool()(get_device_schema)
758
+ mcp.tool()(save_data_file)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: site-calc-investment
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Python client for Site-Calc investment planning (capacity sizing, ROI analysis)
5
5
  Project-URL: Homepage, https://github.com/stranma/site-calc-investment
6
6
  Project-URL: Documentation, https://github.com/stranma/site-calc-investment#readme
@@ -212,6 +212,60 @@ comparison = compare_scenarios(
212
212
  print(comparison) # DataFrame with NPV, IRR, costs, revenues
213
213
  ```
214
214
 
215
+ ## MCP Server (Claude Desktop Integration)
216
+
217
+ The package includes an MCP server for use with Claude Desktop and other LLM tools.
218
+
219
+ ### Installation
220
+
221
+ ```bash
222
+ pip install site-calc-investment[mcp]
223
+ ```
224
+
225
+ ### Claude Desktop Configuration
226
+
227
+ Add to `claude_desktop_config.json`:
228
+
229
+ ```json
230
+ {
231
+ "mcpServers": {
232
+ "site-calc-investment": {
233
+ "command": "uv",
234
+ "args": ["run", "--directory", "/path/to/client-investment", "site-calc-investment-mcp"],
235
+ "env": {
236
+ "INVESTMENT_API_URL": "http://your-api-url",
237
+ "INVESTMENT_API_KEY": "inv_your_key_here",
238
+ "INVESTMENT_DATA_DIR": "/path/to/data/directory"
239
+ }
240
+ }
241
+ }
242
+ }
243
+ ```
244
+
245
+ ### Tools (15)
246
+
247
+ | Tool | Description |
248
+ |------|-------------|
249
+ | `create_scenario` | Create a new draft scenario |
250
+ | `add_device` | Add a device (battery, CHP, PV, etc.) |
251
+ | `set_timespan` | Set optimization time horizon |
252
+ | `set_investment_params` | Set financial parameters (NPV, IRR) |
253
+ | `review_scenario` | Review scenario before submission |
254
+ | `remove_device` | Remove a device |
255
+ | `delete_scenario` | Delete a scenario |
256
+ | `list_scenarios` | List all draft scenarios |
257
+ | `submit_scenario` | Submit for optimization |
258
+ | `get_job_status` | Check job progress |
259
+ | `get_job_result` | Get optimization results |
260
+ | `cancel_job` | Cancel a job |
261
+ | `list_jobs` | List all jobs |
262
+ | `get_device_schema` | Get device property schema |
263
+ | `save_data_file` | Save generated data as CSV |
264
+
265
+ `save_data_file` lets the LLM write generated data (price arrays, demand profiles) to local CSV files, which can then be referenced in `add_device` properties.
266
+
267
+ See [docs/MCP_SERVER_SPEC.md](docs/MCP_SERVER_SPEC.md) for full specification.
268
+
215
269
  ## Documentation
216
270
 
217
271
  Full documentation available at: https://github.com/stranma/site-calc-investment#readme
@@ -1,4 +1,4 @@
1
- site_calc_investment/__init__.py,sha256=rtRAjNce13k5WT1S7WnL6tjnKXA6MdyzHeVggkI9jNc,2084
1
+ site_calc_investment/__init__.py,sha256=5pzvQMGWiyaDU8ncrdevsRNvGHutVboe1C78BKqTMqA,2084
2
2
  site_calc_investment/exceptions.py,sha256=dz1wXM3Y4hHzK-PaQPY7sFPgLwUd6bZyWfJDVH9aBZk,2108
3
3
  site_calc_investment/analysis/__init__.py,sha256=u6nwlhY3gQnxn-nkUGc68ynmoBU7NUt3uoUuM9AfQIs,414
4
4
  site_calc_investment/analysis/comparison.py,sha256=3aqKpVcGy-2YfQZjS6qAzN12vuzvwDmE1JgZn6fuuIE,4487
@@ -6,17 +6,17 @@ site_calc_investment/analysis/financial.py,sha256=gzKYmylGDKZDqx1ZBefZwJMS9NuflH
6
6
  site_calc_investment/api/__init__.py,sha256=LmrMw5jO24beExGtcau7B34NnMkJUmDsHngEdzuCans,140
7
7
  site_calc_investment/api/client.py,sha256=Uh4yYPIC9oL1uiaaTe36xFeb5yfOsxkMetZojxRZoJs,15064
8
8
  site_calc_investment/mcp/__init__.py,sha256=jxRR08m6sGun1lOLQ5Z9X9uB26UqVyT90is7vvvW49w,326
9
- site_calc_investment/mcp/config.py,sha256=uZ_n4jdE_fO0MKsT8GoqQYrX9pUnKmUoRcIQujTIEms,1089
10
- site_calc_investment/mcp/data_loaders.py,sha256=6jvooKD4UoKIaeaU__fEc6t9viaf2FHDpTPh8WSRWYY,6308
9
+ site_calc_investment/mcp/config.py,sha256=FuESohfC0yfk6g1R326DbAZoZN7V1YQ2Jbx7Vi6IN3M,1292
10
+ site_calc_investment/mcp/data_loaders.py,sha256=Y1Ilk2YSh-qd3bDcN_MokxLevljPcDO3VGRnPYYdvvk,8995
11
11
  site_calc_investment/mcp/scenario.py,sha256=APLrYCXdl2VWMebk_kWfZu3Mvpbw9DIFHtnYGV9bH3Q,18774
12
- site_calc_investment/mcp/server.py,sha256=CEHCs4adr270CaSKZdk-xgaZ_SkjzWGL2nMOEI_Olkw,25445
12
+ site_calc_investment/mcp/server.py,sha256=V1LFddWm6XPt1bdEZ4FZ_h7nYZxU7r_56JsitCki27M,27616
13
13
  site_calc_investment/models/__init__.py,sha256=aJsKxZGYe8AwsB7ZVrjRJs961iCd6KNKN5fpeYe52FY,1779
14
14
  site_calc_investment/models/common.py,sha256=r6Acg8uICJTETQ1FoVuBEdM8ThLQqwCVhclYO3kmCAY,5817
15
15
  site_calc_investment/models/devices.py,sha256=RmlbcxjavC3nFRyCJ6n8aUp2ZosVTpszmMmJfqpgd9c,9895
16
16
  site_calc_investment/models/requests.py,sha256=K3ESSgh3Qkq26jIL3TLTD6TRGmttVSiUUPTdbCmwRiw,5089
17
17
  site_calc_investment/models/responses.py,sha256=pmFfhSCAlCUZTxRDh0IdBn5znJiiFjF7U3LHZON_yQY,5482
18
- site_calc_investment-1.2.2.dist-info/METADATA,sha256=GlBIVQTTJQTFae5e-5SrO6fkRCyrrtDBw4a8puq-EiQ,7520
19
- site_calc_investment-1.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
20
- site_calc_investment-1.2.2.dist-info/entry_points.txt,sha256=4q9JDR4ahtnx04H_wnMkl_td1Ki88099NbS7qRrOfg0,75
21
- site_calc_investment-1.2.2.dist-info/licenses/LICENSE,sha256=tmWogc4edAjn_ccDCKSLvMGW7asakJnAlQWuNqgApIs,1071
22
- site_calc_investment-1.2.2.dist-info/RECORD,,
18
+ site_calc_investment-1.2.3.dist-info/METADATA,sha256=ZO-8638LBrDp9whS0TxFN3g1LaooAmGlEY_FCbT_YgM,9189
19
+ site_calc_investment-1.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
20
+ site_calc_investment-1.2.3.dist-info/entry_points.txt,sha256=4q9JDR4ahtnx04H_wnMkl_td1Ki88099NbS7qRrOfg0,75
21
+ site_calc_investment-1.2.3.dist-info/licenses/LICENSE,sha256=tmWogc4edAjn_ccDCKSLvMGW7asakJnAlQWuNqgApIs,1071
22
+ site_calc_investment-1.2.3.dist-info/RECORD,,