lpoaura / gn2pg Goto Github PK
View Code? Open in Web Editor NEWOutil d'import de données entre instances GeoNature (côté client)
Home Page: https://lpoaura.github.io/GN2PG/
License: GNU Affero General Public License v3.0
Outil d'import de données entre instances GeoNature (côté client)
Home Page: https://lpoaura.github.io/GN2PG/
License: GNU Affero General Public License v3.0
Hi,
I'm testing gn2pg app,
Setup and config file init were OK, but launching gn2pg_cli --full returns error :
Traceback (most recent call last):
File "/home/geonatureadmin/venv/bin/gn2pg_cli", line 8, in
sys.exit(run())
File "/home/geonatureadmin/venv/lib/python3.9/site-packages/gn2pg/main.py", line 201, in run
return main(sys.argv[1:])
File "/home/geonatureadmin/venv/lib/python3.9/site-packages/gn2pg/main.py", line 191, in main
full_download(cfg_ctrl)
File "/home/geonatureadmin/venv/lib/python3.9/site-packages/gn2pg/helpers.py", line 101, in full_download
full_download_1source(Data, cfg)
File "/home/geonatureadmin/venv/lib/python3.9/site-packages/gn2pg/helpers.py", line 81, in full_download_1source
downloader.store()
File "/home/geonatureadmin/venv/lib/python3.9/site-packages/gn2pg/download.py", line 88, in store
self._backend.log(
AttributeError: 'StorePostgresql' object has no attribute 'log'
GN2PG takes a non negligible time to import data (1h for 60000 in my case).
Further more, it reaches all the data that are available from the export every time you run it.
Therefore, it would interesting for GN2PG to fetch data that have been created or updated after the last valid import
As a user and not a developer I suppose that the export must be correctly configured in GeoNature and then API selects only the data with create or update date > last import date. You thus need to store the last complete import date.
Will be very interesting to automate import every day/week/month between two or more structures !
For the moment, for each line that is created or updated in the data_json, a dataset is created with a new id.
Therefore, for ten lines from the same dataset in the source, ten datasets are created in my database.
It is an issue coming from
CREATE OR REPLACE FUNCTION gn2pg_import.fct_c_get_or_insert_basic_dataset_from_uuid_name (_uuid uuid, _name text, _id_af int)
Line 58 should be :
BEGIN
INSERT INTO gn_meta.t_datasets (unique_dataset_id, id_acquisition_framework, dataset_name, dataset_shortname, dataset_desc, marine_domain, terrestrial_domain, meta_create_date)
SELECT
_uuid
I will test it and add it in the PR #7
In export views, there is a coalesce on nom_organism / concat(nom_role, prénom_role), but trigger target two fields for persons : nom_role, prenom_role, wich have been concat in export.
The coalesce complicate the recuparation of nom/prenom
Dans le cadre d'une prestation avec le Parc National des Ecrins, je me suis intéressée aux performances des triggers mis en place sur la table data_json
pour mettre à jour la synthèse.
Il y a au total quatre triggers, qui sont définis dans to_gnsynthese.sql
:
Le jeu de données utilisé pour quantifier les performances était constitué de 1128 lignes. Il a été mesuré qu'en moyenne l'activation des triggers augmentait le temps d'exécution de 2,7 fois.
Une solution envisagée pour améliorer cela était de passer d'un traitement AFTER EACH ROW
à un traitement AFTER EACH STATEMENT
.
En outre, il a été identifié que :
DROP TRIGGER IF EXISTS tri_c_upsert_data_to_geonature_with_nomenclature_label ON gn2pg_import.data_json
présents l.165 et l.540 et DROP TRIGGER IF EXISTS tri_c_upsert_data_to_geonature_with_cd_nomenclature ON gn2pg_import.data_json
présents l.553 et l.930)Dans ce contexte, trois solutions ont été étudiées et sont présentées dans la section ci-après.
Dans cette solution, trois triggers sont implémentés : INSERT, UPDATE, DELETE. Chacun prend en charge les trois types de données. Les opérations de INSERT et d'UPDATE ont dû être séparées car la OLD TABLE
nécessaire lors de l'UPDATE, n'est pas définie lors de l'INSERT.
Les performances mesurées étaient comparables avec l'existant.
Dans cette solution, sept triggers ont été implémentés : un trigger INSERT + un trigger UPDATE pour chaque type de données, et un trigger DELETE général.
D'une part, les performances étaient comparables à l'existant et d'autre part, le code était beaucoup plus long.
Dans cette solution, seuls deux triggers sont mis en place : un pour l'INSERT/UPDATE et un pour le DELETE, chacun prenant en charge tous les types de données. Les performances mesurées avec cette solution sont comparables à l'existant.
Aucune des solutions mises en place n'a permis d'améliorer les performances.
Cependant, bien que la solution 2 ne présente aucun avantage par rapport à l'existant, les solutions 1 et 3 évitent les portions de codes répétées (d'autant plus pour la solution 3) et réduisent la longueur du code d'un facteur ~2.
Cela pourrait être avantageux pour simplifier le maintien du code.
Je n'ai pas joint les fichiers de ces solutions en raison de leur longueur (800-900 lignes) mais je peux vous les mettre à disposition si vous le souhaitez.
Hi,
Deleting data does not work well
All seems to be ok on server "giver" side, deleted data are well stored and returned by dedicated route. Client side, gn2pg detect how many data it have to delete, and wich rows (message "deleting object with id XX" return id of data to delete with -v option). Nevertheless, data are not deleted into data_json table.
The JSON key name of the additional data in the supplied data is donnees_additionelles
, whereas the key expected by the trigger is additional_data
, resulting in an empty field in the overview.
1.6.4
No response
La fin des update se termine sur une erreur du à la clé total_filtered
manquante dans l'API de log des DELETE, présente dans la version initiale qui s'appuyait sur la class 'GenericQuery' de Utils-flask-SQLAlchemy.
2023-08-31 23:07:56,778 - DEBUG - store_postgresql:__exit__ - Closing database connection at exit from StorePostgresql
Traceback (most recent call last):
File "/home/lpoaura/.local/bin/gn2pg_cli", line 8, in <module>
sys.exit(run())
File "/home/lpoaura/.local/lib/python3.9/site-packages/gn2pg/main.py", line 194, in run
return main(sys.argv[1:])
File "/home/lpoaura/.local/lib/python3.9/site-packages/gn2pg/main.py", line 187, in main
update(cfg_ctrl)
File "/home/lpoaura/.local/lib/python3.9/site-packages/gn2pg/helpers.py", line 138, in update
update_1source(Data, cfg)
File "/home/lpoaura/.local/lib/python3.9/site-packages/gn2pg/helpers.py", line 118, in update_1source
downloader.update()
File "/home/lpoaura/.local/lib/python3.9/site-packages/gn2pg/download.py", line 259, in update
deleted_pages = self._api_instance.page_list(
File "/home/lpoaura/.local/lib/python3.9/site-packages/gn2pg/api.py", line 189, in page_list
total_filtered = resp["total_filtered"]
KeyError: 'total_filtered'
Connection abort exception occure after " Successfully logged in into GeoNature" and PARAMS.
A token have been added on gn_module_export after version 1.5, may be the problem ?
Nous avons essayé d'importer des données dans la synthèse de GeoNature à partir d'une API d'un autre GeoNature en utilisant GN2PG. Nous avons constaté que nous téléchargions moins de données dans la base (4 205 données) que le nombre total de données de l'export (7 110).
En cherchant d'où venait le problème, nous avons remarqué que certaines données étaient manquantes (par rapport aux données contenues dans le csv) et que certaines étaient répétées.
Le problème vient de l'API d'export de GeoNature et plus particulièrement au niveau de la requête avec une limite et offset.
D'après la documentation de Postgresql: https://www.postgresql.org/docs/9.3/queries-limit.html
"When using LIMIT, it is important to use an ORDER BY clause that constrains the result rows into a unique order. Otherwise you will get an unpredictable subset of the query's rows. "
Nous allons donc mettre un ticket sur le module d'export, et essayer de résoudre le problème.
Bonjour,
Dans le cadre d'une prestation avec le PN des Ecrins pour le SINP , nous devions analyser le module GN2PG pour proposer des améliorations .
Voici ce que nous avons noté :
Il y a des appels séquentiels qui sont réalisés notamment dans les fonctions store
et update
du fichier download.py
:
Lines 100 to 113 in ecd0444
multiprocessing
.
Il y a des listes qui pourraient être remplacées par des générateurs en utilisant des yield
. Exemple de la liste d'URL créée dans la fonction _page_list
du fichier api.py
Lines 193 to 197 in ecd0444
Le processus de récupération du nombre de pages total prend du temps . En effet une première requête est effectuée et récupére 1000 données (valeur renseignée par défaut) et ces dernières ne sont pas utilisées. Du coup,on a identifié 3 solutions possibles :
_page_list
du fichier api.py
("limit"=1 et retrait des paramètres "order_by" et "offset") r = session.get(
url=api_url,
# --> params = {"limit":1}
)
/api/exports/swagger/1/count
Nous avons aussi exploré deux autres pistes d'amélioration : factoriser quelques lignes de codes répétées (notamment dans le fichier gn2pg/download.py ), et un peu de refactorisation (par exemple : utilisation du paramètre params
de la librairie requests
)
Hi,
DB user is given in script to_gnsynthese on lines 1050 and 1592 with "OWNER TO orbadmin"
Créer les fonctions et triggers pour mettre à jour la synthèse depuis les données téléchargées
When I imported data from another GeoNature to mine I checked if all variables were logical and I reported :
I launched gn2pg-cli --full <myconfigfile>
at 2021-04-26 11:48 so the creation date is logical but not the update one.
As it is the first time this added to my synthesis I suggest to have create and update date identical. Otherwise, we can keep create and update dates as the one in the source GeoNature ?
What do you think ?
Dans le cadre d’une prestation avec le PN des Ecrins nous devions proposer des améliorations pour le module GN2PG .
Nous avons d’abord fait un export vers un schéma postgresql que nous avons ensuite essayé d'importer vers la synthèse d'un GeoNature via la commande : gn2pg_cli --custom-script to_gnsynthese <myconfigfile>
.
Pour rappel, ce script (to_gnsynthese.sql) crée des triggers qui insèrent chaque donnée importée dans la synthèse. Lors du lancement de la commande gn2pg_cli --full <myconfigfile>
nous avons rencontré des erreurs d'insertion dans la synthèse. Elles étaient dues à un mauvais format de l'export. Effectivement, ce dernier ne contenait pas les informations nécessaires de chaque Jeu De Donnée/Cadre d'Acquisition (GN2PG attend ces informations au format json). Ce qui amène à la question suivante : souhaitez-vous que l'on crée un script sql pour insérer dans la synthèse en spécifiant le jeu de donnée (à l'instar du module d'import) ? Cela éviterait alors de créer un export spécifique à GN2PG et donc permettrait de le rendre plus adaptable aux exports déjà existants.
Bonjour,
je constate en faisant un --full ce midi que j'ai des sauts dans les offsets, un peu par hasards.
2023-12-06 12:43:30,394 - INFO - store_postgresql:store_data - 1000 items have been stored in db from data of source cen74 (0 error occurred)
2023-12-06 12:43:30,427 - INFO - api:get_page - Download page https://geonature.xxx.fr/api/exports/api/3?limit=1000&orderby=id_synthese&offset=22
2023-12-06 12:43:30,494 - INFO - download:report - Storing 1000 datas (6000/68950 8.70 %) from CEN74 data
2023-12-06 12:44:01,754 - INFO - api:get_page - Download page https://geonature.xxx.fr/api/exports/api/3?limit=1000&orderby=id_synthese&offset=36
Déjà rencontré ? Une raison qui pourrait expliquer ça ?
Je suis sur un serveur mis à jour dernièrement, python 3.9 est bien installé, et j'interroge la BDD du CEN74 qui est bien à jour sur son utilitaire flask-sql-alchemy
La mise en place de la mise à jour incrémentale des données requiert des modifications à GeoNature avec l'ajout d'une journalisation des données modifiées en synthèse dont les modalités restent à définir.
Cette journalisation nécessite également la mise à disposition, côté GeoNature source, d'une API spécifique consultable par intervalles de temps (début/fin avec un delta par défaut, ex. 10 jours, à définir en option?.
Il pourra être intéressant de définir un pas de temps limite de conservation de l'historique pour ne pas surcharger la bdd.
Launching command gn2pg_cli --full myconfigfile, returns synax error (error_count data is not an integer)
sqlalchemy.exc.DataError: (psycopg2.errors.InvalidTextRepresentation) ERREUR: syntaxe en entrée invalide pour le type integer : « data »
LINE 1: ...t, http_status, comment) VALUES ('flaviabase', 0, 'data', 0,...
^
[SQL: INSERT INTO gn2pg_import.download_log (source, controler, error_count, http_status, comment) VALUES (%(source)s, %(controler)s, %(error_count)s, %(http_status)s, %(comment)s)]
[parameters: {'source': 'flaviabase', 'controler': 0, 'error_count': 'data', 'http_status': 0, 'comment': 0}]
Aujourd'hui, le processus d'intégration des données en bdd requiert un champ d'UUID dans les données sources (par défaut, id_perm_sinp
, même si l'UUID est null). Il devrait être possible d'intégrer des données dans clé/valeur UUID.
Hi @lpofredc ,
I don't know what you planned to do but after discussing with JP Milcent from the CBNA, I think it is interesting to configure a begin date of the incremental download (in the config file ?). For large dataset, it will allow a first manual import and then start incremental downloads from a defined date thanks to your log history table.
Was it your vision ?
Thanks for your opinion !
PR intégrant une historisation des données de la synthèse (insert, update, delete):
* Les `delete` sont loggés dans la table gn_synthese.t_log_synthese par triggers * Les `update` & `insert` ne sont pas loggés par triggers car déjà présents dans gn_synthese.synthese par les champs `meta_{create,update}_date`. * Le tout est regroupé dans une vue gn_synthese.v_log_synthese.
L'API, désactible par l'option
['SYNTHESE']['LOG_API'] = False
, est basée sur la classe GenericQuery donc avec des possibilités de requêtage avancées (action supérieure à une date spécifique par exemple).Cette API nécessaire pour la mise à jour incrémentale de Gn2Pg.
Lien avec #789
Bonjour, Une simple question sur ce point.
L'API va chercher l'historique des actions sur une donnée elle-même, pour aller la répliquer sur la base "cliente" via gn2pg. Du coup l'incrémental ne se fait pas en allant checker "les données reçues hier VS les données reçues aujourd"hui'.
Je comprends bien l'intérêt pratique, mais ça veut dire que si je mets à jour ma vue qui sert le client via GN2PG, il faut refaire une synchro complète ? Puisque j'aurai finalement dans cette nouvelle vue, retiré des données dont la dernière action n'est pas forcément "detele", j'aurai de nouvelles données dont la création n'est pas postéieure à la dernière synchro etc ?
Originally posted by @DonovanMaillard in PnX-SI/GeoNature#1456 (comment)
This unique index can bring some problems for example if we import SINP data from INPN in the instance. A unique source wich contains data from several external sources, with possibly the same entity pk value.
Actuellement ce sont les labels qui sont utilisés pour retrouver les correspondances des nomenclatures.
Cependant ceci ne sont pas forcément très stables, et parfois modifiés sur une instance GeoNature.
Les codes des nomenclatures sont plus robustes.
De plus ils disposent déjà de leur fonction get_id_nomenclature
(https://github.com/PnX-SI/Nomenclature-api-module/blob/master/data/nomenclatures.sql#L99)
Il faudrait donc plutôt utiliser ces codes plutôt que les labels.
Pour le moment ce sont les labels qui ont été utilisés car ce sont les labels qui sont utilisés dans l'export SINP fourni par défaut dans le module Export de GeoNature.
Cependant il faudrait peut-être privilégier de créer un autre export, éventuellement taillé sur mesure pour GN2GN, qui utiliserait les codes, et serait plus stable dans le temps et dédié aux échanges entre instances GeoNature.
Il était initialement prévu de se baser sur les UUID des objets dans la synthèse pour mettre à jour les données si elles ont été modifiées dans la source.
Cependant, toutes les données d'un GeoNature n'ont pas forcément d'UUID et cela doit rester possible. Voir PnX-SI/GeoNature#1205
Cependant les UUID sont vraiment idéaux pour les échanges automatiques entre outils et notamment pour GN2GN.
Du coup peut-être à approfondir. Ou alors se baser sur les UUID sans les imposer ? Si une données n'a pas d'UUID dans la source, alors elle n'est pas transmise lors d'un GN2GN ?
Je dresse ici, pour mémo, une liste des améliorations qui seraient intéressantes:
last_ts
de la table increment_log
utilisée à chaque lancement.Cette application, initialement spécifiquement conçue pour les transfert de données entre instances GeoNature (synthèse à synthèse) s'avère finalement plus générique.
Le cœur de cette application permet de transférer n'importe quelle données (synthèse mais aussi tout autre données) issues du module d'exports dans une base de données PostgreSQL (GeoNature ou autre).
Seul le procédé de mise à jour incrémentale sera, pour le moment, spécifique à la synthèse.
De fait, se pose la question de trouver un nom plus proche de la réalité de cette application. nous sommes preneur de toutes idées 😃
Could be good to add in documentation how to automate gn2pg_cli --full
action.
Bonjour,
Après quelques temps de travail, je suis (pas tout seul) enfin arrivé à bout d'un import de GN2GN.
Cependant j'ai rencontré quelques embûches que je voulais partager et que je vais corriger dans une PR dès que possible.
En reprendant les étapes :
Mise en place du schéma dans ma base : OK
Mise en place des triggers pour insertion dans ma synthèse directement (en utilsant to_gnsynthese.sql) : PAS OK pour deux raisons.
D'une part la mise en place de la contrainte unique
CREATE UNIQUE INDEX IF NOT EXISTS uidx_synthese_id_source_id_entity_source_pk_value ON
gn_synthese.synthese (id_source, entity_source_pk_value);
est impossible lorsque j'ai des doublons déjà présents dans ma synthèse.
J'ai du les supprimer à l'aide de
PnX-SI/GeoNature@02b704f
D'autre part, lorsque le cadre d'acquisition des données n'éxistait pas dans ma base, il fallait le créer. Or la création a besoin d'un champ meta_create_date
qu'il a fallu rajouter à la fonction fct_c_get_or_insert_basic_af_from_uuid_name
de telle sorte que le code ressemble à
INSERT INTO gn_meta.t_acquisition_frameworks (unique_acquisition_framework_id, acquisition_framework_name, acquisition_framework_desc, acquisition_framework_start_date, meta_create_date)
SELECT
_uuid,
_name,
'Description of acquisition framework : ' || _name,
now(),
now()
...
Une fois ça j'ai pu observer les données dans la synthèse.
Donc :
Import GNPG OK
Visualisation dans la synthèse OK
Il me reste à faire en sorte que le type
dans la table data_json
soit bien renseigné, parce que je le fais pour l'instant manuellement.
Apparemment Gn2Pg nécessite de créer un index unique sur le champ uuid_role
de la table utilisateurs.t_roles
. Or, par défaut dans GeoNature, cet index n'existe pas.
Je n'ai pas trouvé mention du besoin de cet index dans la doc ou les fichiers SQL d'exemple de Gn2Pg.
J'ai rajouté l'index nécessaire avec la requête:
-- Add unique constraint to utilisateurs.t_roles on uuid_role
CREATE UNIQUE INDEX IF NOT EXISTS uidx_t_roles_uuid_role
ON utilisateurs.t_roles (uuid_role) ;
Exemple de log d'erreur:
2022-05-29 23:17:53,479 - CRITICAL - store_postgresql:store_1_data - One error occured for data from source flavia with id_synthese = 1777427. Error message is (psycopg2.errors.InvalidColumnReference) ERREUR: il n'existe aucune contrainte unique ou contrainte d'exclusion correspondant à la spécification ON CONFLICT
CONTEXT: instruction SQL « INSERT INTO utilisateurs.t_roles (
uuid_role,
nom_role,
prenom_role,
email,
champs_addi
)
VALUES (
(_actor_role ->> 'uuid_actor')::UUID,
_actor_role #>> '{identity,first_name}',
_actor_role #>> '{identity,last_name}',
_actor_role ->> 'email',
jsonb_build_object('source', _source, 'module', 'gn2pg')
)
ON CONFLICT (uuid_role) DO NOTHING »
fonction PL/pgSQL gn2pg_flavia.fct_c_get_or_create_actors_in_usershub(jsonb,character varying), ligne 26 à instruction SQL
instruction SQL « INSERT INTO gn_meta.cor_dataset_actor (id_dataset, id_role, id_nomenclature_actor_role)
VALUES ( _id_dataset
, gn2pg_flavia.fct_c_get_or_create_actors_in_usershub(i.item, _source)
, ref_nomenclatures.get_id_nomenclature('ROLE_ACTEUR', i.item ->> 'cd_nomenclature_actor_role'))
ON CONFLICT DO NOTHING »
fonction PL/pgSQL gn2pg_flavia.fct_c_insert_dataset_actor(integer,jsonb,character varying), ligne 14 à instruction SQL
instruction SQL « SELECT gn2pg_flavia.fct_c_insert_dataset_actor(the_dataset_id, _ds_data -> 'actors', _source) »
fonction PL/pgSQL gn2pg_flavia.fct_c_get_or_insert_dataset_from_jsondata(jsonb,integer,character varying), ligne 46 à PERFORM
instruction SQL « SELECT gn2pg_flavia.fct_c_get_or_insert_dataset_from_jsondata(new.item #> '{jdd_data}', the_id_af, new.source) »
fonction PL/pgSQL gn2pg_flavia.fct_tri_c_upsert_data_to_geonature_with_metadata(), ligne 77 à instruction SQL
[SQL: INSERT INTO gn2pg_flavia.data_json (source, controler, type, id_data, uuid, item, update_ts) VALUES (%(source)s, %(controler)s, %(type)s, %(id_data)s, %(uuid)s, %(item)s, %(update_ts)s) ON CONFLICT ON CONSTRAINT pk_source_data DO UPDATE SET item = %(param_1)s, update_ts = %(param_2)s]
[parameters: {'source': 'flavia', 'controler': 'data', 'type': 'synthese_with_metadata', 'id_data': 1777427, 'uuid': UUID('f8cc435a-655c-415c-bd61-4b0eb537fea2'), 'item': '{"id_synthese": 1777427, "id_source": "685307", "id_perm_sinp": "f8cc435a-655c-415c-bd61-4b0eb537fea2", "id_perm_grp_sinp": "8c370f73-0dce-4a04-bb3e- ... (2937 characters truncated) ... pe_info_geo": "1", "methode_determination": null, "statut_validation": "2", "validation_date": null, "derniere_action": "2022-05-05 16:53:44.822852"}', 'update_ts': datetime.datetime(2022, 5, 29, 23, 17, 53, 473626), 'param_1': '{"id_synthese": 1777427, "id_source": "685307", "id_perm_sinp": "f8cc435a-655c-415c-bd61-4b0eb537fea2", "id_perm_grp_sinp": "8c370f73-0dce-4a04-bb3e- ... (2937 characters truncated) ... pe_info_geo": "1", "methode_determination": null, "statut_validation": "2", "validation_date": null, "derniere_action": "2022-05-05 16:53:44.822852"}', 'param_2': datetime.datetime(2022, 5, 29, 23, 17, 53, 473752)}]
(Background on this error at: https://sqlalche.me/e/14/f405)
With @DonovanMaillard we tried to add data with a cd_nom 'blue'.
Obviously this could not work :
Traceback (most recent call last):
File "/home/geonatureadmin/.local/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1276, in _execute_context
self.dialect.do_execute(
File "/home/geonatureadmin/.local/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 608, in do_execute
cursor.execute(statement, parameters)
psycopg2.errors.InvalidTextRepresentation: ERREUR: syntaxe en entrée invalide pour le type integer : « bleu »
CONTEXT: fonction PL/pgSQL gn2pg.fct_tri_c_upsert_data_to_geonature_with_cd_nomenclature(), ligne 134 à instruction SQL
However, we thought that it was good to store all wrong lines in log and to send an email to the source database manager so that it can modify the data directly in its database.
Therefore, we could add the email in the config file.
Delete process use raw source name, while stored value is a slug.
In consequence, delete process can't find data to delete, searched in database by both source and id.
latest
No response
Lauching gn2pg_cli --custom-script to_gnsynthese my_config
returns
'dict' object does not support indexing , failed to apply script to_gnsynthese
pip installation seems to be ok and data synchronisation to data_json works, but an error occurred on psycopg2-binary installation : but seems to be solved with python3 -m pip install wheel before uninstall et reinstall psycopg2-binary.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.