diff --git a/README.md b/README.md index 2f77bf4..cc42c69 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ # python-webserver-template -Python3 website template including git actions \ No newline at end of file +Python3 website template including git actions + +## Example `curl` Commands + +### Convert Specific Time Between Two Timezones + +```sh +curl -X POST http://localhost:5000/api/v1/convert -H "Content-Type: application/json" -d '{ + "from_tz": "Australia/Sydney", + "to_tz": "UTC", + "time": "2023-10-10T15:00:00" +}' +``` + +### Convert Specific Time Between Multiple Timezones + +```sh +curl -X POST http://localhost:5000/api/v1/convert_multiple -H "Content-Type: application/json" -d '{ + "from_tz": "Australia/Sydney", + "time": "2023-10-10T15:00:00", + "to_tzs": ["UTC", "America/New_York", "Europe/London"] +}' +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8cf7fcb..83d7719 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flask gunicorn requests -python-dotenv \ No newline at end of file +python-dotenv +pytz \ No newline at end of file diff --git a/server.py b/server.py index 240a1a8..0e3148e 100644 --- a/server.py +++ b/server.py @@ -13,8 +13,12 @@ from flask import ( import os import json import requests -from datetime import datetime +from datetime import datetime, timedelta import dotenv +from pytz import timezone +import pytz +import re + dotenv.load_dotenv() @@ -76,6 +80,118 @@ def wellknown(path): def index(): return render_template("index.html") +# Mapping of short timezone names to full names +SHORT_TZ_MAP = { + "GMT": "Etc/GMT", + "PST": "America/Los_Angeles", + "PDT": "America/Los_Angeles", + "MST": "America/Denver", + "MDT": "America/Denver", + "CST": "America/Chicago", + "CDT": "America/Chicago", + "EST": "America/New_York", + "EDT": "America/New_York", + "AKST": "America/Anchorage", + "AKDT": "America/Anchorage", + "HST": "Pacific/Honolulu", + "HDT": "Pacific/Honolulu", + "AST": "America/Puerto_Rico", + "NST": "America/St_Johns", + "BST": "Europe/London", + "CET": "Europe/Paris", + "CEST": "Europe/Paris", + "EET": "Europe/Athens", + "EEST": "Europe/Athens", + "MSK": "Europe/Moscow", + "MSD": "Europe/Moscow", + "IST": "Asia/Kolkata", + "PKT": "Asia/Karachi", + "WIB": "Asia/Jakarta", + "WITA": "Asia/Makassar", + "WIT": "Asia/Jayapura", + "CST": "Asia/Shanghai", + "JST": "Asia/Tokyo", + "KST": "Asia/Seoul", + "AEDT": "Australia/Sydney", + "AEST": "Australia/Sydney", + "ACST": "Australia/Adelaide", + "ACDT": "Australia/Adelaide", + "AWST": "Australia/Perth", +} + +def get_full_timezone(tz): + match = re.match(r"^UTC([+-]\d{1,2}):?(\d{2})?$", tz) + if match: + hours = int(match.group(1)) + minutes = int(match.group(2)) if match.group(2) else 0 + return pytz.FixedOffset(hours * 60 + minutes) + full_tz = SHORT_TZ_MAP.get(tz, tz) + return pytz.timezone(full_tz) if isinstance(full_tz, str) else None + +@app.route("/api/v1/convert", methods=["POST"]) +def convert(): + data = request.json + from_tz = get_full_timezone(data.get("from_tz").replace("_", "/")) + to_tz = get_full_timezone(data.get("to_tz").replace("_", "/")) + time = data.get("time") + + if from_tz is None or to_tz is None: + return jsonify({"error": "Invalid timezone"}), 400 + + # Parse the input time + try: + input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S") + except ValueError: + input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M") + + try: + # Get the timezone + from_tz_time = from_tz.localize(input_time) + + # Convert the time + to_tz_time = from_tz_time.astimezone(to_tz) + except Exception as e: + return jsonify({"error": str(e)}), 400 + + return jsonify( + { + "from": from_tz_time.strftime("%Y-%m-%d %H:%M:%S %Z"), + "to": to_tz_time.strftime("%Y-%m-%d %H:%M:%S %Z"), + } + ) + +@app.route("/api/v1/convert_multiple", methods=["POST"]) +def convert_multiple(): + data = request.json + from_tz = get_full_timezone(data.get("from_tz").replace("_", "/")) + time = data.get("time") + to_tz_list = [(tz, get_full_timezone(tz.replace("_", "/"))) for tz in data.get("to_tzs") if len(tz) > 0] + + if from_tz is None or any(tz[1] is None for tz in to_tz_list): + return jsonify({"error": "Invalid timezone"}), 400 + + # Parse the input time + try: + input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S") + except ValueError: + input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M") + + try: + # Get the timezone + from_tz_time = from_tz.localize(input_time) + + # Convert the time to each timezone in the list + results = {} + for original_tz, tz in to_tz_list: + to_tz_time = from_tz_time.astimezone(tz) + offset = to_tz_time.strftime('%z') + offset_formatted = f"UTC{offset[:3]}:{offset[3:]}" if offset else "" + tz_name = to_tz_time.tzname() if to_tz_time.tzname() else offset_formatted + results[original_tz] = f"{to_tz_time.strftime('%Y-%m-%d %H:%M:%S')} {tz_name}" + except Exception as e: + return jsonify({"error": str(e)}), 400 + + return jsonify(results) @app.route("/<path:path>") def catch_all(path: str): diff --git a/templates/assets/css/index.css b/templates/assets/css/index.css index 9635f9c..23a89c3 100644 --- a/templates/assets/css/index.css +++ b/templates/assets/css/index.css @@ -11,6 +11,52 @@ h1 { margin-top: 10%; text-align: center; } +form { + margin: 20px 0; +} +label { + display: block; + margin: 10px 0 5px; +} +input { + padding: 5px; + width: 80%; + max-width: 300px; +} +button { + margin-top: 10px; + padding: 10px 20px; + background-color: #ffffff; + color: #000000; + border: none; + cursor: pointer; +} +button:hover { + background-color: #dddddd; +} +#results { + margin-top: 20px; + text-align: left; +} +.results-container { + margin-top: 20px; + text-align: center; +} +.results-table { + width: 80%; + margin: 0 auto; + border-collapse: collapse; +} +.results-table th, .results-table td { + border: 1px solid #ffffff; + padding: 10px; +} +.results-table th { + background-color: #333333; +} +.results-table td { + background-color: #444444; +} a { color: #ffffff; text-decoration: none; diff --git a/templates/assets/js/index.js b/templates/assets/js/index.js new file mode 100644 index 0000000..14aae47 --- /dev/null +++ b/templates/assets/js/index.js @@ -0,0 +1,80 @@ +document.getElementById('time-converter-form').addEventListener('submit', function(event) { + event.preventDefault(); + + const fromTz = document.getElementById('from-tz').value; + const time = document.getElementById('time').value; + const toTzs = document.getElementById('to-tzs').value.split(','); + + // Update URL with query parameters + const params = new URLSearchParams({ + from_tz: fromTz, + time: time, + to_tzs: toTzs.join(',') + }); + window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`); + + fetch('/api/v1/convert_multiple', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + from_tz: fromTz, + time: time, + to_tzs: toTzs + }) + }) + .then(response => response.json()) + .then(data => { + const resultsDiv = document.getElementById('results'); + resultsDiv.innerHTML = '<h2>Converted Times:</h2>'; + const table = document.createElement('table'); + table.classList.add('results-table'); + const thead = document.createElement('thead'); + const tbody = document.createElement('tbody'); + + const headerRow = document.createElement('tr'); + const th1 = document.createElement('th'); + th1.textContent = 'Timezone'; + const th2 = document.createElement('th'); + th2.textContent = 'Converted Time'; + headerRow.appendChild(th1); + headerRow.appendChild(th2); + thead.appendChild(headerRow); + + for (const [tz, convertedTime] of Object.entries(data)) { + const row = document.createElement('tr'); + const cell1 = document.createElement('td'); + cell1.textContent = tz; + const cell2 = document.createElement('td'); + cell2.textContent = convertedTime; + row.appendChild(cell1); + row.appendChild(cell2); + tbody.appendChild(row); + } + + table.appendChild(thead); + table.appendChild(tbody); + resultsDiv.appendChild(table); + }) + .catch(error => { + console.error('Error:', error); + }); +}); + +// Load parameters from URL if available +window.addEventListener('load', function() { + const params = new URLSearchParams(window.location.search); + const fromTz = params.get('from_tz'); + const time = params.get('time'); + const toTzs = params.get('to_tzs'); + + if (fromTz && time && toTzs) { + document.getElementById('from-tz').value = fromTz; + document.getElementById('time').value = time; + document.getElementById('to-tzs').value = toTzs; + + // Trigger form submission to display results + document.getElementById('time-converter-form').dispatchEvent(new Event('submit')); + } +}); diff --git a/templates/index.html b/templates/index.html index bb349f8..f24bc94 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Nathan.Woodburn/</title> + <title>Time Converter</title> <link rel="icon" href="/assets/img/favicon.png" type="image/png"> <link rel="stylesheet" href="/assets/css/index.css"> </head> @@ -12,8 +12,22 @@ <body> <div class="spacer"></div> <div class="centre"> - <h1>Nathan.Woodburn/</h1> + <h1>Time Converter</h1> + <form id="time-converter-form"> + <label for="from-tz">From Timezone:</label> + <input type="text" id="from-tz" name="from-tz" required> + <br> + <label for="time">Time:</label> + <input type="datetime-local" id="time" name="time" required> + <br> + <label for="to-tzs">To Timezones (comma separated):</label> + <input type="text" id="to-tzs" name="to-tzs" required> + <br> + <button type="submit">Convert</button> + </form> + <div id="results" class="results-container"></div> </div> + <script src="/assets/js/index.js"></script> </body> </html> \ No newline at end of file