from jyquickhelper import add_notebook_menu
add_notebook_menu()

"""
Explanation: 2A.i - Données non structurées, programmation fonctionnelle
Une table dans une base de données est déjà le résultat d'une réflexion sur la façon de les représenter.
End of explanation
"""


import pyensae.datasource
pyensae.datasource.download_data("twitter_for_network_100000.db.zip")

import numpy as np

def my_sum(l):
    res = 0
    for it in l:
        res += it
    return res
l = list(range(100000))
a = np.arange(100000)

print("User defined method or cross-method")
%timeit my_sum(a) # user defined with numpy array
%timeit sum(a)    # built-in with numpy array
%timeit np.sum(l) # numpy function with list
%timeit my_sum(l) # user definedwith list
print("Builtin function")
%timeit sum(l)    # built-in with list
print("Numpy function")
%timeit np.sum(a) # numpy function
%timeit a.sum()   # numpy method

"""
Explanation: Avant-propos : programmation fonctionnelle ou numpy ?

toolz/cytoolz : programmation fonctionnelle
numpy : calcul matriciel

Données : twitter_for_network_100000.db.zip or twitter_for_network_100000.db.zip (xavierdupre.fr).
End of explanation
"""


import os, psutil, gc, sys
if not sys.platform.startswith("win"):
    import resource

def memory_usage_psutil():
    gc.collect()
    process = psutil.Process(os.getpid())
    mem = process.memory_info()[0] / float(2 ** 20)

    print( "Memory used : %i MB" % mem )
    if not sys.platform.startswith("win"):
        print( "Max memory usage : %i MB" % (resource.getrusage(resource.RUSAGE_SELF).ru_maxrss//1024) )

memory_usage_psutil()

import cytoolz as ct # import groupby, valmap, compose
import cytoolz.curried as ctc ## pipe, map, filter, get
import sqlite3
import pprint
try:
    import ujson as json
except:
    print("ujson not available")
    import json

conn_sqlite = sqlite3.connect("twitter_for_network_100000.db")
cursor_sqlite = conn_sqlite.cursor()

"""
Explanation: Il y a un rapport de 10 dans le temps d'exécution entre la méthode "user defined" et la méthode "builtin".
On retrouve ce même rapport entre la méthode "builtin" et les méthodes numpy.
On peut noter que mélanger les objets numpy et non-numpy donne de très mauvais résultats.
Dans le cas de la programmation fonctionnelle, nous nous situerons plutôt dans le cas "builtin" :
<table><thead><tr><th>Project</th><th>Computation</th><th>Data Structures</th></tr></thead><tbody><tr><td>Code de l'utilisateur</td><td>Python</td><td>Python</td></tr><tr><td>CyToolz</td><td>C</td><td>Python</td></tr><tr><td>Pandas/NumPy</td><td>C</td><td>C</td></tr></tbody></table>

Dans le cas de manipulation de donnée structurée de taille "raisonnable", pandas et numpy restent plus performants. Ils peuvent toutefois être limités par plusieurs points :

manipulation de données plus complexes avec des sous-listes, des champs manquants
ils sont construits sur le principe de chargement en mémoire des données
End of explanation
"""


cursor_sqlite.execute('SELECT content FROM tw_users' )
object_to_sum = ctc.pluck( "followers_count", ctc.map( json.loads, ctc.pluck( 0, cursor_sqlite ) ) )
print(sum(object_to_sum))

memory_usage_psutil()

"""
Explanation: Le code suivant va lire plusieurs gigaoctets de données, et la consommation maximale de mémoire du process ne va augmenter que de quelques Mo. De plus ce code manipule des dictionnaires qu'il serait compliqué de faire rentrer dans un DataFrame pandas.
End of explanation
"""


import pprint

cursor_sqlite.execute('SELECT content FROM tw_users LIMIT 1')
user = cursor_sqlite.fetchone()[0]
print("#"*15 + " user raw json " + "#"*15)
print( user )
print("#"*15 + " user as python dict " + "#"*15)
pprint.pprint( json.loads( user ) )
cursor_sqlite.execute('SELECT content FROM tw_status LIMIT 1')
print("#"*15 + " status as python dict " + "#"*15)
pprint.pprint( json.loads( cursor_sqlite.fetchone()[0] ) )

"""
Explanation: Dans le cadre du TP d'aujourd'hui, les données que nous allons utiliser peuvent largement tenir en mémoire, et de façon générale, lorsqu'on développe des codes pour gérer des gros volumes de données, on les teste sur des volumes de données qui tiennent en mémoire.
Dans le cadre de la gestion de volume important de données, on ne pourra donc pas stocker des résultats intermédiaire, on va donc composer des fonctions pour qu'elles produisent directement le résultat final.
Cas classique :
resultat_intermediaire_1 = f( donnees )
resultat_intermediaire_2 = g( resultat_intermediaire_1 )
resultat_final = h( resultat_intermediaire_2 )
Programmation fonctionnelle :
resultat_final = h( g( f( donnees ) ) )
Données structurées SQL et NOSQL
SQL signifie Structured Query Language, les tables ont un nombre de colonnes fixes, et chaque colonne possède un type particulier.
Comment gère-t-on un nombre d'objets variables ? La plupart du temps avec une table secondaire qui contiendra une ligne par élément de la liste.
Par exemple, si l'on veut stocker la liste des films possédés par une personne.
<table><tbody><tr><td><h5>Table person</h5></td><td><table><thead><tr><th>Id</th><th>Name</th></tr></thead><tbody><tr><td>1</td><td>Jean</td></tr><tr><td>2</td><td>Paul</td></tr><tr><td>3</td><td>Jacques</td></tr></tbody></table></td><td><h5>Table related_items</h5></td><td><table><thead><tr><th>Id</th><th>Person_id</th><th>Value</th></tr></thead><tbody><tr><td>1</td><td>1</td><td>Star wars</td></tr><tr><td>2</td><td>1</td><td>Cyrano</td></tr><tr><td>3</td><td>1</td><td>Lord of the rings</td></tr><tr><td>4</td><td>2</td><td>Mad max</td></tr><tr><td>5</td><td>2</td><td>Dr Horrible</td></tr></tbody></table></td></tbody></table>

Ce système est très "structuré" (comme son nom l'indique) et peut s'avérer assez lourd si l'on a affaire à des données moins bien structurées, avec beaucoup de listes, des données présentes ou non.
On voit donc se développer de plus en plus des systèmes alternatifs, dit NoSQL (pour Not Only Sql).
Nous allons en voir trois :

Sqlite3 support for Json
mongodb
PostGreSql

Ils sont analogues sur la nature des données stockées, elles le sont au format Json.
Json ? Qu'est ce que c'est ?
Il s'agit du format majoritaire pour les API informatiques internet.
Par exemple les données renvoyées par twitter (sur lesquelles nous travaillerons aujourd'hui) sont sous ce format.
Il signifie : JavaScript Object Notation.
Il se base essentiellement sur des dictionnaires (association clé/valeur) et des listes.
Il est très proche des objets python (modulo les false/False)
End of explanation
"""


try:
    import psycopg2
    from psycopg2.extras import Json
    postgre_ok = True
except ImportError:
    postgre_ok = False

if postgre_ok:
    db_name = 'cours_ensae'
    conn_string = "host='localhost' dbname='{0}' user='python' password='kyojin'".format( db_name )
    try:
        conn_psql = psycopg2.connect(conn_string)
        cursor_psql = conn_psql.cursor()
        postgre_ok = True
    except psycopg2.OperationalError:
        postgre_ok = False    

if postgre_ok:
    conn_psql.server_version

if postgre_ok:
    conn_psql.rollback()

if postgre_ok:
    def get_data_sql(doc_id):
        cursor_psql.execute("SELECT id, company FROM document WHERE id = %s", (doc_id,))
        res_1 = cursor_psql.fetchone()
        cursor_psql.execute("SELECT id FROM ticket WHERE document_id = %s ORDER BY id", (doc_id,))
        res_2 = cursor_psql.fetchall()
        tickets_id = [it[0] for it in res_2 ]
        cursor_psql.execute("SELECT id FROM coupon WHERE ticket_id = ANY( %s ) ORDER BY id", (tickets_id,))
        res_3 = cursor_psql.fetchall()
        return res_1 + (res_2,) + (res_3,)

    %timeit get_data_sql(10000) 
    get_data_sql(10000)

if postgre_ok:
    def get_data_sql_join(doc_id):
        cursor_psql.execute("SELECT d.id, d.company, t.id, c.id FROM document as d \
                                                                JOIN ticket as t on d.id = t.document_id \
                                                                JOIN coupon as c on t.id = c.ticket_id \
                             WHERE d.id = %s", (doc_id,))
        return cursor_psql.fetchall()

    %timeit get_data_sql_join(10000) 
    get_data_sql_join(10000)

if postgre_ok:
    def get_data_nosql(doc_id):
        cursor_psql.execute("SELECT id, company, content FROM document_nosql WHERE id = %s", (doc_id,))
        return cursor_psql.fetchone()

    %timeit get_data_nosql(10000)
    get_data_nosql(10000)

"""
Explanation: Le NoSql / json permet donc une alternative au schéma classique suivant : 
<table><tbody><tr><td><h5>Table person</h5></td><td><table><thead><tr><th>Id</th><th>Name</th></tr></thead><tbody><tr><td>1</td><td>Jean</td></tr><tr><td>2</td><td>Paul</td></tr><tr><td>3</td><td>Jacques</td></tr></tbody></table></td><td><h5>Table related_items</h5></td><td><table><thead><tr><th>Id</th><th>Person_id</th><th>Value</th></tr></thead><tbody><tr><td>1</td><td>1</td><td>Star wars</td></tr><tr><td>2</td><td>1</td><td>Cyrano</td></tr><tr><td>3</td><td>1</td><td>Lord of the rings</td></tr><tr><td>4</td><td>2</td><td>Mad max</td></tr><tr><td>5</td><td>2</td><td>Dr Horrible</td></tr></tbody></table></td></tbody></table>

Qui serait :
<table><tbody><tr><td><h5>Table person_with_items</h5></td><td><table><thead><tr><th>Id</th><th>Name</th><th>Item_list</th></tr></thead><tbody><tr><td>1</td><td>Jean</td><td>['Star wars', 'Cyrano', 'Lord of the rings']</td></tr><tr><td>2</td><td>Paul</td><td>['Mad max', 'Dr Horrible']</td></tr><tr><td>3</td><td>Jacques</td><td></td></tr></tbody></table></td></tr></tbody></table>

Cette dernière structure serait vraiment un exemple de nosql dans son sens de Not Only Sql, il y a un mixe de données structurées et non structurées.
Il y a également certaines base de données où il n'y a plus du tout de structure, comme mongodb, qui est qualifiée de document-oriented.
<table><tbody><tr><td><h5>Table person_with_items</h5></td><td><table><tbody><tr><td>{'Id': 1, 'Name': 'Jean', 'Item_list' : ['Star wars', 'Cyrano', 'Lord of the rings']}</td></tr><tr><td>{'Id': 2, 'Name': 'Paul', 'Item_list' : ['Mad max', 'Dr Horrible']}</td></tr><tr><td>{'Id': 3, 'Name': 'Jacques', 'Item_list' : []}</td></tr></tbody></table></td></tr></tbody></table>

Il faut toutefois remarquer que cette dernière structure au moins deux inconvénients par rapport à une structure Sql avec une sous-table :

vous ne pouvez pas accéder directement aux objets de 'Item_list' sans passer par la table person
les informations de Item_list ne peuvent être partagées entre plusieurs objets, on peut donc avoir à restocker les informations

Voir module psycopg.
End of explanation
"""


mongo = False
if mongo:
    import pymongo

    mongo_client = pymongo.MongoClient( 'localhost', 27017 )
    mongo_db = mongo_client.ensae_db

    mongo_db.table_for_ensae.delete_many( {} )
    mongo_db.table_for_ensae.insert_one( {'nom' : 'Martin', 'prenom' : 'Nicolas', 'grades': [20,18,7,12]} )
    mongo_db.table_for_ensae.insert_one( {'nom' : 'Dupont', 'prenom' : 'Jean', 'grades': [11,5,7,12]} )
    mongo_db.table_for_ensae.insert_one( {'nom' : 'Martin', 'prenom' : 'Gilles', 'grades': [10,10,10,10]} )

    user = mongo_db.table_for_ensae.find_one( {'nom' : 'Dupont'} )
    user_list = mongo_db.table_for_ensae.find( {} )
    _ = list(map( pprint.pprint, user_list ))

"""
Explanation: mongodb (pymongo) lui ,ne connait pas de colonnes, que des documents, dont le format est analogue à un objet json.
Cela se traduit par une très grande simplicité, pas besoin de déclarer les tables, ni les bases de données ...
End of explanation
"""


if mongo:
    result = mongo_db.table_for_ensae.group(['nom'], 
                                        None,
                                        {'list': []}, # initial
                                        'function(obj, prev) {prev.list.push(obj)}')
    pprint.pprint( result )

"""
Explanation: Par contre certaines syntaxes usuelles en sql, ici le groupby, ont une écriture nettement plus complexes en mongodb.
End of explanation
"""


cursor_sqlite.execute("SELECT content FROM tw_users LIMIT 10000" )

with open("tw_users.json", 'w') as f:
    for it_user in cursor_sqlite:
        f.write(it_user[0])
        f.write("\n")
        
with open("tw_users.json", 'r') as f:
    nb_total_followers = 0
    for it_user in f:
        nb_total_followers += json.loads( it_user )["followers_count"]

print( nb_total_followers )

"""
Explanation: Mon retour :

mongodb malgré sa simplicité d'utilisation peut être très gourmand en ressources (consommation d'espace disque et/ou mémoire 15 fois supérieure à PostGreSql pour les mêmes données). Je la déconseille pour une application personnelle.
Sqlite3 est plus archaïque, mais le fait d'avoir une base de donnée contenue dans un fichier est très pratique pour certains usages (déploiement chez un client ou pour des élèves)
PostGreSql me semble le plus robuste pour un usage en serveur personnel.

Et les fichiers plats ?
Vous pouvez tout à fait utiliser des fichiers plats.
Ils offrent beaucoup de simplicités d'utilisation.
Ils auront des performances potentiellement très proches pour une lecture complète.
End of explanation
"""


import pandas as pd

df = pd.read_sql(  "SELECT id, screen_name from tw_users", conn_sqlite )
print( df.head() )
print( df.shape )

"""
Explanation: Et pandas ?
Pandas attend lui des données plus ou moins structurées. Vous pouvez charger une base sous pandans avec la syntaxe suivante :
End of explanation
"""


