mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Compare commits
No commits in common. "326ec2485de3a2dbafa40e7f440f11ff9248618f" and "082bbcf22e366ede23db6f3646c69839f39cec2f" have entirely different histories.
326ec2485d
...
082bbcf22e
204 changed files with 123 additions and 3446 deletions
|
|
@ -6,7 +6,7 @@ backend/.idea/*
|
|||
backend/.env
|
||||
recommendations/tests/*
|
||||
model_data/tests/*
|
||||
deployment/*
|
||||
infrastructure/*
|
||||
data_collection/*
|
||||
node_modules/*
|
||||
conservation_areas/*
|
||||
|
|
|
|||
3
.github/workflows/_build_image.yml
vendored
3
.github/workflows/_build_image.yml
vendored
|
|
@ -40,8 +40,6 @@ on:
|
|||
required: false
|
||||
EPC_AUTH_TOKEN:
|
||||
required: false
|
||||
OPEN_EPC_API_TOKEN:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -52,7 +50,6 @@ jobs:
|
|||
DEV_DB_PORT: ${{ secrets.DEV_DB_PORT }}
|
||||
DEV_DB_NAME: ${{ secrets.DEV_DB_NAME }}
|
||||
EPC_AUTH_TOKEN: ${{ secrets.EPC_AUTH_TOKEN }}
|
||||
OPEN_EPC_API_TOKEN: ${{ secrets.OPEN_EPC_API_TOKEN }}
|
||||
|
||||
outputs:
|
||||
image_digest: ${{ steps.digest.outputs.image_digest }}
|
||||
|
|
|
|||
50
.github/workflows/deploy_terraform.yml
vendored
50
.github/workflows/deploy_terraform.yml
vendored
|
|
@ -62,20 +62,20 @@ jobs:
|
|||
- uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Terraform Init
|
||||
working-directory: deployment/terraform/shared
|
||||
working-directory: infrastructure/terraform/shared
|
||||
run: terraform init -reconfigure
|
||||
|
||||
- name: Terraform Workspace
|
||||
working-directory: deployment/terraform/shared
|
||||
working-directory: infrastructure/terraform/shared
|
||||
run: terraform workspace select ${STAGE} || terraform workspace new ${STAGE}
|
||||
|
||||
- name: Terraform Plan
|
||||
working-directory: deployment/terraform/shared
|
||||
working-directory: infrastructure/terraform/shared
|
||||
run: terraform plan -var-file=${STAGE}.tfvars -out=tfplan
|
||||
|
||||
- name: Terraform Apply
|
||||
if: env.TERRAFORM_APPLY == 'true'
|
||||
working-directory: deployment/terraform/shared
|
||||
working-directory: infrastructure/terraform/shared
|
||||
run: terraform apply -auto-approve tfplan
|
||||
|
||||
# ============================================================
|
||||
|
|
@ -101,7 +101,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: ara_engine
|
||||
lambda_path: deployment/terraform/lambda/engine
|
||||
lambda_path: infrastructure/terraform/lambda/engine
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: engine-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.ara_engine_image.outputs.image_digest }}
|
||||
|
|
@ -133,7 +133,6 @@ jobs:
|
|||
DEV_DB_PORT=$DEV_DB_PORT
|
||||
DEV_DB_NAME=$DEV_DB_NAME
|
||||
EPC_AUTH_TOKEN=$EPC_AUTH_TOKEN
|
||||
OPEN_EPC_API_TOKEN=$OPEN_EPC_API_TOKEN
|
||||
secrets:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
|
||||
|
|
@ -142,7 +141,6 @@ jobs:
|
|||
DEV_DB_PORT: ${{ secrets.DEV_DB_PORT }}
|
||||
DEV_DB_NAME: ${{ secrets.DEV_DB_NAME }}
|
||||
EPC_AUTH_TOKEN: ${{ secrets.DEV_EPC_AUTH_TOKEN }}
|
||||
OPEN_EPC_API_TOKEN: ${{ secrets.DEV_OPEN_EPC_API_TOKEN }}
|
||||
|
||||
# ============================================================
|
||||
# Deploy Address 2 UPRN Lambda
|
||||
|
|
@ -152,7 +150,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: address2uprn
|
||||
lambda_path: deployment/terraform/lambda/address2UPRN
|
||||
lambda_path: infrastructure/terraform/lambda/address2UPRN
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: address2uprn-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.address2uprn_image.outputs.image_digest }}
|
||||
|
|
@ -171,7 +169,7 @@ jobs:
|
|||
uses: ./.github/workflows/_build_image.yml
|
||||
with:
|
||||
ecr_repo: postcode_splitter-${{ needs.determine_stage.outputs.stage }}
|
||||
dockerfile_path: applications/postcode_splitter/Dockerfile
|
||||
dockerfile_path: backend/postcode_splitter/handler/Dockerfile
|
||||
build_context: .
|
||||
build_args: |
|
||||
DEV_DB_HOST=$DEV_DB_HOST
|
||||
|
|
@ -193,7 +191,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: postcodeSplitter
|
||||
lambda_path: deployment/terraform/lambda/postcodeSplitter
|
||||
lambda_path: infrastructure/terraform/lambda/postcodeSplitter
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: postcode_splitter-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.postcodeSplitter_image.outputs.image_digest }}
|
||||
|
|
@ -233,7 +231,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: bulk_address2uprn_combiner
|
||||
lambda_path: deployment/terraform/lambda/bulk_address2uprn_combiner
|
||||
lambda_path: infrastructure/terraform/lambda/bulk_address2uprn_combiner
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: bulk_address2uprn_combiner-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.bulk_address2uprn_combiner_image.outputs.image_digest }}
|
||||
|
|
@ -273,7 +271,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: condition-etl
|
||||
lambda_path: deployment/terraform/lambda/condition-etl
|
||||
lambda_path: infrastructure/terraform/lambda/condition-etl
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: condition-etl-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.condition_etl_image.outputs.image_digest }}
|
||||
|
|
@ -313,7 +311,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: categorisation
|
||||
lambda_path: deployment/terraform/lambda/categorisation
|
||||
lambda_path: infrastructure/terraform/lambda/categorisation
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: categorisation-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.categorisation_image.outputs.image_digest }}
|
||||
|
|
@ -353,7 +351,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: ordnanceSurvey
|
||||
lambda_path: deployment/terraform/lambda/ordnanceSurvey
|
||||
lambda_path: infrastructure/terraform/lambda/ordnanceSurvey
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: ordnance-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.ordnanceSurvey_image.outputs.image_digest }}
|
||||
|
|
@ -388,7 +386,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: pashub_to_ara
|
||||
lambda_path: deployment/terraform/lambda/pashub_to_ara
|
||||
lambda_path: infrastructure/terraform/lambda/pashub_to_ara
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: pashub_to_ara-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.pashub_to_ara_image.outputs.image_digest }}
|
||||
|
|
@ -421,7 +419,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: ara_fast_api
|
||||
lambda_path: deployment/terraform/lambda/fast-api
|
||||
lambda_path: infrastructure/terraform/lambda/fast-api
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
terraform_apply: ${{ needs.determine_stage.outputs.terraform_apply }}
|
||||
secrets:
|
||||
|
|
@ -460,17 +458,17 @@ jobs:
|
|||
- uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Terraform Init
|
||||
working-directory: deployment/terraform/cdn_certificate
|
||||
working-directory: infrastructure/terraform/cdn_certificate
|
||||
run: terraform init -reconfigure
|
||||
|
||||
- name: Terraform Workspace
|
||||
working-directory: deployment/terraform/cdn_certificate
|
||||
working-directory: infrastructure/terraform/cdn_certificate
|
||||
run: |
|
||||
terraform workspace select $STAGE \
|
||||
|| terraform workspace new $STAGE
|
||||
|
||||
- name: Terraform Plan
|
||||
working-directory: deployment/terraform/cdn_certificate
|
||||
working-directory: infrastructure/terraform/cdn_certificate
|
||||
run: |
|
||||
terraform plan \
|
||||
-var="stage=${STAGE}" \
|
||||
|
|
@ -478,7 +476,7 @@ jobs:
|
|||
|
||||
- name: Terraform Apply
|
||||
if: env.TERRAFORM_APPLY == 'true'
|
||||
working-directory: deployment/terraform/cdn_certificate
|
||||
working-directory: infrastructure/terraform/cdn_certificate
|
||||
run: terraform apply -auto-approve tfplan
|
||||
|
||||
|
||||
|
|
@ -505,17 +503,17 @@ jobs:
|
|||
- uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Terraform Init
|
||||
working-directory: deployment/terraform/cdn
|
||||
working-directory: infrastructure/terraform/cdn
|
||||
run: terraform init -reconfigure
|
||||
|
||||
- name: Terraform Workspace
|
||||
working-directory: deployment/terraform/cdn
|
||||
working-directory: infrastructure/terraform/cdn
|
||||
run: |
|
||||
terraform workspace select $STAGE \
|
||||
|| terraform workspace new $STAGE
|
||||
|
||||
- name: Terraform Plan
|
||||
working-directory: deployment/terraform/cdn
|
||||
working-directory: infrastructure/terraform/cdn
|
||||
run: |
|
||||
terraform plan \
|
||||
-var="stage=${STAGE}" \
|
||||
|
|
@ -523,7 +521,7 @@ jobs:
|
|||
|
||||
- name: Terraform Apply
|
||||
if: env.TERRAFORM_APPLY == 'true'
|
||||
working-directory: deployment/terraform/cdn
|
||||
working-directory: infrastructure/terraform/cdn
|
||||
run: terraform apply -auto-approve tfplan
|
||||
|
||||
# ============================================================
|
||||
|
|
@ -564,7 +562,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: magic_plan
|
||||
lambda_path: deployment/terraform/lambda/magic_plan
|
||||
lambda_path: infrastructure/terraform/lambda/magic_plan
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: magic-plan-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.magic_plan_image.outputs.image_digest }}
|
||||
|
|
@ -587,7 +585,7 @@ jobs:
|
|||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: hubspot-etl-to-ara
|
||||
lambda_path: deployment/terraform/lambda/hubspot_deal_etl
|
||||
lambda_path: infrastructure/terraform/lambda/hubspot_deal_etl
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: hubspot-etl-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.hubspot_etl_image.outputs.image_digest }}
|
||||
|
|
|
|||
7
.github/workflows/lambda_smoke_tests.yml
vendored
7
.github/workflows/lambda_smoke_tests.yml
vendored
|
|
@ -36,13 +36,6 @@ jobs:
|
|||
build_context: .
|
||||
service_name: postcode-splitter
|
||||
|
||||
postcode_splitter_ddd_smoke_test:
|
||||
uses: ./.github/workflows/_smoke_test_lambda.yml
|
||||
with:
|
||||
dockerfile_path: applications/postcode_splitter/Dockerfile
|
||||
build_context: .
|
||||
service_name: postcode-splitter-ddd
|
||||
|
||||
# ============================================================
|
||||
# Bulk Address2UPRN Combiner
|
||||
# ============================================================
|
||||
|
|
|
|||
12
.github/workflows/unit_tests.yml
vendored
12
.github/workflows/unit_tests.yml
vendored
|
|
@ -60,15 +60,3 @@ jobs:
|
|||
-e DB_PASSWORD=test \
|
||||
-e DB_PORT=5432 \
|
||||
model-test pytest -vv -m 'not integration'
|
||||
|
||||
# The DDD rewrite (tests/) defines SQLModel table classes that map to the
|
||||
# same physical tables as the legacy backend models. Both sets share the
|
||||
# one global SQLModel.metadata, so they cannot be imported into the same
|
||||
# pytest process. It runs as a separate invocation until the legacy
|
||||
# models are retired. Its DB is spawned in-process by pytest-postgresql,
|
||||
# so no DB service or env is required.
|
||||
- name: Run DDD tests
|
||||
run: |
|
||||
docker run --rm \
|
||||
--network host \
|
||||
model-test pytest -vv tests/
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -121,7 +121,6 @@ celerybeat.pid
|
|||
|
||||
# Environments
|
||||
.env
|
||||
.env.local
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
|
|
|
|||
29
AGENTS.md
Normal file
29
AGENTS.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
<!-- BACKLOG.MD MCP GUIDELINES START -->
|
||||
|
||||
<CRITICAL_INSTRUCTION>
|
||||
|
||||
## BACKLOG WORKFLOW INSTRUCTIONS
|
||||
|
||||
This project uses Backlog.md MCP for all task and project management activities.
|
||||
|
||||
**CRITICAL GUIDANCE**
|
||||
|
||||
- If your client supports MCP resources, read `backlog://workflow/overview` to understand when and how to use Backlog for this project.
|
||||
- If your client only supports tools or the above request fails, call `backlog.get_backlog_instructions()` to load the tool-oriented overview. Use the `instruction` selector when you need `task-creation`, `task-execution`, or `task-finalization`.
|
||||
|
||||
- **First time working here?** Read the overview resource IMMEDIATELY to learn the workflow
|
||||
- **Already familiar?** You should have the overview cached ("## Backlog.md Overview (MCP)")
|
||||
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
||||
|
||||
These guides cover:
|
||||
- Decision framework for when to create tasks
|
||||
- Search-first workflow to avoid duplicates
|
||||
- Links to detailed guides for task creation, execution, and finalization
|
||||
- MCP tools reference
|
||||
|
||||
You MUST read the overview resource to understand the complete workflow. The information is NOT summarized here.
|
||||
|
||||
</CRITICAL_INSTRUCTION>
|
||||
|
||||
<!-- BACKLOG.MD MCP GUIDELINES END -->
|
||||
29
CLAUDE.md
29
CLAUDE.md
|
|
@ -1,4 +1,33 @@
|
|||
|
||||
<!-- BACKLOG.MD MCP GUIDELINES START -->
|
||||
|
||||
<CRITICAL_INSTRUCTION>
|
||||
|
||||
## BACKLOG WORKFLOW INSTRUCTIONS
|
||||
|
||||
This project uses Backlog.md MCP for all task and project management activities.
|
||||
|
||||
**CRITICAL GUIDANCE**
|
||||
|
||||
- If your client supports MCP resources, read `backlog://workflow/overview` to understand when and how to use Backlog for this project.
|
||||
- If your client only supports tools or the above request fails, call `backlog.get_backlog_instructions()` to load the tool-oriented overview. Use the `instruction` selector when you need `task-creation`, `task-execution`, or `task-finalization`.
|
||||
|
||||
- **First time working here?** Read the overview resource IMMEDIATELY to learn the workflow
|
||||
- **Already familiar?** You should have the overview cached ("## Backlog.md Overview (MCP)")
|
||||
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
||||
|
||||
These guides cover:
|
||||
- Decision framework for when to create tasks
|
||||
- Search-first workflow to avoid duplicates
|
||||
- Links to detailed guides for task creation, execution, and finalization
|
||||
- MCP tools reference
|
||||
|
||||
You MUST read the overview resource to understand the complete workflow. The information is NOT summarized here.
|
||||
|
||||
</CRITICAL_INSTRUCTION>
|
||||
|
||||
<!-- BACKLOG.MD MCP GUIDELINES END -->
|
||||
|
||||
## Available Skills
|
||||
|
||||
Five Claude Code skills are installed in this repo's dev container. Each maps to a phase of the feature lifecycle.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ model_data/local_data/
|
|||
backend/node_modules/
|
||||
backend/.idea/
|
||||
backend/.env
|
||||
deployment/
|
||||
infrastructure/
|
||||
data_collection/
|
||||
node_modules/
|
||||
conservation_areas/
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Invoke `/ubiquitous-language` in any session to extract new terms from the conve
|
|||
|------|------------|------------------|
|
||||
| **UPRN** | Unique Property Reference Number — the government-issued permanent identifier for a physical address in the UK. | "property ID", "address ID", "code" |
|
||||
| **Postcode** | A UK postal code used to group nearby addresses; the primary search key for finding EPC records. | "zip code", "postal code" |
|
||||
| **User Address** | A structured dataclass (`domain.addresses.user_address.UserAddress`) capturing a customer-supplied address: a free-text `user_address` line, a canonical `postcode` (sanitised on construction), and an optional `internal_reference`. The bare string sense -- the raw free-text address line as it arrives from upstream ingestion, before being wrapped -- remains valid when discussing CSV columns, API payloads, or other upstream contexts; in domain code, prefer the dataclass. | "user input", "raw address", "user_inputed_address" |
|
||||
| **User Address** | A free-text address string provided by a user or imported from a customer dataset, before any normalisation or matching. | "user input", "raw address", "user_inputed_address" |
|
||||
| **Dwelling** | A single residential unit that can hold an EPC — a house, flat, or maisonette. | "property", "unit", "home" |
|
||||
|
||||
## Address Matching
|
||||
|
|
@ -72,7 +72,7 @@ Invoke `/ubiquitous-language` in any session to extract new terms from the conve
|
|||
|
||||
## Flagged ambiguities
|
||||
|
||||
- **"address"** appears as both the raw **User Address** (free-text from customer data, or the structured `UserAddress` dataclass that wraps it) and a structured field on an **EPC Search Result** (normalised address lines). Always qualify: "user address" vs "EPC address" or "address line 1". Within `domain/`, **User Address** specifically means the `UserAddress` dataclass; in upstream ingestion contexts (CSV columns, SQS payloads) it can still mean the raw string sense.
|
||||
- **"address"** appears as both the raw **User Address** (free-text from customer data) and a structured field on an **EPC Search Result** (normalised address lines). Always qualify: "user address" vs "EPC address" or "address line 1".
|
||||
- **"score"** is used for the `AddressMatch.score()` function output, the `lexiscore` DataFrame column, and informally in conversation. Prefer **Lexiscore** in domain discussions; reserve "score" for method-level code comments.
|
||||
- **"user_inputed_address"** in `backend/address2UPRN/main.py` is a misspelling and a synonym for **User Address** — the canonical term. New code should use `user_address`.
|
||||
- **"EPC"** is overloaded as both the document (an Energy Performance Certificate) and the rating band letter. Use **EPC** for the document and **EPC Band** for the letter.
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
FROM public.ecr.aws/lambda/python:3.11
|
||||
|
||||
# Postgres host/port/database are baked into the image at build time from
|
||||
# the deploy workflow's --build-arg values (GitHub Actions DEV_DB_* secrets),
|
||||
# mirroring backend/postcode_splitter/handler/Dockerfile. They map onto the
|
||||
# POSTGRES_* names PostgresConfig.from_env reads. Username/password are NOT
|
||||
# baked in -- Terraform injects those as Lambda env vars from Secrets Manager.
|
||||
ARG DEV_DB_HOST
|
||||
ARG DEV_DB_PORT
|
||||
ARG DEV_DB_NAME
|
||||
|
||||
ENV POSTGRES_HOST=${DEV_DB_HOST}
|
||||
ENV POSTGRES_PORT=${DEV_DB_PORT}
|
||||
ENV POSTGRES_DATABASE=${DEV_DB_NAME}
|
||||
|
||||
WORKDIR /var/task
|
||||
|
||||
COPY applications/postcode_splitter/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the layered source the handler imports from. The new splitter pulls
|
||||
# only DDD-shaped packages — no pandas, no legacy backend/.
|
||||
COPY domain/ domain/
|
||||
COPY infrastructure/ infrastructure/
|
||||
COPY orchestration/ orchestration/
|
||||
COPY repositories/ repositories/
|
||||
COPY utilities/ utilities/
|
||||
COPY applications/ applications/
|
||||
|
||||
# Place the handler at the Lambda task root so the runtime can resolve
|
||||
# ``main.handler`` without an extra package prefix.
|
||||
COPY applications/postcode_splitter/handler.py /var/task/main.py
|
||||
|
||||
CMD ["main.handler"]
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import boto3
|
||||
|
||||
from applications.postcode_splitter.postcode_splitter_trigger_body import (
|
||||
PostcodeSplitterTriggerBody,
|
||||
)
|
||||
from infrastructure.address2uprn_queue_client import Address2UprnQueueClient
|
||||
from infrastructure.csv_s3_client import CsvS3Client
|
||||
from orchestration.postcode_splitter_orchestrator import PostcodeSplitterOrchestrator
|
||||
from orchestration.task_orchestrator import TaskOrchestrator
|
||||
from repositories.user_address.user_address_csv_s3_repository import (
|
||||
UserAddressCsvS3Repository,
|
||||
)
|
||||
from utilities.aws_lambda.subtask_handler import subtask_handler
|
||||
|
||||
|
||||
@subtask_handler()
|
||||
def handler(
|
||||
body: dict[str, Any], context: Any, task_orchestrator: TaskOrchestrator
|
||||
) -> dict[str, list[str]]:
|
||||
trigger = PostcodeSplitterTriggerBody.model_validate(body)
|
||||
|
||||
bucket = os.environ["S3_BUCKET_NAME"]
|
||||
queue_url = os.environ["ADDRESS2UPRN_QUEUE_URL"]
|
||||
|
||||
# boto3.client is overloaded per-service in the installed stubs; cast
|
||||
# to Any so the strict-mode checker treats it as opaque.
|
||||
boto3_client: Any = boto3.client # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
||||
boto_s3: Any = boto3_client("s3")
|
||||
boto_sqs: Any = boto3_client("sqs")
|
||||
|
||||
csv_client = CsvS3Client(boto_s3, bucket)
|
||||
user_address_repo = UserAddressCsvS3Repository(csv_client, bucket)
|
||||
queue_client = Address2UprnQueueClient(boto_sqs, queue_url)
|
||||
|
||||
splitter = PostcodeSplitterOrchestrator(
|
||||
task_orchestrator=task_orchestrator,
|
||||
user_address_repo=user_address_repo,
|
||||
queue_client=queue_client,
|
||||
)
|
||||
|
||||
child_ids = splitter.split_and_dispatch(
|
||||
parent_task_id=trigger.task_id,
|
||||
parent_subtask_id=trigger.sub_task_id,
|
||||
input_s3_uri=trigger.s3_uri,
|
||||
)
|
||||
|
||||
return {"child_subtask_ids": [str(cid) for cid in child_ids]}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# Local-test environment for the postcode_splitter Lambda.
|
||||
#
|
||||
# cp .env.local.example .env.local then fill in the values below.
|
||||
#
|
||||
# .env.local is gitignored. The container hits REAL AWS and a REAL Postgres,
|
||||
# so every value here points at infrastructure that actually exists.
|
||||
#
|
||||
# NOTE: the new DDD code uses different env var names than the repo root
|
||||
# .env. The mapping (root .env name -> var here) is given per section.
|
||||
# Keep comments on their own lines — docker-compose's env_file parser folds a
|
||||
# trailing "# ..." into the value.
|
||||
|
||||
# --- Postgres (orchestration/default_orchestrator -> PostgresConfig.from_env) ---
|
||||
# POSTGRES_HOST <- DB_HOST, PORT <- DB_PORT, USERNAME <- DB_USERNAME,
|
||||
# PASSWORD <- DB_PASSWORD, DATABASE <- DB_NAME.
|
||||
POSTGRES_HOST=
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USERNAME=
|
||||
POSTGRES_PASSWORD=
|
||||
POSTGRES_DATABASE=
|
||||
# POSTGRES_DRIVER=psycopg2 (optional; defaults to psycopg2)
|
||||
|
||||
# --- Handler config (applications/postcode_splitter/handler.py) ---
|
||||
# S3_BUCKET_NAME: bucket holding the input address CSV (root .env: DATA_BUCKET).
|
||||
# ADDRESS2UPRN_QUEUE_URL: SQS queue the splitter fans batches out to; not in
|
||||
# the root .env (Terraform sets it in prod).
|
||||
S3_BUCKET_NAME=
|
||||
ADDRESS2UPRN_QUEUE_URL=
|
||||
|
||||
# --- AWS credentials for boto3 (S3 + SQS clients) ---
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=eu-west-2
|
||||
# AWS_SESSION_TOKEN= (only if using temporary/SSO credentials)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
services:
|
||||
postcode-splitter:
|
||||
build:
|
||||
context: ../../../
|
||||
dockerfile: applications/postcode_splitter/Dockerfile
|
||||
ports:
|
||||
- "9001:8080"
|
||||
env_file:
|
||||
- .env.local
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import requests
|
||||
|
||||
HOST = "localhost"
|
||||
PORT = "9001"
|
||||
|
||||
LAMBDA_URL = f"http://{HOST}:{PORT}/2015-03-31/functions/function/invocations"
|
||||
|
||||
payload = {
|
||||
"Records": [
|
||||
{
|
||||
"body": json.dumps(
|
||||
{
|
||||
"task_id": "f4b3332f-c0cc-481f-96a5-d39860a647cf",
|
||||
"sub_task_id": "14c042de-40c4-473b-8cd8-72c983a94a8d",
|
||||
"s3_uri": "s3://retrofit-data-dev/ara_raw_inputs/calico/Calico Homes Full list EPC Properties(Sheet2) (1) (1).csv",
|
||||
}
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = requests.post(LAMBDA_URL, json=payload)
|
||||
|
||||
print("Status code:", response.status_code)
|
||||
print("Response:")
|
||||
print(response.text)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -f .env.local ]; then
|
||||
cp .env.local.example .env.local
|
||||
echo "Created .env.local from the template — fill it in, then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker compose build --no-cache
|
||||
docker compose up --force-recreate
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class PostcodeSplitterTriggerBody(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
task_id: UUID
|
||||
sub_task_id: UUID
|
||||
s3_uri: str
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
boto3
|
||||
pydantic
|
||||
sqlmodel
|
||||
psycopg2-binary
|
||||
|
|
@ -79,23 +79,23 @@ def app():
|
|||
"""
|
||||
|
||||
data_folder = "/workspaces/model/asset_list"
|
||||
data_filename = "lincs_address_list.xlsx"
|
||||
sheet_name = "Sheet1"
|
||||
postcode_column = "Postcode"
|
||||
address1_column = "Deal Name"
|
||||
data_filename = "input.xlsx"
|
||||
sheet_name = "Handovers"
|
||||
postcode_column = "POSTCODE"
|
||||
address1_column = "Full Addres"
|
||||
address1_method = None
|
||||
fulladdress_column = "Deal Name"
|
||||
fulladdress_column = "Full Addres"
|
||||
address_cols_to_concat = []
|
||||
missing_postcodes_method = None
|
||||
landlord_year_built = None
|
||||
landlord_os_uprn = None
|
||||
landlord_property_type = None # Good to include if landlord gave
|
||||
landlord_built_form = None # Good to include if landlord gave
|
||||
landlord_os_uprn = "domna_found_uprn"
|
||||
landlord_property_type = "PROPERTY TYPE" # Good to include if landlord gave
|
||||
landlord_built_form = "Type Description" # Good to include if landlord gave
|
||||
landlord_wall_construction = None
|
||||
landlord_roof_construction = None
|
||||
landlord_heating_system = None
|
||||
landlord_existing_pv = None
|
||||
landlord_property_id = "landlord_id"
|
||||
landlord_property_id = "PROP REF"
|
||||
landlord_sap = None
|
||||
outcomes_filename = None
|
||||
outcomes_sheetname = None
|
||||
|
|
@ -468,3 +468,9 @@ def app():
|
|||
asset_list.duplicated_addresses.to_excel(
|
||||
writer, sheet_name="Duplicate Properties", index=False
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
for key,value in dict.items():
|
||||
lsakjfldsa
|
||||
|
|
@ -6,13 +6,11 @@ ARG DEV_DB_HOST
|
|||
ARG DEV_DB_PORT
|
||||
ARG DEV_DB_NAME
|
||||
ARG EPC_AUTH_TOKEN
|
||||
ARG OPEN_EPC_API_TOKEN
|
||||
|
||||
ENV DB_HOST=${DEV_DB_HOST}
|
||||
ENV DB_PORT=${DEV_DB_PORT}
|
||||
ENV DB_NAME=${DEV_DB_NAME}
|
||||
ENV EPC_AUTH_TOKEN=${EPC_AUTH_TOKEN}
|
||||
ENV OPEN_EPC_API_TOKEN=${OPEN_EPC_API_TOKEN}
|
||||
|
||||
|
||||
# Set working directory (Lambda task root)
|
||||
|
|
|
|||
|
|
@ -8,5 +8,4 @@ boto3==1.35.44
|
|||
sqlmodel
|
||||
sqlalchemy==2.0.36
|
||||
psycopg2-binary==2.9.10
|
||||
pydantic-settings==2.6.0
|
||||
httpx
|
||||
pydantic-settings==2.6.0
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Iterator
|
||||
|
||||
from domain.addresses.user_address import UserAddress
|
||||
from domain.postcode import Postcode
|
||||
|
||||
|
||||
def iter_postcode_grouped_batches(
|
||||
addresses: Iterable[UserAddress],
|
||||
*,
|
||||
max_batch_size: int = 500,
|
||||
) -> Iterator[list[UserAddress]]:
|
||||
if max_batch_size < 1:
|
||||
raise ValueError("max_batch_size must be >= 1")
|
||||
|
||||
groups = _group_by_postcode_in_order(addresses)
|
||||
|
||||
buffer: list[UserAddress] = []
|
||||
for group in groups.values():
|
||||
group_len = len(group)
|
||||
|
||||
# Oversize single-Postcode group: flush buffer first, then dispatch
|
||||
# the group as its own batch. Mirrors the legacy
|
||||
# ``if group_len >= batch_size`` branch.
|
||||
if group_len >= max_batch_size:
|
||||
if buffer:
|
||||
yield buffer
|
||||
buffer = []
|
||||
yield group
|
||||
continue
|
||||
|
||||
# Adding this group would overflow: flush buffer before appending.
|
||||
if len(buffer) + group_len > max_batch_size:
|
||||
yield buffer
|
||||
buffer = []
|
||||
|
||||
buffer.extend(group)
|
||||
|
||||
# Final flush.
|
||||
if buffer:
|
||||
yield buffer
|
||||
|
||||
|
||||
def _group_by_postcode_in_order(
|
||||
addresses: Iterable[UserAddress],
|
||||
) -> dict[Postcode, list[UserAddress]]:
|
||||
groups: dict[Postcode, list[UserAddress]] = {}
|
||||
for address in addresses:
|
||||
groups.setdefault(address.postcode, []).append(address)
|
||||
return groups
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
from domain.postcode import Postcode
|
||||
|
||||
|
||||
def _empty_source_row() -> dict[str, str]:
|
||||
return {}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UserAddress:
|
||||
user_address: str
|
||||
postcode: Postcode
|
||||
internal_reference: Optional[str] = None
|
||||
source_row: dict[str, str] = field(default_factory=_empty_source_row, compare=False)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Postcode:
|
||||
value: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Frozen dataclass: bypass the descriptor with object.__setattr__.
|
||||
object.__setattr__(self, "value", "".join(self.value.split()).upper())
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Any, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
||||
class SubTaskStatus(str, Enum):
|
||||
WAITING = "waiting"
|
||||
IN_PROGRESS = "in progress"
|
||||
COMPLETE = "complete"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubTask:
|
||||
id: UUID
|
||||
task_id: UUID
|
||||
status: SubTaskStatus = SubTaskStatus.WAITING
|
||||
inputs: Optional[dict[str, Any]] = None
|
||||
outputs: Optional[dict[str, Any]] = None
|
||||
cloud_logs_url: Optional[str] = None
|
||||
job_started: Optional[datetime] = None
|
||||
job_completed: Optional[datetime] = None
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls, *, task_id: UUID, inputs: Optional[dict[str, Any]] = None
|
||||
) -> "SubTask":
|
||||
return cls(
|
||||
id=uuid4(),
|
||||
task_id=task_id,
|
||||
status=SubTaskStatus.WAITING,
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
def start(self, cloud_logs_url: Optional[str] = None) -> None:
|
||||
if self.status not in (SubTaskStatus.WAITING, SubTaskStatus.IN_PROGRESS):
|
||||
raise ValueError(f"cannot start subtask in status {self.status}")
|
||||
if self.job_started is None:
|
||||
self.job_started = datetime.now(timezone.utc)
|
||||
self.status = SubTaskStatus.IN_PROGRESS
|
||||
if cloud_logs_url is not None:
|
||||
self.cloud_logs_url = cloud_logs_url
|
||||
|
||||
def complete(self, result: Any = None) -> None:
|
||||
self.status = SubTaskStatus.COMPLETE
|
||||
self.job_completed = datetime.now(timezone.utc)
|
||||
if result is not None:
|
||||
self.outputs = {"result": result}
|
||||
|
||||
def fail(self, error: BaseException) -> None:
|
||||
self.status = SubTaskStatus.FAILED
|
||||
self.job_completed = datetime.now(timezone.utc)
|
||||
self.outputs = {"error": str(error)}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from domain.tasks.subtasks import SubTaskStatus
|
||||
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
WAITING = "waiting"
|
||||
IN_PROGRESS = "in progress"
|
||||
COMPLETE = "complete"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class Source(str, Enum):
|
||||
PORTFOLIO = "portfolio_id"
|
||||
HUBSPOT_DEAL = "hubspot_deal_id"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
id: UUID
|
||||
task_source: str
|
||||
status: TaskStatus = TaskStatus.WAITING
|
||||
service: Optional[str] = None
|
||||
source: Optional[Source] = None
|
||||
source_id: Optional[str] = None
|
||||
job_started: Optional[datetime] = None
|
||||
job_completed: Optional[datetime] = None
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
task_source: str,
|
||||
service: Optional[str] = None,
|
||||
source: Optional[Source] = None,
|
||||
source_id: Optional[str] = None,
|
||||
) -> "Task":
|
||||
if not task_source.strip():
|
||||
raise ValueError("task_source must be non-empty")
|
||||
return cls(
|
||||
id=uuid4(),
|
||||
task_source=task_source,
|
||||
service=service,
|
||||
source=source,
|
||||
source_id=source_id,
|
||||
status=TaskStatus.WAITING,
|
||||
job_started=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
if self.status not in (TaskStatus.WAITING, TaskStatus.IN_PROGRESS):
|
||||
raise ValueError(f"cannot start task in status {self.status}")
|
||||
if self.job_started is None:
|
||||
self.job_started = datetime.now(timezone.utc)
|
||||
self.status = TaskStatus.IN_PROGRESS
|
||||
|
||||
def complete(self) -> None:
|
||||
self.status = TaskStatus.COMPLETE
|
||||
self.job_completed = datetime.now(timezone.utc)
|
||||
|
||||
def fail(self) -> None:
|
||||
self.status = TaskStatus.FAILED
|
||||
self.job_completed = datetime.now(timezone.utc)
|
||||
|
||||
def recalculate_from_subtasks(self, statuses: list[SubTaskStatus]) -> None:
|
||||
"""Recompute Task.status from its SubTasks' statuses.
|
||||
|
||||
Rule (preserved from legacy _update_task_progress):
|
||||
- any FAILED → FAILED
|
||||
- all COMPLETE → COMPLETE
|
||||
- any IN_PROGRESS → IN_PROGRESS
|
||||
- otherwise → WAITING
|
||||
|
||||
Empty list is a no-op (newly-created task with no subtasks).
|
||||
"""
|
||||
if not statuses:
|
||||
return
|
||||
now = datetime.now(timezone.utc)
|
||||
if SubTaskStatus.FAILED in statuses:
|
||||
self.status = TaskStatus.FAILED
|
||||
self.job_completed = now
|
||||
elif all(s is SubTaskStatus.COMPLETE for s in statuses):
|
||||
self.status = TaskStatus.COMPLETE
|
||||
self.job_completed = now
|
||||
elif SubTaskStatus.IN_PROGRESS in statuses:
|
||||
self.status = TaskStatus.IN_PROGRESS
|
||||
self.job_completed = None
|
||||
else:
|
||||
self.status = TaskStatus.WAITING
|
||||
self.job_completed = None
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
from uuid import UUID
|
||||
|
||||
from infrastructure.sqs_client import SqsClient
|
||||
|
||||
|
||||
class Address2UprnQueueClient(SqsClient):
|
||||
def publish(
|
||||
self,
|
||||
*,
|
||||
parent_task_id: UUID,
|
||||
child_subtask_id: UUID,
|
||||
s3_uri: str,
|
||||
) -> str:
|
||||
return self.send(
|
||||
{
|
||||
"task_id": str(parent_task_id),
|
||||
"sub_task_id": str(child_subtask_id),
|
||||
"s3_uri": s3_uri,
|
||||
}
|
||||
)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import csv
|
||||
from io import StringIO
|
||||
|
||||
from infrastructure.s3_client import S3Client
|
||||
from infrastructure.s3_uri import parse_s3_uri
|
||||
|
||||
|
||||
class CsvS3Client(S3Client):
|
||||
def read_rows(self, s3_uri: str) -> list[dict[str, str]]:
|
||||
bucket, key = parse_s3_uri(s3_uri)
|
||||
if bucket != self.bucket:
|
||||
raise ValueError(
|
||||
f"s3_uri bucket {bucket!r} does not match client bucket {self.bucket!r}"
|
||||
)
|
||||
raw = self.get_object(key)
|
||||
text = raw.decode("utf-8-sig")
|
||||
reader = csv.DictReader(StringIO(text))
|
||||
return [dict(row) for row in reader]
|
||||
|
||||
def save_rows(self, rows: list[dict[str, str]], key: str) -> str:
|
||||
if not rows:
|
||||
raise ValueError("Cannot save an empty rows list: header is unknown")
|
||||
buffer = StringIO()
|
||||
fieldnames = list(rows[0].keys())
|
||||
writer = csv.DictWriter(buffer, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
return self.put_object(key, buffer.getvalue().encode("utf-8"))
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Mapping
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PostgresConfig:
|
||||
host: str
|
||||
port: int
|
||||
username: str
|
||||
password: str
|
||||
database: str
|
||||
driver: str = "psycopg2"
|
||||
pool_size: int = 3
|
||||
max_overflow: int = 5
|
||||
pool_pre_ping: bool = True
|
||||
pool_recycle: int = 300
|
||||
|
||||
def url(self) -> str:
|
||||
return (
|
||||
f"postgresql+{self.driver}://"
|
||||
f"{self.username}:{self.password}@{self.host}:{self.port}/{self.database}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, env: Mapping[str, str]) -> "PostgresConfig":
|
||||
return cls(
|
||||
host=env["POSTGRES_HOST"],
|
||||
port=int(env["POSTGRES_PORT"]),
|
||||
username=env["POSTGRES_USERNAME"],
|
||||
password=env["POSTGRES_PASSWORD"],
|
||||
database=env["POSTGRES_DATABASE"],
|
||||
driver=env.get("POSTGRES_DRIVER", "psycopg2"),
|
||||
)
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
from sqlalchemy.engine import Engine
|
||||
from sqlmodel import Session, create_engine
|
||||
|
||||
from infrastructure.postgres.config import PostgresConfig
|
||||
|
||||
|
||||
def make_engine(config: PostgresConfig) -> Engine:
|
||||
return create_engine(
|
||||
config.url(),
|
||||
pool_size=config.pool_size,
|
||||
max_overflow=config.max_overflow,
|
||||
pool_pre_ping=config.pool_pre_ping,
|
||||
pool_recycle=config.pool_recycle,
|
||||
)
|
||||
|
||||
|
||||
def make_session(engine: Engine) -> Session:
|
||||
return Session(engine)
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
from datetime import datetime, timezone
|
||||
from typing import ClassVar, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class SubTaskRow(SQLModel, table=True):
|
||||
__tablename__: ClassVar[str] = "sub_task" # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True, index=True)
|
||||
task_id: UUID = Field(foreign_key="tasks.id")
|
||||
job_started: Optional[datetime] = None
|
||||
job_completed: Optional[datetime] = None
|
||||
status: str = Field(default="waiting")
|
||||
inputs: Optional[str] = None
|
||||
outputs: Optional[str] = None
|
||||
cloud_logs_url: Optional[str] = None
|
||||
updated_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
from datetime import datetime, timezone
|
||||
from typing import ClassVar, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import Enum as SAEnum
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
from domain.tasks.tasks import Source
|
||||
|
||||
|
||||
class TaskRow(SQLModel, table=True):
|
||||
__tablename__: ClassVar[str] = "tasks" # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True, index=True)
|
||||
task_source: str
|
||||
job_started: Optional[datetime] = None
|
||||
job_completed: Optional[datetime] = None
|
||||
status: str = Field(default="waiting")
|
||||
service: Optional[str] = None
|
||||
updated_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
source: Optional[Source] = Field(
|
||||
default=None,
|
||||
sa_column=Column(
|
||||
SAEnum(
|
||||
Source,
|
||||
name="source",
|
||||
values_callable=lambda cls: [m.value for m in cls], # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType, reportUnknownVariableType]
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
source_id: Optional[str] = None
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
from typing import Any
|
||||
|
||||
|
||||
class S3Client:
|
||||
def __init__(self, boto_s3_client: Any, bucket: str) -> None:
|
||||
self._client = boto_s3_client
|
||||
self._bucket = bucket
|
||||
|
||||
@property
|
||||
def bucket(self) -> str:
|
||||
return self._bucket
|
||||
|
||||
def get_object(self, key: str) -> bytes:
|
||||
response: dict[str, Any] = self._client.get_object(
|
||||
Bucket=self._bucket, Key=key
|
||||
)
|
||||
body: bytes = response["Body"].read()
|
||||
return body
|
||||
|
||||
def put_object(self, key: str, body: bytes) -> str:
|
||||
self._client.put_object(Bucket=self._bucket, Key=key, Body=body)
|
||||
return f"s3://{self._bucket}/{key}"
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
from urllib.parse import unquote
|
||||
|
||||
|
||||
def parse_s3_uri(s3_uri: str) -> tuple[str, str]:
|
||||
if s3_uri.startswith("s3://"):
|
||||
parts = s3_uri[len("s3://") :].split("/", 1)
|
||||
if len(parts) < 2 or not parts[0] or not parts[1]:
|
||||
raise ValueError("S3 URI must include both a bucket and a key")
|
||||
return parts[0], parts[1]
|
||||
|
||||
if "?" not in s3_uri:
|
||||
raise ValueError(f"Not an s3:// URI and has no query string: {s3_uri!r}")
|
||||
base, query = s3_uri.split("?", 1)
|
||||
|
||||
if "/s3/object/" not in base:
|
||||
raise ValueError(f"Console URL has no '/s3/object/' segment: {s3_uri!r}")
|
||||
bucket = base.split("/s3/object/", 1)[1]
|
||||
|
||||
params: dict[str, str] = {}
|
||||
for item in query.split("&"):
|
||||
if "=" in item:
|
||||
name, value = item.split("=", 1)
|
||||
params[name] = value
|
||||
key = unquote(params.get("prefix", ""))
|
||||
return bucket, key
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SqsClient:
|
||||
def __init__(self, boto_sqs_client: Any, queue_url: str) -> None:
|
||||
self._client = boto_sqs_client
|
||||
self._queue_url = queue_url
|
||||
|
||||
@property
|
||||
def queue_url(self) -> str:
|
||||
return self._queue_url
|
||||
|
||||
def send(self, body: dict[str, Any]) -> str:
|
||||
response: dict[str, Any] = self._client.send_message(
|
||||
QueueUrl=self._queue_url,
|
||||
MessageBody=json.dumps(body),
|
||||
)
|
||||
message_id: str = response["MessageId"]
|
||||
return message_id
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
### 2. Add infrastructure prerequisites (shared stack)
|
||||
- Add a new ECR repository in:
|
||||
|
||||
deployment/terraform/shared/main.tf
|
||||
infrastructure/terraform/shared/main.tf
|
||||
|
||||
- Create a PR to deploy this to main then dev in order to deploy the shared stack
|
||||
|
||||
|
|
@ -38,8 +38,22 @@ module "lambda" {
|
|||
{
|
||||
STAGE = var.stage
|
||||
LOG_LEVEL = "info"
|
||||
POSTGRES_USERNAME = local.db_credentials.db_assessment_model_username
|
||||
POSTGRES_PASSWORD = local.db_credentials.db_assessment_model_password
|
||||
DB_USERNAME = local.db_credentials.db_assessment_model_username
|
||||
DB_PASSWORD = local.db_credentials.db_assessment_model_password
|
||||
GOOGLE_SOLAR_API_KEY = "test"
|
||||
SAP_PREDICTIONS_BUCKET = "test"
|
||||
CARBON_PREDICTIONS_BUCKET = "test"
|
||||
HEAT_PREDICTIONS_BUCKET = "test"
|
||||
HEATING_KWH_PREDICTIONS_BUCKET = "test"
|
||||
HOTWATER_KWH_PREDICTIONS_BUCKET = "test"
|
||||
API_KEY = "test"
|
||||
ENVIRONMENT = "test"
|
||||
SECRET_KEY = "test"
|
||||
PLAN_TRIGGER_BUCKET = "test"
|
||||
DATA_BUCKET = "test"
|
||||
EPC_AUTH_TOKEN = "test"
|
||||
ENGINE_SQS_URL = "test"
|
||||
ENERGY_ASSESSMENTS_BUCKET = "test"
|
||||
ADDRESS2UPRN_QUEUE_URL = data.terraform_remote_state.address2uprn.outputs.address2uprn_queue_url
|
||||
S3_BUCKET_NAME = data.terraform_remote_state.shared.outputs.retrofit_sap_data_bucket_name
|
||||
},
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue