diff --git a/app.py b/app.py index cb89d57b..281f9e9f 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,114 @@ -from flask import Flask +from flask import Flask, jsonify, request, send_from_directory +from tariff_utils import calculate_start_time +import os +from datetime import datetime, timedelta +import random + +api_key = os.getenv("OCTOPUS_KEY") + app = Flask(__name__) +# Read API Key from environment variable +# VALID_API_KEYS = {os.getenv('API_KEY')} # Assuming there's only one key for simplicity + +# def require_api_key(f): +# def decorated(*args, **kwargs): +# api_key = request.headers.get('API-Key') +# if api_key not in VALID_API_KEYS: +# abort(401) # Unauthorized access if the API key is not valid +# return f(*args, **kwargs) +# return decorated + @app.route('/') +# @require_api_key def hello_world(): - return 'Hello from Koyeb' + return jsonify(message="Hello, Happy Flasking!") + +@app.route('/api/spec') +def api_spec(): + return send_from_directory('static', 'api_spec.yaml') + +# --- New route: serve XML files from /static/xml --- +@app.route('/xml/') +def serve_xml(filename: str): + """Serve XML files from the /static/xml directory via /xml/.xml. + + If the requested file does not exist, return a JSON 404 with a clear message. + Only .xml files are allowed (anything else 404s). + """ + if not filename.lower().endswith('.xml'): + abort(404) + + xml_dir = os.path.join(app.root_path, 'static', 'xml') + file_path = os.path.join(xml_dir, filename) + + # Ensure the file exists; if not, return a simple JSON 404 response + if not os.path.isfile(file_path): + return jsonify(error="File not found"), 404 + + # send_from_directory safely serves files from a specific folder + return send_from_directory(xml_dir, filename, mimetype='application/xml') + +# send_from_directory safely serves files from a specific folder +# \1# --- New route: serve HTML files from /static/html --- +@app.route('/html/') +def serve_html(filename: str): + """Serve HTML files from the /static/html directory via /html/.html. + + If the requested file does not exist, return a JSON 404 with a clear message. + Only .html files are allowed (anything else 404s). + """ + if not filename.lower().endswith('.html'): + abort(404) + + html_dir = os.path.join(app.root_path, 'static', 'html') + file_path = os.path.join(html_dir, filename) + + # Ensure the file exists; if not, return a simple JSON 404 response + if not os.path.isfile(file_path): + return jsonify(error="File not found"), 404 + + # send_from_directory safely serves files from a specific folder + return send_from_directory(html_dir, filename, mimetype='text/html') + +@app.route('/tariff') +def tariff(): + # Retrieve the numHours parameter from the request's query string + num_hours_str = request.args.get('numHours', default=None) + + if num_hours_str is None: + return jsonify(error="numHours parameter is required"), 400 + + try: + num_hours = int(num_hours_str) + except ValueError: + return jsonify(error="numHours must be an integer"), 400 + + # Use the external module to calculate the start time + start_time_str = calculate_start_time(num_hours, api_key) + return jsonify(startTime=start_time_str) + +@app.route('/demo_status') +def demo_status(): + connection_type = request.args.get('type', default=None) + + if connection_type not in ["FIX", "MQ", "SFTP", "ALL"]: + return jsonify(error="Invalid connection type. Allowed values are FIX, MQ, SFTP, ALL."), 400 + + return jsonify(message=f'All your {"" if connection_type == "ALL" else connection_type} connections are up and running') + +@app.route('/demo_details') +def demo_details(): + connection_id = request.args.get('id', default=None) + + if not connection_id: + return jsonify(error="ID parameter is required"), 400 + + current_time = datetime.utcnow() + random_minutes = random.randint(1, 20) + last_connection_time = current_time - timedelta(minutes=random_minutes) + return jsonify(message=f"Connection {connection_id} is up", lastConnectionTime=last_connection_time.isoformat() + "Z") -if __name__ == "__main__": - app.run() +if __name__ == '__main__': + app.run(debug=True) diff --git a/app_api.py b/app_api.py new file mode 100644 index 00000000..6c9eca0c --- /dev/null +++ b/app_api.py @@ -0,0 +1,25 @@ +from flask import Flask +from flask_restful import Api, Resource +from flasgger import Swagger + +app = Flask(__name__) +api = Api(app) +swagger = Swagger(app) + +class Hello(Resource): + def get(self): + """ + A hello world endpoint + --- + responses: + 200: + description: Returns a greeting + examples: + application/json: {"hello": "world"} + """ + return {'hello': 'world'} + +api.add_resource(Hello, '/hello') + +if __name__ == '__main__': + app.run(debug=True) diff --git a/app_rest.py b/app_rest.py new file mode 100644 index 00000000..a91618a7 --- /dev/null +++ b/app_rest.py @@ -0,0 +1,16 @@ +from flask import Flask +from flask_restplus import Api, Resource + +app = Flask(__name__) +api = Api(app, version='1.0', title='My API', description='A simple API') + +ns = api.namespace('my_namespace', description='Namespace operations') + +@ns.route('/hello') +class HelloWorld(Resource): + def get(self): + '''Returns a greeting''' + return {'hello': 'world'} + +if __name__ == '__main__': + app.run(debug=True) diff --git a/app_simple_api.py b/app_simple_api.py new file mode 100644 index 00000000..6b47597f --- /dev/null +++ b/app_simple_api.py @@ -0,0 +1,10 @@ +from flask import Flask, jsonify + +app = Flask(__name__) + +@app.route('/') +def hello_world(): + return jsonify(message="Hello, World!") + +if __name__ == '__main__': + app.run(debug=True) diff --git a/app_web.py b/app_web.py new file mode 100644 index 00000000..b34a470d --- /dev/null +++ b/app_web.py @@ -0,0 +1,42 @@ +from flask import Flask, render_template +from flask_restplus import Api, Resource +app = Flask(__name__) + +@app.route('/') +def home(): + return render_template('index.html') + +@app.route('/about') +def about(): + return render_template('about.html') + +@app.route('/portfolio') +def portfolio(): + return render_template('portfolio.html') + +@app.route('/services') +def services(): + return render_template('services.html') + +@app.route('/contact') +def contact(): + return render_template('contact.html') + +@app.route('/blog') +def blog(): + return render_template('blog.html') + + +api = Api(app, version='1.0', title='My Octopus API', description='A simple API to retrieve my Octopus tariff data') + +ns = api.namespace('my_tariff_namespace', description='Tariff Namespace operations') + +@ns.route('/tariff') +class HelloWorld(Resource): + def get(self): + '''Returns a greeting''' + return {'hello': 'world'} + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/requirements.txt b/requirements.txt index ccecbc16..a0f1c863 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,6 @@ itsdangerous==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.1 Werkzeug==2.2.2 +Flask-RESTX +requests==2.26.0 +pytz \ No newline at end of file diff --git a/static/api_spec.yaml b/static/api_spec.yaml new file mode 100644 index 00000000..6465983c --- /dev/null +++ b/static/api_spec.yaml @@ -0,0 +1,128 @@ +openapi: 3.1.0 +info: + title: Hello World API + description: A simple API to return a Hello World message + version: "1.0" +servers: + - url: https://sharp-moria-avon18-b017b82a.koyeb.app/ +paths: + /: + get: + summary: Returns a Hello World message + operationId: helloWorld + responses: + 200: + description: Successful response + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Hello, World!" + 401: + description: Unauthorized access + /tariff: + get: + summary: Calculate and return a future start time when the appliance should start in order to minimize energy cost + operationId: cheapestStartTime + parameters: + - in: query + name: numHours + schema: + type: integer + required: true + description: Number of hours of the appliance going to run for + responses: + 200: + description: Successful response with the future start time + content: + application/json: + schema: + type: object + properties: + startTime: + type: string + example: "2024-08-10 17:00:00" + 400: + description: Bad request response when input validation fails + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "numHours parameter is required" + 401: + description: Unauthorized access + /demo_status: + get: + summary: Returns the status of connections + operationId: demoStatus + parameters: + - in: query + name: type + schema: + type: string + enum: [FIX, MQ, SFTP, ALL] + required: true + description: The type of connection to check + responses: + 200: + description: Successful response confirming all connections are up + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "All your connections are up and running" + 400: + description: Bad request response when input validation fails + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Invalid connection type. Allowed values are FIX, MQ, SFTP, ALL." + /demo_details: + get: + summary: Returns the details of a specific connection + operationId: demoDetails + parameters: + - in: query + name: id + schema: + type: string + required: true + description: The ID of the connection to check + responses: + 200: + description: Successful response with connection details + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Connection 123 is up" + lastConnectionTime: + type: string + format: date-time + example: "2025-01-08T12:00:00Z" + 400: + description: Bad request response when input validation fails + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "ID parameter is required" diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 00000000..52d94b74 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,82 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +header { + background-color: #333; + color: #fff; + padding: 1em 0; + text-align: center; +} + +nav { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1em; +} + +.logo { + font-size: 1.5em; +} + +.menu { + display: flex; + gap: 1em; +} + +.menu a { + color: #fff; + text-decoration: none; +} + +.hamburger { + display: none; + cursor: pointer; + font-size: 1.5em; +} + +main { + padding: 2em; + text-align: center; +} + +footer { + background-color: #333; + color: #fff; + text-align: center; + padding: 1em 0; + position: fixed; + bottom: 0; + width: 100%; +} + +.menu.open { + display: flex !important; /* Ensure that the menu displays as a flex container when open */ +} + + +@media (max-width: 768px) { + .menu { + display: none; + flex-direction: column; + background-color: #333; + position: absolute; + top: 50px; + right: 0; + width: 100%; + text-align: center; + } + + .menu a { + padding: 1em; + border-top: 1px solid #fff; + } + + .hamburger { + display: block; + } +} diff --git a/static/html/fixreft.html b/static/html/fixreft.html new file mode 100644 index 00000000..403bea3e --- /dev/null +++ b/static/html/fixreft.html @@ -0,0 +1,396 @@ + + + + + + Advanced FIX Message Parser + + + +
+
+

FIX Message Parser

+ +
+

FIX Dictionary Location

+
+ +
+
+ +
+

FIX Message

+

Enter a raw FIX message payload below. The message will be parsed into a human-readable format.

+ + +
+ Separator: + + + +
+ + + +

Parsed Output

+
+
+
+
+ + + + \ No newline at end of file diff --git a/static/js/scripts.js b/static/js/scripts.js new file mode 100644 index 00000000..4aab8ece --- /dev/null +++ b/static/js/scripts.js @@ -0,0 +1,8 @@ +document.addEventListener('DOMContentLoaded', () => { + const hamburger = document.getElementById('hamburger'); + const menu = document.getElementById('menu'); + + hamburger.addEventListener('click', () => { + menu.classList.toggle('open'); + }); +}); diff --git a/static/octopus_spec.yaml b/static/octopus_spec.yaml new file mode 100644 index 00000000..227bce39 --- /dev/null +++ b/static/octopus_spec.yaml @@ -0,0 +1,43 @@ +openapi: 3.1.0 +info: + title: Octopus Energy Electricity Tariffs API + description: API to retrieve electricity tariff details for specific products. + version: 1.0.0 +servers: + - url: https://api.octopus.energy/v1 + description: Octopus Energy API server +paths: + /products/AGILE-FLEX-22-11-25/electricity-tariffs/E-1R-AGILE-FLEX-22-11-25-C/standard-unit-rates/: + get: + operationId: getStandardUnitRates + summary: Retrieve standard unit rates for a specific electricity tariff. + responses: + '200': + description: A list of standard unit rates for the tariff. + content: + application/json: + schema: + type: array + items: + type: object + properties: + value_exc_vat: + type: number + format: float + value_inc_vat: + type: number + format: float + valid_from: + type: string + format: date-time + valid_to: + type: string + format: date-time + security: + - basicAuth: [] + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic diff --git a/tariff_utils.py b/tariff_utils.py new file mode 100644 index 00000000..0ba720fb --- /dev/null +++ b/tariff_utils.py @@ -0,0 +1,83 @@ +from datetime import datetime, timedelta +import requests +import pytz + +scan_hours = 12 + +def calculate_start_time(num_hours, api_key): + """ + Calculate the start time from now given the number of hours. + + :param num_hours: int - Number of hours to add to the current time + :return: str - The start time formatted as 'YYYY-MM-DD HH:MM:SS' + """ + + # api_url = 'https://api.octopus.energy/v1/products/AGILE-FLEX-22-11-25/electricity-tariffs/E-1R-AGILE-FLEX-22-11-25-C/standard-unit-rates/' + api_url = 'https://api.octopus.energy/v1/products/AGILE-24-10-01/electricity-tariffs/E-1R-AGILE-24-10-01-C/standard-unit-rates/' + + response = requests.get(api_url, auth=(api_key, '')) + best_start_time = None + best_tariff = float('inf') + + + print(f'Returned status code: {response.status_code}') + + if response.status_code == 200: + + # Filter and sort the time slots within the next 12 hours + now = datetime.now() # Current time + begin_time = now + timedelta(minutes=30) + end_time = begin_time + timedelta(hours=scan_hours) # Time 12 hours from now + + # Parse the JSON response + data = response.json() + tariff_data = response.json()['results'] + available_slots = [] + + for slot in tariff_data: + # Parsing the datetime strings + valid_from = datetime.strptime(slot['valid_from'].replace('Z', ' UTC'), "%Y-%m-%dT%H:%M:%S %Z") + valid_to = datetime.strptime(slot['valid_to'].replace('Z', ' UTC'), "%Y-%m-%dT%H:%M:%S %Z") + + if valid_from >= begin_time and valid_to <= end_time: + available_slots.append({ + 'valid_from': valid_from, + 'valid_to': valid_to, + 'tariff': slot['value_inc_vat'] + }) + + + available_slots.sort(key=lambda x: x['valid_from']) + required_slots = num_hours * 2 + + for i in range(len(available_slots) - required_slots + 1): + consecutive_slots = available_slots[i:i + required_slots] + + # Check if all the slots are consecutive (i.e., exactly 30 minutes apart) + is_consecutive = all( + consecutive_slots[j]['valid_from'] == consecutive_slots[j-1]['valid_to'] + for j in range(1, required_slots) + ) + + if is_consecutive: + total_tariff = sum(slot['tariff'] for slot in consecutive_slots) + avg_tariff = total_tariff / required_slots + + if total_tariff < best_tariff: + best_tariff = total_tariff + best_start_time = consecutive_slots[0]['valid_from'] + print(f'BEST TIMESLOT FOUND at [{best_start_time}] for [{best_tariff}].') + else: + print(f'Not consecutive: {consecutive_slots[0]['valid_from']}') + + else: + print(f'Error: {response.status_code} - {response.reason}') + + if best_start_time: + london_tz = pytz.timezone('Europe/London') + best_start_time_uk = best_start_time.astimezone(london_tz) + + return best_start_time_uk.strftime('%Y-%m-%d %H:%M:%S') + else: + return None + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 00000000..8c4771c5 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,32 @@ + + + + + + {% block title %}My Website{% endblock %} + + + + +
+ +
+
+ {% block content %}{% endblock %} +
+
+

© 2024 My Website

+
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..90e2be05 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block title %}Home - My Website{% endblock %} + +{% block content %} +
+

Welcome to My Personal Website

+

This is the home page content.

+
+{% endblock %}