-
Notifications
You must be signed in to change notification settings - Fork 8
Hannah #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Hannah #6
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,22 @@ def fetch_with_retry(url: str, params: dict, max_retries: int = 3, timeout: int | |
| Log each retry attempt with the error and delay. | ||
| """ | ||
| # TODO: implement retry loop with exponential backoff | ||
| raise NotImplementedError | ||
| for attempt in range(1, max_retries + 1): | ||
| try: | ||
| response = requests.get(url, params=params, timeout=timeout) | ||
| if response.status_code >= 500: | ||
| raise requests.HTTPError(f"Server error: {response.status_code}") | ||
| elif response.status_code >= 400: | ||
| response.raise_for_status() # Will raise HTTPError for 4xx | ||
| return response.json() | ||
| except (requests.ConnectionError, requests.Timeout, requests.HTTPError) as e: | ||
| if attempt == max_retries or isinstance(e, requests.HTTPError) and e.response.status_code < 500: | ||
| logger.error(f"Request failed after {attempt} attempts: {e}") | ||
| raise | ||
| delay = 2 ** (attempt - 1) # Exponential backoff: 1s, 2s, 4s... | ||
| logger.warning(f"Request error: {e}. Retrying in {delay} seconds... (Attempt {attempt}/{max_retries})") | ||
| time.sleep(delay) | ||
| raise RuntimeError("Exceeded maximum retry attempts") | ||
|
|
||
|
|
||
| def fetch_api_records() -> list[dict]: | ||
|
|
@@ -38,4 +53,23 @@ def fetch_api_records() -> list[dict]: | |
| # - Call fetch_with_retry with API_URL and params | ||
| # - The API returns {"hourly": {"time": [...], "temperature_2m": [...], "relative_humidity_2m": [...]}} | ||
| # - Flatten to a list of dicts; set station="Open-Meteo Copenhagen" for all records | ||
| raise NotImplementedError | ||
| for attempt in range(1, 4): | ||
| try: | ||
| data = fetch_with_retry(API_URL, params) | ||
| hourly = data.get("hourly", {}) | ||
| times = hourly.get("time", []) | ||
| temps = hourly.get("temperature_2m", []) | ||
| hums = hourly.get("relative_humidity_2m", []) | ||
| records = [] | ||
| for time_str, temp, hum in zip(times, temps, hums): | ||
| records.append({ | ||
| "station": "Open-Meteo Copenhagen", | ||
| "timestamp": time_str, | ||
| "temperature_c": temp, | ||
| "humidity_pct": hum, | ||
| }) | ||
| return records | ||
| except Exception as e: | ||
| logger.error(f"Failed to fetch API records: {e}") | ||
| return [] | ||
| raise RuntimeError("Exceeded maximum retry attempts") | ||
|
Comment on lines
+56
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fetch_with_retry() already retries with exponential backoff. Wrapping it in another for attempt in range(1, 4) loop is unnecessary. Also, the except Exception block returns [] on the first failure, which skips retries entirely. If the API is temporarily down, you silently get zero records instead of retrying. Suggested shape: data = fetch_with_retry(API_URL, params)
hourly = data.get("hourly", {})
# ... flatten and return recordsOnly return [] when the API response has no hourly data, not when a network error occurs. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| # Step 1 — Task 4: Pydantic Validation | ||
| # Define the WeatherReading model that every ingested record must pass. | ||
| # Both the API and CSV data flow through this model before reaching the database. | ||
| from pydantic import BaseModel, Field, field_validator | ||
| from pydantic import BaseModel, Field, field_validator, ValidationError | ||
|
|
||
|
|
||
| class WeatherReading(BaseModel): | ||
|
|
@@ -14,4 +14,24 @@ class WeatherReading(BaseModel): | |
| @classmethod | ||
| def clean_station(cls, v: str) -> str: | ||
| # TODO: strip whitespace and convert to title case | ||
| raise NotImplementedError | ||
| v = v.strip() | ||
| v = v.title() | ||
| return v | ||
|
|
||
| def validate_readings(raw_records: list[dict]) -> tuple[list[WeatherReading], list[dict]]: | ||
| """Validate a list of raw records, returning valid records and errors.""" | ||
| valid = [] | ||
| errors = [] | ||
|
|
||
| for i, record in enumerate(raw_records): | ||
| try: | ||
| reading = WeatherReading(**record) | ||
| valid.append(reading) | ||
| except ValidationError as e: | ||
| errors.append({ | ||
| "index": i, | ||
| "record": record, | ||
| "errors": e.errors(), | ||
| }) | ||
|
|
||
| return valid, errors | ||
|
Comment on lines
+21
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You implemented validation twice: here in validate_readings() and correctly in validate.py as validate_records(). You can remove this as pipeline only uses validate.py. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,53 @@ | |
| def run_pipeline() -> None: | ||
| OUTPUT_DIR.mkdir(exist_ok=True) | ||
|
|
||
| # step 1 - fetch from API | ||
| api_records = fetch_api_records() | ||
|
|
||
| # step 2 - read from CSV | ||
| csv_records = read_csv_records(CSV_PATH) | ||
|
|
||
| # step 3 - combine both | ||
| all_records = api_records + csv_records | ||
|
|
||
| # step 4 - open database once | ||
| conn = get_connection() | ||
| create_tables(conn) | ||
|
|
||
| # step 5 - insert all raw records | ||
| insert_raw(conn, api_records, "api") | ||
| insert_raw(conn, csv_records, "csv") | ||
| conn.commit() | ||
|
|
||
| # step 6 - validate all records | ||
| valid_api, errors_api = validate_records(api_records, "api") | ||
| valid_csv, errors_csv = validate_records(csv_records, "csv") | ||
| valid = valid_api + valid_csv | ||
| errors = errors_api + errors_csv | ||
|
|
||
| # step 7 - upsert valid records | ||
| upsert_readings(conn, valid) | ||
| conn.commit() | ||
|
|
||
| # step 8 - save error report | ||
| error_report_path = OUTPUT_DIR / "error_report.json" | ||
| with error_report_path.open("w", encoding="utf-8") as f: | ||
| json.dump(errors, f, indent=2, default=str) | ||
|
|
||
| # step 9 - print summary | ||
| print("=== Pipeline Summary ===") | ||
| print(f"API records fetched: {len(api_records)}") | ||
| print(f"CSV records read: {len(csv_records)}") | ||
| print(f"Total raw records: {len(all_records)}") | ||
| print(f"Valid records: {len(valid)}") | ||
| print(f"Invalid records: {len(errors)}") | ||
| print(f"Records in database: {count_readings(conn)}") | ||
| print(f"Error report: {error_report_path}") | ||
|
|
||
| conn.close() | ||
|
|
||
|
|
||
|
|
||
| # TODO — implement each step in order: | ||
| # | ||
| # 1. Fetch records from Open-Meteo API using fetch_api_records() | ||
|
|
@@ -43,7 +90,6 @@ def run_pipeline() -> None: | |
| # Records in database: 169 | ||
| # Error report: output/error_report.json | ||
|
|
||
| raise NotImplementedError | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can remove the TODO blocks after implementing |
||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
raise requests.HTTPError(f"Server error: {response.status_code}") creates an error without a .response object. Your retry check on line 31 (e.response.status_code) can crash if that path is hit.
Prefer:
or attach the response: response.raise_for_status() already handles this correctly for 5xx.