-
Notifications
You must be signed in to change notification settings - Fork 8
Mareh A. #7
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?
Mareh A. #7
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 |
|---|---|---|
|
|
@@ -18,8 +18,33 @@ def fetch_with_retry(url: str, params: dict, max_retries: int = 3, timeout: int | |
| Fail immediately on: 4xx status codes. | ||
| Log each retry attempt with the error and delay. | ||
| """ | ||
| # TODO: implement retry loop with exponential backoff | ||
| raise NotImplementedError | ||
| for attempt in range(max_retries): | ||
| try: | ||
| response = requests.get( | ||
| url, | ||
| params=params, | ||
| timeout=timeout | ||
| ) | ||
| response.raise_for_status() | ||
| return response.json() | ||
| except(requests.exceptions.HTTPError) as e: | ||
| if response.status_code < 500: | ||
| logger.error(f"HTTP error {response.status_code} Cannot retry!") | ||
| raise e | ||
| if response.status_code >= 500: | ||
| logger.warning(f"Transient HTTP error {response.status_code} Retrying...") | ||
| if attempt == max_retries - 1: | ||
| raise e | ||
| wait_time = 2 ** attempt | ||
| logger.warning(f"Attempt {attempt + 1} failed with error: {response.status}") | ||
| time.sleep(wait_time) | ||
| except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: | ||
| logger.warning(f"Transient network error: {e}. Retrying...") | ||
| if attempt == max_retries - 1: | ||
| raise e | ||
| wait_time = 2 ** attempt | ||
| logger.warning(f"Attempt {attempt + 1} failed with error: {e} Retrying in {wait_time} seconds...") | ||
| time.sleep(wait_time) | ||
|
|
||
|
|
||
| def fetch_api_records() -> list[dict]: | ||
|
|
@@ -34,8 +59,23 @@ def fetch_api_records() -> list[dict]: | |
| "hourly": "temperature_2m,relative_humidity_2m", | ||
| "forecast_days": 7, | ||
| } | ||
| # TODO: | ||
| # - 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 | ||
|
|
||
| try: | ||
| data = fetch_with_retry(API_URL,params) | ||
| hourly = data.get("hourly",{}) | ||
| records = [] | ||
| temperatures = hourly.get("temperature_2m", []) | ||
| humidities = hourly.get("relative_humidity_2m", []) | ||
| for i, timestamp in enumerate(hourly.get("time",[])): | ||
|
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. This makes
Author
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. Thank you for the note! I have now moved them outside of the loop. Is this considered a sufficient solution to maintain performance? |
||
| records.append({ | ||
| "timestamp": timestamp, | ||
| "temperature_c": temperatures[i], | ||
| "humidity_pct": humidities[i], | ||
| "station": "Open-Meteo Copenhagen", | ||
| }) | ||
|
|
||
| logger.info(f"Fetched {len(records)} weather records") | ||
| return records | ||
| except Exception: | ||
| return [] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,5 +16,23 @@ def read_csv_records(path: Path) -> list[dict]: | |
| - Convert temperature_c to float and humidity_pct to int where possible. | ||
| - Leave unconvertible values (e.g. "N/A", "") as-is so validation can catch them. | ||
| """ | ||
| # TODO: implement CSV reading and normalization | ||
| raise NotImplementedError | ||
| with open(path, "r", newline="", encoding="utf-8") as file: | ||
| reader = csv.DictReader(file) | ||
| normalize_row =[] | ||
| for row in reader: | ||
| try: | ||
| temperature_c = float(row.get("temperature_c")) | ||
| except (ValueError, TypeError): | ||
|
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. Great that you catch (ValueError, TypeError) instead of just ValueError! If a value happens to arrive as None or an unsupported object type, this script handles it safely without crashing :D well done |
||
| temperature_c = row.get("temperature_c") | ||
| try: | ||
| humidity_pct = int(row.get("humidity_pct")) | ||
| except (ValueError, TypeError): | ||
| humidity_pct = row.get("humidity_pct") | ||
| normalize_row.append({ | ||
| "station": row.get("station"), | ||
| "timestamp": row.get("timestamp"), | ||
| "temperature_c": temperature_c, | ||
| "humidity_pct": humidity_pct | ||
| }) | ||
| return normalize_row | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,17 +17,31 @@ | |
|
|
||
| def run_pipeline() -> None: | ||
| OUTPUT_DIR.mkdir(exist_ok=True) | ||
|
|
||
| # TODO — implement each step in order: | ||
| # | ||
| # 1. Fetch records from Open-Meteo API using fetch_api_records() | ||
| # 2. Read records from CSV using read_csv_records(CSV_PATH) | ||
| # 3. Open a DB connection, create tables, insert all raw records (both sources) | ||
| # 4. Validate all records — collect valid WeatherReading objects and error dicts | ||
| # 5. Upsert valid records into weather_readings | ||
| # 6. Save error dicts as JSON to output/error_report.json | ||
| # 7. Print the pipeline summary in the format below. | ||
| # | ||
| api_fetching = fetch_api_records() | ||
| csv_reading = read_csv_records(CSV_PATH) | ||
| db_conn = get_connection() | ||
| create_tables(db_conn) | ||
| insert_raw(db_conn, api_fetching, source="API") | ||
| insert_raw(db_conn, csv_reading, source="CSV") | ||
| db_conn.commit() | ||
| api_valid, api_errors = validate_records(api_fetching, source="API") | ||
| csv_valid, csv_errors = validate_records(csv_reading, source="CSV") | ||
| all_valid_readings = api_valid + csv_valid | ||
| all_errors = api_errors + csv_errors | ||
| upsert_readings(db_conn, all_valid_readings) | ||
| db_conn.commit() | ||
| with open(OUTPUT_DIR / "error_report.json", "w") as f: | ||
| json.dump(all_errors, f, indent=2) | ||
| total_in_db = count_readings(db_conn) | ||
| print("=== Pipeline Summary ===") | ||
| print(f"API records fetched: {len(api_fetching)}") | ||
| print(f"CSV records read: {len(csv_reading)}") | ||
| print(f"Total raw records: {len(api_fetching) + len(csv_reading)}") | ||
| print(f"Valid records: {len(all_valid_readings)}") | ||
| print(f"Invalid records: {len(all_errors)}") | ||
| print(f"Records in database: {total_in_db}") | ||
| print(f"Error report: {OUTPUT_DIR / 'error_report.json'}") | ||
| db_conn.close() | ||
|
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. nitpick: Relying on |
||
| # Note: the API count varies by time of day (Open-Meteo returns up to 168 hourly | ||
| # records for 7 forecast days; the exact number depends on the current UTC hour). | ||
| # The CSV contributes 6 invalid records and 4 valid ones; the duplicate Copenhagen | ||
|
|
@@ -43,8 +57,6 @@ def run_pipeline() -> None: | |
| # Records in database: 169 | ||
| # Error report: output/error_report.json | ||
|
|
||
| raise NotImplementedError | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| logging.basicConfig(level=logging.INFO) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import requests | ||
| import json | ||
| from pathlib import Path | ||
| import os | ||
| from dotenv import load_dotenv | ||
|
|
||
| load_dotenv() | ||
| token = os.environ.get("AZURE_TOKEN") | ||
| subscription_id = os.environ.get("AZURE_SUBSCRIPTION_ID") | ||
|
|
||
| OUTPUT_DIR = Path("output") | ||
| OUTPUT_DIR.mkdir(exist_ok=True) | ||
|
|
||
| url = f"https://management.azure.com/subscriptions/{subscription_id}/resourcegroups?api-version=2024-03-01" | ||
| headers = {"Authorization": f"Bearer {token}"} | ||
|
|
||
| response = requests.get(url, headers=headers, timeout=10) | ||
| response.raise_for_status() | ||
|
|
||
| #for rg in response.json()["value"]: | ||
| #print(f"{rg['name']}: {rg['location']}") | ||
|
|
||
|
|
||
| azure_data = response.json() | ||
| output_file = OUTPUT_DIR / "azure_resource_groups.json" | ||
| with open(output_file, "w") as f: | ||
| json.dump(azure_data, f, indent=2) | ||
|
|
||
| print(f"Successfully saved full response to {output_file}") |
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.
Very good use of
executemanyinstead of looping and running separate database roundtrips viaconn.execute()for each row! This is significantly faster.