mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Compare commits
17 commits
0192004bd7
...
be6744492f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be6744492f | ||
|
|
133190f093 | ||
|
|
b64ce2c7fe | ||
|
|
639f46df09 | ||
|
|
047cf031a5 | ||
|
|
f4f8960a18 | ||
|
|
33a237104c | ||
|
|
a1dba64e51 | ||
|
|
dd255a0de6 | ||
|
|
b20d8145f3 | ||
|
|
9cb02ebad2 | ||
|
|
36f5ad6532 | ||
|
|
2afbf05ce0 | ||
|
|
6e7a5ab863 | ||
|
|
38d50b8da9 | ||
|
|
af01f7e5b0 | ||
|
|
d33ec39aea |
13 changed files with 593 additions and 213 deletions
|
|
@ -21,6 +21,7 @@ ipykernel>=6.25,<7
|
|||
dotenv
|
||||
psycopg[binary]
|
||||
pytest-postgresql
|
||||
httpx
|
||||
# Formatting
|
||||
black==26.1.0
|
||||
boto3-stubs
|
||||
6
.github/workflows/deploy_fastapi_backend.yml
vendored
6
.github/workflows/deploy_fastapi_backend.yml
vendored
|
|
@ -77,6 +77,9 @@ jobs:
|
|||
echo "::set-output name=hotwater_kwh_predictions_bucket::${{ secrets[format('{0}_HOTWATER_KWH_PREDICTIONS_BUCKET', github.ref_name)] }}"
|
||||
echo "::set-output name=energy_asessments_bucket::${{ secrets[format('{0}_ENERGY_ASSESSMENTS_BUCKET', github.ref_name)] }}"
|
||||
echo "::set-output name=google_solar_api_key::${{ secrets[format('{0}_GOOGLE_SOLAR_API_KEY', github.ref_name)] }}"
|
||||
echo "::set-output name=sap_baseline_predictions_bucket::${{ secrets[format('{0}_SAP_BASELINE_PREDICTIONS_BUCKET', github.ref_name)] }}"
|
||||
echo "::set-output name=carbon_baseline_predictions_bucket::${{ secrets[format('{0}_CARBON_BASELINE_PREDICTIONS_BUCKET', github.ref_name)] }}"
|
||||
echo "::set-output name=heat_baseline_predictions_bucket::${{ secrets[format('{0}_HEAT_BASELINE_PREDICTIONS_BUCKET', github.ref_name)] }}"
|
||||
|
||||
- name: Setup Docker
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
|
@ -129,6 +132,9 @@ jobs:
|
|||
DB_NAME: ${{ steps.set_db_credentials.outputs.db_name }}
|
||||
ECR_URI: ${{ steps.set_ecr_credentials.outputs.ecr_uri }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
SAP_BASELINE_PREDICTIONS_BUCKET: ${{ steps.set_api_secrets.outputs.sap_baseline_predictions_bucket }}
|
||||
CARBON_BASELINE_PREDICTIONS_BUCKET: ${{ steps.set_api_secrets.outputs.carbon_baseline_predictions_bucket }}
|
||||
HEAT_BASELINE_PREDICTIONS_BUCKET: ${{ steps.set_api_secrets.outputs.heat_baseline_predictions_bucket }}
|
||||
run: |
|
||||
# Fetch database credentials from AWS Secrets Manager
|
||||
SECRET_VALUE=$(aws secretsmanager get-secret-value --secret-id ${{ github.ref_name }}/assessment_model/db_credentials --query SecretString)
|
||||
|
|
|
|||
1
.idea/Model.iml
generated
1
.idea/Model.iml
generated
|
|
@ -6,6 +6,7 @@
|
|||
<sourceFolder url="file://$MODULE_DIR$/model_data" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/infrastructure/terraform/.terraform" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Fastapi-backend" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from sqlalchemy import create_engine
|
||||
from contextlib import contextmanager
|
||||
from backend.app.config import get_settings
|
||||
from sqlmodel import Session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
connection_string = (
|
||||
"postgresql+{drivername}://{username}:{password}@{server}:{port}/{dbname}"
|
||||
|
|
@ -56,3 +56,8 @@ def db_read_session():
|
|||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def get_session():
|
||||
with db_session() as session:
|
||||
yield session
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Dict
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from uuid import UUID
|
||||
import json # ← REQUIRED for json.loads
|
||||
|
|
@ -15,9 +17,10 @@ from backend.app.tasks.schema import (
|
|||
# Correct location of interfaces
|
||||
from backend.app.db.functions.tasks.Tasks import TasksInterface, SubTaskInterface
|
||||
|
||||
from backend.app.db.connection import get_db_session
|
||||
from backend.app.db.models.tasks import Task, SubTask
|
||||
from sqlmodel import select
|
||||
from backend.app.db.connection import get_db_session, get_session
|
||||
from backend.app.db.models.tasks import SourceEnum, Task, SubTask
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
|
|
@ -70,6 +73,37 @@ async def get_task(task_id: UUID):
|
|||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/by-source/{source}/{source_id}/{service}",
|
||||
summary="Get the most recent task by source, source_id, and service",
|
||||
)
|
||||
async def get_task_by_source(
|
||||
source: SourceEnum,
|
||||
source_id: str,
|
||||
service: str,
|
||||
session: Session = Depends(get_session),
|
||||
) -> Dict[str, Task]:
|
||||
task = (
|
||||
session.execute(
|
||||
select(Task)
|
||||
.where(
|
||||
Task.source == source,
|
||||
Task.source_id == source_id,
|
||||
Task.service == service,
|
||||
)
|
||||
.order_by(Task.job_started.desc())
|
||||
.limit(1)
|
||||
)
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
return {"task": task}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Update Task Status
|
||||
# ============================================================
|
||||
|
|
|
|||
96
backend/app/tests/tasks/test_get_task.py
Normal file
96
backend/app/tests/tasks/test_get_task.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.db.connection import get_session
|
||||
from backend.app.dependencies import validate_token
|
||||
from backend.app.db.models.tasks import SourceEnum, Task
|
||||
from backend.app.tasks.router import router
|
||||
|
||||
|
||||
def test_get_task_by_source(db_session: Session) -> None:
|
||||
# arrange
|
||||
current_categorisation_task = Task(
|
||||
id=uuid.uuid4(),
|
||||
task_source="",
|
||||
job_started=datetime(2026, 1, 1, 9, 0, 0),
|
||||
job_completed=None,
|
||||
status="in progress",
|
||||
service="plan_categorisation",
|
||||
updated_at=datetime(2026, 1, 1, 9, 0, 0),
|
||||
source=SourceEnum.PORTFOLIO,
|
||||
source_id="100",
|
||||
)
|
||||
|
||||
previous_categorisation_task = Task(
|
||||
id=uuid.uuid4(),
|
||||
task_source="",
|
||||
job_started=datetime(2025, 12, 31, 9, 0, 0),
|
||||
job_completed=datetime(2025, 12, 31, 9, 10, 0),
|
||||
status="complete",
|
||||
service="plan_categorisation",
|
||||
updated_at=datetime(2025, 12, 31, 9, 10, 0),
|
||||
source=SourceEnum.PORTFOLIO,
|
||||
source_id="100",
|
||||
)
|
||||
|
||||
other_portfolio_categorisation_task = Task(
|
||||
id=uuid.uuid4(),
|
||||
task_source="",
|
||||
job_started=datetime(2026, 1, 1, 9, 0, 0),
|
||||
job_completed=None,
|
||||
status="in progress",
|
||||
service="plan_categorisation",
|
||||
updated_at=datetime(2026, 1, 1, 9, 0, 0),
|
||||
source=SourceEnum.PORTFOLIO,
|
||||
source_id="101",
|
||||
)
|
||||
|
||||
engine_task = Task(
|
||||
id=uuid.uuid4(),
|
||||
task_source="",
|
||||
job_started=datetime(2026, 1, 1, 9, 0, 0),
|
||||
job_completed=None,
|
||||
status="in progress",
|
||||
service="plan_engine",
|
||||
updated_at=datetime(2026, 1, 1, 9, 0, 0),
|
||||
source=SourceEnum.PORTFOLIO,
|
||||
source_id="100",
|
||||
)
|
||||
|
||||
db_session.add_all(
|
||||
[
|
||||
current_categorisation_task,
|
||||
previous_categorisation_task,
|
||||
other_portfolio_categorisation_task,
|
||||
engine_task,
|
||||
]
|
||||
)
|
||||
db_session.commit()
|
||||
# db_session.flush()
|
||||
|
||||
# debug: confirm data is visible in this session
|
||||
all_tasks = db_session.execute(select(Task)).scalars().all()
|
||||
print(f"Tasks in db: {[(t.service, t.source_id, t.source) for t in all_tasks]}")
|
||||
|
||||
# act
|
||||
test_app = FastAPI()
|
||||
test_app.include_router(router)
|
||||
|
||||
def override_get_session():
|
||||
yield db_session
|
||||
|
||||
test_app.dependency_overrides[get_session] = override_get_session
|
||||
test_app.dependency_overrides[validate_token] = lambda: None
|
||||
|
||||
client = TestClient(test_app)
|
||||
response = client.get("/tasks/by-source/portfolio_id/100/plan_categorisation")
|
||||
|
||||
test_app.dependency_overrides.clear()
|
||||
|
||||
# assert
|
||||
assert response.status_code == 200
|
||||
assert response.json()["task"]["id"] == str(current_categorisation_task.id)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlmodel import SQLModel
|
||||
from backend.app.db.base import Base
|
||||
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ def engine(postgresql):
|
|||
|
||||
# Create tables once per test session
|
||||
Base.metadata.create_all(engine)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
# Yeild will split this function into two phase. 1) setup and 2) teardown, the latter of which will run after all
|
||||
# tests have completed
|
||||
|
|
@ -3,12 +3,25 @@ import numpy as np
|
|||
from pathlib import Path
|
||||
import time
|
||||
|
||||
from sqlmodel import Session
|
||||
|
||||
from backend.export.property_scenarios.main import process_export
|
||||
from backend.export.property_scenarios.input_schema import ExportRequest
|
||||
from backend.app.db.models.portfolio import PropertyModel, Epc, Portfolio, PortfolioStatus, PortfolioGoal, \
|
||||
PropertyCreationStatus, PropertyDetailsEpcModel
|
||||
from backend.app.db.models.recommendations import PlanModel, Recommendation, PlanRecommendations, \
|
||||
RecommendationMaterials
|
||||
from backend.app.db.models.portfolio import (
|
||||
PropertyModel,
|
||||
Epc,
|
||||
Portfolio,
|
||||
PortfolioStatus,
|
||||
PortfolioGoal,
|
||||
PropertyCreationStatus,
|
||||
PropertyDetailsEpcModel,
|
||||
)
|
||||
from backend.app.db.models.recommendations import (
|
||||
PlanModel,
|
||||
Recommendation,
|
||||
PlanRecommendations,
|
||||
RecommendationMaterials,
|
||||
)
|
||||
from backend.app.db.models.materials import Material
|
||||
from utils.logger import setup_logger
|
||||
|
||||
|
|
@ -22,7 +35,7 @@ def load_csv(name: str) -> pd.DataFrame:
|
|||
return df
|
||||
|
||||
|
||||
def test_default_export_integration(db_session):
|
||||
def test_default_export_integration(db_session: Session):
|
||||
# ----------------------------------------
|
||||
# 1) Load csvs
|
||||
# ----------------------------------------
|
||||
|
|
@ -78,11 +91,13 @@ def test_default_export_integration(db_session):
|
|||
else None
|
||||
)
|
||||
|
||||
prop = PropertyModel(**{
|
||||
col: row_dict[col]
|
||||
for col in PropertyModel.__table__.columns.keys()
|
||||
if col in row_dict
|
||||
})
|
||||
prop = PropertyModel(
|
||||
**{
|
||||
col: row_dict[col]
|
||||
for col in PropertyModel.__table__.columns.keys()
|
||||
if col in row_dict
|
||||
}
|
||||
)
|
||||
|
||||
prop.creation_status = PropertyCreationStatus[
|
||||
row_dict["creation_status"].split(".")[-1]
|
||||
|
|
@ -90,9 +105,7 @@ def test_default_export_integration(db_session):
|
|||
prop.status = PortfolioStatus[row_dict["status"].split(".")[-1]]
|
||||
|
||||
if row_dict.get("current_epc_rating"):
|
||||
prop.current_epc_rating = Epc[
|
||||
row_dict["current_epc_rating"].split(".")[-1]
|
||||
]
|
||||
prop.current_epc_rating = Epc[row_dict["current_epc_rating"].split(".")[-1]]
|
||||
|
||||
properties.append(prop)
|
||||
|
||||
|
|
@ -112,7 +125,8 @@ def test_default_export_integration(db_session):
|
|||
epc_data = {
|
||||
col.name: row_dict[col.name]
|
||||
for col in PropertyDetailsEpcModel.__table__.columns.values()
|
||||
if col.name in row_dict and col.name not in ["id", "property_id", "portfolio_id"]
|
||||
if col.name in row_dict
|
||||
and col.name not in ["id", "property_id", "portfolio_id"]
|
||||
}
|
||||
|
||||
epc = PropertyDetailsEpcModel(
|
||||
|
|
@ -142,11 +156,13 @@ def test_default_export_integration(db_session):
|
|||
|
||||
row_dict["scenario_id"] = None
|
||||
|
||||
plan = PlanModel(**{
|
||||
col: row_dict[col]
|
||||
for col in PlanModel.__table__.columns.keys()
|
||||
if col in row_dict
|
||||
})
|
||||
plan = PlanModel(
|
||||
**{
|
||||
col: row_dict[col]
|
||||
for col in PlanModel.__table__.columns.keys()
|
||||
if col in row_dict
|
||||
}
|
||||
)
|
||||
|
||||
plans.append(plan)
|
||||
|
||||
|
|
@ -158,11 +174,13 @@ def test_default_export_integration(db_session):
|
|||
# ----------------------------------------
|
||||
|
||||
recs = [
|
||||
Recommendation(**{
|
||||
col: row[col]
|
||||
for col in Recommendation.__table__.columns.keys()
|
||||
if col in row
|
||||
})
|
||||
Recommendation(
|
||||
**{
|
||||
col: row[col]
|
||||
for col in Recommendation.__table__.columns.keys()
|
||||
if col in row
|
||||
}
|
||||
)
|
||||
for _, row in recommendations_df.iterrows()
|
||||
]
|
||||
|
||||
|
|
@ -203,28 +221,19 @@ def test_default_export_integration(db_session):
|
|||
# ----------------------------------------
|
||||
|
||||
logger.info(
|
||||
"Recommendation count in DB: %s",
|
||||
db_session.query(Recommendation).count()
|
||||
"Recommendation count in DB: %s", db_session.query(Recommendation).count()
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Property count in DB: %s",
|
||||
db_session.query(PropertyModel).count()
|
||||
)
|
||||
logger.info("Property count in DB: %s", db_session.query(PropertyModel).count())
|
||||
|
||||
logger.info(
|
||||
"Property EPC in DB: %s",
|
||||
db_session.query(PropertyDetailsEpcModel).count()
|
||||
"Property EPC in DB: %s", db_session.query(PropertyDetailsEpcModel).count()
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Plan count in DB: %s",
|
||||
db_session.query(PlanModel).count()
|
||||
)
|
||||
logger.info("Plan count in DB: %s", db_session.query(PlanModel).count())
|
||||
|
||||
logger.info(
|
||||
"PlanRecommendatons count in DB: %s",
|
||||
db_session.query(PlanModel).count()
|
||||
"PlanRecommendatons count in DB: %s", db_session.query(PlanModel).count()
|
||||
)
|
||||
|
||||
logger.info("Starting process_export")
|
||||
|
|
@ -232,17 +241,23 @@ def test_default_export_integration(db_session):
|
|||
|
||||
result = process_export(payload, session=db_session)
|
||||
|
||||
logger.info("process_export finished in %.2f seconds", time.perf_counter() - process_t0)
|
||||
logger.info(
|
||||
"process_export finished in %.2f seconds", time.perf_counter() - process_t0
|
||||
)
|
||||
|
||||
# ----------------------------------------
|
||||
# 8) Assertions
|
||||
# ----------------------------------------
|
||||
|
||||
assert "default_plans" in result, "Expected 'default_plans' in export result, got {}".format(result.keys())
|
||||
assert (
|
||||
"default_plans" in result
|
||||
), "Expected 'default_plans' in export result, got {}".format(result.keys())
|
||||
|
||||
df = result["default_plans"]
|
||||
|
||||
assert df.shape[0] == 10, "Expected 10 properties in the export, got {}".format(df.shape[0])
|
||||
assert df.shape[0] == 10, "Expected 10 properties in the export, got {}".format(
|
||||
df.shape[0]
|
||||
)
|
||||
|
||||
failed = df[df["predicted_post_works_sap"] < 69]
|
||||
failed_property_types = failed["property_type"].value_counts().to_dict()
|
||||
|
|
@ -251,19 +266,28 @@ def test_default_export_integration(db_session):
|
|||
|
||||
assert failed.shape[0]
|
||||
|
||||
assert df["total_retrofit_cost"].sum() == 41706.585999999996, (
|
||||
"Expected total retrofit cost to be 10000, got {}".format(df["total_retrofit_cost"].sum())
|
||||
assert (
|
||||
df["total_retrofit_cost"].sum() == 41706.585999999996
|
||||
), "Expected total retrofit cost to be 10000, got {}".format(
|
||||
df["total_retrofit_cost"].sum()
|
||||
)
|
||||
|
||||
assert df["predicted_post_works_sap"].sum() == 698.1, (
|
||||
"Expected total predicted post works SAP to be 698.1, got {}".format(df["predicted_post_works_sap"].sum())
|
||||
assert (
|
||||
df["predicted_post_works_sap"].sum() == 698.1
|
||||
), "Expected total predicted post works SAP to be 698.1, got {}".format(
|
||||
df["predicted_post_works_sap"].sum()
|
||||
)
|
||||
|
||||
assert df["sap_points"].sum() == 100.10000000000001, (
|
||||
"Expected total SAP points increase to be 100.10000000000001, got {}".format(df["sap_points"].sum())
|
||||
assert (
|
||||
df["sap_points"].sum() == 100.10000000000001
|
||||
), "Expected total SAP points increase to be 100.10000000000001, got {}".format(
|
||||
df["sap_points"].sum()
|
||||
)
|
||||
|
||||
assert df.shape == (10, 95), "Expected dataframe shape to be (10, 11), got {}".format(df.shape)
|
||||
assert df.shape == (
|
||||
10,
|
||||
95,
|
||||
), "Expected dataframe shape to be (10, 11), got {}".format(df.shape)
|
||||
|
||||
|
||||
def test_solar_with_battery_example(db_session):
|
||||
|
|
@ -271,116 +295,251 @@ def test_solar_with_battery_example(db_session):
|
|||
test_property_id = 1
|
||||
|
||||
portfolio_df = pd.DataFrame(
|
||||
[{'id': test_portfolio_id, 'name': 'Example', 'budget': None,
|
||||
'status': 'PortfolioStatus.SCOPING', 'goal': 'PortfolioGoal.NONE', 'cost': None, 'number_of_properties': None,
|
||||
'co2_equivalent_savings': None, 'energy_savings': None, 'energy_cost_savings': None,
|
||||
'property_valuation_increase': None, 'rental_yield_increase': None, 'total_work_hours': None,
|
||||
'labour_days': None, 'created_at': '2026-02-12 21:23:37.862000+00:00',
|
||||
'updated_at': '2026-02-12 21:23:37.862000+00:00', 'epc_breakdown_pre_retrofit': None,
|
||||
'epc_breakdown_post_retrofit': None, 'n_units_to_retrofit': None, 'co2_per_unit_pre_retrofit': None,
|
||||
'co2_per_unit_post_retrofit': None, 'energy_bill_per_unit_pre_retrofit': None,
|
||||
'energy_bill_per_unit_post_retrofit': None, 'energy_consumption_per_unit_pre_retrofit': None,
|
||||
'energy_consumption_per_unit_post_retrofit': None, 'valuation_improvement_per_unit': None,
|
||||
'cost_per_unit': None, 'cost_per_co2_saved': None, 'cost_per_sap_point': None,
|
||||
'valuation_return_on_investment': None}]
|
||||
[
|
||||
{
|
||||
"id": test_portfolio_id,
|
||||
"name": "Example",
|
||||
"budget": None,
|
||||
"status": "PortfolioStatus.SCOPING",
|
||||
"goal": "PortfolioGoal.NONE",
|
||||
"cost": None,
|
||||
"number_of_properties": None,
|
||||
"co2_equivalent_savings": None,
|
||||
"energy_savings": None,
|
||||
"energy_cost_savings": None,
|
||||
"property_valuation_increase": None,
|
||||
"rental_yield_increase": None,
|
||||
"total_work_hours": None,
|
||||
"labour_days": None,
|
||||
"created_at": "2026-02-12 21:23:37.862000+00:00",
|
||||
"updated_at": "2026-02-12 21:23:37.862000+00:00",
|
||||
"epc_breakdown_pre_retrofit": None,
|
||||
"epc_breakdown_post_retrofit": None,
|
||||
"n_units_to_retrofit": None,
|
||||
"co2_per_unit_pre_retrofit": None,
|
||||
"co2_per_unit_post_retrofit": None,
|
||||
"energy_bill_per_unit_pre_retrofit": None,
|
||||
"energy_bill_per_unit_post_retrofit": None,
|
||||
"energy_consumption_per_unit_pre_retrofit": None,
|
||||
"energy_consumption_per_unit_post_retrofit": None,
|
||||
"valuation_improvement_per_unit": None,
|
||||
"cost_per_unit": None,
|
||||
"cost_per_co2_saved": None,
|
||||
"cost_per_sap_point": None,
|
||||
"valuation_return_on_investment": None,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
properties_df = pd.DataFrame(
|
||||
[{'id': test_property_id, 'portfolio_id': test_portfolio_id, 'creation_status': 'PropertyCreationStatus.READY',
|
||||
'uprn': 100090438731, 'landlord_property_id': 'BARR052', 'building_reference_number': 3460742868.0,
|
||||
'status': 'PortfolioStatus.ASSESSMENT', 'address': '52, Barrack Street', 'postcode': 'CO1 2LR',
|
||||
'has_pre_condition_report': True, 'has_recommendations': True, 'created_at': '2026-02-12 21:59:02.744427',
|
||||
'updated_at': '2026-02-19 16:18:57.941443', 'property_type': 'House', 'built_form': 'End-Terrace',
|
||||
'local_authority': 'Colchester', 'constituency': 'Colchester', 'number_of_rooms': 4.0, 'year_built': 1900.0,
|
||||
'tenure': 'rental (private)', 'current_epc_rating': 'Epc.E', 'current_sap_points': 53.0,
|
||||
'current_valuation': 0.0, 'installed_measures_sap_point_adjustment': 0.0,
|
||||
'is_sap_points_adjusted_for_installed_measures': False, 'original_sap_points': 53.0}]
|
||||
[
|
||||
{
|
||||
"id": test_property_id,
|
||||
"portfolio_id": test_portfolio_id,
|
||||
"creation_status": "PropertyCreationStatus.READY",
|
||||
"uprn": 100090438731,
|
||||
"landlord_property_id": "BARR052",
|
||||
"building_reference_number": 3460742868.0,
|
||||
"status": "PortfolioStatus.ASSESSMENT",
|
||||
"address": "52, Barrack Street",
|
||||
"postcode": "CO1 2LR",
|
||||
"has_pre_condition_report": True,
|
||||
"has_recommendations": True,
|
||||
"created_at": "2026-02-12 21:59:02.744427",
|
||||
"updated_at": "2026-02-19 16:18:57.941443",
|
||||
"property_type": "House",
|
||||
"built_form": "End-Terrace",
|
||||
"local_authority": "Colchester",
|
||||
"constituency": "Colchester",
|
||||
"number_of_rooms": 4.0,
|
||||
"year_built": 1900.0,
|
||||
"tenure": "rental (private)",
|
||||
"current_epc_rating": "Epc.E",
|
||||
"current_sap_points": 53.0,
|
||||
"current_valuation": 0.0,
|
||||
"installed_measures_sap_point_adjustment": 0.0,
|
||||
"is_sap_points_adjusted_for_installed_measures": False,
|
||||
"original_sap_points": 53.0,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
property_details_epc_df = pd.DataFrame(
|
||||
[
|
||||
{'id': 1534934, 'property_id': test_property_id, 'portfolio_id': test_portfolio_id,
|
||||
'full_address': '48, Medcalf Road', 'lodgement_date': '2018-09-05', 'is_expired': False,
|
||||
'total_floor_area': 68.0, 'walls': 'Solid brick, as built, no insulation', 'walls_rating': 1,
|
||||
'roof': 'Pitched, no insulation', 'roof_rating': 1.0, 'floor': 'Solid, no insulation',
|
||||
'floor_rating': None,
|
||||
'windows': 'Fully double glazed', 'windows_rating': 4, 'heating': 'Boiler and radiators, mains gas',
|
||||
'heating_rating': 4, 'heating_controls': 'Programmer, room thermostat and trvs',
|
||||
'heating_controls_rating': 4,
|
||||
'hot_water': 'From main system', 'hot_water_rating': 4,
|
||||
'lighting': 'Low energy lighting in all fixed outlets', 'lighting_rating': 5,
|
||||
'mainfuel': 'Mains gas not community', 'ventilation': 'natural', 'solar_pv': 0.0, 'solar_hot_water': False,
|
||||
'wind_turbine': 0.0, 'floor_height': 2.55, 'number_heated_rooms': None, 'heat_loss_corridor': False,
|
||||
'unheated_corridor_length': None, 'number_of_open_fireplaces': 0, 'number_of_extensions': 0,
|
||||
'number_of_storeys': None, 'mains_gas': True, 'energy_tariff': 'Single',
|
||||
'primary_energy_consumption': 278.0,
|
||||
'co2_emissions': 3.81, 'current_energy_demand': 14643.366,
|
||||
'current_energy_demand_heating_hotwater': 12185.6,
|
||||
'estimated': False, 'sap_05_overwritten': False, 'sap_05_score': None, 'sap_05_epc_rating': None,
|
||||
'heating_cost_current': 711.0628, 'hot_water_cost_current': 139.06198, 'lighting_cost_current': 70.770935,
|
||||
'appliances_cost_current': 609.7844, 'gas_standing_charge': 128.0785,
|
||||
'electricity_standing_charge': 199.8375,
|
||||
'original_co2_emissions': 3.81, 'original_primary_energy_consumption': 278.0,
|
||||
'original_current_energy_demand': 14643.366, 'original_current_energy_demand_heating_hotwater': 12185.6,
|
||||
'installed_measures_co2_adjustment': 0.0, 'installed_measures_energy_demand_adjustment': 0.0,
|
||||
'installed_measures_total_energy_bill_adjustment': 0.0, 'installed_measures_heat_demand_adjustment': 0.0,
|
||||
'is_epc_adjusted_for_installed_measures': False}
|
||||
{
|
||||
"id": 1534934,
|
||||
"property_id": test_property_id,
|
||||
"portfolio_id": test_portfolio_id,
|
||||
"full_address": "48, Medcalf Road",
|
||||
"lodgement_date": "2018-09-05",
|
||||
"is_expired": False,
|
||||
"total_floor_area": 68.0,
|
||||
"walls": "Solid brick, as built, no insulation",
|
||||
"walls_rating": 1,
|
||||
"roof": "Pitched, no insulation",
|
||||
"roof_rating": 1.0,
|
||||
"floor": "Solid, no insulation",
|
||||
"floor_rating": None,
|
||||
"windows": "Fully double glazed",
|
||||
"windows_rating": 4,
|
||||
"heating": "Boiler and radiators, mains gas",
|
||||
"heating_rating": 4,
|
||||
"heating_controls": "Programmer, room thermostat and trvs",
|
||||
"heating_controls_rating": 4,
|
||||
"hot_water": "From main system",
|
||||
"hot_water_rating": 4,
|
||||
"lighting": "Low energy lighting in all fixed outlets",
|
||||
"lighting_rating": 5,
|
||||
"mainfuel": "Mains gas not community",
|
||||
"ventilation": "natural",
|
||||
"solar_pv": 0.0,
|
||||
"solar_hot_water": False,
|
||||
"wind_turbine": 0.0,
|
||||
"floor_height": 2.55,
|
||||
"number_heated_rooms": None,
|
||||
"heat_loss_corridor": False,
|
||||
"unheated_corridor_length": None,
|
||||
"number_of_open_fireplaces": 0,
|
||||
"number_of_extensions": 0,
|
||||
"number_of_storeys": None,
|
||||
"mains_gas": True,
|
||||
"energy_tariff": "Single",
|
||||
"primary_energy_consumption": 278.0,
|
||||
"co2_emissions": 3.81,
|
||||
"current_energy_demand": 14643.366,
|
||||
"current_energy_demand_heating_hotwater": 12185.6,
|
||||
"estimated": False,
|
||||
"sap_05_overwritten": False,
|
||||
"sap_05_score": None,
|
||||
"sap_05_epc_rating": None,
|
||||
"heating_cost_current": 711.0628,
|
||||
"hot_water_cost_current": 139.06198,
|
||||
"lighting_cost_current": 70.770935,
|
||||
"appliances_cost_current": 609.7844,
|
||||
"gas_standing_charge": 128.0785,
|
||||
"electricity_standing_charge": 199.8375,
|
||||
"original_co2_emissions": 3.81,
|
||||
"original_primary_energy_consumption": 278.0,
|
||||
"original_current_energy_demand": 14643.366,
|
||||
"original_current_energy_demand_heating_hotwater": 12185.6,
|
||||
"installed_measures_co2_adjustment": 0.0,
|
||||
"installed_measures_energy_demand_adjustment": 0.0,
|
||||
"installed_measures_total_energy_bill_adjustment": 0.0,
|
||||
"installed_measures_heat_demand_adjustment": 0.0,
|
||||
"is_epc_adjusted_for_installed_measures": False,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
plans_df = pd.DataFrame(
|
||||
[
|
||||
{'id': 0, 'name': None, 'portfolio_id': test_portfolio_id, 'property_id': test_property_id,
|
||||
'scenario_id': 1060, 'created_at': '2026-02-19 16:14:45.560816', 'is_default': True,
|
||||
'valuation_increase_lower_bound': 0.0302,
|
||||
'valuation_increase_upper_bound': 0.07, 'valuation_increase_average': 0.048226666, 'plan_type': None,
|
||||
'post_sap_points': 71.5, 'post_epc_rating': 'Epc.C', 'post_co2_emissions': 4.1813498,
|
||||
'co2_savings': 0.71865046, 'post_energy_bill': 1447.5204, 'energy_bill_savings': 691.6662,
|
||||
'post_energy_consumption': 15303.688, 'energy_consumption_savings': 3276.7622,
|
||||
'valuation_post_retrofit': None, 'valuation_increase': None, 'cost_of_works': 6984.568,
|
||||
'contingency_cost': 1003.9568}
|
||||
{
|
||||
"id": 0,
|
||||
"name": None,
|
||||
"portfolio_id": test_portfolio_id,
|
||||
"property_id": test_property_id,
|
||||
"scenario_id": 1060,
|
||||
"created_at": "2026-02-19 16:14:45.560816",
|
||||
"is_default": True,
|
||||
"valuation_increase_lower_bound": 0.0302,
|
||||
"valuation_increase_upper_bound": 0.07,
|
||||
"valuation_increase_average": 0.048226666,
|
||||
"plan_type": None,
|
||||
"post_sap_points": 71.5,
|
||||
"post_epc_rating": "Epc.C",
|
||||
"post_co2_emissions": 4.1813498,
|
||||
"co2_savings": 0.71865046,
|
||||
"post_energy_bill": 1447.5204,
|
||||
"energy_bill_savings": 691.6662,
|
||||
"post_energy_consumption": 15303.688,
|
||||
"energy_consumption_savings": 3276.7622,
|
||||
"valuation_post_retrofit": None,
|
||||
"valuation_increase": None,
|
||||
"cost_of_works": 6984.568,
|
||||
"contingency_cost": 1003.9568,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
plan_recs_df = pd.DataFrame(
|
||||
[{'id': 0, 'plan_id': 0, 'recommendation_id': 0}]
|
||||
)
|
||||
plan_recs_df = pd.DataFrame([{"id": 0, "plan_id": 0, "recommendation_id": 0}])
|
||||
|
||||
recommendations_df = pd.DataFrame(
|
||||
[{'id': 0, 'property_id': test_property_id, 'created_at': '2026-02-19 16:14:45.560816',
|
||||
'type': 'solar_pv', 'measure_type': 'solar_pv',
|
||||
'description': 'Fit solar',
|
||||
'estimated_cost': 10000, 'default': True, 'starting_u_value': None, 'new_u_value': None, 'sap_points': 1.5,
|
||||
'heat_demand': 14.9, 'kwh_savings': 1041.2, 'co2_equivalent_savings': 0.2, 'energy_savings': 14.9,
|
||||
'energy_cost_savings': 72.639015, 'property_valuation_increase': None, 'rental_yield_increase': None,
|
||||
'total_work_hours': 4.16, 'labour_days': 1.0, 'already_installed': False, 'plan_name': 'whatever'}
|
||||
]
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"property_id": test_property_id,
|
||||
"created_at": "2026-02-19 16:14:45.560816",
|
||||
"type": "solar_pv",
|
||||
"measure_type": "solar_pv",
|
||||
"description": "Fit solar",
|
||||
"estimated_cost": 10000,
|
||||
"default": True,
|
||||
"starting_u_value": None,
|
||||
"new_u_value": None,
|
||||
"sap_points": 1.5,
|
||||
"heat_demand": 14.9,
|
||||
"kwh_savings": 1041.2,
|
||||
"co2_equivalent_savings": 0.2,
|
||||
"energy_savings": 14.9,
|
||||
"energy_cost_savings": 72.639015,
|
||||
"property_valuation_increase": None,
|
||||
"rental_yield_increase": None,
|
||||
"total_work_hours": 4.16,
|
||||
"labour_days": 1.0,
|
||||
"already_installed": False,
|
||||
"plan_name": "whatever",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
recommendations_materials_df = pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"id": 0, "recommendation_id": 0, "material_id": 0, "depth": None, "quantity": 1.0,
|
||||
"id": 0,
|
||||
"recommendation_id": 0,
|
||||
"material_id": 0,
|
||||
"depth": None,
|
||||
"quantity": 1.0,
|
||||
"quantity_unit": "part",
|
||||
"estimated_cost": 10000, "created_at": '2026-02-19 16:14:45.560816',
|
||||
"updated_at": '2026-02-19 16:14:45.560816',
|
||||
"estimated_cost": 10000,
|
||||
"created_at": "2026-02-19 16:14:45.560816",
|
||||
"updated_at": "2026-02-19 16:14:45.560816",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
materials_df = pd.DataFrame(
|
||||
[
|
||||
{'id': 0, 'type': 'solar_pv', 'description': 'Some solar product',
|
||||
'depth': 75.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'Test',
|
||||
'created_at': "'2026-02-19 16:14:45.560816", 'is_active': True,
|
||||
'prime_material_cost': None,
|
||||
'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.0, 'plant_cost': 0.0,
|
||||
'total_cost': 10000,
|
||||
'notes': None, 'is_installer_quote': True, 'innovation_rate': 0.25, 'size': None, 'size_unit': None,
|
||||
'includes_scaffolding': True, 'includes_battery': True, 'battery_size': 5.8}
|
||||
{
|
||||
"id": 0,
|
||||
"type": "solar_pv",
|
||||
"description": "Some solar product",
|
||||
"depth": 75.0,
|
||||
"depth_unit": "mm",
|
||||
"cost": None,
|
||||
"cost_unit": "gbp_per_m2",
|
||||
"r_value_per_mm": 0.030303031,
|
||||
"r_value_unit": "square_meter_kelvin_per_watt",
|
||||
"thermal_conductivity": 0.033,
|
||||
"thermal_conductivity_unit": "watt_per_meter_kelvin",
|
||||
"link": "Test",
|
||||
"created_at": "'2026-02-19 16:14:45.560816",
|
||||
"is_active": True,
|
||||
"prime_material_cost": None,
|
||||
"material_cost": 0.0,
|
||||
"labour_cost": 0.0,
|
||||
"labour_hours_per_unit": 0.0,
|
||||
"plant_cost": 0.0,
|
||||
"total_cost": 10000,
|
||||
"notes": None,
|
||||
"is_installer_quote": True,
|
||||
"innovation_rate": 0.25,
|
||||
"size": None,
|
||||
"size_unit": None,
|
||||
"includes_scaffolding": True,
|
||||
"includes_battery": True,
|
||||
"battery_size": 5.8,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -463,7 +622,7 @@ def test_solar_with_battery_example(db_session):
|
|||
already_installed=row.already_installed,
|
||||
sap_points=row.sap_points,
|
||||
type=row.type,
|
||||
description=row.description
|
||||
description=row.description,
|
||||
)
|
||||
db_session.add(rec)
|
||||
db_session.flush()
|
||||
|
|
@ -515,13 +674,15 @@ def test_solar_with_battery_example(db_session):
|
|||
|
||||
db_session.commit()
|
||||
|
||||
payload = ExportRequest.model_validate({
|
||||
"task_id": "test",
|
||||
"subtask_id": "test",
|
||||
"portfolio_id": test_portfolio_id,
|
||||
"scenario_ids": [],
|
||||
"default_plans_only": True,
|
||||
})
|
||||
payload = ExportRequest.model_validate(
|
||||
{
|
||||
"task_id": "test",
|
||||
"subtask_id": "test",
|
||||
"portfolio_id": test_portfolio_id,
|
||||
"scenario_ids": [],
|
||||
"default_plans_only": True,
|
||||
}
|
||||
)
|
||||
|
||||
result = process_export(payload, session=db_session)
|
||||
|
||||
|
|
@ -534,7 +695,9 @@ def test_solar_with_battery_example(db_session):
|
|||
# solar_pv should NOT exist
|
||||
assert "solar_pv" not in df.columns
|
||||
|
||||
assert df.shape[0] == 1, "Expected 1 property in the export, got {}".format(df.shape[0])
|
||||
assert df.shape[0] == 1, "Expected 1 property in the export, got {}".format(
|
||||
df.shape[0]
|
||||
)
|
||||
|
||||
# Cost should land in correct column
|
||||
assert df["solar_pv_with_battery"].iloc[0] == 10000
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ data "terraform_remote_state" "shared" {
|
|||
backend = "s3"
|
||||
config = {
|
||||
bucket = "assessment-model-terraform-state"
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ data "terraform_remote_state" "engine" {
|
|||
backend = "s3"
|
||||
config = {
|
||||
bucket = "ara-engine-terraform-state",
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ data "terraform_remote_state" "categorisation" {
|
|||
backend = "s3"
|
||||
config = {
|
||||
bucket = "categorisation-terraform-state",
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -43,26 +43,26 @@ locals {
|
|||
# FastAPI Lambda + API Gateway
|
||||
############################################
|
||||
module "fastapi" {
|
||||
source = "../../modules/lambda_with_api_gateway"
|
||||
source = "../../modules/lambda_with_api_gateway"
|
||||
|
||||
name = "fastapi"
|
||||
stage = var.stage
|
||||
source_dir = "${path.root}/../../../../"
|
||||
handler = "backend.app.main.handler"
|
||||
runtime = "python3.11"
|
||||
timeout = 600
|
||||
memory_size = 512
|
||||
artifact_bucket = data.terraform_remote_state.shared.outputs.ara_fast_api_state_bucket
|
||||
name = "fastapi"
|
||||
stage = var.stage
|
||||
source_dir = "${path.root}/../../../../"
|
||||
handler = "backend.app.main.handler"
|
||||
runtime = "python3.11"
|
||||
timeout = 600
|
||||
memory_size = 512
|
||||
artifact_bucket = data.terraform_remote_state.shared.outputs.ara_fast_api_state_bucket
|
||||
requirements_file = "${path.root}/../../../../backend/app/requirements/requirements.txt"
|
||||
|
||||
domain_name = "api.${var.domain_name}"
|
||||
|
||||
environment = {
|
||||
ENVIRONMENT = var.stage
|
||||
API_KEY = var.api_key
|
||||
SECRET_KEY = var.secret_key
|
||||
ENVIRONMENT = var.stage
|
||||
API_KEY = var.api_key
|
||||
SECRET_KEY = var.secret_key
|
||||
# DOMAIN_NAME = var.domain_name
|
||||
EPC_AUTH_TOKEN = var.epc_auth_token
|
||||
EPC_AUTH_TOKEN = var.epc_auth_token
|
||||
GOOGLE_SOLAR_API_KEY = var.google_solar_api_key
|
||||
|
||||
DB_HOST = var.db_host
|
||||
|
|
@ -71,14 +71,17 @@ module "fastapi" {
|
|||
DB_USERNAME = local.db_credentials.db_assessment_model_username
|
||||
DB_PASSWORD = local.db_credentials.db_assessment_model_password
|
||||
|
||||
PLAN_TRIGGER_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_plan_trigger_bucket_name
|
||||
DATA_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_data_bucket_name
|
||||
SAP_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_predictions_bucket_name
|
||||
CARBON_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_carbon_predictions_bucket_name
|
||||
HEAT_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heat_predictions_bucket_name
|
||||
HEATING_KWH_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heating_kwh_predictions_bucket_name
|
||||
HOTWATER_KWH_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_hotwater_kwh_predictions_bucket_name
|
||||
ENERGY_ASSESSMENTS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_energy_assessments_bucket_name
|
||||
PLAN_TRIGGER_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_plan_trigger_bucket_name
|
||||
DATA_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_data_bucket_name
|
||||
SAP_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_predictions_bucket_name
|
||||
CARBON_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_carbon_predictions_bucket_name
|
||||
HEAT_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heat_predictions_bucket_name
|
||||
HEATING_KWH_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heating_kwh_predictions_bucket_name
|
||||
HOTWATER_KWH_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_hotwater_kwh_predictions_bucket_name
|
||||
ENERGY_ASSESSMENTS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_energy_assessments_bucket_name
|
||||
SAP_BASELINE_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_baseline_predictions_bucket_name
|
||||
CARBON_BASELINE_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_carbon_baseline_predictions_bucket_name
|
||||
HEAT_BASELINE_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heat_baseline_predictions_bucket_name
|
||||
|
||||
ENGINE_SQS_URL = data.terraform_remote_state.engine.outputs.ara_engine_queue_url
|
||||
CATEGORISATION_SQS_URL = data.terraform_remote_state.categorisation.outputs.categorisation_queue_url
|
||||
|
|
@ -121,4 +124,4 @@ resource "aws_iam_role_policy_attachment" "fastapi_sqs_send" {
|
|||
resource "aws_iam_role_policy_attachment" "fastapi_s3_read_and_write" {
|
||||
role = module.fastapi.role_name
|
||||
policy_arn = data.terraform_remote_state.shared.outputs.fast_api_s3_read_and_write_arn
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ terraform {
|
|||
}
|
||||
}
|
||||
backend "s3" {
|
||||
bucket = "assessment-model-terraform-state"
|
||||
region = "eu-west-2"
|
||||
key = "terraform.tfstate"
|
||||
bucket = "assessment-model-terraform-state"
|
||||
region = "eu-west-2"
|
||||
key = "terraform.tfstate"
|
||||
}
|
||||
|
||||
required_version = ">= 1.2.0"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
region = var.region
|
||||
}
|
||||
|
||||
# Additional provider for resources that need to be in us-east-1, specifically the SSL certificate
|
||||
|
|
@ -47,30 +47,30 @@ resource "aws_security_group" "allow_db" {
|
|||
|
||||
ingress {
|
||||
# TLS (change to whatever ports you need)
|
||||
from_port = 5432
|
||||
to_port = 5432
|
||||
protocol = "tcp"
|
||||
from_port = 5432
|
||||
to_port = 5432
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "default" {
|
||||
allocated_storage = var.allocated_storage
|
||||
engine = "postgres"
|
||||
engine_version = "14.17"
|
||||
instance_class = var.instance_class
|
||||
db_name = var.database_name
|
||||
username = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_username"]
|
||||
password = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_password"]
|
||||
parameter_group_name = "default.postgres14"
|
||||
skip_final_snapshot = true
|
||||
allocated_storage = var.allocated_storage
|
||||
engine = "postgres"
|
||||
engine_version = "14.17"
|
||||
instance_class = var.instance_class
|
||||
db_name = var.database_name
|
||||
username = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_username"]
|
||||
password = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_password"]
|
||||
parameter_group_name = "default.postgres14"
|
||||
skip_final_snapshot = true
|
||||
vpc_security_group_ids = [aws_security_group.allow_db.id]
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
|
|
@ -87,7 +87,7 @@ resource "aws_db_instance" "default" {
|
|||
storage_type = "gp3"
|
||||
|
||||
# Automated backups configuration
|
||||
backup_retention_period = 14
|
||||
backup_retention_period = 14
|
||||
backup_window = "03:00-04:00"
|
||||
maintenance_window = "Sun:02:00-Sun:02:30"
|
||||
copy_tags_to_snapshot = true
|
||||
|
|
@ -103,7 +103,7 @@ module "s3_presignable_bucket" {
|
|||
}
|
||||
|
||||
output "retrofit_plan_trigger_bucket_name" {
|
||||
value = module.s3_presignable_bucket.bucket_name
|
||||
value = module.s3_presignable_bucket.bucket_name
|
||||
description = "Name of the retrofit plan trigger bucket"
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ module "retrofit_sap_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_sap_predictions_bucket_name" {
|
||||
value = module.retrofit_sap_predictions.bucket_name
|
||||
value = module.retrofit_sap_predictions.bucket_name
|
||||
description = "Name of the retrofit SAP predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ module "retrofit_sap_data" {
|
|||
}
|
||||
|
||||
output "retrofit_sap_data_bucket_name" {
|
||||
value = module.retrofit_sap_data.bucket_name
|
||||
value = module.retrofit_sap_data.bucket_name
|
||||
description = "Name of the retrofit SAP data bucket"
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ module "retrofit_carbon_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_carbon_predictions_bucket_name" {
|
||||
value = module.retrofit_carbon_predictions.bucket_name
|
||||
value = module.retrofit_carbon_predictions.bucket_name
|
||||
description = "Name of the retrofit carbon predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ module "retrofit_heat_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_heat_predictions_bucket_name" {
|
||||
value = module.retrofit_heat_predictions.bucket_name
|
||||
value = module.retrofit_heat_predictions.bucket_name
|
||||
description = "Name of the retrofit heat predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ module "retrofit_heating_kwh_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_heating_kwh_predictions_bucket_name" {
|
||||
value = module.retrofit_heating_kwh_predictions.bucket_name
|
||||
value = module.retrofit_heating_kwh_predictions.bucket_name
|
||||
description = "Name of the retrofit heating kWh predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +229,7 @@ module "retrofit_hotwater_kwh_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_hotwater_kwh_predictions_bucket_name" {
|
||||
value = module.retrofit_hotwater_kwh_predictions.bucket_name
|
||||
value = module.retrofit_hotwater_kwh_predictions.bucket_name
|
||||
description = "Name of the retrofit hotwater kWh predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +239,33 @@ module "retrofit_sap_baseline_predictions" {
|
|||
allowed_origins = var.allowed_origins
|
||||
}
|
||||
|
||||
output "retrofit_sap_baseline_predictions_bucket_name" {
|
||||
value = module.retrofit_sap_baseline_predictions.bucket_name
|
||||
description = "Name of the retrofit SAP baseline predictions bucket"
|
||||
}
|
||||
|
||||
module "retrofit_carbon_baseline_predictions" {
|
||||
source = "../modules/s3"
|
||||
bucketname = "retrofit-carbon-baseline-predictions-${var.stage}"
|
||||
allowed_origins = var.allowed_origins
|
||||
}
|
||||
|
||||
output "retrofit_carbon_baseline_predictions_bucket_name" {
|
||||
value = module.retrofit_carbon_baseline_predictions.bucket_name
|
||||
description = "Name of the retrofit carbon baseline predictions bucket"
|
||||
}
|
||||
|
||||
module "retrofit_heat_baseline_predictions" {
|
||||
source = "../modules/s3"
|
||||
bucketname = "retrofit-heat-baseline-predictions-${var.stage}"
|
||||
allowed_origins = var.allowed_origins
|
||||
}
|
||||
|
||||
output "retrofit_heat_baseline_predictions_bucket_name" {
|
||||
value = module.retrofit_heat_baseline_predictions.bucket_name
|
||||
description = "Name of the retrofit heat baseline predictions bucket"
|
||||
}
|
||||
|
||||
// We make this bucket presignable, because we want to generate download links for the frontend
|
||||
module "retrofit_energy_assessments" {
|
||||
source = "../modules/s3_presignable_bucket"
|
||||
|
|
@ -248,7 +275,7 @@ module "retrofit_energy_assessments" {
|
|||
}
|
||||
|
||||
output "retrofit_energy_assessments_bucket_name" {
|
||||
value = module.retrofit_energy_assessments.bucket_name
|
||||
value = module.retrofit_energy_assessments.bucket_name
|
||||
description = "Name of the retrofit energy assessments bucket"
|
||||
}
|
||||
|
||||
|
|
@ -327,6 +354,16 @@ module "sap_baseline_ecr" {
|
|||
source = "../modules/ecr"
|
||||
}
|
||||
|
||||
module "heat_baseline_ecr" {
|
||||
ecr_name = "heat-baseline-prediction-${var.stage}"
|
||||
source = "../modules/ecr"
|
||||
}
|
||||
|
||||
module "carbon_baseline_ecr" {
|
||||
ecr_name = "carbon-baseline-prediction-${var.stage}"
|
||||
source = "../modules/ecr"
|
||||
}
|
||||
|
||||
################################################
|
||||
# SES - Email sending
|
||||
################################################
|
||||
|
|
@ -352,7 +389,7 @@ module "address2uprn_state_bucket" {
|
|||
module "address2uprn_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "address2uprn"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -383,14 +420,14 @@ module "condition_etl_state_bucket" {
|
|||
module "condition_etl_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "condition-etl"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
|
||||
}
|
||||
|
||||
# Condition Data S3 Bucket to store initial data
|
||||
module "condition_data_bucket" {
|
||||
source = "../modules/s3"
|
||||
bucketname = "condition-data-${var.stage}"
|
||||
source = "../modules/s3"
|
||||
bucketname = "condition-data-${var.stage}"
|
||||
allowed_origins = var.allowed_origins
|
||||
}
|
||||
|
||||
|
|
@ -421,7 +458,7 @@ module "postcode_splitter_state_bucket" {
|
|||
module "postcode_splitter_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "postcode_splitter"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -452,7 +489,7 @@ module "categorisation_state_bucket" {
|
|||
module "categorisation_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "categorisation"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -468,7 +505,7 @@ module "ordnance_state_bucket" {
|
|||
module "ordnance_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "ordnance"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -499,7 +536,7 @@ module "engine_state_bucket" {
|
|||
module "engine_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "engine"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
}
|
||||
|
||||
# S3 policy for Engine to read and write from various S3 buckets
|
||||
|
|
@ -508,7 +545,7 @@ module "engine_s3_read_and_write" {
|
|||
|
||||
policy_name = "EngineReadandWriteS3"
|
||||
policy_description = "Allow Engine Lambda to read from and write to various S3 buckets"
|
||||
bucket_arns = [
|
||||
bucket_arns = [
|
||||
"arn:aws:s3:::${module.s3_presignable_bucket.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_data.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_predictions.bucket_name}",
|
||||
|
|
@ -516,10 +553,13 @@ module "engine_s3_read_and_write" {
|
|||
"arn:aws:s3:::${module.retrofit_heat_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_heating_kwh_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_hotwater_kwh_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_energy_assessments.bucket_name}"
|
||||
"arn:aws:s3:::${module.retrofit_energy_assessments.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_baseline_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_carbon_baseline_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_heat_baseline_predictions.bucket_name}"
|
||||
]
|
||||
actions = ["s3:*"]
|
||||
resource_paths = ["/*"]
|
||||
actions = ["s3:*"]
|
||||
resource_paths = ["/*"]
|
||||
}
|
||||
|
||||
output "engine_s3_read_and_write_arn" {
|
||||
|
|
@ -544,7 +584,7 @@ module "fast_api_s3_read_and_write" {
|
|||
|
||||
policy_name = "FastAPIReadandWriteS3"
|
||||
policy_description = "Allow FastAPI Lambda to read from and write to various S3 buckets"
|
||||
bucket_arns = [
|
||||
bucket_arns = [
|
||||
"arn:aws:s3:::${module.s3_presignable_bucket.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_data.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_predictions.bucket_name}",
|
||||
|
|
@ -554,8 +594,8 @@ module "fast_api_s3_read_and_write" {
|
|||
"arn:aws:s3:::${module.retrofit_hotwater_kwh_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_energy_assessments.bucket_name}"
|
||||
]
|
||||
actions = ["s3:GetObject", "s3:ListBucket"]
|
||||
resource_paths = ["/*"]
|
||||
actions = ["s3:GetObject", "s3:ListBucket"]
|
||||
resource_paths = ["/*"]
|
||||
}
|
||||
|
||||
output "fast_api_s3_read_and_write_arn" {
|
||||
|
|
|
|||
23
pytest.ini
23
pytest.ini
|
|
@ -2,5 +2,24 @@
|
|||
pythonpath = .
|
||||
log_cli = true
|
||||
log_cli_level = INFO
|
||||
addopts = --cov-report term-missing --cov=etl/epc --cov=recommendations --cov=backend --cov=etl/epc_clean --cov=etl/spatial
|
||||
testpaths = recommendations/tests backend/tests etl/epc/tests etl/epc_clean/tests etl/spatial/tests backend/condition/tests backend/address2UPRN/tests backend/onboarders/tests backend/categorisation/tests backend/export/tests
|
||||
|
||||
addopts =
|
||||
--cov-report term-missing
|
||||
--cov=etl/epc
|
||||
--cov=etl/epc_clean
|
||||
--cov=etl/spatial
|
||||
--cov=recommendations
|
||||
--cov=backend
|
||||
|
||||
testpaths =
|
||||
backend/tests
|
||||
backend/address2UPRN/tests
|
||||
backend/categorisation/tests
|
||||
backend/condition/tests
|
||||
backend/export/tests
|
||||
backend/onboarders/tests
|
||||
backend/app/tests
|
||||
etl/epc/tests
|
||||
etl/epc_clean/tests
|
||||
etl/spatial/tests
|
||||
recommendations/tests
|
||||
|
|
@ -28,6 +28,9 @@ provider:
|
|||
HOTWATER_KWH_PREDICTIONS_BUCKET: ${env:HOTWATER_KWH_PREDICTIONS_BUCKET}
|
||||
ENERGY_ASSESSMENTS_BUCKET: ${env:ENERGY_ASSESSMENTS_BUCKET}
|
||||
GOOGLE_SOLAR_API_KEY: ${env:GOOGLE_SOLAR_API_KEY}
|
||||
SAP_BASELINE_PREDICTIONS_BUCKET: ${env:SAP_BASELINE_PREDICTIONS_BUCKET}
|
||||
CARBON_BASELINE_PREDICTIONS_BUCKET: ${env:CARBON_BASELINE_PREDICTIONS_BUCKET}
|
||||
HEAT_BASELINE_PREDICTIONS_BUCKET: ${env:HEAT_BASELINE_PREDICTIONS_BUCKET}
|
||||
ENGINE_SQS_URL:
|
||||
Ref: EngineQueue
|
||||
# hardcode the categorisation queue for now as it's created in terraform
|
||||
|
|
@ -177,3 +180,9 @@ resources:
|
|||
- arn:aws:s3:::${env:HEATING_KWH_PREDICTIONS_BUCKET}/*
|
||||
- arn:aws:s3:::${env:HOTWATER_KWH_PREDICTIONS_BUCKET}
|
||||
- arn:aws:s3:::${env:HOTWATER_KWH_PREDICTIONS_BUCKET}/*
|
||||
- arn:aws:s3:::${env:SAP_BASELINE_PREDICTIONS_BUCKET}
|
||||
- arn:aws:s3:::${env:SAP_BASELINE_PREDICTIONS_BUCKET}/*
|
||||
- arn:aws:s3:::${env:CARBON_BASELINE_PREDICTIONS_BUCKET}
|
||||
- arn:aws:s3:::${env:CARBON_BASELINE_PREDICTIONS_BUCKET}/*
|
||||
- arn:aws:s3:::${env:HEAT_BASELINE_PREDICTIONS_BUCKET}
|
||||
- arn:aws:s3:::${env:HEAT_BASELINE_PREDICTIONS_BUCKET}/*
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ pytest-cov
|
|||
pytest-mock
|
||||
dotenv
|
||||
psycopg[binary]
|
||||
pytest-postgresql
|
||||
pytest-postgresql
|
||||
httpx
|
||||
Loading…
Add table
Reference in a new issue