from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, Markup, session, Response
from flask_socketio import SocketIO
from kubernetes import client, config, utils
import os
import psutil
import mysql.connector
import subprocess
from pods import *
from nodes import *
from config import *
from cluster import *
from deploiment import *
from configmap import *
from namespace import *
from service import *
from ingress import *
from pvc import *
from pv import *
from statefulset import *
from replicaset import *
from jobs import *
from cronjobs import *
from secret import *
from trigger import *
from kubeconfig_generator import *
import openai
from endpoint import *

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key_here'
socketio = SocketIO(app)

mydb = mysql.connector.connect(
    host="localhost",
    user="root",
    passwd="",
    database="database"
)
mycursor = mydb.cursor()

openai.api_key = ''

                                           #route index de l'application

@app.route('/', methods=['GET'])
def index():
    return redirect(url_for('login'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        txtname1 = request.form['txtname1']
        txtpass1 = request.form['txtpass1']
        mycursor.execute('SELECT roles FROM prs_info WHERE prs_name = %s AND prs_pass = %s', (txtname1, txtpass1,))
        roles = mycursor.fetchone()

        if roles:
            role = roles[0]
            session['username'] = txtname1
            return jsonify(success="Connexion réussie", role=role,)
        else:
            return jsonify(error="Veuillez vérifier votre nom d'utilisateur et mot de passe.")
    else:
        return render_template('login.html')

@app.route('/register', methods=['POST'])
def register():
    if request.method == 'POST':
        txtname = request.form['txtname']
        txtpass = request.form['txtpass']
        txtmail = request.form['txtmail']
        fonction = request.form['fonction']
        hash = request.form['txthash']

        mycursor.execute('SELECT * FROM cluster WHERE hash = %s', (hash,))
        cluster = mycursor.fetchone()

        if not cluster:
            return jsonify(error=f"Le hash spécifié n'existe pas dans la base de données.")
        elif not txtname or not txtpass or not txtmail or not fonction or not hash:
            return jsonify(error=f"Veuillez remplir tous les champs.")
        else:
            mycursor.execute('SELECT * FROM prs_info WHERE prs_name = %s', (txtname,))
            existing_user = mycursor.fetchone()

            if existing_user:
                return jsonify(error=f"Ce nom d'utilisateur est déjà enregistré.")
            else:
                mycursor.execute('INSERT INTO prs_info VALUES (NULL, %s, %s, %s, %s)', (txtname, txtpass, txtmail, fonction))
                mydb.commit()

                action = f"L'utilisateur {txtname} a été ajouté dans prs_info avec le hash {hash}"
                mycursor.execute('INSERT INTO tracking (action, prs_info_id, cluster_id) VALUES (%s, %s, %s)',(action, mycursor.lastrowid, cluster[0]))

                return jsonify(success=f"Compte enregistré avec succès!")

@app.route('/delete_user', methods=['POST'])
def delete_user():
    if request.method == 'POST':
        user_id = request.form['user_id']
        mycursor.execute('DELETE FROM prs_info WHERE id = %s', (user_id,))
        mydb.commit()

        return jsonify(success="Utilisateur supprimé avec succès!")

@app.route('/logout')
def logout():
    session.pop('username', None)
    return redirect(url_for('login'))

@app.route('/home')
def home():
    if 'username' in session:
        username = session['username']
        mycursor.execute('SELECT * FROM prs_info WHERE prs_name = %s', (username,))
        user = mycursor.fetchone()

        logs = get_logs()
        cluster_details = get_cluster_details()
        Cluster = get_cluster()
        config.load_kube_config()
        contexts, _ = config.list_kube_config_contexts()
        clusters = [context['name'] for context in contexts]

        v1 = client.CoreV1Api()
        top_pods = v1.list_namespaced_pod(namespace='default', watch=False)
        pods_data = []
        for pod in top_pods.items:
            pods_data.append({
            "namespace": pod.metadata.namespace,
            "name": pod.metadata.name,
            "status": pod.status.phase
            })

        nodes = v1.list_node(watch=False)
        nodes_data = []
        for node in nodes.items:
                nodes_data.append({
                    "name": node.metadata.name,
                    "labels": node.metadata.labels,
                    "status": node.status.conditions,
                    "capacity": node.status.capacity,
                    "operating_system": node.status.node_info.os_image,
                    "addresses": [address.address for address in node.status.addresses],
                    "pod_cidr": node.spec.pod_cidr
                })

        return render_template('admin.html',nodes=nodes_data, pods=pods_data, clusters=clusters, logs=logs, cluster_details=cluster_details, user=user, username=username, Cluster=Cluster)

    return redirect(url_for('login'))

@app.route('/devOps')
def devOps():
    if 'username' in session:
        username = session['username']

        mycursor.execute('SELECT * FROM prs_info WHERE prs_name = %s', (username,))
        user = mycursor.fetchone()

        mycursor.execute('''
            SELECT cluster.name_cluster
            FROM cluster
            JOIN tracking ON cluster.id = tracking.cluster_id
            JOIN prs_info ON tracking.prs_info_id = prs_info.id
            WHERE prs_info.prs_name = %s
        ''', (username,))
        cluster = mycursor.fetchall()

        logs = get_logs()
        cluster_details = get_cluster_details()

        return render_template('devops.html', username=username, logs=logs, cluster_details=cluster_details, cluster=cluster, user=user)

    return redirect(url_for('login'))

@app.route('/testeur')
def testeur():
    if 'username' in session:
        username = session['username']
        mycursor.execute('SELECT * FROM prs_info WHERE prs_name = %s', (username,))
        user = mycursor.fetchone()

        mycursor.execute('''
            SELECT cluster.name_cluster
            FROM cluster
            JOIN tracking ON cluster.id = tracking.cluster_id
            JOIN prs_info ON tracking.prs_info_id = prs_info.id
            WHERE prs_info.prs_name = %s
        ''', (username,))
        cluster = mycursor.fetchall()

        logs = get_logs()
        cluster_details = get_cluster_details()
        return render_template('testeur.html', username=username,  logs=logs, cluster_details=cluster_details, cluster=cluster, user=user)

    return redirect(url_for('login'))

@app.route('/connect', methods=['POST'])
def connect():
    selected_cluster = request.form.get('cluster')
    try:
        config.load_kube_config(context=selected_cluster)
        os.environ['KUBECONFIG'] = config.KUBE_CONFIG_DEFAULT_LOCATION
        return redirect(url_for('home'))
    except Exception as e:
        return f"Erreur de connexion au cluster : {str(e)}"

@app.route('/cluster_details')
def cluster_details():
    try:
        command = ["kubectl", "describe", "nodes"]
        result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
        cluster_details = result.stdout
        return cluster_details
    except subprocess.CalledProcessError as e:
        error_message = f"Erreur : {e.stderr}"
        return jsonify({"error": error_message})

                        #route pour les opérations sur le user

@app.route('/user', methods=['GET'])
def user():
    if 'username' in session:
        username = session['username']
        mycursor.execute('SELECT * FROM prs_info WHERE prs_name = %s', (username,))
        user = mycursor.fetchone()

        mycursor.execute('SELECT * FROM prs_info')
        utilisateur = mycursor.fetchall()

        contexts, _ = config.list_kube_config_contexts()
        clusters = [context['name'] for context in contexts]

        return render_template('user.html', clusters=clusters, user=user, username=username, utilisateur=utilisateur)

    return redirect(url_for('login'))

@app.route('/historique', methods=['GET'])
def get_historique():
    username = request.args.get('username')
    mycursor.execute('''
            SELECT action
            FROM tracking
            JOIN prs_info ON tracking.prs_info_id = prs_info.id
            WHERE prs_info.prs_name = %s
        ''', (username,))

    historique = mycursor.fetchall()
    return jsonify(historique)

@app.route('/update_user', methods=['POST'])
def update_user():
    try:
        user_id = request.form.get('user_id')
        new_name = request.form.get('new_name')
        new_email = request.form.get('new_email')
        mycursor.execute('SELECT * FROM prs_info WHERE id = %s', (user_id,))
        existing_user = mycursor.fetchone()

        if not existing_user:
            return jsonify(success=False, error="Utilisateur non trouvé.")

        mycursor.execute('UPDATE prs_info SET prs_name = %s, prs_mail = %s WHERE id = %s', (new_name, new_email, user_id))
        mydb.commit()

        return jsonify(success=True, message="Utilisateur mis à jour avec succès.")
    except Exception as e:
        return jsonify(success=False, error=str(e))

                                    #route pour les opérations sur le namespace

@app.route('/namespace', methods=['GET'])
def namespace():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        namespace = get_namespace()
        return render_template('namespace.html', user=user, cluster=cluster, username=username, namespace=namespace)
    return redirect(url_for('login'))

@app.route('/add_namespace', methods=['POST'])
def add_namespace():
    namespace_name = request.form['namespace_name']
    username = session['username']
    try:
        v1 = client.CoreV1Api()
        body = client.V1Namespace(metadata=client.V1ObjectMeta(name=namespace_name))
        v1.create_namespace(body=body)

        cluster_name = get_clustername(username)

        action = f"L'utilisateur {username} a ajouté un namespace sous le nom {namespace_name} sur le cluster {cluster_name}"

        add_tracking(username, action)

        return jsonify(success=f"namespace créé avec succès!")
    except Exception as e:
        return jsonify(error=f"Erreur lors de la création du namespace")

@app.route('/delete_namespace', methods=['GET'])
def delete_namespace():
    namespace_name = request.args.get('namespace_name')
    username = session['username']
    try:
        subprocess.run(["kubectl", "delete", "namespace", namespace_name])

        cluster_name = get_clustername(username)

        action = f"L'utilisateur {username} a supprimé un namespace sous le nom {namespace_name} sur le cluster {cluster_name}"

        add_tracking(username, action)

        return jsonify(success=True, message=f"Le namespace '{namespace_name}' a été supprimé avec succès.")
    except Exception as e:
        return jsonify(success=False, message=f"Erreur lors de la suppression du namespace : {e}")

@app.route('/namespace_details', methods=['GET'])
def get_namespace_details():
    namespace_name = request.args.get('namespace_name')
    try:
        process = subprocess.Popen(["kubectl", "describe", "namespace", namespace_name], stdout=subprocess.PIPE)
        output, _ = process.communicate()
        return output.decode('utf-8')
    except Exception as e:
        return f"Erreur lors de la récupération des détails du namespace : {e}"

                                         #lancement du socket pour le terminale

@socketio.on('connect')
def handle_connect():
    print('Client connected')

@socketio.on('disconnect')
def handle_disconnect():
    print('Client disconnected')

@socketio.on('user_input')
def handle_user_input(input):
    commands = input.split('\n')
    for command in commands:
        try:
            result = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, text=True)
            socketio.emit('terminal_output', result)
        except subprocess.CalledProcessError as e:
            socketio.emit('terminal_output', e.output)

                                        #route pour le monitoring

@app.route('/cpu')
def get_cpu_usage():
    cpu_usage = psutil.cpu_percent(interval=1)
    return jsonify(cpu_usage=cpu_usage)

@app.route('/ram')
def get_ram_usage():
    ram = psutil.virtual_memory()
    ram_usage = ram.percent
    return jsonify(ram_usage=ram_usage)

@app.route('/disk')
def get_disk_usage():
    disk_usage = psutil.disk_usage('/')
    disk_data = {
        'total': disk_usage.total,
        'used': disk_usage.used,
        'free': disk_usage.free,
        'percent': disk_usage.percent,
    }
    return jsonify(disk_data)

@app.route('/network_data')
def network_data():
    network_info = psutil.net_io_counters(pernic=True)
    interfaces = list(network_info.keys())
    data = [network_info[iface] for iface in interfaces]
    labels = interfaces
    interface_values = [d.bytes_sent + d.bytes_recv for d in data]
    network_bandwidth = psutil.net_if_stats()
    bandwidth_labels = interfaces
    bandwidth_values = [network_bandwidth[iface].speed for iface in interfaces]
    chart_data = {
        'labels': labels,
        'interface_values': interface_values,
        'bandwidth_labels': bandwidth_labels,
        'bandwidth_values': bandwidth_values,
    }
    return jsonify(chart_data)

                                        #route pour les opérations globales

@app.route('/install', methods=['GET', 'POST'])
def install():
    username = session['username']
    if request.method == 'POST':
        file = request.files['yaml_file']
        resource_type = request.form.get('resource_type')
        if file and resource_type:
            file_path = os.path.join(temp_yaml_dir, file.filename)
            file.save(file_path)
            yaml_object_name = os.path.basename(file_path)
            if check_if_resource_exists(resource_type, yaml_object_name):
                message = f'L\'objet YAML "{yaml_object_name}" existe déjà dans le cluster.'
                return jsonify({'error': message})
            success, error_message = install_yaml(file_path)

            cluster_name = get_clustername(username)

            action = f"L'utilisateur {username} a installé un {resource_type} sous le nom {yaml_object_name} sur le cluster {cluster_name}"

            add_tracking(username, action)

            os.remove(file_path)
            if success:
                message = f'Fichier YAML "{yaml_object_name}" installé avec succès dans le cluster.'
                return jsonify({'success': message})
            else:
                message = f'Erreur lors de l\'installation du fichier YAML : {error_message}'
                return jsonify({'error': message})

@app.route('/install_component', methods=['POST'])
def install_component():
    component_yaml = request.form['component_yaml']
    try:
        with open('component.yaml', 'w') as f:
            f.write(component_yaml)

        subprocess.run(['kubectl', 'apply', '-f', 'component.yaml'])

        save_folder = 'fichier_installer'
        if not os.path.exists(save_folder):
            os.makedirs(save_folder)

        os.rename('component.yaml', os.path.join(save_folder, 'component.yaml'))

        message = f'Composant installé et fichier YAML sauvegardé avec succès!'
        return jsonify({'success': message})
    except Exception as e:
        message = f"Erreur lors de l'installation du composant : {str(e)}"
        return jsonify({'success': message})

                                        #route pour les opérations sur les pods

@app.route('/open_pod_shell/<pod_name>')
def open_pod_shell(pod_name):
    command = f'kubectl exec -ti {pod_name} /bin/bash'
    try:
        subprocess.run(command, shell=True, check=True)
    except subprocess.CalledProcessError as e:
        return f"Erreur lors de l'ouverture du shell : {e}"
    return "Shell ouvert avec succès"

@app.route('/execute_command', methods=['POST'])
def execute_command():
    data = request.get_json()
    command = data['command']
    pod_name = data['podName']
    try:
        result = subprocess.check_output(f'kubectl exec -it {pod_name} /bin/bash -- {command}', shell=True, stderr=subprocess.STDOUT, text=True)
        return jsonify(result)
    except subprocess.CalledProcessError as e:
        return jsonify(e.output)

@app.route('/delete_pod', methods=['POST'])
def delete_pod():
    namespace = request.form.get('namespace')
    pod_name = request.form.get('name')
    core_api = client.CoreV1Api()
    username = session['username']
    try:
        delete_options = client.V1DeleteOptions()
        core_api.delete_namespaced_pod(pod_name, namespace, body=delete_options)

        cluster_name = get_clustername(username)

        action = f"L'utilisateur {username} a supprimer un pod sous le nom {pod_name} sur le cluster {cluster_name}"

        add_tracking(username, action)

        return redirect(url_for('pods', success_message='Le pod a été supprimé avec succès.'))
    except client.rest.ApiException as e:
        error_message = f"Erreur lors de la suppression du pod : {e.reason}"
        return redirect(url_for('pods', error_message=error_message))

@app.route('/logs/<namespace>/<pod_name>', methods=['GET'])
def logs(namespace, pod_name):
    logs = get_pod_logs(namespace, pod_name)
    if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        return jsonify(logs=logs.splitlines())
    return render_template('pods.html', logs=logs)

@app.route('/pods', methods=['GET', 'POST'])
def pods():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')  # Récupère le namespace sélectionné depuis les paramètres d'URL
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        pods = get_pods_in_namespace(selected_namespace)  # Utilisez le namespace sélectionné ici pour récupérer les pods
        return render_template('pods.html', pods=pods, user=user, username=username, cluster=cluster, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)

    return redirect(url_for('login'))

@app.route('/pod_details/<namespace>/<pod_name>', methods=['GET'])
def pod_details(namespace, pod_name):
    pod_details = get_pod_details(namespace, pod_name)
    formatted_pod_details = Markup(pod_details)
    return formatted_pod_details

@app.route('/pod_yaml/<namespace>/<pod_name>', methods=['GET'])
def get_pod_yaml(namespace, pod_name):
    pod_yaml = get_pod_yaml(namespace, pod_name)
    formated_pod_yaml = Markup(pod_yaml)
    return formated_pod_yaml

@app.route('/update_pod', methods=['POST'])
def update_pod():
    try:
        namespace = request.form.get('namespace')
        pod_name = request.form.get('pod_name')
        new_image = request.form.get('new_image')
        configuration = client.Configuration()
        configuration.namespace = namespace
        api_instance = client.CoreV1Api(client.ApiClient(configuration))
        pod = api_instance.read_namespaced_pod(name=pod_name, namespace=namespace)
        pod.spec.containers[0].image = new_image
        api_instance.patch_namespaced_pod(name=pod_name, namespace=namespace, body=pod)

        return jsonify(success=True, message="Pod mis à jour avec succès.")
    except Exception as e:
        return jsonify(success=False, error=str(e))

                                    #route pour les opérations sur le déploiments

@app.route('/deployment', methods=['GET', 'POST'])
def deployment():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        deployments = get_deployments_in_namespace(selected_namespace)
        return render_template('deployment.html', deployments=deployments, user=user, cluster=cluster, username=username, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)

    return redirect(url_for('login'))
@app.route('/delete_deployment', methods=['POST'])
def delete_deployment():
    namespace = request.form.get('namespace')
    name = request.form.get('name')
    k8s_apps_v1 = client.AppsV1Api()
    username = session['username']
    try:
        delete_options = client.V1DeleteOptions()
        k8s_apps_v1.delete_namespaced_deployment(name, namespace, body=delete_options)

        cluster_name = get_clustername(username)

        action = f"L'utilisateur {username} a supprimé un déploiment sous le nom {name} sur le cluster {cluster_name}"

        add_tracking(username, action)

        return redirect(url_for('deployment', success_message='Le déploiement a été supprimé avec succès.'))
    except client.rest.ApiException as e:
        error_message = f"Erreur lors de la suppression du déploiement : {e.reason}"
        return redirect(url_for('deployment', error_message=error_message))

@app.route('/deploy_details/<namespace>/<deployment_name>', methods=['GET'])
def deploy_details(namespace, deployment_name):
    deploy_details = get_deploy_details(namespace, deployment_name)
    formatted_deploy_details = Markup(deploy_details)
    return formatted_deploy_details

@app.route('/update_deployment', methods=['PATCH'])
def update_deployment():
    data = request.get_json()
    namespace = data.get('namespace')
    deployment_name = data.get('deploymentName')
    replicas = int(data.get('replicas'))
    new_image = data.get('image')
    username = session['username']
    try:
        v1 = client.AppsV1Api()
        deployment = v1.read_namespaced_deployment(name=deployment_name, namespace=namespace)
        deployment.spec.replicas = replicas
        deployment.spec.template.spec.containers[0].image = new_image
        v1.patch_namespaced_deployment(name=deployment_name, namespace=namespace, body=deployment)
        cluster_name = get_clustername(username)

        action = f"L'utilisateur {username} a mis à jour un déploiment sous le nom {deployment_name} sur le cluster {cluster_name}"

        add_tracking(username, action)

        return jsonify({'message': 'Déploiement mis à jour avec succès'}), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

                                        #route pour les opérations sur le configmap

@app.route('/configmap', methods=['GET', 'POST'])
def configmap():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        configmap_list = get_configmap_in_namespace(selected_namespace)
        return render_template('configmap.html', user=user, cluster=cluster, username=username, configmaps=configmap_list, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)

    return redirect(url_for('login'))

@app.route('/configmap_details/<namespace>/<configMap_name>', methods=['GET'])
def config_details(namespace, configMap_name):
    config_details = get_config_details(namespace, configMap_name)
    formatted_config_details = Markup(config_details)
    return formatted_config_details

                                        #route pour les opérations sur le secret

@app.route('/secret', methods=['GET', 'POST'])
def secret():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        secrets = get_secret_in_namespace(selected_namespace)
        return render_template('secret.html', secrets=secrets, user=user, username=username, cluster=cluster, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

@app.route('/secret_details/<namespace>/<secret_name>', methods=['GET'])
def secret_details(namespace, secret_name):
    secret_details = get_secret_details(namespace, secret_name)
    formatted_secret_details = Markup(secret_details)
    return formatted_secret_details

                                        #route pour les opérations sur le service

@app.route('/service', methods=['GET', 'POST'])
def service():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        service_list = get_service_in_namespace(selected_namespace)
        return render_template('service.html', services=service_list, username=username, cluster=cluster, user=user, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

@app.route('/service_details/<namespace>/<service_name>', methods=['GET'])
def service_details(namespace, service_name):
    service_details = get_service_details(namespace, service_name)
    formatted_service_details = Markup(service_details)
    return formatted_service_details

@app.route('/delete_service', methods=['POST'])
def delete_service():
    namespace = request.form.get('namespace')
    service_name = request.form.get('name')
    username = session['username']
    try:
        cmd = f'kubectl delete service {service_name} -n {namespace}'
        result = subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True)

        cluster_name = get_clustername(username)

        action = f"L'utilisateur {username} a supprimé un service sous le nom {service_name} sur le cluster {cluster_name}"

        add_tracking(username, action)

        return redirect(url_for('service', success_message='Le déploiement a été supprimé avec succès.'))

    except Exception as e:
        return f"Erreur lors de la suppression du service : {str(e)}"

@app.route('/update_service', methods=['PATCH'])
def update_service():
    try:
        namespace = request.json.get('namespace')
        service_name = request.json.get('serviceName')
        cluster_ip = request.json.get('clusterIP')
        port = int(request.json.get('port'))
        node_port = int(request.json.get('nodePort'))
        service_type = request.json.get('serviceType')
        config.load_kube_config()
        v1 = client.CoreV1Api()
        service = v1.read_namespaced_service(name=service_name, namespace=namespace)
        service.spec.cluster_ip = cluster_ip
        service.spec.ports[0].port = port
        service.spec.ports[0].node_port = node_port
        service.spec.type = service_type
        v1.replace_namespaced_service(name=service_name, namespace=namespace, body=service)

        return jsonify(success=True, message="Service mis à jour avec succès.")

    except Exception as e:
        return jsonify(success=False, error=str(e)), 500

                                        #route pour les opérations sur l'ingress

@app.route('/ingress', methods=['GET', 'POST'])
def ingress():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        ingress_list = get_ingresses_in_namespace(selected_namespace)
        return render_template('ingress.html', ingresses=ingress_list, username=username, user=user, cluster=cluster, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

@app.route('/ingress_details/<namespace>/<ingress_name>', methods=['GET'])
def ingress_details(namespace, ingress_name):
    ingress_details = get_ingress_details(namespace, ingress_name)
    formatted_ingress_details = Markup(ingress_details)
    return formatted_ingress_details

                                        #route pour les opérations sur le pvc

@app.route('/pvc', methods=['GET', 'POST'])
def pvc():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        pvc_list = get_persistent_volume_claims_in_namespace(selected_namespace)
        return render_template('pvc.html', pvc_list=pvc_list, username=username, user=user, cluster=cluster, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

@app.route('/pvc_details/<namespace>/<pvc_name>', methods=['GET'])
def pvc_details(namespace, pvc_name):
    pvc_details = get_pvc_details(namespace, pvc_name)
    formatted_pvc_details = Markup(pvc_details)
    return formatted_pvc_details

                                        #route pour les opérations sur le pv

@app.route('/pv', methods=['GET', 'POST'])
def pv():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        pv_list = get_persistent_volume_in_namespace(selected_namespace)
        return render_template('pv.html', pv_list=pv_list, username=username, user=user, cluster=cluster, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

@app.route('/pv_details/<namespace>/<pv_name>', methods=['GET'])
def pv_details(namespace, pv_name):
    pv_details = get_pv_details(namespace, pv_name)
    formatted_pv_details = Markup(pv_details)
    return formatted_pv_details

                                        #route pour les opérations sur le statefullset

@app.route('/statefullset', methods=['GET', 'POST'])
def statefullset():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        statefullset_list = get_statefulsets_in_namespace(selected_namespace)
        return render_template('statefullset.html', statefullset_list=statefullset_list, user=user, username=username, cluster=cluster, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

@app.route('/statefullset_details/<namespace>/<statefullset_name>', methods=['GET'])
def statefullset_details(namespace, statefullset_name):
    statefullset_details = get_statefullset_details(namespace, statefullset_name)
    formatted_statefullset_details = Markup(statefullset_details)
    return formatted_statefullset_details

                                        #route pour les opérations sur le replicaset

@app.route('/replicatset', methods=['GET', 'POST'])
def replicatset():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        replicatset_list = get_replicasets_in_namespace(selected_namespace)
        return render_template('replicatset.html', replicatset_list=replicatset_list, username=username, user=user, cluster=cluster, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

@app.route('/replicatset_details/<namespace>/<statefullset_name>', methods=['GET'])
def replicatset_details(namespace, replicatset_name):
    replicatset_details = get_replicatset_details(namespace, replicatset_name)
    formatted_replicatset_details = Markup(replicatset_details)
    return formatted_replicatset_details

                                        #route pour les opérations sur le jobs

@app.route('/jobs', methods=['GET', 'POST'])
def jobs():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        jobs_list = get_jobs_in_namespace(selected_namespace)
        return render_template('jobs.html', jobs_listt=jobs_list, user=user, username=username, cluster=cluster, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

                                        #route pour les opérations sur le cronjobs

@app.route('/cronjobs', methods=['GET', 'POST'])
def cronjobs():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        yaml_files = [f for f in os.listdir(yaml_dir) if f.endswith(('.yaml', '.yml'))]
        namespaces = get_available_namespaces()
        cronjobs_list = get_cronjobs_in_namespace(selected_namespace)
        return render_template('cronjobs.html', cronjobs_listt=cronjobs_list, user=user, username=username, cluster=cluster, yaml_files=yaml_files, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

@app.route('/generate_kubeconfig', methods=['POST'])
def generate_kubeconfig_route():
    username = request.form['username']
    client_cert_path = request.form['client_cert']
    client_key_path = request.form['client_key']

    kubeconfig_content = generate_kubeconfig(username, client_cert_path, client_key_path)

    response = Response(kubeconfig_content, content_type='application/x-yaml')
    response.headers['Content-Disposition'] = f'attachment; filename={username}_kubeconfig.yaml'
    return response

@app.route('/ia')
def ia():
    return render_template('ia.html')

@app.route('/get_openai_response', methods=['POST'])
def get_openai_response():
    user_message = request.json['message']

    try:
        response = openai.Completion.create(
            engine="text-davinci-003",
            prompt=f"L'utilisateur dit : {user_message}\nAssistance virtuelle :",
            max_tokens=400
        )

        openai_response = response.choices[0].text.strip()
        return jsonify({'message': openai_response})
    except Exception as e:
        print(f"Erreur OpenAI : {str(e)}")
        return jsonify({'message': "Erreur lors de la génération de la réponse."})

@app.route('/endpoint', methods=['GET', 'POST'])
def endpoint():
    if 'username' in session:
        username = session['username']
        user = get_user_info(username)
        cluster = get_clusterall(username)
        selected_namespace = request.args.get('namespace', 'default')
        namespaces = get_available_namespaces()
        endpoints = get_endpoints(selected_namespace)
        return render_template('endpoint.html', endpoint=endpoints, user=user, username=username, cluster=cluster, namespaces=namespaces, selected_namespace=selected_namespace)
    return redirect(url_for('login'))

if __name__ == '__main__':
    socketio.run(app, host='relia-kubernetes.controls', debug=True)


import streamlit as st
import os
import pandas as pd

st.write("Social sentiment stock eports")

# Determine path to sentiment trending csv files

path = './Data'

# Get reports from the directionary
files = os.listdir(path)

#Extract the reports
files_list = [os.path.splittext(file)[0].lower() for file in files]
st.write(files_list)

# Set numerical or alphabetical filing system
files_alpha = set([filename[0] for filename in files_list])

sel_alpha = st.selectbox("Latest sentiment reports", list(files_alpha))
filtered_files = [file for file in files_list if file[0]==sel_alpha]
sel_filtered_file = st.selectbox("Chose report", filtered_files)
if sel_filtered_file:
    st.write()
    df = pd.read_csv(f"{path}//{sel_filtered_file}.csv")
    st.DataFram(df)

# Command: streamlit run app.py

# LSTM MODEL

def scale_array(features, target, train_proportion:float = 0.8, scaler: bool = True):
    x = np.array(features)
    y = np.array(target).reshape(-1,1)
    split = int(0.8 * len(x))
    X_train = X[: split]
    X_test = X[split:]
    y_train = y[: split]
    y_test = y[split:]

    if scaler:
        scaler = MinMaxScaler()
        scaler.fit(X_train)
        X_train = scaler.transform(X_train)
        X_test = scaler.transform(X_test)
        scaler.fit(y_train)
        y_train = scaler.transform(y_train)
        y_test = scaler.transform(y_test)
    else:
        pass
    X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1))
    X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], 1))

    return X_train, X_test, y_train, y_test, scaler

def create_LSTM_model(
    train_set: np.ndarray,
    dropout: float = 0.2,
    layer_one_dropout: float = 0.6,
    number_layers: int = 4,
    optimizer: str = 'adam',
    loss: str = 'mean_squared_error'):
    model = Squential()
    number_units = X_train.shape[1]
    dropout_fraction = dropout
    model.add(LSTM(
        units=number_units,
        return_sequences=True,
        input_shape=(X_train.shape[1], 1))
        )
    model.add(Dropout(layer_one_dropout))
    layer_counter = 1
    while layer_counter < (number_layers - 1):

        model.add(LSTM(units=number_units, return_sequences = True))
        model.add(Dropout(dropout_fraction))
        layer_counter+=1

    model.add(LSTM(units=number_units))
    model.add(Dropout(dropout_fraction))

    model.add(Dense(1))

    model.compile(optimizer=optimizer, loss=loss)

    return model

def calculate_strategy_returns(prices_df, trading_threshold, shorting: bool = False):
    '''
    prices_df: pd.DataFrame containing an 'Actual' and 'Predicted' column representing actual and model-predicted prices respectively

    '''
     # Calculate actual daily returns
    prices_df['actual_returns'] = prices_df['Actual'].pct_change()
    # Create a 'last close' column
    prices_df['last_close'] = prices_df['Actual'].shift()
    # Calculate the predicted daily returns, by taking the predicted price as a proportion of the last close
    prices_df['predicted_returns'] = (prices_df['Predicted'] - prices_df['last_close'])/prices_df['last_close']

    # Actual signal = 1 if actual returns more than threshold,  -1 if less than threshold
    prices_df['actual_signal'] = 0
    prices_df.loc[prices_df['actual_returns'] > trading_threshold , 'actual_signal'] = 1
    if shorting == True:
        prices_df.loc[prices_df['actual_returns'] < -trading_threshold , 'actual_signal'] = -1

    # Strategy signal = 1 if predicted returns > threshold, -1 if less than threshold
    prices_df['strategy_signal'] = 0
    prices_df.loc[prices_df['predicted_returns'] > trading_threshold , 'strategy_signal'] = 1
    if shorting == True:
        prices_df.loc[prices_df['predicted_returns'] < -trading_threshold , 'strategy_signal'] = -1

    # Compute strategy returns
    prices_df['strategy_returns'] = prices_df['actual_returns'] * prices_df['strategy_signal']

    return prices_df

def calculate_RMSE(y_actual, y_predicted):
    MSE = np.square(np.subtract(y_actual, y_predicted)).mean()
    RMSE = math.sqrt(MSE)
    return RMSE

# Set target cumulative returns as a threshold for model to achieve.
target_cumulative_return = 1.01

# Set returns threshold for strategy to fire trading signal
trading_threshold = 0.00

# Set maximum numberof iterations to run
max_iter = 3

tickers = technicals.index.get_level_values('symbol').unique().to_list()
# Initialise list to hold tickers that have successfully trained models that achieve the target cumulative returns:
modelled_tickers = []
trading_signals = []

for ticker in tickers:
    print("="*50)
    print(f"Initialising training for {ticker}")

    # Create signal dataframe as a copy
    signal = technicals.copy().loc[ticker].dropna()

    # Create blank row for current trading day and append to end of dataframe
    most_recent_timestamp = signal.index.get_level_values('timestamp').max() + timedelta(minutes = 1)
    signal.loc[most_recent_timestamp, ['target']] = np.nan

    # # Create target
    signal['target'] = signal['close']

    # Shift indicators to predict current trading day close
    signal.iloc[:, :-1]  = signal.iloc[:, :-1].shift()

    # Drop first row with NaNs resulting from data shift
    signal = signal.iloc[1:, :]

    # Ensure all data is 'float' type while also dropping null values due to value shifts and unavailable NaN indicator data.
    signal = signal.astype('float')

    # Set features and target
    X = signal.iloc[:, :-1]
    y = signal['target']

    # Use predefined scale_array function to transform data and perform train/test split
    X_train, X_test, y_train, y_test, scaler = scale_array(X, y, train_proportion = 0.8)

    # Record start time
    start_time = time.time()

    # (Re)set iter_counter and strategy_cumulative_return to 0
    strategy_cumulative_return = 0
    iter_counter = 0

    # While loop that repeatedly trains LSTM models to adjust weights until it can hit the target cumulative return. Loop stops if max_iter is hit or if returns are achieved on backtesting
    while strategy_cumulative_return < target_cumulative_return and iter_counter != max_iter:

        strategy_cumulative_return = 0
        # Start iteration counter
        iter_counter+=1

        # Create model if first iteration. Reset model if subsequent iterations
        model = create_LSTM_model(X_train,
                                  dropout=0.4,
                                  layer_one_dropout=0.6,
                                  number_layers=6
                                 )

        # Set early stopping such that each iteration stops running epochs if validation loss is not improving (i.e. minimising further)
        callback = EarlyStopping(
            monitor='val_loss',
            patience=20, mode='auto',
            restore_best_weights=True
        )

        # Print message to allow visual confirmation of iteration training is currently at.
        print("="*50)
        print(f"Training {ticker} model iteration {iter_counter} ...please wait.\n")

        # Train the model
        history = model.fit(
            X_train, y_train,
            epochs=1000, batch_size=32,
            shuffle=False,
            validation_split = 0.1,
            verbose = 0,
            callbacks = callback
        )
        # Print confirmation that current iteration has ended.
        print(f"Iteration {iter_counter} ended.")

        # Evaluate loss when predicting test data. Sliced out entry -1 as y_test[-1] target is NaN
        model_loss = model.evaluate(X_test[:-1], y_test[:-1], verbose=0)

        # Make predictions
        predicted = model.predict(X_test)

        # Recover the original prices instead of the scaled version
        predicted_prices = scaler.inverse_transform(predicted)
        real_prices = scaler.inverse_transform(y_test.reshape(-1, 1))

        # Create a DataFrame of Real and Predicted values
        prices = pd.DataFrame({
            "Actual": real_prices.ravel(),
            "Predicted": predicted_prices.ravel()
        }, index = signal.index[-len(real_prices): ])

        # Use predefined calculate_strategy_returns function to calculate and append strategy returns column to 'prices' dataframe
        prices = calculate_strategy_returns(prices, trading_threshold, shorting = False)

        # Compute strategy cumulative returns
        strategy_cumulative_return = (1+prices['strategy_returns']).cumprod()[-1]

        rmse = calculate_RMSE(prices['Actual'], prices['Predicted'])

        # Print performance metrics of the model given the feature weights produced by current iteration
        print(f"LSTM Method iteration {iter_counter} for {ticker} - Performance")
        print("-"*50)
        print(f"Model loss on testing dataset: \n{model_loss:.4f}")
        print(f"RMSE: \n{rmse:.4f}")
        print(f"Cumulative return on testing dataset: \n{strategy_cumulative_return:.4f}")

    # Append ticker to modelled_tickers:
    modelled_tickers.append(ticker)

    if strategy_cumulative_return >= target_cumulative_return:
        print(f"Target cumulative returns achieved\n")
        # Calculate cumulative returns at their best and worst time points over time.
        min_return = (1+prices['strategy_returns']).cumprod().min()
        max_return = (1+prices['strategy_returns']).cumprod().max()

        # Print cumulative return performance
        print(f"From {prices.index.min()} to {prices.index.max()}, the cumulative return of the current model is {strategy_cumulative_return:.2f}.")
        print(f"At its lowest, the model recorded a cumulative return of {min_return:.2f}.")
        print(f"At its highest, the model recorded a cumulative return of {max_return:.2f}.")

        # Convert model to json
        model_json = model.to_json()

        # Save model layout as json
        path = f"../Resources/LSTM_model_weights/{date.today()}"
        # Check whether the specified path exists or not
        isExist = os.path.exists(path)
        if not isExist:
            # Create a new directory because it does not exist
            os.makedirs(path)

        file_path = Path(f"../Resources/LSTM_model_weights/{date.today()}/{ticker}.json")
        with open(file_path, "w") as json_file:
            json_file.write(model_json)

        # Save weights
        model.save_weights(f"../Resources/LSTM_model_weights/{date.today()}/{ticker}.h5")

        # Append the trading signal predicted by model
        trading_signals.append(prices.loc[prices.index.max(), 'strategy_signal'])

    else:
        st.code(f"The LSTM model was not able to achieve the target cumulative returns on the testing dataset within {max_iter} iterations.\n")
        trading_signals.append(0)

st.code("*"*50)
st.code(f"Training completed.")


from tkinter import *

root=Tk()
root.title("Simple Calculator")
root.configure(bg="black")

def quit():
    root.destroy()

def button_click(number):
    current=box.get()
    box.delete(0,END)
    box.insert(0,str(current) + str(number))

def button_clear():
    box.delete(0,END)

def button_add():
    first_number=box.get()
    global f_num
    global math
    math="addition"
    f_num=int(first_number)
    box.delete(0,END)

def button_equal():
    second_number=box.get()
    box.delete(0,END)
    if math == "addition":
        box.insert(0,f_num + int(second_number))
    if math == "subtraction":
        box.insert(0,f_num - int(second_number))
    if math == "multiplication":
        box.insert(0,f_num * int(second_number))
    if math == "division":
        box.insert(0,f_num / int(second_number))

def button_subtract():
    first_number=box.get()
    global f_num
    global math
    math="subtraction"
    f_num=int(first_number)
    box.delete(0,END)

def button_multiply():
    first_number=box.get()
    global f_num
    global math
    math="multiplication"
    f_num=int(first_number)
    box.delete(0,END)

def button_divide():
    first_number=box.get()
    global f_num
    global math
    math="division"
    f_num=int(first_number)
    box.delete(0,END)

box=Entry(root,width=30,fg="white",bg="black",borderwidth=5)
box.grid(row=0,column=0,columnspan=3)

button_1=Button(root,text="1",padx=40,pady=20,command=lambda:button_click(1),fg="white",bg="black")
button_2=Button(root,text="2",padx=40,pady=20,command=lambda:button_click(2),fg="white",bg="black")
button_3=Button(root,text="3",padx=40,pady=20,command=lambda:button_click(3),fg="white",bg="black")
button_4=Button(root,text="4",padx=40,pady=20,command=lambda:button_click(4),fg="white",bg="black")
button_5=Button(root,text="5",padx=40,pady=20,command=lambda:button_click(5),fg="white",bg="black")
button_6=Button(root,text="6",padx=40,pady=20,command=lambda:button_click(6),fg="white",bg="black")
button_7=Button(root,text="7",padx=40,pady=20,command=lambda:button_click(7),fg="white",bg="black")
button_8=Button(root,text="8",padx=40,pady=20,command=lambda:button_click(8),fg="white",bg="black")
button_9=Button(root,text="9",padx=40,pady=20,command=lambda:button_click(9),fg="white",bg="black")
button_0=Button(root,text="0",padx=40,pady=20,command=lambda:button_click(0),fg="white",bg="black")
button_add=Button(root,text="+",padx=39,pady=20,command=button_add,fg="white",bg="black")
button_equal=Button(root,text="=",padx=86,pady=20,command=button_equal,fg="white",bg="black")
button_clear=Button(root,text="clear",padx=75,pady=20,command=button_clear,fg="white",bg="black")
button_subtract=Button(root,text="-",padx=42,pady=20,command=button_subtract,fg="white",bg="black")
button_multiply=Button(root,text="*",padx=41,pady=20,command=button_multiply,fg="white",bg="black")
button_divide=Button(root,text="/",padx=42,pady=20,command=button_divide,fg="white",bg="black")

button_7.grid(row=1,column=0)
button_8.grid(row=1,column=1)
button_9.grid(row=1,column=2)
button_4.grid(row=2,column=0)
button_5.grid(row=2,column=1)
button_6.grid(row=2,column=2)
button_1.grid(row=3,column=0)
button_2.grid(row=3,column=1)
button_3.grid(row=3,column=2)
button_0.grid(row=4,column=0)
button_clear.grid(row=4,column=1,columnspan=2)
button_add.grid(row=5,column=0)
button_equal.grid(row=5,column=1,columnspan=2)
button_subtract.grid(row=6,column=0)
button_multiply.grid(row=6,column=1)
button_divide.grid(row=6,column=2)

quit_button=Button(root,text="quit",command=quit,fg="white",bg="black")
quit_button.grid(row=99,column=0)
footer=Label(root,text="smithmule",fg="white",bg="black")
footer.grid(row=99,column=1)
root.mainloop()


#!/usr/bin/py
# Head ends here
def pairs(a,k):
    # a is the list of numbers and k is the difference value
    count = 0
    compls = {}
    for i in a:
        if i in compls:
            count += compls[i]
        compls[i + k] = compls[i + k] + 1 if i+k in compls else 1
        compls[i - k] = compls[i - k] + 1 if i-k in compls else 1
    return count
# Tail starts here
if __name__ == '__main__':
    a = input().strip()
    a = list(map(int, a.split(' ')))
    _a_size=a[0]
    _k=a[1]
    b = input().strip()
    b = list(map(int, b.split(' ')))
    print(pairs(b,_k))


#!/usr/bin/env python3
import sys
from pprint import pprint

from lib import awx, general

def main():
    general.preliminary_checks(["AWX_API_TOKEN"])
    a = awx.AWX()
    output = a.sync_invenstory()
    job_id = output.get("workflow_job", None)
    if job_id:
        print("To see the output of the job: (will take around 1m45)")
        print(
            f"Job ID: {job_id}\thttps://awx.global.ftlprod.net/#/jobs/workflow/{job_id}/output"
        )
    else:
        print("Didn't receive the job_id, verify manually.")
        pprint(output)
        sys.exit(1)

if __name__ == "__main__":
    main()


# Standard library
import logging
from turtle import RawTurtle

# Third party modulesc

# Custom modules

# Global setup
SPEED = 10
DEBUG_MODE = True
LOG_LEVEL = 'INFO'
SHAPE_WIDTH_PIXELS = .5
SHAPE_HEIGHT_PIXELS = .5

# Logging configuration
FORMAT = '%(levelname)s %(message)s'
logging.basicConfig(format=FORMAT)
logger = logging.getLogger(__file__)
logger.setLevel(LOG_LEVEL.upper())

class Snake(RawTurtle):
    '''
    Controls the snake in the game
    '''

    body = []
    screen = None

    def __init__(self, canvas):
        super().__init__(canvas)
        self.canvas = canvas
        self.init_body()
        self.head = self.body[0]

    def init_body(self):
        starting_positions = [(0, 0), (-SHAPE_WIDTH_PIXELS, 0), (-SHAPE_WIDTH_PIXELS, 0)]
        for pos in starting_positions:
            self.add_segment(pos)

    def add_segment(self, pos):
        x = RawTurtle(self.canvas, shape='square')
        x.penup()
        x.goto(pos)
        x.color('white')
        x.speed('fastest')
        x.shapesize(SHAPE_WIDTH_PIXELS, SHAPE_WIDTH_PIXELS, 1)
        x.screen.bgcolor('black')
        self.body.append(x)

    def extend(self):
        self.add_segment(self.body[-1].pos())

    def move(self):
        for seg_num in range( len(self.body) - 1 , 0 , -1): # Start with a three segment snake
            # Each piece moves to the position of the piece in front of it to get a good animation
            new_x = self.body[seg_num - 1].xcor()
            new_y = self.body[seg_num - 1].ycor()
            self.body[seg_num].goto(new_x, new_y)
        self.body[0].forward(SPEED)
        logger.debug(f'Head position is {self.body[0].pos()}')
        logger.debug(f'Head angle is {self.body[0].heading()}')

    def turn_right(self):
        logger.debug(f'Moving right')
        if self.body[0].heading() != 180:
            for x in self.body:
                x.setheading(0)

    def turn_left(self):
        logger.debug(f'Moving left')
        if self.body[0].heading() != 0:
            for x in self.body:
                x.setheading(180)

    def turn_down(self):
        logger.debug(f'Moving down')
        if self.body[0].heading() != 90:
            for x in self.body:
                x.setheading(270)

    def turn_up(self):
        logger.debug(f'Moving up')
        if self.body[0].heading() != 270:
            for x in self.body:
                x.setheading(90)


from tkinter import Tk, Button, PhotoImage

import PIL.Image
import PIL.ImageTk

from modules.gui.debug_tabs import *
from modules.gui.emulator_controls import EmulatorControls, DebugEmulatorControls
from modules.sprites import generate_placeholder_image
from modules.version import pokebot_name, pokebot_version

class EmulatorScreen:
    def __init__(self, window: Tk):
        self.window = window
        self.frame: Union[ttk.Frame, None] = None
        self.canvas: Union[Canvas, None] = None
        self.current_canvas_image: Union[PhotoImage, None] = None
        self._current_canvas_image_id: int | None = None
        self._placeholder_image: Union[PhotoImage, None] = None
        self.center_of_canvas: tuple[int, int] = (240, 160)

        self.width: int = 240
        self.height: int = 160
        self._scale: int = 2

        self._stepping_mode: bool = False
        self._stepping_button: Union[Button, None] = None
        self._current_step: int = 0

        self._controls: EmulatorControls | None = None

    def _initialise_controls(self, debug: bool = False) -> None:
        if debug:
            controls = DebugEmulatorControls(self.window)
            controls.add_tab(TasksTab())
            controls.add_tab(BattleTab())
            controls.add_tab(PlayerTab())
            controls.add_tab(MapTab(self.canvas))
            controls.add_tab(DaycareTab())
            controls.add_tab(SymbolsTab())
            controls.add_tab(EventFlagsTab())
            controls.add_tab(InputsTab())
        else:
            controls = EmulatorControls(self.window)
        self._controls = controls

    def enable(self) -> None:
        self.window.title(f"{context.profile.path.name} | {pokebot_name} {pokebot_version}")
        self.window.resizable(context.debug, context.debug)
        self.window.rowconfigure(0, weight=1)
        self.window.columnconfigure(0, weight=1)

        self.frame = ttk.Frame(self.window)
        self.frame.grid(sticky="NSWE")
        self.frame.rowconfigure(0, weight=1)
        self.frame.columnconfigure(0, weight=1)

        self._add_canvas()
        self._initialise_controls(context.debug)
        self.scale = 2

    def disable(self) -> None:
        if self.frame:
            self.frame.destroy()
        self.window.geometry("540x400")
        self.window.resizable(context.debug, True)

    def update(self) -> None:
        if context.emulator._performance_tracker.time_since_last_render() >= (1 / 60) * 1_000_000_000:
            if context.emulator.get_video_enabled():
                self._update_image(context.emulator.get_current_screen_image())
            else:
                self._update_window()
            context.emulator._performance_tracker.track_render()

        previous_step = self._current_step
        while self._stepping_mode and previous_step == self._current_step:
            self.window.update_idletasks()
            self.window.update()
            time.sleep(1 / 60)

    def on_settings_updated(self) -> None:
        self._controls.update()
        if not context.video and self._placeholder_image is None:
            self._generate_placeholder_image()
        elif context.video and self._placeholder_image is not None:
            self._placeholder_image = None

    @property
    def scale(self) -> int:
        return self._scale

    @scale.setter
    def scale(self, scale: int) -> None:
        self._controls.remove_from_window()
        self._scale = scale
        if scale > 1:
            self.window.geometry(
                "%dx%d"
                % (
                    self.width * self._scale + self._controls.get_additional_width(),
                    self.height * self._scale + self._controls.get_additional_height(),
                )
            )
        else:
            self.window.geometry(f"{self.width}x{self.height}")

        self.window.rowconfigure(0, weight=0, minsize=self.height * self._scale)
        self.window.rowconfigure(1, weight=1)
        self.window.columnconfigure(0, weight=0, minsize=self.width * self._scale)
        self.window.columnconfigure(1, weight=1)

        self.canvas.config(width=self.width * self._scale, height=self.height * self._scale)
        self.center_of_canvas = (self._scale * self.width // 2, self._scale * self.height // 2)

        if not context.video:
            self._generate_placeholder_image()

        if scale > 1:
            self._controls.add_to_window()

    def toggle_stepping_mode(self) -> None:
        self._stepping_mode = not self._stepping_mode
        if self._stepping_mode:

            def next_step():
                self._current_step += 1

            self._stepping_button = Button(
                self.window, text="тоЮ", padx=8, background="red", foreground="white", command=next_step, cursor="hand2"
            )
            self._stepping_button.place(x=0, y=0)
            self._current_step = 0
        else:
            self._stepping_button.destroy()

    def _generate_placeholder_image(self):
        image = generate_placeholder_image(self.width * self.scale, self.height * self.scale)
        self._placeholder_image = PIL.ImageTk.PhotoImage(image)
        self.current_canvas_image = self._placeholder_image
        self.canvas.create_image(self.center_of_canvas, image=self.current_canvas_image, state="normal")

    def _update_image(self, image: PIL.Image):
        if self._current_canvas_image_id:
            self.canvas.delete(self._current_canvas_image_id)
        self.current_canvas_image = PIL.ImageTk.PhotoImage(
            image=image.resize((self.width * self.scale, self.height * self.scale), resample=False)
        )
        self._current_canvas_image_id = self.canvas.create_image(
            self.center_of_canvas, image=self.current_canvas_image, state="normal"
        )
        self._update_window()

    def _update_window(self):
        if self.scale > 1:
            self._controls.on_frame_render()

        self.window.update_idletasks()
        self.window.update()

    def _add_canvas(self) -> None:
        self.canvas = Canvas(self.window, width=480, height=320)
        self.canvas.grid(sticky="NW", row=0, column=0)
        if context.debug:

            def handle_click_on_video_output(event):
                if context.video:
                    self._controls.on_video_output_click((event.x // self.scale, event.y // self.scale), self.scale)

            self.canvas.bind("<Button-1>", handle_click_on_video_output)


"""
*** Simple Mixed Integer Linear Programming model

For more details please refer to Chapter 2 (Gcode2.3), of the following book:
Soroudi, Alireza. Power System Optimization Modeling in GAMS. Springer, 2017.
--------------------------------------------------------------------------------
Model type: MIP
--------------------------------------------------------------------------------
Contributed by
Dr. Alireza Soroudi
IEEE Senior Member
email: alireza.soroudi@gmail.com
We do request that publications derived from the use of the developed GAMS code
explicitly acknowledge that fact by citing
Soroudi, Alireza. Power System Optimization Modeling in GAMS. Springer, 2017.
DOI: doi.org/10.1007/978-3-319-62350-4
"""
from gamspy import Container
from gamspy import Equation
from gamspy import Model
from gamspy import Variable

def main():
    m = Container()

    # VARIABLES #
    x = Variable(m, name="x", type="free")
    of = Variable(m, name="of", type="free")
    y = Variable(m, name="y", type="binary")

    # EQUATIONS #
    eq1 = Equation(m, name="eq1", type="regular")
    eq2 = Equation(m, name="eq2", type="regular")
    eq3 = Equation(m, name="eq3", type="regular")

    eq1.expr = -3 * x + 2 * y >= 1
    eq2.expr = -8 * x + 10 * y <= 10
    eq3.expr = x + y == of

    MIP1 = Model(
        m,
        name="MIP1",
        equations=m.getEquations(),
        problem="mip",
        sense="max",
        objective=of,
    )

    x.up.assign = 0.3
    MIP1.solve()

    print("Objective Function Value:  ", round(of.toValue(), 4))
    print("y:  ", round(y.toValue(), 4))
    print("x:  ", round(x.toValue(), 4))

if __name__ == "__main__":
    main()


from __future__ import division

from collections import namedtuple
from datetime import datetime, timedelta
from itertools import combinations
from uuid import uuid4

import networkx as nx
import numpy as np

from projectpredict.exceptions import InvalidProject

ExpectedCompletionTimeSamples = namedtuple('ExpectedCompletionTimeSamples', ['samples', 'is_final_task'])
Statistic = namedtuple('Statistic', ['mean', 'var'])

def datetime_stats(datetimes):
    """Gets the mean and variance of a collection of datetimes

    Args:
        datetimes (iterable(datetime)): The datetimes to compute the statistics on.

    Returns:
        dict: A dictionary containing keys for the mean and variance. The mean is a datetime, and the variance is a
            timedelta.
    """
    seconds_timestamp = [(datetime_ - datetime.utcfromtimestamp(0)).total_seconds() for datetime_ in datetimes]
    return {'mean': datetime.utcfromtimestamp(np.mean(seconds_timestamp)),
            'variance': timedelta(seconds=np.var(seconds_timestamp))}

def timedelta_stats(timedeltas):
    """Gets the mean and variance of a collection of timedeltas

    Args:
        timedeltas (iterable(timedelta)): The timedeltas to compute the statistics on.

    Returns:
        dict: A dictionary containing keys for the mean and variance. both the mean and variance are datetimes.
    """
    seconds_timestamp = [timedelta_.total_seconds() for timedelta_ in timedeltas]
    return {'mean': timedelta(seconds=np.mean(seconds_timestamp)),
            'variance': timedelta(seconds=np.var(seconds_timestamp))}

class TaskSample(object):
    """A wrapper for a sample of the derived statistics for a Task

    Attributes:
        duration (timedelta): The sampled duration of the task
        earliest_start (datetime): The sampled earliest start date of the task
        latest_finish(datetime): The sampled latest finish date of the task
        latest_start (datetime): The latest start date of the task. Must be set independently of the constructor
        earliest_finish (datetime): The earliest finish date of the task. Must be set independently of the constructor.

    Args:
        duration (timedelta): The sampled duration of the task
        earliest_start (datetime): The sampled earliest start date of the task
        latest_finish(datetime): The sampled latest finish date of the task
    """
    __slots__ = ['duration', 'earliest_start', 'latest_finish', 'latest_start', 'earliest_finish', '_total_float']

    def __init__(self, duration, earliest_start, latest_finish):
        self.duration = duration
        self.earliest_start = earliest_start
        self.latest_finish = latest_finish
        self.latest_start = None
        self.earliest_finish = None
        self._total_float = None

    @property
    def total_float(self):
        """timedelta: The total float of the task. Earliest finish mst be set before calculation.
        """
        if self._total_float:
            return self._total_float
        elif self.latest_finish and self.earliest_finish:
            self._total_float = self.latest_finish - self.earliest_finish
        return self._total_float

    @classmethod
    def from_task(cls, task, current_time):
        """Constructs a TaskSample from a task

        Args:
            task (Task): The task to sample
            current_time (datetime): The current datetime used to sample the task

        Returns:
            TaskSample: The constructed sample
        """
        return cls(
            task.get_duration_sample(current_time),
            task.get_earliest_start_sample(current_time),
            task.get_latest_finish_sample())

class TaskStatistics(object):
    """A container for the relevant derived statistics for a Task

    Attributes:
        latest_start (dict): A dict containing the mean and variance of the latest start date of the task in 'mean' and
            'variance' keys respectively.
        earliest_finish (dict): A dict containing the mean and variance of the earliest finish date of the task in
            'mean' and 'variance' keys respectively.
        total_float (dict): A dict containing the mean and variance of the total float date of the task in 'mean' and
            'variance' keys respectively.

    Args:
        latest_start (dict): A dict containing the mean and variance of the latest start date of the task in 'mean' and
            'variance' keys respectively.
        earliest_finish (dict): A dict containing the mean and variance of the earliest finish date of the task in
            'mean' and 'variance' keys respectively.
        total_float (dict): A dict containing the mean and variance of the total float date of the task in 'mean' and
            'variance' keys respectively.

    """
    __slots__ = ['latest_start', 'earliest_finish', 'total_float']

    def __init__(self, latest_start, earliest_finish, total_float):
        self.latest_start = latest_start
        self.earliest_finish = earliest_finish
        self.total_float = total_float

    @classmethod
    def from_samples(cls, samples):
        """Construct a TaskStatistics object from samples

        Args:
            samples (iterable(TaskSample)): The samples to compute the statistics from.

        Returns:
            TaskStatistics: The constructed TaskStatistics
        """
        return cls(
            datetime_stats([sample.latest_start for sample in samples]),
            datetime_stats([sample.earliest_finish for sample in samples]),
            timedelta_stats(sample.total_float for sample in samples)
        )

    def __repr__(self):
        return str({
            'latest_start': self.latest_start,
            'earliest-finish': self.earliest_finish,
            'total_float': self.total_float
        })

DependencySummary = namedtuple('DependencySummary', ['source', 'destination'])

class Project(nx.DiGraph):
    """A project

    Note:
        This must be an acyclic graph.

    Attributes:
        name (str): The name of the project
        uid (UUID): The UUID of the project
        model: A model used to predict the duration of tasks from their data

    Args:
        name (str): The name of the project
        model (optional): A model used to predict the duration of tasks from their data
        uid (UUID, optional): The UUID of the project
        tasks (iterable(Task), optional): A collections of Tasks associated with this project
        dependencies (iterable(dict), optional): The dependencies associated with the project in the form of dicts of
            'source' and 'destination' keys.
    """

    def __init__(self, name, model=None, uid=None, tasks=None, dependencies=None):
        super(Project, self).__init__()
        self.uid = uid or uuid4()
        self.name = name
        self.model = model
        for task in (tasks or []):
            self.add_task(task)
        for dependency in (dependencies or []):
            source = self.get_task_from_id(dependency['source'])
            target = self.get_task_from_id(dependency['destination'])
            self.add_edge(source, target)

    def validate(self):
        """Validates the Project meets the requirements to do inference

        Checks:
        * The Project is a directed acyclic graph
        * Every terminal Task (one without successors) has a latest start date PDF

        Raises:
            InvalidProject: If the project does not conform to the requirements.
        """
        errors = []
        if not nx.is_directed_acyclic_graph(self):
            errors.append('Project is not acyclic')
        for task in self.tasks:
            if not list(self.successors(task)) and task.latest_finish_date_pdf is None:
                errors.append('Task {} (id={}) requires a latest finish date pdf'.format(task.name, task.uid))
        if errors:
            raise InvalidProject(errors)

    @property
    def dependencies(self):
        """list[tuple(Task, Task)]: The dependencies in the project where the first element of the tuple is the source
        task and the second element of the tuple is the dependent task.
        """
        return self.edges

    @property
    def tasks(self):
        """iterable(Task): The tasks of this project
        """
        return self.nodes

    @property
    def dependencies_summary(self):
        """list[DependencySummary]: The dependencies of this project
        """
        return [DependencySummary(source.uid, target.uid) for source, target in self.edges]

    def get_task_from_id(self, id_):
        """Gets a task from an id

        Args:
            id_ (UUID): The UUID of the project to get

        Returns:
            Task|None: The task with the associated with the id or None if task is not found
        """
        return next((task for task in self.tasks if task.uid == id_), None)

    def add_task(self, task):
        """Adds a Task to this Project and determines the duration PDF of the task from the model if not previously specified.

        Args:
            task (Task): The Task to add to the project
        """
        task.project_uid = self.uid
        if not task.duration_pdf:
            if not self.model:
                msg = ('Could not add task. Either a duration pdf must be specified or a model must be specified for '
                       'the project')
                raise InvalidProject([msg])
            task.set_duration_pdf(self.model)
        self.add_node(task)

    def add_tasks(self, tasks):
        """Adds multiple Tasks to this Project and determines the duration PDF of the task from the model if not
            previously specified.

        Args:
            tasks (iterable(Task)): The Task to add to the project
        """
        for task in tasks:
            self.add_task(task)

    def add_dependency(self, parent, child):
        """Adds a Task dependency to this Project

        Args:
            parent (Task): The parent task
            child (Task): The child task, i.e. the Task which depends on the parent
        """
        if parent.uid == child.uid:
            raise InvalidProject('Parent and child Tasks must be different')
        if child not in self.tasks:
            self.add_task(child)
        if parent not in self.tasks:
            self.add_task(parent)
        self.add_edge(parent, child)

    def add_dependencies(self, dependencies):
        """Adds multiple Task dependencies to this Project

        Args:
            dependencies (list[tuple(Task, Task)]): A list of tuples of Task dependencies in the form of
                (parent task, child task)
        """
        for dependency in dependencies:
            self.add_dependency(*dependency)

    def calculate_earliest_finish_times(self, current_time=None, iterations=1000):
        """Generates samples of the earliest finish times for each uncompleted node in the project.

        Args:
            current_time (datetime): the time at which to take the samples
            iterations (int, optional): The number of samples to generate. Defaults to 1000

        Returns:
            dict{Task: [datetime]}: A dictionary of the samples for each task.
        """
        self.validate()
        samples = self._get_samples(
            forward_sample_func=self.earliest_finish_sample_func,
            iterations=iterations,
            current_time=current_time)
        return {task: [task_sample.earliest_finish for task_sample in task_samples]
                for task, task_samples in samples.items()}

    @staticmethod
    def earliest_finish_sample_func(task, parents, children, samples, **kwargs):
        if not parents:
            earliest_finish = samples[task].earliest_start + samples[task].duration
        else:
            earliest_start = max(samples[parent_task].earliest_finish for parent_task in parents)
            samples[task].earliest_start = earliest_start
            earliest_finish = earliest_start + samples[task].duration

        samples[task].earliest_finish = earliest_finish

    def calculate_latest_start_times(self, iterations=1000):
        """Generates samples of the latest start times for each uncompleted node in the project.

        Args:
            iterations (int, optional): The number of samples to generate. Defaults to 1000

        Returns:
            dict{Task: [datetime]}: A dictionary of the samples for each task.
        """
        self.validate()
        samples = self._get_samples(
            backward_sample_func=self.latest_start_sample_func,
            iterations=iterations)
        return {task: [task_sample.latest_start for task_sample in task_samples]
                for task, task_samples in samples.items()}

    @staticmethod
    def latest_start_sample_func(task, parents, children, samples, **kwargs):
        if not children:
            latest_start = samples[task].latest_finish - samples[task].duration
        else:
            latest_finish = min(samples[child_task].latest_start for child_task in children)
            samples[task].latest_finish = latest_finish
            latest_start = latest_finish - samples[task].duration
        samples[task].latest_start = latest_start

    def _get_samples(
            self,
            forward_sample_func=None,
            backward_sample_func=None,
            iterations=1000,
            current_time=None,
            **kwargs):
        current_time = current_time or datetime.utcnow()
        sample_collection = {node: [] for node in self.nodes}
        for _ in range(iterations):
            samples = {task: TaskSample.from_task(task, current_time) for task in self.nodes}
            if forward_sample_func:
                for task in nx.topological_sort(self):
                    children, parents = self._get_parents_and_children(task)
                    forward_sample_func(task, parents, children, samples, **kwargs)
            if backward_sample_func:
                for task in reversed(list(nx.topological_sort(self))):
                    children, parents = self._get_parents_and_children(task)
                    backward_sample_func(task, parents, children, samples, **kwargs)
            for task, sample in samples.items():
                sample_collection[task].append(sample)
        return sample_collection

    def _get_parents_and_children(self, task):
        parents = list(parent for parent in self.predecessors(task) if not parent.is_completed)
        children = list(child for child in self.successors(task) if not child.is_completed)
        return children, parents

    def calculate_task_statistics(self, current_time=None, iterations=1000):
        self.validate()
        samples = self._get_samples(
            forward_sample_func=self.earliest_finish_sample_func,
            backward_sample_func=self.latest_start_sample_func,
            iterations=iterations,
            current_time=current_time)
        return {task: TaskStatistics.from_samples(task_samples) for task, task_samples in samples.items()}

    def recommend_next(
            self,
            current_time=None,
            constraints=None,
            iterations=1000,
            score_func=None,
            selection_func=None,
            min_number=1,
            max_number=1,
            batch_wait=False,
            selection_func_arguments=None,
            **score_func_arguments):
        """Get the recommended next tasks

        Args:
            current_time (datetime, optional): The current time (in UTC) to query the project.
                Defaults to the current time.
            constraints (iterable(callable)): A list of constraints to apply to the selected tasks. These must be
                functions which task in two parameters -- the project (self) and the set of Tasks under consideration.
            iterations (int, optional): The number of iterations to query the project for each considered set of Tasks.
                Defaults to 1000.
            score_func (func, optional): The function used to score the results of a Task set. Defaults to a function
                which returns a dict containing the mean and precision (inverse variance) of the total float of each
                task weighted by the Tasks' deadline weight. The function must take keyword arguments which can be
                specified as keyword arguments to this function (see score_func_arguments).
            selection_func (func, optional): The function used to select which task set is best from the results
                returned from the score_func. Defaults to a function which scales the total float and precision each
                between 0 and 1 and sums them according to a weighting parameter (see selection_func_arguments). The
                function must accept a dict of Task set to score and keyword arguments which can be specified by the
                selection_func_arguments parameter of this function.
            min_number (int, optional): The minimum number of tasks which can can be recommended. Defaults to 1.
            max_number (int, optional): The maximum number of tasks which can be recommended. Defaults to 1.
            batch_wait (bool, optional): Do all tasks for a proposed tuple of Tasks need to be completed before the next
                tasks can begin? Defaults to False.
            selection_func_arguments (dict, optional): The arguments to be passed to the selection_func.
            **score_func_arguments: The arguments to pass to the score_func

        Returns:
            tuple(Task): The recommended tasks to complete next
        """
        self.validate()
        score_func = score_func or self._default_recommendation_score_func
        selection_func = selection_func or self._default_recommendation_selection_func
        selection_func_arguments = selection_func_arguments or {}
        current_time = current_time or datetime.utcnow()
        if not constraints:
            constraints = []

        possible_tasks = [node for node in self.nodes
                          if not node.is_completed
                          and not [parent_node for parent_node in self.predecessors(node)
                                   if not parent_node.is_completed]]
        if not max_number:
            max_number = len(possible_tasks)
        scores = {}
        for n in reversed(range(min_number, max_number + 1)):
            for task_set in (task_set for task_set in combinations(possible_tasks, n)
                             if all(constraint(self, task_set) for constraint in constraints)):
                mean_durations = {task: task.mean_duration for task in task_set}
                samples = self._get_samples(
                    forward_sample_func=self.recommendation_sample_func,
                    backward_sample_func=self.latest_start_sample_func,
                    current_time=current_time,
                    iterations=iterations,
                    mean_duration_lookup=mean_durations,
                    tasks_under_sample=task_set,
                    batch_wait=batch_wait)
                scores[tuple(task_set)] = score_func(samples, **score_func_arguments)
        if not scores:
            raise ValueError('No tasks found which meet constraints')
        return selection_func(scores, **selection_func_arguments)

    @staticmethod
    def recommendation_sample_func(task, parents, children, samples, **kwargs):
        tasks_under_sample = kwargs['tasks_under_sample']
        if task in kwargs['tasks_under_sample']:
            earliest_finish = samples[task].earliest_start + samples[task].duration
        else:
            if not parents:
                if kwargs['batch_wait']:
                    additional_wait = min(samples[task_].duration for task_ in tasks_under_sample)
                else:
                    additional_wait = max(samples[task_].duration for task_ in tasks_under_sample)
                earliest_finish = samples[task].earliest_start + samples[task].duration + additional_wait
            else:
                earliest_start = (max(samples[parent_task].earliest_finish for parent_task in parents))
                samples[task].earliest_start = earliest_start
                earliest_finish = earliest_start + samples[task].duration
        samples[task].earliest_finish = earliest_finish

    @staticmethod
    def _default_recommendation_score_func(samples, **kwargs):
        task_float_stats = {task: timedelta_stats([sample.total_float for sample in task_samples])
                            for task, task_samples in samples.items()}
        float_score = sum(
            score['mean'].total_seconds() * task.deadline_weight for task, score in task_float_stats.items())
        precision = sum(1 / max(score['variance'].total_seconds(), 1e-6) * task.deadline_weight
                        for task, score in task_float_stats.items())
        return {'total_float_score': float_score, 'precision': precision}

    @staticmethod
    def _default_recommendation_selection_func(scores, **kwargs):

        def scale_scores(field):
            min_score = min(score[field] for score in scores.values())
            max_score = max(score[field] for score in scores.values())
            score_diff = max_score - min_score
            return {task_set: (score[field] - min_score) / max(score_diff, 1e-6) for task_set, score in scores.items()}

        float_scores = scale_scores('total_float_score')
        precision_scores = scale_scores('precision')
        risk_tolerance = kwargs.get('risk_tolerance', 0.5)
        combined_scores = {
            task_set: risk_tolerance * float_scores[task_set] + (1 - risk_tolerance) * precision_scores[task_set]
            for task_set in scores}
        return max(combined_scores, key=combined_scores.get)

    def get_starting_and_terminal_tasks(self):
        """Gets the starting tasks (ones without predecessors) and terminal tasks (ones without successors)

        Returns:
            tuple(list[Task], list[Task]): The starting and terminal tasks in the form of
                (starting tasks, terminal tasks)
        """
        start_tasks = []
        terminal_tasks = []
        for task in self.tasks:
            if len(list(self.predecessors(task))) == 0:
                start_tasks.append(task)
            if len(list(self.successors(task))) == 0:
                terminal_tasks.append(task)
        return start_tasks, terminal_tasks

    def update_from_dict(self, data):
        """Updates the Project using a dictionary of new values

        Args:
            data (dict): The new values
        """
        if 'name' in data:
            self.name = data['name']
        new_tasks = data.get('tasks', [])
        new_dependencies = [(dependency['source'], dependency['destination']) for dependency in
                            data.get('dependencies', [])]
        for task in new_tasks:
            if task not in self.tasks:
                self.add_task(task)
        removed_tasks = [task for task in self.tasks if task not in new_tasks]
        for task in removed_tasks:
            self.remove_node(task)
        edges = [(dependency[0].uid, dependency[1].uid) for dependency in self.dependencies]
        for dependency in new_dependencies:
            if dependency not in edges:
                dependency = (self.get_task_from_id(dependency[0]), self.get_task_from_id(dependency[1]))
                self.add_edge(*dependency)

        removed_dependencies = [dependency for dependency in edges if dependency not in new_dependencies]
        for dependency in removed_dependencies:
            try:
                self.remove_edge(self.get_task_from_id(dependency[0]), self.get_task_from_id(dependency[1]))
            except nx.NetworkXError:
                pass

    @classmethod
    def from_dict(cls, data_in, model):
        """Constructs a Project from a dictionary of values and a model

        Args:
            data_in (dict): The data to construct the Project from
            model: The model used to predict the durations of tasks

        Returns:
            Project: The constructed project
        """
        return cls(
            data_in['name'],
            model,
            uid=data_in.get('uid', None),
            tasks=data_in.get('tasks', []),
            dependencies=data_in.get('dependencies', [])
        )


from torch import nn
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
from .layers import Conv1DLayer

class Encoder(nn.Module):
    def __init__(self, num_chars, embed_dim=512,
                 conv_layers=3, kernel_size=5, dropout=0.5):
        super(Encoder, self).__init__()
        self.embedding = nn.Embedding(num_chars, embed_dim)
        self.conv_layers = nn.ModuleList([
            Conv1DLayer(embed_dim, embed_dim, kernel_size, 'relu', dropout)
            for _ in range(conv_layers)
        ])

        self.lstm = nn.LSTM(input_size=embed_dim,
                            hidden_size=embed_dim // 2,
                            batch_first=True, bidirectional=True)

    def forward(self, chars, lengths):
        # chars: (batch_size, char_length)
        # lengths: (batch_size, )

        embeds = self.embedding(chars)
        embeds = embeds.transpose(1, 2)
        # embeds: (batch_size, embed_dim, char_length)

        features = embeds
        for layer in self.conv_layers:
            features = layer(features)
        features = features.transpose(1, 2)
        # features: (batch_size, char_length, embed_dim)

        features = pack_padded_sequence(features, lengths, batch_first=True,
                                        enforce_sorted=False)
        features, _ = self.lstm(features)
        features, _ = pad_packed_sequence(features, batch_first=True)
        # features: (batch_size, char_length, embed_dim)

        return features