Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@
# echo "POSTGRES_URL=$(az keyvault secret show --vault-name kv-hyf-data --name postgres-url --query value -o tsv)" >> .env
# echo "AZURE_STORAGE_CONNECTION_STRING=$(az keyvault secret show --vault-name kv-hyf-data --name storage-connection-string --query value -o tsv)" >> .env

POSTGRES_URL=postgresql://<user>:<password>@<server>.postgres.database.azure.com:5432/<db>?sslmode=require
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net
# Your personal schema on the shared Postgres server — replace with your GitHub handle.
# All tables are created here so they don't collide with other students.
DB_SCHEMA=dev_<your_github_handle>
LOG_LEVEL=INFO
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## Architecture

```text
[Your API] ──► pipeline.py ──► Pydantic validation ──► Postgres INSERT
[Your API] ──► pipeline.py ──► Pydantic validation ──► Postgres INSERT (your schema)
──► Blob Storage (raw JSON)
```

Expand All @@ -18,6 +18,8 @@
cp .env.example .env
echo "POSTGRES_URL=$(az keyvault secret show --vault-name kv-hyf-data --name postgres-url --query value -o tsv)" >> .env
echo "AZURE_STORAGE_CONNECTION_STRING=$(az keyvault secret show --vault-name kv-hyf-data --name storage-connection-string --query value -o tsv)" >> .env
# Set your personal schema (replace alice with your GitHub handle):
echo "DB_SCHEMA=dev_alice" >> .env

# 2. Install dependencies
uv sync
Expand Down Expand Up @@ -57,12 +59,26 @@ az containerapp job create \
--env-vars \
POSTGRES_URL="$(az keyvault secret show --vault-name kv-hyf-data --name postgres-url --query value -o tsv)" \
AZURE_STORAGE_CONNECTION_STRING="$(az keyvault secret show --vault-name kv-hyf-data --name storage-connection-string --query value -o tsv)" \
DB_SCHEMA=dev_alice \
LOG_LEVEL=INFO

# Trigger a manual run for testing (without waiting for the schedule)
az containerapp job start --name my-pipeline-job --resource-group rg-hyf-data
```

## Enable ACR push from CI (optional)

The `push-to-acr` job in `.github/workflows/ci.yml` is commented out by default.
To enable it, add two secrets in your repo's **Settings → Secrets and variables → Actions**:

| Secret name | Value |
|-------------|-------|
| `ACR_USERNAME` | `hyfregistry` |
| `ACR_PASSWORD` | Ask your teacher for the ACR password |

Then uncomment the `push-to-acr` job in `ci.yml`. Every push to `main` will build
and push the image automatically.

## Install psql

`psql` is the Postgres command-line client used to verify results. Install it once:
Expand All @@ -88,8 +104,8 @@ Download and run the installer from [postgresql.org/download/windows](https://ww
# Check job execution
az containerapp job execution list --name my-pipeline-job --resource-group rg-hyf-data --output table

# Check Postgres (set POSTGRES_URL first — see Run locally above)
psql "$POSTGRES_URL" -c "SELECT COUNT(*) FROM your_table_name;" # replace with your table name
# Check Postgres (replace dev_alice with your schema, <your_table> with your table name)
psql "$POSTGRES_URL" -c "SELECT COUNT(*) FROM dev_alice.<your_table>;"

# Check Blob Storage
az storage blob list --account-name hyfstoragedev --container-name raw --prefix pipeline/ --output table
Expand Down
60 changes: 38 additions & 22 deletions src/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
import os
from contextlib import closing
from datetime import datetime
from datetime import datetime, timezone

import pandas as pd
import psycopg2
Expand All @@ -15,34 +15,48 @@


def insert_readings(df: pd.DataFrame) -> None:
"""Insert a DataFrame of readings into Postgres."""
"""Insert a DataFrame of readings into Postgres.

Creates the table in your personal schema (DB_SCHEMA env var, e.g. dev_alice).
All CREATE TABLE and INSERT statements run inside that schema so your tables
never collide with other students on the shared server.
"""
db_url = os.environ["POSTGRES_URL"]
schema = os.environ.get("DB_SCHEMA", "public")

with closing(psycopg2.connect(db_url)) as conn:
cur = conn.cursor()

# TODO: Replace 'weather_readings' with a unique name (e.g. alice_weather_readings)
# to avoid collisions on the shared Postgres server. See Week 7 Gotcha #8.
cur.execute("""
CREATE TABLE IF NOT EXISTS weather_readings (
id SERIAL PRIMARY KEY,
city TEXT NOT NULL,
temperature REAL NOT NULL,
humidity REAL NOT NULL,
timestamp TEXT NOT NULL
)
""")

for _, row in df.iterrows():
with conn.cursor() as cur:
cur.execute(
"INSERT INTO weather_readings (city, temperature, humidity, timestamp) VALUES (%s, %s, %s, %s)",
(row["city"], row["temperature"], row["humidity"], row["timestamp"]),
f"CREATE SCHEMA IF NOT EXISTS {schema}" # noqa: S608
)
cur.execute(f"SET search_path TO {schema}") # noqa: S608

# TODO: Replace 'weather_readings' with a name that describes your data.
cur.execute("""
CREATE TABLE IF NOT EXISTS weather_readings (
id SERIAL PRIMARY KEY,
city TEXT NOT NULL,
temperature REAL NOT NULL,
humidity REAL NOT NULL,
timestamp TEXT NOT NULL
)
""")

for _, row in df.iterrows():
cur.execute(
"INSERT INTO weather_readings (city, temperature, humidity, timestamp)"
" VALUES (%s, %s, %s, %s)",
(
row["city"],
row["temperature"],
row["humidity"],
row["timestamp"],
),
)

conn.commit()
cur.close()

log.info("Inserted %d rows into Postgres", len(df))
log.info("Inserted %d rows into %s.weather_readings", len(df), schema)


def upload_raw_json(raw_data) -> None:
Expand All @@ -55,7 +69,9 @@ def upload_raw_json(raw_data) -> None:
except ResourceExistsError:
pass

blob_name = f"pipeline/{datetime.utcnow().strftime('%Y-%m-%d_%H%M%S')}.json"
blob_name = (
f"pipeline/{datetime.now(timezone.utc).strftime('%Y-%m-%d_%H%M%S')}.json"
)
container.upload_blob(
name=blob_name,
data=json.dumps(raw_data).encode("utf-8"),
Expand Down
Loading