ZEP-02 : Elaboration d'une API

a marqué ce sujet comme résolu.

Si on décide par exemple d'implémenter une API en lecture seule uniquement pour des membres, on pourrait décider de certains noms de paramètres GET qui pourraient rentrer en conflits avec d'autres, si on a penser à toute la chaine. C'est donc pour ça que quitte à penser à toute la chaine, autant la spécifier entièrement.

firm1

Avec d'autres quoi ? J'ai du mal à te suivre.

+1 -0

Désolé pour le double-post, mais un peu trop gros pour une édition :

Si le problème est que certains paramètres de requêtes GET sur la liste des membres entrent en collision avec des paramètres de requêtes GET d'autres APIs, à mon avis la seule façon de s'en assurer est de dresser la liste exhaustive de tous les paramètres de toutes les méthodes de toutes les APIs (et pas d'un seul module comme tu le signalais plus haut). Ca, ça me semble totalement voué à l'échec en l'état actuel des choses. Il n'y a qu'à lire les discussions en cours sur le module de tutoriels pour se rendre compte qu'il serait plus malin de ne pas brancher d'API dessus tel qu'il est actuellement, sachant qu'on voit poindre des refontes fonctionnelles et techniques assez lourdes de ce module.

J'ai parcouru la doc du framework donné par Andr0 et il me semble tout à fait aller dans le bon sens d'une organisation saine du "branchement" d'une API sur une application web existante. Grosso modo : "vous avez déjà un modèle de données, vous me spécifiez comment les récupérer en fonction des paramètres d'entrée, les adapters, bridges et autres joyeusetés à utiliser pour traduire un modèle vers un autre, enfin, vous me dîtes comment construire la réponse HTTP et je me vis ma vie avec ça de mon côté".

La tâche qui consiste à spécifier toutes les méthodes de toutes les APIs, aujourd'hui, avec un modèle qui va bouger de façon significative dans un futur proche, ça me semble être de la pure folie.

Un seul module, oui, ça me semble déjà beaucoup plus raisonnable. Est-ce-que ça règle le problème de collision de query parameters entre différents modules, non, très clairement.

Maintenant dans ce genre de développements, j'ai tendance à préférer l'approche prototype. On a une solution technique proposée par Andr0, et en la mettant en place on va certainement se rendre compte de tout plein de difficultés techniques qu'on n'avait pas nécessairement envisagées.

Si j'ai proposé d'aller taper dans les membres, c'est que c'est à mon sens le POC parfait. Pour tester la mise en place du framework on peut se contenter de lister les membres, on pourrait agir de la sorte :

  • v0.0 : afficher un profil utilisateur sur GET /membres/{id}. Objectif : mise en place simple du framework et routage sur une URL

  • v0.1 : lister les membres "brut" on renvoit le modèle tel qu'il est en base, pas de pagination (on a 700 membres, ça devrait aller…), pas de gestion différentielle (la liste complète à chaque fois). Objectif : renvoyer une liste au lieu d'un objet

  • v0.2 : implémentation pagination / polling différentiel. Objectif : aller toucher à la requête HTTP en lecture et à la réponse HTTP en écriture, + manipulation des queryset

  • v0.3 : adaptation/ponts : on renvoie un modèle différent du modèle stocké en base. Objectif : définition de notre propre Serializer

  • v0.4 : gestion de l'authentification. sur un GET de l'URL /membres/mycredentials ou membres/monprofil (avec un jeton passé en paramètres ou dans un en-tête HTTP), … l'utilisateur dispose de plus de champs (l'adresse email) que les champs disponibles au public. Objectif : grosses décisions à prendre vis à vis de l'authentification, OAuth et compagnie. NB : le login, de toute façon, fait selon moi partie de l'API membres…

  • v0.5 : gestion du PUT : l'utilisateur authentifié peut modifier sa fiche profil via l'API. Objectifs : interaction en écriture avec le code côté serveur, transparente ?

  • v0.6 : gestion du POST : création de compte via l'API membres.

  • v0.7 : gestion des paramètres de requête. Possibilité de rechercher, filtrer, trier la liste des membres à travers les paramètres fournis à la requête GET /membres. Objectif : établir la liaison entre la couche API et la couche d'accès aux données. Que peut-on reprendre du code utilisé actuellement pour les vues ? Peut on factoriser du code pour faire une page de recherche parmi les membres sur le site ?

  • v0.8 : gérer les associations dans le modèle renvoyé au client. Ex : extraire un bout (l'url, le nom, …) d'un cours en bêta pour le mettre dans le contenu de la réponse envoyé lors de la consultation de la fiche d'un membre. Objectif : aller plus loin que "un modèle en base" <-> "un modèle renvoyé au client". (je ne sais pas comment fonctionne l'ORM de Django ni même s'il y en a un).

Voilà pour un prototype déjà un peu abouti mais "cassable".

On s'en fiche, cette API toute seule n'a pas vraiment d'intérêt à part pour faire des trucs rigolos du type stats (halloffame) sur les membres : "calendrier d'inscription", kikipostmeter, utilisateur avec la bio la plus fournie, ceux qui ont rédigé le plus de cours, ceux qui ont le plus de cours en bêta mais publient jamais rien, … Personne ne s'en servira comme ça, toute seule, personne ne va attendre son implémentation avec impatience. Par contre le jour où on crée une vraie API, ce sont des données qu'il faudra présenter pour être sérieux (une app où je ne peux pas consulter le profil d'un autre membre ?)

C'est juste un prototype tout autant en termes fonctionnels (regardez les jolis graphes qu'on fait avec l'API du site) que techniques (où sont les faiblesses de Django Rest Framework ? quelles sont les points faibles de l'organisation du code côté serveur lorsqu'il faut exporter les modèles ?).

Mais c'est un prototype simple :

  • parce que le modèle de données est bien plus important que son rendu (exit les tutoriels pour lesquels le MD a une importance cruciale)

  • parce que y'a nettement moins de données à traiter que dans les forums et qu'il y a moins de "noeuds hierarchiques" à gérer (pas de catégorie de forums, …)

  • parce que les données ne sont pas extrêmement sensibles (si je flingue ma bio en bidouillant c'est vraiment pas la fin du monde). Donc même si on devait la releaser pour que les gens jouent avec, y'a très peu de danger.

  • parce que la plus grande partie du modèle est publique (permet d'avancer quand même sans authentification, de se voir progresser) exit donc les messages privés qui auraient pu faire l'objet d'un bon proto aussi.

  • parce qu'aucun "client" (tierce-partie) ne va se jeter dessus pour l'utiliser ou ne va être impatient. Pas besoin de releaser à tout prix.

  • parce que mine de rien, même si certains champs sur la bio évoluent (cf. une autre ZEP) ça reste assez peu sensible en termes de modèle

  • parce que ça reste un showcase intéressant le jour où l'on veut investir un peu plus la communauté et/ou des développeurs dans l'API : regardez, c'est vos profils, on a fait un proto et ce qu'on a fait est très différent visuellement de la présentation sur le site. On est ouvert, nos données aussi, aidez-nous à les distribuer.

C'est pas pour m'accrocher à cette idée coûte que coûte, mais vraiment pour qu'on s'y jette. Parce que sincèrement dans un projet communautaire comme ça partir sur un proto c'est salvateur pour un "gros morceau" comme celui-là. Surtout qu'il est quand même assez indépendant du reste du site.

J'ai l'impression qu'on se tire dans les pattes pour des broutilles, alors qu'on a tous à gagner à se lancer dans une implémentation, même naïve, même si on doit la foutre à la poubelle plus tard, pour y voir plus clair.

+5 -0

Fort occupé, je peux venir faire un tour que maintenant sur cette ZEP. Je tiens d'ailleurs à remercier Javier pour son expertise dans le domaine mais surtout pour la lecture de postes interminables ! ^^

Ceci étant dit, je vois un gros intérêt dans l'élaboration d'une API draft, dans un premier temps, pour le module des membres. Cela pour toutes les raisons citées par Javier mais aussi pour d'autres raisons :

  • Appliquer un workflow complet pour le module des forums demandera de développer le module des membres puisqu'il n'est pas possible de créer un topic ou poster un message dans les forums sans être connecté. Dans tous les cas, c'est nécessaire de développer l'API du module des membres.
  • Après avoir lu toutes les bonnes pratiques, les contraintes et les usages d'une API par Javier, il y a du taff rien qu'avec le module des membres. Même si j'ai déjà été amené à développer des APIs, je n'avais jamais été aussi loin dans l'utilisation des en-têtes HTTP. Cela nous permettra de mettre en lumière toutes les petites contraintes techniques vis-à-vis du back-end (et il va y en avoir) avant de s'attaquer à de plus gros modules (les forums dans un premier temps, les articles et les tutoriels dans un second).

J'aimerais donc mettre à jour le premier poste de cette ZEP en ce sens. J'ai fais un résumé des éléments apportés par Javier et j'y ai apporté un point de vue technique par rapport aux données et aux possibilités offertes par le back-end. J'aimerais donc avoir votre avis sur ce qui va suivre :

Version de l'API

1
http://api.zestedesavoir.com/v0.0/...
  • La version est indiquée dans l'URL d'appel de l'API.
  • L'api est spécifié comme sous-domaine de l'URL pour ne pas la surcharger.

Pour la prod, nous pouvons imaginer une adresse plus ou moins équivalente :

1
http://api.preprod.zestedesavoir.com/v0.0/...

User-Agent

L'en-tête HTTP User-Agent doit être renseigné par le client.

Cross-Origin Request Sharing

Le serveur doit renvoyer les en-tête HTTP suivants afin d'éviter les soucis de CORS :

1
2
3
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link
Access-Control-Allow-Credentials: true

Considération générales

Formatage des erreurs

Lorsqu'un appel API provoque une erreur, il faut s'appuyer sur les codes HTTP standards. La réponse HTTP doit donc systématiquement utiliser un code interprétable pour le client. En plus de cela, le contenu de la réponse doit indiquer un code (par défaut le même que celui de la réponse HTTP, mais peut être différents pour donner de plus amples informations) ainsi qu'un message d'erreur compréhensible par le développeur de l'API.

Exemple, une réponse HTTP 404 devrait avoir le body suivant :

1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:404, 
            message:"Ce membre n'existe pas"
        }
    ]
}

Format d'entrée

  • Seul le JSON est accepté.
  • Lecture du header Content-Type, seule la valeur application/json est acceptée. Les autres formats donnent lieu à un code d'erreur HTTP 406 Not Acceptable. (NB : Twitter fait la différence entre le format des données envoyées et le format dans lequel les données sont envoyées. Dans le cas présent, ils renverraient une 404. A nous de décider ce qu'on fait).
1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:406, 
            message:"Le client doit envoyer des données au format application/json et positionner le header HTTP Content-Type de façon à le signaler"
        }
    ]
}

Format de sortie

  • Uniquement en JSON.

  • Lecture du header HTTP Accept, si la valeur application/json est absente, envoi d'un code HTTP 406 et de l'erreur suivante :

1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:406, 
            message:"Le client doit accepter des réponses au format JSON et le signaler en positionnant le header HTTP Accept avec la valeur application/json"
        }
    ]
}

Caching

  1. Lorsqu'une réponse est envoyée à un client, un ETag est calculé (une fonction de hashage est appliquée au contenu renvoyé) est envoyée au client dans le header de la réponse HTTP prévu à cet effet (ETag).

  2. Lorsqu'une requête parvient au serveur, ce dernier lit la valeur présente dans le header HTTP If-None-Modified. Lorsque celle-ci correspond à la valeur qui lui serait retournée, il ne faut pas lui faire parvenir la réponse mais simplement le code HTTP 304 Not Modified.

Pagination

  • Les résultats de requête doivent être paginés. Par défaut, une page de résultat doit contenir 50 résultats

  • L'utilisateur indique la page de résultats qu'il souhaite récupérer à l'aide du paramètre &page=. Dans le cas où ce paramètre est invalide (n'est pas un nombre entier, ou en dehors des limites de la pagination) l'API retourne une erreur 400 :

1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:4001, 
            message:"le paramètre indiqué pour la pagination n'est pas un nombre entier valide"
        }
    ]
}
1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:4002,
            message:"la page indiquée n'existe pas"
        }
    ]
}
  • L'API fournit des indications dans les headers de la réponse HTTP afin d'aider le client à naviguer dans les ressources paginées à l'aide du header Link. de la façon suivante :
1
2
3
4
Link: <http://zestedesavoir.com/membres/?search=toto&page=4>; rel="next",
  <http://zestedesavoir.com/membres/?search=toto&page=5>; rel="last",
  <http://zestedesavoir.com/membres/?search=toto&page=1>; rel="first",
  <http://zestedesavoir.com/membres/?search=toto&page=2>; rel="prev"

NB : Ne pas oublier de remettre tous les paramètres utilisés par le client dans l'URL (ici search=)

  • Dans de futures versions, l'utilisateur pourra indiquer le nombre de résultats souhaités en positionnant le paramètre &perPage= dans sa requête

Format de dates

Le format de dates retenu pour la communication avec le serveur est le format ISO 8601 (attention Java 7 nécessaire pour les utilisateurs de Java), pas de soucis en Python a priori.

En ce qui concerne les en-têtes HTTP, voici la liste des formats à gérer côté serveur.

API Membres

Objectif

Fournir aux clients un ensemble d'outils pour consulter la liste des membres et leurs profils.

Représentation d'un membre

Exemple d'objet
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "id": 123456,
    "username": "Clem",
    "show_email": true,
    "email": "clem@zestedesavoir.com",
    "is_staff": true,
    "is_active": true,
    "site": "www.zestedesavoir.com",
    "avatar_url": "http://i.imgur.com/k6R1plZ.png",
    "biography": "Quae dum ita struuntur, indicatum est apud Tyrum indumentum regale textum occulte, incertum quo locante vel cuius usibus apparatum. ideoque rector provinciae tunc pater Apollinaris eiusdem nominis ut conscius ductus est aliique congregati sunt ex diversis civitatibus multi, qui atrocium criminum ponderibus urgebantur.",
    "sign": "",
    "email_for_answer": true,
    "last_visit": "1977-04-22T06:00:00Z",
    "date_joined": "1977-04-22T06:00:00Z"
}

J'ai délibérément omis les champs liés à la modération et spécifique au site comme le survol ou le clique pour le menu.

Colonnes
Colonne Type Description
id long Identifiant de l'utilisateur
username String Nom d'utilisateur
show_email boolean Affiche ou non l'e-mail. Si faux, le champ email ne sera pas spécifié dans l'objet
email String E-mail de l'utilisateur
is_staff boolean Est un membre du staff ou non
is_active boolean Est un compte actif ou non
site String Site web de l'utilisateur
avatar_url String Url vers l'avater de l'utilisateur
biography String Biographie de l'utilisateur
sign String Signature de l'utilisateur
email_for_answer boolean Permet l'envoie d'e-mail depuis ZdS
last_visit Date Dernière visite de l'utilisateur sur ZdS

v0.0 : Afficher un profil utilisateur

Objectif

Mise en place simple du framework et routage sur une URL

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
GET /v0.0/membres/{id} Aucun Renvoie la fiche profil d'un membre 400 si l'identifiant fourni ne correspond pas au format d'identifiant attendu. 404 si le membre n'existe pas

v0.1 : Lister les membres

Objectif

Renvoyer une liste au lieu d'un objet.

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
GET /v0.1/membres/ Aucun pour le moment, search pour de futures versions Renvoie la liste des membres sans restriction (pagination ou différentielle) Aucun autre que les codes standard

v0.2 : Pagination et différentiel

Objectif

Aller toucher à la requête HTTP en lecture et à la réponse HTTP en écriture, + manipulation des queryset

v0.3 : adaptation/ponts

Objectif

Définition de notre propre Serializer : On renvoie un modèle différent du modèle stocké en base.

v0.4 : Gestion de l'authentification.

Objectif

Décisions à prendre vis à vis de l'authentification, OAuth et compagnie

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
POST /v0.4/login/ Aucun, le login et le passwod est contenu dans la requête Authentification de l'utilisateur dans l'API A définir
GET /v0.4/membres/monprofil Aucun Affiche toutes les données personnelles d'un utilisateur connecté A définir

v0.5 : gestion du PUT

Objectifs

Interaction en écriture avec le code côté serveur

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
PUT /v0.5/membres/ Aucun, les informations se trouvent en paramètre de la requête Utilisateur authentifié peut modifier sa fiche profil via l'API A définir

v0.6 : gestion du POST

Objectif

Création de compte via l'API membres.

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
POST /v0.6/membres/ Aucun, les informations se trouvent en paramètre de la requête Utilisateur non identifié peut se créer un compte A définir

v0.7 : Gestion des paramètres de requête.

Objectif

Etablir la liaison entre la couche API et la couche d'accès aux données. Que peut-on reprendre du code utilisé actuellement pour les vues ? Peut on factoriser du code pour faire une page de recherche parmi les membres sur le site ?

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
GET /v0.7/membres/ search Possibilité de rechercher, filtrer, trier la liste des membres Aucun autre que les codes standard

v0.8 : Gérer les associations dans le modèle renvoyé au client.

Objectif

Aller plus loin que "un modèle en base" <-> "un modèle renvoyé au client".

Ex : extraire un bout (l'url, le nom, …) d'un cours en bêta pour le mettre dans le contenu de la réponse envoyé lors de la consultation de la fiche d'un membre.


Voilà, j'espère ne pas avoir fait trop d'erreurs (notamment dans les différentes versions possibles de l'API des membres). N'hésitez pas à donner votre avis pour améliorer (ou refuser) cette première approche de l'API.

Si tout le monde est d'accord, je modifierais le premier poste de cet ZEP en conséquence.

+4 -0

Ca a l'air correct. Cependant, pour les urls, pour la preprod, je verrai plus api.preprod.zestedesavoir.com, qui montre bien que c'est les api de la preprod. Et concernant l'etag, je déconseille fortement de le mettre en bdd (mysql surtout), à cause des accès concurentiels mal gérés (j'ai eu plein de pb au taff avec ça…). Je te conseille de passer par un truc gérant la concurrence, type redis par exemple.

+0 -0
1
/v0.7/membre/

Il manque un "s" à membres à plusieurs endroits.

Mais sinon c'est good pour moi.

Alex-D

C'est carrément voulu mon bon monsieur. Il y a deux écoles sur ce point. Ceux qui parlent d'accéder à une ressource (les URLs seront sans "s") et ceux qui parlent d'accéder à tous les membres de la ressource membre (avec un "s"). Je suis un partisan de la première catégorie. Je trouve ça plus clair et ça raccourcis d'un caractère l'URL.

J'ai donc subtilement retiré les "s" des URLs donnés par Javier. :-°

Ca a l'air correct. Cependant, pour les urls, pour la preprod, je verrai plus api.preprod.zestedesavoir.com, qui montre bien que c'est les api de la preprod. Et concernant l'etag, je déconseille fortement de le mettre en bdd (mysql surtout), à cause des accès concurentiels mal gérés (j'ai eu plein de pb au taff avec ça…). Je te conseille de passer par un truc gérant la concurrence, type redis par exemple.

Talus

  • api.preprod : Je ne savais pas trop si c'était faisable avec l'offre qu'on a pris pour le NDD. Mais je modifie en conséquence alors.
  • eTag : C'est modifié.

Bah déjà félicitations et merci d'avoir tout bien formalisé.

C'est vraiment du bon boulot, on avance, ça fait réellement plaisir.

Ensuite mes remarques :

Pour le 's' j'ai l'habitude d'en mettre un. Maintenant je comprends ton point de vue et je m'y ferai sans problème mais ça me fait bizarre.

v0.0

On n'a pas la date d'inscription ?

v0.2

Il faudrait compléter (on le fera peut-être quand on y arrivera) mais c'est ici que va se poser la question du stockage de l'ETag notamment. Donc y'a de grosses specs à faire sur ce point.

Talus a parlé de Redis, oui, ça me semble être une bonne idée. L'écueil principal à évaluer c'est de trouver les points de coupe (cf AOP ) qui vont venir invalider le cache.

On peut partir sur une implémentation naïve dans un premier temps : les données sont toujours récupérées en base, le ETag systématiquement calculé, par contre 304 si les valeurs concordent entre If-None-Match et ETag.

A nous de voir quand est-ce-qu'on implémente la solution consistant à stocker l'ETag, étant donné qu'on va retrouver ça partout dans toutes les APIs, il va falloir y songer rapidement.

A noter également, que le ETag est dépendant des paramètres de requête. Ainsi, si l'on utilise un cache, la clé sera la requête entière (path + query parameters).

Ca va nécessiter une batterie de tests importantes, car c'est un sujet extrêmement sensible mais qu'on ne peut pas ignorer dans le cadre d'une API grande échelle. Le jour où on a 200 clients (mobiles, ou autres) qui font du polling toutes les 2 secondes sur la liste des forums, on sera bien bien content d'avoir un système d'ETag performant pour ne pas écrouler complètement la base, le serveur, etc.

Autant y réfléchir le plus tôt possible.

v0.3

On pourrait presque la remonter beaucoup plus tôt que ça en y réfléchissant. Tu l'as d'ailleurs écrit : "j'ai omis les champs…", bah en pratique, si j'ai tout bien compris au Django Rest Framework, ça passera par l'écriture d'un Serializer, non ?

Autant l'écrire le plus tôt possible avant de se tourner vers de grosses problématiques comme le polling différentiel, la pagination et l'authentification.

v0.4

Il faut se pencher dessus rapidement aussi. Et là très honnêtement je vais avoir besoin de vous, et je pense de façon plus générale qu'on va avoir besoin des experts du back.

Mécanique d'authentification ?

OAuth ? Ou truc custom ?

Credentials

Par quel biais l'utilisateur passe-t-il ses credentials lors de toutes les requêtes qui suivront ?

Client-tiers (applications)

Autre question au niveau de l'authentification, est-ce-qu'on peut authentifier des clients qui ne sont pas des utilisateurs loggés, je m'explique.

Il se peut qu'une application décide de crawler le site pour une raison X ou Y, mais de ne pas le faire en tant qu'utilisateur du site.

Par exemple : si j'ai envie de développer un "lecteur d'activité" de ZdS (graphes, stats, …), je vais avoir besoin de récupérer fréquemment la liste des forums, des cours, des membres, … Mais uniquement les données publiques. Si j'ai 350 clients connectés à un instant donné, je les agrège sur MON serveur et ne polle qu'une seule fois pour tous mes clients (si nouvelles données, je mets à jour les graphes pour TOUS les clients). J'ai pas besoin d'OAuth dans ce cas, je ne "prends pas la place" d'un de mes clients, je suis moi (en tant qu'application reconnue par ZdS et qui dispose de mon propre jeton d'authentification).

Bon c'est un cas marginal, j'en conviens, mais ça m'étonnerait qu'il ne se pose pas.

A mon avis il faut approfondir aussi tôt que possible la mécanique de login.

v0.5

Pour moi le PUT se fait sur membre/{id} avec une 401 si l'utilisateur n'est pas authentifié, 403 s'il essaie de modifier quelqu'un d'autre que lui. J'aime pas trop les PATH conditionnels comme ça difficiles à retrouver dans des logs par exemple. Tu vas savoir que 568 personnes ont modifié leur profil, mais tu ne sauras pas qui c'est à moins d'aller lire le jeton d'authentification (éventuellement dans un header HTTP, bonjour…) et de faire l'association avec le login, etc. Indémerdable.

Il faut ajouter (en plus de la colonne paramètres) le contenu de la requête PUT. En l'occurence là il y a une vraie décision à prendre : si l'utilisateur poste (put…) un sous-ensemble des champs de son profil, que fait-on du reste ? Comment l'utilisateur supprime-t-il l'un des champs (on parlait de réseaux sociaux dans un autre sujet) ?

v0.6

Idem : définir les champs nécessaires à la création de compte, qui si absents renvoient une erreur 400. Et les champs facultatifs qui seront traités quand même par le serveur.

v0.7

Il y aura plus de paramètres que ça je pense. Autant voir large histoire de se rendre compte des difficultés techniques que ça pourrait poser.

  • date d'inscription : before / after

  • lastVisit : before / after

  • filtre sur les groupe (staff / admin)

v0.8

C'est le plus gros morceau. Je me demande s'il ne faut pas la laisser de côté.

Voilà, encore une fois merci et j'espère qu'on va bien avancer sur ce sujet !

+0 -0

En fait, pour l'URL faut être cohérent :

  • le site lui-même adopte /membres/ pour parler du module membres
  • la thèse sur REST me semble indiquer qu'il faut utiliser /membres/
    • /membres/ = tous les membres
    • /membres/{id} = un membre en particulier

Ca serait pas mal de limiter le nombre de formes d'URL, sinon ça va être un bordel sans nom et on comprendra pas instinctivement qu'il faille ou non le "s". Si on le met tout le temps, on est tranquille.

OAuth ? Ou truc custom ?

Restons classiques : OAuth. Au moins il y a des libs dans tous les langages qui se respectent. Voire OAuth2.

Par quel biais l'utilisateur passe-t-il ses credentials lors de toutes les requêtes qui suivront ?

Token, bien sûr.

Token, bien sûr.

Alex-D

Oui mais où ? C'était plus ça la question ;) (me doute bien que ça sera un token ^^ ).

T'as deux écoles. Le token en query parameter ou plus masqué dans le header Authorization (ce que préconise OAuth si je me souviens bien). Il m'est arrivé de faire le choix du token, encore une fois parce qu'il est plus simple de suivre un parcours utilisateur en lisant des logs. Mais c'est moins élégant.

Encore une fois, c'est résolu avec un système d'extraction de logs puissant (qui t'affiche les headers HTTP).

+0 -0

Avant toute chose, j'ai rajouté un "s" aux URLs. D'abord parce que vous semblez tous avoir cette mauvaise habitude ( :-° ) etc'est assez cohérent d'utiliser un "s" avec nos URLs actuelles du site.

Ceci étant dit, passons à la (loooongue) réponse de Javier !

v0.0

On n'a pas la date d'inscription ?

Après re-vérification dans la documentation, elle existe bien et on l'utilise sur le profil des membres. J'ai édité mon précédent post en conséquence.

v0.2

[…]

On peut partir sur une implémentation naïve dans un premier temps : les données sont toujours récupérées en base, le ETag systématiquement calculé, par contre 304 si les valeurs concordent entre If-None-Match et ETag.

Si nous sommes d'accord que cette solution consiste simplement à mettre en place le système d'ETag sans résoudre les problématiques derrières à son utilisation, je suis pour cette première ébauche mais il faut fixer rapidement comment nous pouvons le stocker.

A nous de voir quand est-ce-qu'on implémente la solution consistant à stocker l'ETag, étant donné qu'on va retrouver ça partout dans toutes les APIs, il va falloir y songer rapidement.

Malheureusement, je n'ai pas assez de compétences dans ce domaine pour donner un avis pertinent mais quelles sont les solutions pour stocker cette information ? Si elle est complèxe et nécessite de grosses modifications back, à quel moment pouvons-nous penser à l'implémenter ? Si nous avons toute la logique et l'implémentation naive fonctionnelle, je serais plutôt d'avis de la faire passer en v0.8.

Qu'en pensez-vous ?

v0.3

On pourrait presque la remonter beaucoup plus tôt que ça en y réfléchissant. Tu l'as d'ailleurs écrit : "j'ai omis les champs…", bah en pratique, si j'ai tout bien compris au Django Rest Framework, ça passera par l'écriture d'un Serializer, non ?

Autant l'écrire le plus tôt possible avant de se tourner vers de grosses problématiques comme le polling différentiel, la pagination et l'authentification.

Théoriquement, un Serializer ne prend pas beaucoup de temps à développer. Nous pourrions le développer plus tôt mais ça me semble important de ne pas le faire pour avoir une API fonctionnelle entre chaque sous-version sans trop se surcharger de travail. Ou alors, switcher la v0.2 et la v0.3.

Qu'en pensez-vous ?

v0.4

Il faut se pencher dessus rapidement aussi. Et là très honnêtement je vais avoir besoin de vous, et je pense de façon plus générale qu'on va avoir besoin des experts du back.

Mécanique d'authentification ?

OAuth ? Ou truc custom ?

Je vais être d'accord avec mes pairs, OAuth. Même si ça risque de demander du développement back pour pouvoir s'authentifier ainsi sur le site.

Credentials

Par quel biais l'utilisateur passe-t-il ses credentials lors de toutes les requêtes qui suivront ?

Nous partons sur un en-tête Authorization Bearer.

Tout le monde d'accord ?

Client-tiers (applications)

Autre question au niveau de l'authentification, est-ce-qu'on peut authentifier des clients qui ne sont pas des utilisateurs loggés, je m'explique.

Il se peut qu'une application décide de crawler le site pour une raison X ou Y, mais de ne pas le faire en tant qu'utilisateur du site.

Par exemple : si j'ai envie de développer un "lecteur d'activité" de ZdS (graphes, stats, …), je vais avoir besoin de récupérer fréquemment la liste des forums, des cours, des membres, … Mais uniquement les données publiques. Si j'ai 350 clients connectés à un instant donné, je les agrège sur MON serveur et ne polle qu'une seule fois pour tous mes clients (si nouvelles données, je mets à jour les graphes pour TOUS les clients). J'ai pas besoin d'OAuth dans ce cas, je ne "prends pas la place" d'un de mes clients, je suis moi (en tant qu'application reconnue par ZdS et qui dispose de mon propre jeton d'authentification).

Bon c'est un cas marginal, j'en conviens, mais ça m'étonnerait qu'il ne se pose pas.

J'ai du mal à voir le problème. Il y aura des URLs dans l'API qui nécessitera l'authentification et d'autres non. A partir de là, n'importe quel client pourra interroger ces URLs, non ?

v0.5

Pour moi le PUT se fait sur membre/{id} avec une 401 si l'utilisateur n'est pas authentifié, 403 s'il essaie de modifier quelqu'un d'autre que lui.

C'est modifié.

J'aime pas trop les PATH conditionnels comme ça difficiles à retrouver dans des logs par exemple. Tu vas savoir que 568 personnes ont modifié leur profil, mais tu ne sauras pas qui c'est à moins d'aller lire le jeton d'authentification (éventuellement dans un header HTTP, bonjour…) et de faire l'association avec le login, etc. Indémerdable.

Il faut ajouter (en plus de la colonne paramètres) le contenu de la requête PUT. En l'occurence là il y a une vraie décision à prendre : si l'utilisateur poste (put…) un sous-ensemble des champs de son profil, que fait-on du reste ? Comment l'utilisateur supprime-t-il l'un des champs (on parlait de réseaux sociaux dans un autre sujet) ?

Pour moi, l'API met à jour les informations envoyées dans la requête PUT et laisse inchanger les autres informations. Je ne vois pas trop où se situe le problème ?

v0.6

Idem : définir les champs nécessaires à la création de compte, qui si absents renvoient une erreur 400. Et les champs facultatifs qui seront traités quand même par le serveur.

Partons du principe que les informations nécessaires sont celles que nous donnons à l'inscription de l'utilisateur sur le site ; à savoir qu'il faut mettre à jour quelques informations non visible à l'utilisateur :

  • Le nom de l'utilisateur
  • Son mot de passe
  • Son adresse e-mail
  • Est staff
  • Est actif
  • Dernière connexion
  • Date d'inscription

v0.7

Il y aura plus de paramètres que ça je pense. Autant voir large histoire de se rendre compte des difficultés techniques que ça pourrait poser.

  • date d'inscription : before / after

  • lastVisit : before / after

  • filtre sur les groupe (staff / admin)

Devons-nous être exhaustif à ce sujet ? Nous pouvons mais faisons une liste vraiment complète alors.

v0.8

C'est le plus gros morceau. Je me demande s'il ne faut pas la laisser de côté.

Je suis du même avis. C'est un très gros morceau avec des dépendances vers les autres ressources.

Merci pour tes retours Javier en tout cas !

(de retour de vacances :) )

v0.2

Si nous sommes d'accord que cette solution consiste simplement à mettre en place le système d'ETag sans résoudre les problématiques derrières à son utilisation, je suis pour cette première ébauche mais il faut fixer rapidement comment nous pouvons le stocker.

Qu'en pensez-vous ?

Comme tu l'as dit.

D'abord on recalcule l'ETag à chaque requête (très lourd, pas pérenne, on ne releasera pas de version publique comme ça, mais on pourra commencer à coder des tests fonctionnels qui marchent). En 0.8 on prend la décision quant au stockage des ETags (cache redis, …).

v0.3

Qu'en pensez-vous ?

Oui, on switche et on écrit le Serializer au plus tôt.

v0.4

OAuth, OK.

Nous partons sur un en-tête Authorization Bearer.

Tout le monde d'accord ? OK

J'ai du mal à voir le problème. Il y aura des URLs dans l'API qui nécessitera l'authentification et d'autres non. A partir de là, n'importe quel client pourra interroger ces URLs, non ?

Donc on partirait du principe que les clients-tiers (non liés à un compte utilisateur) n'utiliseront que les APIs publiques. Pourquoi pas, on verra bien si ça coince aux entournures.

Pour moi, l'API met à jour les informations envoyées dans la requête PUT et laisse inchanger les autres informations. Je ne vois pas trop où se situe le problème ?

Dans le cas où tu souhaites effacer des valeurs. La fameuse différence : chaîne vide =/= null. En JS (et notamment dans la sérialisation JSON) les champs que tu set à null sont ignorés.

J'ai peur que dans le cas où l'on considère "j'ai pas l'info, je laisse tel quel" on se retrouve coincé avec des champs non effaçables. A voir, le proto est là pour régler ce genre de soucis.

v0.7

Devons-nous être exhaustif à ce sujet ? Nous pouvons mais faisons une liste vraiment complète alors.

Ca serait mieux je pense, oui :\

v0.8

Je suis du même avis. C'est un très gros morceau avec des dépendances vers les autres ressources.

On laisse ça de côté.

Autre question de taille. Lors de l'édition du profil d'un membre. Les données ont changé. Prochaine requête du client sur /api/v1/membres : que fait-on ? On "sait" que les données ont changé (calcul de l'ETag, …) mais dans ce cas est-ce-qu'on lui renvoie la totalité des données ?

+0 -0

Bien le bonjour à tous,

Excusez moi pour ma longue absence mais comme Javier l'a fait remarqué il y a quelques jours, j'étais en vacances ce mois de septembre. J'ai pris des vacances aussi bien de mon boulot que de la programmation, voire par moment du net compris (mon dieu que ça fait du bien).

Je pense que nous arrivons doucement au bout de la rédaction de cette ZEP ! Voici un récapitulatif complet de tout ce qui a été dit précédemment sur l'élaboration d'une API des membres (devons-nous renommer le sujet de cette ZEP en ce sens d'ailleurs ?) en incluant les dernières remarques de Javier.

J'indique avec des balises d'information les parties modifiées par rapport au précédent récapitulatif.

API des membres

Informations sur la requête HTTP

Version de l'API
1
http://api.zestedesavoir.com/v0.0/...
  • La version est indiquée dans l'URL d'appel de l'API.
  • L'api est spécifié comme sous-domaine de l'URL pour ne pas la surcharger.

Pour la prod, nous pouvons imaginer une adresse plus ou moins équivalente :

1
http://api.preprod.zestedesavoir.com/v0.0/...
User-Agent

Cette section a été modifiée.

L'en-tête HTTP User-Agent doit être renseigné par le client.

Pour des informations complémentaires sur les User-Agent, rendez-vous sur son article Wikipedia.

Cross-Origin Request Sharing

Cette section a été modifiée.

Le serveur doit renvoyer les en-tête HTTP suivants afin d'éviter les soucis de CORS :

1
2
3
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link
Access-Control-Allow-Credentials: true

Pour des informations complémentaires sur CORS, rendez-vous sur son article Wikipedia version anglaise.

Considération générales

Formatage des erreurs

Lorsqu'un appel API provoque une erreur, il faut s'appuyer sur les codes HTTP standards. La réponse HTTP doit donc systématiquement utiliser un code interprétable pour le client. En plus de cela, le contenu de la réponse doit indiquer un code (par défaut le même que celui de la réponse HTTP, mais peut être différents pour donner de plus amples informations) ainsi qu'un message d'erreur compréhensible par le développeur de l'API.

Exemple, une réponse HTTP 404 devrait avoir le body suivant :

1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:404, 
            message:"Ce membre n'existe pas"
        }
    ]
}
Format d'entrée
  • Seul le JSON est accepté.
  • Lecture du header Content-Type, seule la valeur application/json est acceptée. Les autres formats donnent lieu à un code d'erreur HTTP 406 Not Acceptable. (NB : Twitter fait la différence entre le format des données envoyées et le format dans lequel les données sont envoyées. Dans le cas présent, ils renverraient une 404. A nous de décider ce qu'on fait).
1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:406, 
            message:"Le client doit envoyer des données au format application/json et positionner le header HTTP Content-Type de façon à le signaler"
        }
    ]
}
Format de sortie
  • Uniquement en JSON.
  • Lecture du header HTTP Accept, si la valeur application/json est absente, envoi d'un code HTTP 406 et de l'erreur suivante :
1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:406, 
            message:"Le client doit accepter des réponses au format JSON et le signaler en positionnant le header HTTP Accept avec la valeur application/json"
        }
    ]
}
Caching

Cette section a été modifiée.

Pour obtenir un premier système de cache, l'ETag sera utilisé. L'avantage se situe principalement sur un niveau : d'alléger le back-office dans sa charge de travail. Il ne sera pas toujours nécessaire d'aller chercher l'information s'il remarque qu'elle n'a pas été modifiée entre deux appels.

Pour y parvenir, voici le fonctionnement basique d'un ETag :

  1. Lorsqu'un client fait appel à l'API, la réponse inclue un ETag avec une valeur correspondant à un hash de la donnée retournée par l'API. La valeur de cette ETag est sauvegardée pour une prochaine utilisation si nécessaire.
  2. Au prochain même appel du client sur l'API, le client peut inclure dans le header de sa requête, l'attribut If-None-Match avec le précédent ETag renvoyé par le back-office.
  3. Si l'ETag n'a pas changé, le code de la réponse sera 304 - Not Modified et aucune donnée ne sera renvoyée.
  4. Si l'ETAg a changé, le back-office fera la requête et renverra la donnée avec un nouveau ETag. Ce nouveau ETag sera sauvegardé et utilisé pour de prochains appels.
Pagination
  • Les résultats de requête doivent être paginés. Par défaut, une page de résultat doit contenir 50 résultats

  • L'utilisateur indique la page de résultats qu'il souhaite récupérer à l'aide du paramètre &page=. Dans le cas où ce paramètre est invalide (n'est pas un nombre entier ou en dehors des limites de la pagination), l'API retourne une erreur 4001 ou 4002 :

1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:4001, 
            message:"le paramètre indiqué pour la pagination n'est pas un nombre entier valide"
        }
    ]
}
1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:4002,
            message:"la page indiquée n'existe pas"
        }
    ]
}
  • L'API fournit des indications dans les headers de la réponse HTTP afin d'aider le client à naviguer dans les ressources paginées à l'aide du header Link. de la façon suivante :
1
2
3
4
Link: <http://zestedesavoir.com/membres/?search=toto&page=4>; rel="next",
  <http://zestedesavoir.com/membres/?search=toto&page=5>; rel="last",
  <http://zestedesavoir.com/membres/?search=toto&page=1>; rel="first",
  <http://zestedesavoir.com/membres/?search=toto&page=2>; rel="prev"

NB : Ne pas oublier de remettre tous les paramètres utilisés par le client dans l'URL (ici search=)

  • Dans de futures versions, l'utilisateur pourra indiquer le nombre de résultats souhaités en positionnant le paramètre &perPage= dans sa requête
Format de dates

Le format de dates retenu pour la communication avec le serveur est le format ISO 8601 (attention Java 7 nécessaire pour les utilisateurs de Java), pas de soucis en Python a priori.

En ce qui concerne les en-têtes HTTP, voici la liste des formats à gérer côté serveur.

Les différentes versions de l'API

Objectif

Fournir aux clients un ensemble d'outils pour consulter la liste des membres et leurs profils.

Représentation d'un membre
Exemple d'objet
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "id": 123456,
    "username": "Clem",
    "show_email": true,
    "email": "clem@zestedesavoir.com",
    "is_staff": true,
    "is_active": true,
    "site": "www.zestedesavoir.com",
    "avatar_url": "http://i.imgur.com/k6R1plZ.png",
    "biography": "Quae dum ita struuntur, indicatum est apud Tyrum indumentum regale textum occulte, incertum quo locante vel cuius usibus apparatum. ideoque rector provinciae tunc pater Apollinaris eiusdem nominis ut conscius ductus est aliique congregati sunt ex diversis civitatibus multi, qui atrocium criminum ponderibus urgebantur.",
    "sign": "",
    "email_for_answer": true,
    "last_visit": "1977-04-22T06:00:00Z",
    "date_joined": "1977-04-22T06:00:00Z"
}

J'ai délibérément omis les champs liés à la modération et spécifique au site comme le survol ou le clique pour le menu.

Colonnes
Colonne Type Description
id long Identifiant de l'utilisateur
username String Nom d'utilisateur
show_email boolean Affiche ou non l'e-mail. Si faux, le champ email ne sera pas spécifié dans l'objet
email String E-mail de l'utilisateur
is_staff boolean Est un membre du staff ou non
is_active boolean Est un compte actif ou non
site String Site web de l'utilisateur
avatar_url String Url vers l'avater de l'utilisateur
biography String Biographie de l'utilisateur
sign String Signature de l'utilisateur
email_for_answer boolean Permet l'envoie d'e-mail depuis ZdS
last_visit Date Dernière visite de l'utilisateur sur ZdS
v0.0 : Afficher un profil utilisateur
Objectif

Mise en place simple du framework et routage sur une URL

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
GET /v0.0/membres/{id} Aucun Renvoie la fiche profil d'un membre 400 si l'identifiant fourni ne correspond pas au format d'identifiant attendu. 404 si le membre n'existe pas
v0.1 : Lister les membres
Objectif

Renvoyer une liste au lieu d'un objet.

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
GET /v0.1/membres/ Aucun pour le moment, search pour de futures versions Renvoie la liste des membres sans restriction (pagination ou différentielle) Aucun autre que les codes standard
v0.2 : Adaptation/ponts

Cette section a été échangée avec la version suivante.

Objectif

Définition de notre propre Serializer : On renvoie un modèle différent du modèle stocké en base.

v0.3 : Pagination et différentiel

Cette section a été modifiée.

Objectif

Aller toucher à la requête HTTP en lecture et écriture avec la manipulation des QuerySet et calcul du ETag.

Pour cette version, l'ETag ne sera pas sauvegardée et sera donc à chaque fois recalculée mais cela permettra d'avoir une version fonctionnelle et testable de l'ETag avant de statuer sur sa persistance.

v0.4 : Gestion de l'authentification.

Cette section a été modifiée.

Objectif

Permettre une authentification OAuth.

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
POST /v0.4/login/ Aucun, le login et le passwod est contenu dans la requête Authentification de l'utilisateur dans l'API A définir
GET /v0.4/membres/monprofil Aucun Affiche toutes les données personnelles d'un utilisateur connecté A définir
v0.5 : gestion du PUT
Objectifs

Interaction en écriture avec le code côté serveur

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
PUT /v0.5/membres/ Aucun, les informations se trouvent en paramètre de la requête Utilisateur authentifié peut modifier sa fiche profil via l'API A définir
v0.6 : gestion du POST
Objectif

Création de compte via l'API membres.

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
POST /v0.6/membres/ Aucun, les informations se trouvent en paramètre de la requête Utilisateur non identifié peut se créer un compte A définir

Questions à régler pendant ou après la réalisation de la ZEP

Cette section a été ajoutée.

  1. Comment sauvegarder l'ETag pour chaque donnée par chaque client ?
  2. Comment retirer des données de son profil ? Champ null ou vide ?

Futur de l'API des membres (possiblement hors scope de cette ZEP)

Cette section a été modifiée.

v1.x : Gestion des paramètres de requête.
Objectif

Etablir la liaison entre la couche API et la couche d'accès aux données. Que peut-on reprendre du code utilisé actuellement pour les vues ? Peut on factoriser du code pour faire une page de recherche parmi les membres sur le site ?

Routes
Méthode URL Paramètres et signification Description Codes d'erreur
GET /v0.7/membres/ search Possibilité de rechercher, filtrer, trier la liste des membres Aucun autre que les codes standard
v1.x : Gérer les associations dans le modèle renvoyé au client.
Objectif

Aller plus loin que "un modèle en base" <-> "un modèle renvoyé au client".

Ex : extraire un bout (l'url, le nom, …) d'un cours en bêta pour le mettre dans le contenu de la réponse envoyé lors de la consultation de la fiche d'un membre.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte