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
53 changes: 53 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Tests

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
code-quality:
name: Lint
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Set up Python
run: uv python install

- name: Install dependencies
run: uv sync

- name: Run linter
run: uv run ruff check .

- name: Run formatter check
run: uv run ruff format --check .

test:
name: Test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Set up Python
run: uv python install

- name: Install dependencies
run: uv sync

- name: Run tests
run: uv run pytest tests/ -v
15 changes: 12 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
format: ## Run black and isort
black .
isort .
install: ## Install dependencies using uv
uv sync

test: ## Run tests
uv run pytest tests/ -v

lint: ## Run ruff linter
uv run ruff check .

format: ## Run ruff formatter and linter
uv run ruff check --fix .
uv run ruff format .

update_reqs: ## Update requirements.txt
pipreqs --force .
77 changes: 75 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import json
import os
import re

import pandas as pd
from datetime import datetime
from flask import Flask, render_template, request, send_file, session

from core import get_flight, make_ics_from_selected_df_index
from core import get_flight, make_ics_from_selected_df_index, make_ics_from_manual_data

app = Flask(__name__)

Expand Down Expand Up @@ -42,11 +43,83 @@ def create_ical_from_selected(index):
return "No flight data found", 400

df = pd.read_json(df_json, orient="split")

# Check if custom times were provided
custom_departure = request.form.get("custom_departure")
custom_arrival = request.form.get("custom_arrival")

if custom_departure and custom_arrival:
# Update the DataFrame with custom times
df.at[index, "scheduled_departure"] = custom_departure
df.at[index, "scheduled_arrival"] = custom_arrival

ics_data = make_ics_from_selected_df_index(df, index)
flight = df.iloc[index]["flight_number"]

return send_file(ics_data, as_attachment=True, download_name=f"{flight}.ics")


@app.route("/manual_entry")
def manual_entry():
return render_template("manual_entry.html")


@app.route("/create_manual_event", methods=["POST"])
def create_manual_event():
try:
# Get all form data
flight_data = {
"flight_number": request.form.get("flight_number"),
"airline_name": request.form.get("airline_name"),
"origin_airport": request.form.get("origin_airport"),
"origin_airport_code": request.form.get("origin_airport_code"),
"destination_airport": request.form.get("destination_airport"),
"destination_airport_code": request.form.get("destination_airport_code"),
"scheduled_departure": request.form.get("scheduled_departure"),
"scheduled_arrival": request.form.get("scheduled_arrival"),
"origin_timezone": request.form.get("origin_timezone"),
"destination_timezone": request.form.get("destination_timezone"),
}

# Validate required fields
required_fields = [
"flight_number",
"airline_name",
"origin_airport",
"origin_airport_code",
"destination_airport",
"destination_airport_code",
"scheduled_departure",
"scheduled_arrival",
"origin_timezone",
"destination_timezone",
]
for field in required_fields:
if not flight_data.get(field):
raise ValueError(f"Missing required field: {field}")

# Validate airport codes (should be 3 uppercase letters)
if not re.match(r"^[A-Z]{3}$", flight_data["origin_airport_code"]):
raise ValueError("Origin airport code must be 3 uppercase letters")
if not re.match(r"^[A-Z]{3}$", flight_data["destination_airport_code"]):
raise ValueError("Destination airport code must be 3 uppercase letters")

# Validate datetime format (YYYY-MM-DD HH:MM)
try:
datetime.strptime(flight_data["scheduled_departure"], "%Y-%m-%d %H:%M")
datetime.strptime(flight_data["scheduled_arrival"], "%Y-%m-%d %H:%M")
except ValueError:
raise ValueError("Invalid datetime format. Use: yyyy-mm-dd hh:mm")

# Create iCal file from manual data
ics_data = make_ics_from_manual_data(flight_data)
flight = flight_data["flight_number"]

return send_file(ics_data, as_attachment=True, download_name=f"{flight}.ics")
except Exception as e:
error_message = str(e)
return render_template("manual_entry.html", error=error_message)


if __name__ == "__main__":
app.run()
53 changes: 47 additions & 6 deletions core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import io
import re
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone as dt_timezone

import icalendar
import pandas as pd
Expand Down Expand Up @@ -207,8 +207,8 @@ def make_ical_event(data: dict):
event = icalendar.Event()
event.add(
"summary",
f'🛫 {data["airline_name"]} {data["origin_airport_code"]} ➡️ '
f'{data["destination_airport_code"]} {data["flight_number"]}',
f"🛫 {data['airline_name']} {data['origin_airport_code']} ➡️ "
f"{data['destination_airport_code']} {data['flight_number']}",
)
origin_tz = timezone(data["origin_timezone"])
destination_tz = timezone(data["destination_timezone"])
Expand All @@ -222,12 +222,12 @@ def make_ical_event(data: dict):

event.add("dtstart", dtstart)
event.add("dtend", dtend)
event.add("location", f'{data["origin_airport"]}')
event.add("location", f"{data['origin_airport']}")
event.add(
"description",
f'{data["airline_name"]} flight {data["flight_number"]} / Departs {data["origin_airport"]}, {data["origin_airport_code"]}',
f"{data['airline_name']} flight {data['flight_number']} / Departs {data['origin_airport']}, {data['origin_airport_code']}",
)
event.add("dtstamp", datetime.now())
event.add("dtstamp", datetime.now(dt_timezone.utc))

event.add("status", "CONFIRMED")

Expand All @@ -238,3 +238,44 @@ def make_ical_event(data: dict):
def save_ical_event(ical_event: bytes):
ical_bytes = io.BytesIO(ical_event)
return ical_bytes


def make_ics_from_manual_data(data: dict):
"""Create iCal event from manually entered data."""
cal = icalendar.Calendar()
cal.add("prodid", "-//eluceo/ical//2.0/EN")
cal.add("version", "2.0")
cal.add("calscale", "GREGORIAN")
cal.add("method", "REQUEST")

event = icalendar.Event()
event.add(
"summary",
f"🛫 {data['airline_name']} {data['origin_airport_code']} ➡️ "
f"{data['destination_airport_code']} {data['flight_number']}",
)

origin_tz = timezone(data["origin_timezone"])
destination_tz = timezone(data["destination_timezone"])

# Parse datetime format "YYYY-MM-DD HH:MM" to datetime
dtstart = origin_tz.localize(
datetime.strptime(data["scheduled_departure"], "%Y-%m-%d %H:%M")
)
dtend = destination_tz.localize(
datetime.strptime(data["scheduled_arrival"], "%Y-%m-%d %H:%M")
)

event.add("dtstart", dtstart)
event.add("dtend", dtend)
event.add("location", f"{data['origin_airport']}")
event.add(
"description",
f"{data['airline_name']} flight {data['flight_number']} / Departs {data['origin_airport']}, {data['origin_airport_code']}",
)
event.add("dtstamp", datetime.now(dt_timezone.utc))
event.add("status", "CONFIRMED")

cal.add_component(event)
ical_event = cal.to_ical()
return save_ical_event(ical_event)
21 changes: 21 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[project]
name = "flightcal"
version = "0.1.0"
description = "Flight calendar event generator"
requires-python = ">=3.9"
dependencies = [
"Flask==3.0.3",
"icalendar==6.0.1",
"pandas>=2.2.0",
"pyflightdata==0.8.6.2",
"pytz==2024.1",
"gunicorn==23.0.0",
"numpy>=1.26.0",
]

[dependency-groups]
dev = [
"ruff>=0.8.0",
"pytest>=8.0.0",
"pytest-mock>=3.12.0",
]
7 changes: 0 additions & 7 deletions requirements.txt

This file was deleted.

24 changes: 21 additions & 3 deletions templates/index.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down Expand Up @@ -64,6 +66,20 @@
input[type="submit"]:hover {
background-color: #0056b3;
}
.manual-entry-button {
margin-top: 15px;
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: #28a745;
color: white;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.manual-entry-button:hover {
background-color: #218838;
}
.emoji {
font-size: 5rem;
cursor: pointer;
Expand Down Expand Up @@ -95,6 +111,7 @@ <h1>Create Flight Calendar Event</h1>
<div class="error-message">
{{ error }}
</div>
<button class="manual-entry-button" onclick="window.location.href='/manual_entry'">Enter Flight Details Manually</button>
{% endif %}

<p>Enter your flight number and date to create an iCal event.</p>
Expand All @@ -103,7 +120,7 @@ <h1>Create Flight Calendar Event</h1>
<input type="text" id="flight_number" name="flight_number" required oninput="this.value = this.value.toUpperCase().replace(/[^A-Z0-9]/g, '')"><br><br>

<label for="flight_date">Flight Date:</label>
<input type="date" id="flight_date" name="flight_date" required><br><br>
<input type="text" id="flight_date" name="flight_date" required placeholder="yyyy-mm-dd" pattern="\d{4}-\d{2}-\d{2}" title="Date format: yyyy-mm-dd"><br><br>

<input type="submit" value="Create Event">
</form>
Expand All @@ -113,8 +130,9 @@ <h1>Create Flight Calendar Event</h1>
Made with ❤️ by <a href="https://mschm.id" target="_blank">Michael Schmid</a>
</footer>
<script>
document.querySelector('.emoji').addEventListener('click', function() {
document.querySelector('.emoji').addEventListener('click', function() {
this.classList.toggle('clicked');
});
</script>
</body>
</body>
</html>
Loading