From b1e92d1f14ad660ef93dfe8092d6a30cf2061f08 Mon Sep 17 00:00:00 2001 From: Damon Rand Date: Tue, 6 Jan 2026 03:08:36 +0000 Subject: [PATCH 1/3] Add flux/flows schema separation support (v2.0.0) Breaking change: Environment configuration now requires a `flux` section for database connections. See CHANGELOG.md for migration guide. Core changes: - Add flux_db_url parameter to report command for separate flux database - Add configurable schema names (flows_schema, flux_schema) with defaults - Route meter readings function to flux schema (get_meter_readings_5m) - Route BESS readings to flux.mg_bess_readings_30m - Route market data to flux.market_data - Plot meter readings continue using flows schema tables Files modified: - src/skypro/commands/report/main.py - add flux_db_url and schema params - src/skypro/commands/report/readings.py - route meter/BESS to flux - src/skypro/commands/report/rates.py - add flux_schema for market data - src/skypro/common/data/get_meter_readings.py - parameterized schema - src/skypro/common/data/get_plot_meter_readings.py - parameterized schema - src/skypro/common/data/get_bess_readings.py - parameterized schema - src/skypro/common/data/get_timeseries.py - parameterized schema Documentation: - Add CHANGELOG.md documenting 1.2.0 and 2.0.0 changes - Add CLAUDE.md with codebase documentation - Update README.md with new env.json structure - Add dbtests/ to .gitignore for database integration test outputs Testing: - Update integration test fixture env.json with flux section - All 8 tests pass (unit + integration) Version bump: 1.2.0 -> 2.0.0a1 --- .gitignore | 8 ++ CHANGELOG.md | 60 +++++++++ CLAUDE.md | 115 ++++++++++++++++++ README.md | 10 +- pyproject.toml | 2 +- src/skypro/commands/report/main.py | 28 ++++- src/skypro/commands/report/rates.py | 5 +- src/skypro/commands/report/readings.py | 9 +- src/skypro/common/data/get_bess_readings.py | 11 +- src/skypro/common/data/get_meter_readings.py | 9 +- .../common/data/get_plot_meter_readings.py | 21 ++-- src/skypro/common/data/get_timeseries.py | 17 +-- src/tests/integration/fixtures/env.json | 4 + 13 files changed, 266 insertions(+), 33 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 16ad1f5..8bdfa79 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,8 @@ celerybeat.pid # Environments .env +local.env +local.*.env.json .venv env/ venv/ @@ -170,3 +172,9 @@ simulation_output.csv simulation_output_summary.csv reporting_output_summary.csv docs/sim_examples/data/outputs + +# Database integration tests (contains env-specific outputs) +dbtests/ + +# Claude Code +.claude/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ac6bb0c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.0.0] - Unreleased + +### Breaking Changes + +- **Environment configuration**: Added required `flux` section to env.json for database connections +- The `skypro report` command now requires a `flux` database URL in the environment configuration + +### Added + +- Support for separate `flows` and `flux` database schemas +- Configurable schema names via env.json (`schema` field in `flows` and `flux` sections) +- Schema defaults: `flows.schema = "flows"`, `flux.schema = "flux"` + +### Changed + +- Meter readings function (`get_meter_readings_5m`) now queries from `flux` schema +- BESS readings now query from `flux.mg_bess_readings_30m` +- Market data (imbalance prices) now query from `flux.market_data` +- Plot meter readings continue to query from `flows` schema tables + +### Migration Guide + +Update your env.json to include the `flux` section: + +```json +{ + "flows": { "dbUrl": "postgres://..." }, + "flux": { "dbUrl": "postgres://...", "schema": "flux" }, + "rates": { "dbUrl": "postgres://..." } +} +``` + +For databases where all data is in the `flows` schema (legacy setup), set `flux.schema = "flows"`: + +```json +{ + "flux": { "dbUrl": "postgres://...", "schema": "flows" } +} +``` + +## [1.2.0] - 2024 + +### Added + +- Example configurations for simulations (self-contained examples with data) +- LP optimiser with constraint management example +- Multiple tagged load profiles example +- File-based rates example +- Peak configuration can now be disabled in price curve algorithm + +### Fixed + +- Database-based simulations now work correctly diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..404b372 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,115 @@ +# Skypro Codebase + +Python CLI for microgrid simulation and reporting. Published to test.pypi.org. + +## Project Structure + +``` +src/skypro/ +├── main.py # CLI entry point +├── commands/ +│ ├── simulator/ # `skypro simulate` command +│ └── report/ # `skypro report` command +├── common/ +│ └── rates/ # Rate modelling (volumetric, fixed, market, internal) +└── reporting_webapp/ # DEPRECATED Streamlit app +``` + +## CLI Commands + +### `skypro simulate` +```bash +skypro simulate -c -o ./output.csv --plot +skypro simulate --help +``` + +Projects microgrid behaviour over historical data with configurable control strategies: +- `perfectHindsightOptimiser` - LP-based optimal hindsight +- `priceCurveAlgo` - Real-time price curve following + +### `skypro report` +```bash +skypro report -c -m 2025-04 --plot +skypro report --help +``` + +Analyzes real metering data, generates supplier invoice estimates, reports data inconsistencies as Notices. + +## Seven Energy Flows + +``` +solar_to_batt solar_to_load solar_to_grid +grid_to_batt grid_to_load +batt_to_load batt_to_grid +``` + +Each flow has associated market rates (actual £) and internal rates (notional value for optimization). + +## Configuration + +### Environment (`~/.simt/env.json`) + +Since v2.0.0, the environment file requires a `flux` section for the flux database: + +```json +{ + "vars": { "SKYPRO_DIR": "/path/to/skyprospector.com" }, + "flows": { "dbUrl": "postgres://..." }, + "flux": { "dbUrl": "postgres://...", "schema": "flux" }, + "rates": { "dbUrl": "postgres://..." } +} +``` + +**Schema configuration:** +- `flows.schema` - defaults to "flows" (plot meter tables) +- `flux.schema` - defaults to "flux" (meter readings function, BESS readings, market data) + +For legacy databases where everything is in `flows` schema, set `flux.schema = "flows"`. + +### Simulation Config (YAML) +See `src/tests/integration/fixtures/simulation/config.yaml` for annotated example. + +Key sections: +- `timeFrame` - start/end dates +- `site.gridConnection` - import/export limits (kW) +- `site.bess` - energyCapacity (kWh), nameplatePower (kW), chargeEfficiency +- `site.solar/load` - profile sources (CSV dirs, constants, scaling) +- `rates` - JSON/YAML files for each of the 7 flows +- `strategy` - control algorithm config +- `output` - CSV paths, aggregation, detail level + +## Development + +### Install +```bash +pip install --upgrade --extra-index-url https://test.pypi.org/simple/ skypro +``` + +### Run Tests +```bash +PYTHONPATH=src python -m unittest discover --start-directory src +``` + +### Publish +1. Update version in `pyproject.toml` +2. `poetry build` +3. `poetry publish -r test-pypi` + +## Key Concepts + +### Rates +- **Volumetric** (p/kWh) - DUoS, supplier fees, levies +- **Fixed** (p/day, p/kVA/day) - standing charges +- **Market** - actual cashflows with suppliers +- **Internal** - opportunity cost for optimization + +### OSAM (P395) +On-site Allocation Methodology for calculating final demand levies. Runs in parallel with Skypro's own methodology; discrepancies reported as Notices. + +### Control Strategies +- **Perfect Hindsight LP** - Optimal solution knowing future prices +- **Price Curve Algorithm** - Real-time with NIV chase, peak shaving, load following + +## Dependencies + +pandas, plotly, pulp, pendulum, sqlalchemy, psycopg2-binary, marshmallow, pyyaml diff --git a/README.md b/README.md index 5807064..a175f52 100644 --- a/README.md +++ b/README.md @@ -35,16 +35,22 @@ The structure of this file should be as follows: "flows": { "dbUrl": "url-with-credentials-for-the-flows-database" }, + "flux": { + "dbUrl": "url-with-credentials-for-the-flux-database", + "schema": "flux" + }, "rates": { "dbUrl": "url-with-credentials-for-the-rates-database" } } ``` The `vars` section allows you to define arbitrary variables that are resolved in configuration file paths. -For example, if you're configuring a simulation run and all the load profiles are in a certain directory, then you could configure a variable like `"PROFILE_DIR": "~/myprofiles"`, and then +For example, if you're configuring a simulation run and all the load profiles are in a certain directory, then you could configure a variable like `"PROFILE_DIR": "~/myprofiles"`, and then anywhere you use `$PROFILE_DIR` in the configuration it will be resolved appropriately. -The `flows` section configures how to access the Flows database - this is only used if Skypro is configured to pull data from the Flows database. +The `flows` section configures how to access the Flows database - this is only used if Skypro is configured to pull data from the Flows database (e.g., plot meter readings). + +The `flux` section (added in v2.0.0) configures how to access the Flux database - this is used for meter readings, BESS readings, and market data. The optional `schema` field defaults to "flux". For legacy databases where all data is in the `flows` schema, set `"schema": "flows"`. The `rates` section configures how to access the Rates database - this is only used if Skypro is configured to pull data from a Rates database. diff --git a/pyproject.toml b/pyproject.toml index 52c26d9..036befd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "skypro" -version = "1.2.0" +version = "2.0.0a1" description = "Skyprospector by Cepro" authors = ["damonrand "] license = "AGPL-3.0" diff --git a/src/skypro/commands/report/main.py b/src/skypro/commands/report/main.py index f857381..30d8e98 100644 --- a/src/skypro/commands/report/main.py +++ b/src/skypro/commands/report/main.py @@ -4,6 +4,20 @@ from datetime import timedelta, datetime from typing import Optional, Callable, List, Dict + +def get_db_url(env_var_name: str, env_config: dict, config_key: str) -> str: + """Get DB URL from environment variable, falling back to env.json config.""" + url = os.environ.get(env_var_name) + if url: + return url + return env_config[config_key]["dbUrl"] + + +def get_schema_name(env_config: dict, config_key: str, default: str) -> str: + """Get schema name from env config, with a default fallback.""" + config_section = env_config.get(config_key, {}) + return config_section.get("schema", default) + import numpy as np import pandas as pd @@ -104,8 +118,11 @@ def file_path_resolver_func(file: str) -> str: # Run the actual reporting logic result = report( config=config, - flows_db_url=env_config["flows"]["dbUrl"], - rates_db_url=env_config["rates"]["dbUrl"], + flows_db_url=get_db_url("FLOWS_DB_URL", env_config, "flows"), + rates_db_url=get_db_url("RATES_DB_URL", env_config, "rates"), + flux_db_url=get_db_url("FLUX_DB_URL", env_config, "flux"), + flows_schema=get_schema_name(env_config, "flows", default="flows"), + flux_schema=get_schema_name(env_config, "flux", default="flux"), start=start, end=end, step_size=step_size, @@ -179,6 +196,9 @@ def report( config: Config, flows_db_url: str, rates_db_url: str, + flux_db_url: str, + flows_schema: str, + flux_schema: str, start: datetime, end: datetime, step_size: timedelta, @@ -201,12 +221,14 @@ def report( file_path_resolver_func=file_path_resolver_func, flows_db_engine=flows_db_url, rates_db_engine=rates_db_url, + flux_db_engine=flux_db_url, + flux_schema=flux_schema, ) notices.extend(new_notices) _log_rates_to_screen(rates, time_index) - readings, new_notices = get_readings(config, time_index, flows_db_url, file_path_resolver_func) + readings, new_notices = get_readings(config, time_index, flows_db_url, flux_db_url, flows_schema, flux_schema, file_path_resolver_func) notices.extend(new_notices) df, new_notices = calc_flows( diff --git a/src/skypro/commands/report/rates.py b/src/skypro/commands/report/rates.py index 3c3a612..09bd932 100644 --- a/src/skypro/commands/report/rates.py +++ b/src/skypro/commands/report/rates.py @@ -32,6 +32,8 @@ def get_rates_from_config( file_path_resolver_func: Callable, flows_db_engine, rates_db_engine, + flux_db_engine, + flux_schema: str = "flux", ) -> Tuple[ParsedRates, List[Notice]]: """ This reads the rates configuration block and returns the ParsedRates, and a list of Notices if there are issues with data quality. @@ -46,7 +48,8 @@ def get_rates_from_config( start=time_index[0], end=time_index[-1], file_path_resolver_func=file_path_resolver_func, - db_engine=flows_db_engine, + db_engine=flux_db_engine, # Market data is in flux schema + schema=flux_schema, context="elexon imbalance data" ) notices.extend(new_notices) diff --git a/src/skypro/commands/report/readings.py b/src/skypro/commands/report/readings.py index 736a188..972a8f6 100644 --- a/src/skypro/commands/report/readings.py +++ b/src/skypro/commands/report/readings.py @@ -25,7 +25,7 @@ class AllReadings: plot_meter: pd.DataFrame -def get_readings(config, time_index, flows_db_url, file_path_resolver_func) -> Tuple[AllReadings, List[Notice]]: +def get_readings(config, time_index, flows_db_url, flux_db_url, flows_schema, flux_schema, file_path_resolver_func) -> Tuple[AllReadings, List[Notice]]: """ Pulls in the various meter and BESS readings (these will be resolved either from DB or local CSV files) """ @@ -39,7 +39,8 @@ def _get_meter_readings_wrapped(source: MeterReadingDataSource, context: str) -> start=time_index[0], end=time_index[-1], file_path_resolver_func=file_path_resolver_func, - db_engine=flows_db_url, + db_engine=flux_db_url, # Meter readings function is in flux schema + schema=flux_schema, context=context ) notices.extend(new_notices_2) @@ -61,6 +62,7 @@ def _get_meter_readings_wrapped(source: MeterReadingDataSource, context: str) -> end=time_index[-1], file_path_resolver_func=file_path_resolver_func, db_engine=flows_db_url, + schema=flows_schema, context="plot meter readings" ) notices.extend(new_notices) @@ -71,7 +73,8 @@ def _get_meter_readings_wrapped(source: MeterReadingDataSource, context: str) -> start=time_index[0], end=time_index[-1], file_path_resolver_func=file_path_resolver_func, - db_engine=flows_db_url, + db_engine=flux_db_url, # BESS readings are in flux schema + schema=flux_schema, context="bess readings" ) notices.extend(new_notices) diff --git a/src/skypro/common/data/get_bess_readings.py b/src/skypro/common/data/get_bess_readings.py index 33196cd..6295ff2 100644 --- a/src/skypro/common/data/get_bess_readings.py +++ b/src/skypro/common/data/get_bess_readings.py @@ -15,7 +15,8 @@ def get_bess_readings( end: Optional[datetime], file_path_resolver_func: Optional[Callable], db_engine: Optional, - context: Optional[str], + schema: str = "flux", + context: Optional[str] = None, ) -> Tuple[pd.DataFrame, List[Notice]]: """ This reads a data source - either CSVs from disk or directly from a database - and returns BESS readings in a dataframe alongside a list of warnings. @@ -33,7 +34,8 @@ def get_bess_readings( source=source.flows_bess_readings_data_source, start=start, end=end, - db_engine=db_engine + db_engine=db_engine, + schema=schema ) elif source.csv_bess_readings_data_source: df = _get_csv_bess_readings( @@ -52,7 +54,8 @@ def _get_flows_bess_readings( source: FlowsBessReadingsDataSource, start: datetime, end: datetime, - db_engine + db_engine, + schema: str = "flux" ) -> pd.DataFrame: """ Pulls readings about the identified BESS from the mg_bess_readings table. @@ -60,7 +63,7 @@ def _get_flows_bess_readings( query = ( f"SELECT time_b, device_id, soe_avg, target_power_avg " - "FROM flows.mg_bess_readings_30m WHERE " + f"FROM {schema}.mg_bess_readings_30m WHERE " f"time_b >= '{start.isoformat()}' AND " f"time_b < '{end.isoformat()}' AND " f"device_id = '{source.bess_id}' " diff --git a/src/skypro/common/data/get_meter_readings.py b/src/skypro/common/data/get_meter_readings.py index f971c15..4e85fad 100644 --- a/src/skypro/common/data/get_meter_readings.py +++ b/src/skypro/common/data/get_meter_readings.py @@ -15,7 +15,8 @@ def get_meter_readings( end: Optional[datetime], file_path_resolver_func: Optional[Callable], db_engine: Optional, - context: Optional[str], + schema: str = "flux", + context: Optional[str] = None, ) -> Tuple[pd.DataFrame, List[Notice]]: """ This reads a data source - either CSVs from disk or directly from a database - and returns meter readings in a dataframe alongside a list of warnings. @@ -33,7 +34,8 @@ def get_meter_readings( source=source.flows_meter_readings_data_source, start=start, end=end, - db_engine=db_engine + db_engine=db_engine, + schema=schema ) elif source.csv_meter_readings_data_source: df = _get_csv_meter_readings( @@ -53,6 +55,7 @@ def _get_flows_meter_readings( start: datetime, end: datetime, db_engine, + schema: str = "flux", ) -> pd.DataFrame: """ Pulls readings about the identified meter from the mg_meter_readings table. @@ -60,7 +63,7 @@ def _get_flows_meter_readings( query = ( f"SELECT time_b, device_id, energy_imported_active_delta, energy_exported_active_delta, " "energy_imported_active_min, energy_exported_active_min " - f"FROM get_meter_readings_5m(start_time => '{start.isoformat()}'::timestamptz, end_time => '{end.isoformat()}'::timestamptz, device_ids => ARRAY['{source.meter_id}'::uuid]) " + f"FROM {schema}.get_meter_readings_5m(start_time => '{start.isoformat()}'::timestamptz, end_time => '{end.isoformat()}'::timestamptz, device_ids => ARRAY['{source.meter_id}'::uuid]) " f"order by time_b" ) df = pd.read_sql(query, con=db_engine) diff --git a/src/skypro/common/data/get_plot_meter_readings.py b/src/skypro/common/data/get_plot_meter_readings.py index e963d30..b8e25de 100644 --- a/src/skypro/common/data/get_plot_meter_readings.py +++ b/src/skypro/common/data/get_plot_meter_readings.py @@ -16,7 +16,8 @@ def get_plot_meter_readings( end: Optional[datetime], file_path_resolver_func: Optional[Callable], db_engine: Optional, - context: Optional[str], + schema: str = "flows", + context: Optional[str] = None, ) -> Tuple[pd.DataFrame, List[Notice]]: """ This reads a data source - either CSVs from disk or directly from a database - and returns plot-level meter readings in a dataframe alongside a list of warnings. @@ -34,7 +35,8 @@ def get_plot_meter_readings( source=source.flows_plot_meter_readings_data_source, start=start, end=end, - db_engine=db_engine + db_engine=db_engine, + schema=schema ) elif source.csv_plot_meter_readings_data_source: df = _get_csv_plot_meter_readings( @@ -53,7 +55,8 @@ def _get_flows_plot_meter_readings( source: FlowsPlotMeterReadingsDataSource, start: datetime, end: datetime, - db_engine + db_engine, + schema: str = "flows", ) -> pd.DataFrame: """ Reads Emlite plot meter readings from the flows database that are on the given feeders. @@ -68,12 +71,12 @@ def _get_flows_plot_meter_readings( "mr.register_id as register_id, " "mr2.nature as nature, " "rih.kwh AS kwh " - "FROM flows.register_interval_hh rih " - "JOIN flows.meter_registers mr ON mr.register_id = rih.register_id " - "JOIN flows.meter_registers mr2 on mr.register_id = mr2.register_id " - "JOIN flows.service_head_meter shm on shm.meter = mr2.meter_id " - "JOIN flows.service_head_registry shr on shr.id = shm.service_head " - "JOIN flows.feeder_registry fr on fr.id = shr.feeder " + f"FROM {schema}.register_interval_hh rih " + f"JOIN {schema}.meter_registers mr ON mr.register_id = rih.register_id " + f"JOIN {schema}.meter_registers mr2 on mr.register_id = mr2.register_id " + f"JOIN {schema}.service_head_meter shm on shm.meter = mr2.meter_id " + f"JOIN {schema}.service_head_registry shr on shr.id = shm.service_head " + f"JOIN {schema}.feeder_registry fr on fr.id = shr.feeder " f"WHERE rih.timestamp >= '{start.isoformat()}' " f"AND rih.timestamp < '{end.isoformat()}' " f"AND fr.id = ANY(ARRAY[{feeder_id_list_str}]) " diff --git a/src/skypro/common/data/get_timeseries.py b/src/skypro/common/data/get_timeseries.py index 3b4ffed..b0d2b66 100644 --- a/src/skypro/common/data/get_timeseries.py +++ b/src/skypro/common/data/get_timeseries.py @@ -14,7 +14,8 @@ def get_timeseries( end: Optional[datetime], file_path_resolver_func: Optional[Callable], db_engine: Optional, - context: Optional[str], + schema: str = "flux", + context: Optional[str] = None, ) -> Tuple[pd.DataFrame, List[Notice]]: """ This reads a data source - either from the flows database or CSV files - and returns a generic time series @@ -25,7 +26,8 @@ def get_timeseries( source=source.flows_market_data_source, start=start, end=end, - db_engine=db_engine + db_engine=db_engine, + schema=schema ) elif source.csv_timeseries_data_source: df = _get_csv_timeseries( @@ -44,7 +46,8 @@ def _get_flows_market_data( source: FlowsMarketDataSource, start: datetime, end: datetime, - db_engine + db_engine, + schema: str = "flux" ) -> pd.DataFrame: """ Reads data of the given type from the market_data flows table. The returned dataframe will have columns for: @@ -64,8 +67,8 @@ def _get_flows_market_data( # If the data is 'predictive' then we need to pull not just the latest values, but all the updates that happened # along the way. query = ( - "SELECT time, created_at, value FROM flows.market_data " - "JOIN flows.market_data_types on market_data.type = market_data_types.id " + f"SELECT time, created_at, value FROM {schema}.market_data " + f"JOIN {schema}.market_data_types on market_data.type = market_data_types.id " f"WHERE " f" time >= '{start.isoformat()}' AND " f" time <= '{end.isoformat()}' AND " @@ -77,8 +80,8 @@ def _get_flows_market_data( # SELECT DISTINCT ON clause. query = ( " WITH data AS ( " - " SELECT time, created_at, value FROM flows.market_data " - " JOIN flows.market_data_types on market_data.type = market_data_types.id " + f" SELECT time, created_at, value FROM {schema}.market_data " + f" JOIN {schema}.market_data_types on market_data.type = market_data_types.id " f" WHERE " f" time >= '{start.isoformat()}' AND " f" time <= '{end.isoformat()}' AND " diff --git a/src/tests/integration/fixtures/env.json b/src/tests/integration/fixtures/env.json index ba5be1e..3988eec 100644 --- a/src/tests/integration/fixtures/env.json +++ b/src/tests/integration/fixtures/env.json @@ -4,6 +4,10 @@ "flows": { "dbUrl": "postgresql://username:password@someserver.com:5687/database" }, + "flux": { + "dbUrl": "postgresql://username:password@someserver.com:5687/database", + "schema": "flows" + }, "rates": { "dbUrl": "postgresql://username:password@someserver.com:5687/database" } From 4e611fdceebf4057678d3dee75bf6bcf6055692c Mon Sep 17 00:00:00 2001 From: Damon Rand Date: Tue, 6 Jan 2026 03:57:03 +0000 Subject: [PATCH 2/3] Bump version to 2.0.0 for PyPI release - Changed version from 2.0.0a1 to 2.0.0 - Published to main PyPI --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 036befd..876b2d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "skypro" -version = "2.0.0a1" +version = "2.0.0" description = "Skyprospector by Cepro" authors = ["damonrand "] license = "AGPL-3.0" From 2471bfdde05bc5063231fd4947b02b73f87b4cca Mon Sep 17 00:00:00 2001 From: Damon Rand Date: Fri, 9 Jan 2026 04:23:57 +0000 Subject: [PATCH 3/3] Fix lint: move helper functions after imports Move get_db_url and get_schema_name functions after all imports to fix E402 "module level import not at top of file" errors. --- src/skypro/commands/report/main.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/skypro/commands/report/main.py b/src/skypro/commands/report/main.py index 30d8e98..999135e 100644 --- a/src/skypro/commands/report/main.py +++ b/src/skypro/commands/report/main.py @@ -4,20 +4,6 @@ from datetime import timedelta, datetime from typing import Optional, Callable, List, Dict - -def get_db_url(env_var_name: str, env_config: dict, config_key: str) -> str: - """Get DB URL from environment variable, falling back to env.json config.""" - url = os.environ.get(env_var_name) - if url: - return url - return env_config[config_key]["dbUrl"] - - -def get_schema_name(env_config: dict, config_key: str, default: str) -> str: - """Get schema name from env config, with a default fallback.""" - config_section = env_config.get(config_key, {}) - return config_section.get("schema", default) - import numpy as np import pandas as pd @@ -47,6 +33,20 @@ def get_schema_name(env_config: dict, config_key: str, default: str) -> str: TIMEZONE_STR = "Europe/London" +def get_db_url(env_var_name: str, env_config: dict, config_key: str) -> str: + """Get DB URL from environment variable, falling back to env.json config.""" + url = os.environ.get(env_var_name) + if url: + return url + return env_config[config_key]["dbUrl"] + + +def get_schema_name(env_config: dict, config_key: str, default: str) -> str: + """Get schema name from env config, with a default fallback.""" + config_section = env_config.get(config_key, {}) + return config_section.get("schema", default) + + @dataclass class Report: """