From 9ed8a608db229b796245d7c4c11ca01070a446ec Mon Sep 17 00:00:00 2001 From: JPy Date: Mon, 23 Jul 2018 19:24:50 +0200 Subject: [PATCH] more on uploading files / created a new collection to split columns from dsi --- README.md | 13 +- screenshots/endpoints_dataset_inputs.png | Bin 0 -> 66358 bytes solidata_api/_choices/__init__.py | 4 +- solidata_api/_choices/_choices_docs.py | 18 ++ solidata_api/_choices/_choices_files.py | 1 + solidata_api/_choices/_choices_open_level.py | 19 ++ solidata_api/_choices/_choices_user.py | 21 +- solidata_api/_core/pandas_ops/__init__.py | 1 + .../_core/pandas_ops/pd_read_files.py | 32 +++ solidata_api/_core/queries_db/__init__.py | 7 +- solidata_api/_core/utils/app_logs.py | 32 ++- solidata_api/_models/models_dataset_input.py | 61 +++-- solidata_api/_models/models_dataset_raw.py | 67 ++++++ solidata_api/_models/models_generic.py | 141 +++++++++--- solidata_api/_models/models_project.py | 59 +++-- solidata_api/_models/models_user.py | 30 +-- solidata_api/_serializers/schema_generic.py | 109 +++++++-- solidata_api/_serializers/schema_users.py | 31 ++- solidata_api/api/__init__.py | 2 +- .../api/api_auth/endpoint_user_login.py | 2 +- .../api_dataset_inputs/endpoint_dsi_create.py | 216 ++++++++++++------ .../api/api_projects/endpoint_prj_create.py | 34 ++- .../api/api_users/endpoint_usr_register.py | 21 +- solidata_api/application.py | 3 +- solidata_api/config.py | 1 + .../2017_communes_fr_com_code.csv | 0 26 files changed, 690 insertions(+), 235 deletions(-) create mode 100644 screenshots/endpoints_dataset_inputs.png create mode 100644 solidata_api/_choices/_choices_docs.py create mode 100644 solidata_api/_choices/_choices_open_level.py create mode 100644 solidata_api/_core/pandas_ops/pd_read_files.py create mode 100644 solidata_api/_models/models_dataset_raw.py create mode 100644 uploads/data_sources/2017_communes_fr_com_code.csv diff --git a/README.md b/README.md index a56e8f2..b47b255 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ------- ## PRESENTATION -part 2a/3 of the TADATA! sofware suite (ApiViz / Solidata / OpenScraper) +part 2a/3 of the TADATA! sofware suite ([ApiViz](https://github.com/entrepreneur-interet-general/CIS-front) / Solidata / [OpenScraper](https://github.com/entrepreneur-interet-general/OpenScraper) ) #### Building a public service for managing, consolidating, and sharing data @@ -17,7 +17,7 @@ part 2a/3 of the TADATA! sofware suite (ApiViz / Solidata / OpenScraper) - consolidate your data : apply your own datamodel, simplify columns, apply metadatas, ... ; - share the consolidated datas on API endpoints with the level of openness you decide (opendata, commons, collective, private) ; - manage all your data and your recipes by projects ; -- be able to manage projects by teams and share data/recipes/ +- be able to manage projects by teams and share data/recipes/datamodels... ------ @@ -95,5 +95,12 @@ part 2a/3 of the TADATA! sofware suite (ApiViz / Solidata / OpenScraper) ![alt text](./screenshots/endpoints_users.png "endpoint users") ------- -![alt text](./screenshots/endpoints_auth_server.png "endpoint users") +![alt text](./screenshots/endpoints_auth_server.png "endpoint auth users") + +------- +![alt text](./screenshots/endpoints_projects.png "endpoint projects") + +------- +![alt text](./screenshots/endpoints_dataset_inputs.png "endpoint dataset inputs") + diff --git a/screenshots/endpoints_dataset_inputs.png b/screenshots/endpoints_dataset_inputs.png new file mode 100644 index 0000000000000000000000000000000000000000..4fd72cc2cf218493debe6f3c82d2cc1f737a2b9c GIT binary patch literal 66358 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 z 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 T9rQfO?=Gj0n;#<_b-wXG$Cvky literal 0 HcmV?d00001 diff --git a/solidata_api/_choices/__init__.py b/solidata_api/_choices/__init__.py index 2b07f3b..8848adb 100644 --- a/solidata_api/_choices/__init__.py +++ b/solidata_api/_choices/__init__.py @@ -1,4 +1,6 @@ from ._choices_files import * +from ._choices_open_level import * from ._choices_licences import * -from ._choices_user import * \ No newline at end of file +from ._choices_user import * +from._choices_docs import * \ No newline at end of file diff --git a/solidata_api/_choices/_choices_docs.py b/solidata_api/_choices/_choices_docs.py new file mode 100644 index 0000000..64c6605 --- /dev/null +++ b/solidata_api/_choices/_choices_docs.py @@ -0,0 +1,18 @@ + +# -*- encoding: utf-8 -*- + +""" +_choices_docs.py +- all choices related to documents +""" +# from copy import copy, deepcopy + +from log_config import log, pformat + +log.debug("... loading _choices_docs.py ...") + + + +doc_src_type_list = ["api","xls","xlsx","xml","csv"] + +doc_type_list = ["usr","prj","dmt","dmf","dsi","dsr","rec","dso","tag"] diff --git a/solidata_api/_choices/_choices_files.py b/solidata_api/_choices/_choices_files.py index 5e523ac..0896011 100644 --- a/solidata_api/_choices/_choices_files.py +++ b/solidata_api/_choices/_choices_files.py @@ -11,6 +11,7 @@ log.debug("... loading _choices_files.py ...") + authorized_filetypes = [ "csv", "xls", "xlsx", "xml" # ... ] diff --git a/solidata_api/_choices/_choices_open_level.py b/solidata_api/_choices/_choices_open_level.py new file mode 100644 index 0000000..0f41c0f --- /dev/null +++ b/solidata_api/_choices/_choices_open_level.py @@ -0,0 +1,19 @@ +# -*- encoding: utf-8 -*- + +""" +_choices_open_level.py +- all choices related to open data levels +""" +# from copy import copy, deepcopy + +from log_config import log, pformat + +log.debug("... loading _choices_open_level.py ...") + + +open_level_choices = [ + "open_data", + "collective", + "commons", + "private" +] \ No newline at end of file diff --git a/solidata_api/_choices/_choices_user.py b/solidata_api/_choices/_choices_user.py index 5d4fc82..be98549 100644 --- a/solidata_api/_choices/_choices_user.py +++ b/solidata_api/_choices/_choices_user.py @@ -11,6 +11,8 @@ log.debug("... loading _choices_user.py ...") +from ._choices_docs import * + ### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### ### CHOICES ONLY FOR ADMIN ### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### @@ -20,13 +22,13 @@ "staff", ### can edit all datamodels : dmf + dmt # "collective", "registred", - "guest", + "guest", ### not registred yet "anonymous" ] bad_passwords = [ 'test', - 'password', + 'password', 'Password', '12345' ] @@ -35,7 +37,6 @@ "can_edit_dmt", "can_edit_dmf", "can_edit_dsi", - # "can_edit_dc", "can_edit_rec", "can_only_view" ] @@ -49,12 +50,13 @@ ### user fields as recorded in DB - most exhaustive ### check 'schema_users.py' for coherence and description user_fields_admin_can_update = { - "infos" : ["name", "surname", "email"], - "auth" : ["pwd", "conf_usr", "role", "refr_tok", "blklst_usr"], - "preferences" : ["lang", "fav_list"], - "datasets" : ["proj_", "dm_", "dsi_", "dso_", "dc_", "rec_"], - "profile" : ["profiles"], - "professional" : ["struct", "struct_profiles"] + "infos" : ["name", "surname", "email"], + "auth" : ["pwd", "conf_usr", "role", "refr_tok", "is_blacklisted"], + # "preferences" : ["lang", "fav_list"], + # "datasets" : doc_type_list, + "datasets" : [ ds+"_list" for ds in doc_type_list ], + "profile" : ["lang", "usr_profiles"], + "professional_infos" : ["structure", "struct_profiles", "structure_url"] } @@ -87,6 +89,7 @@ +### choices about user's profiles user_profiles = [ "helper", diff --git a/solidata_api/_core/pandas_ops/__init__.py b/solidata_api/_core/pandas_ops/__init__.py index e69de29..67807c0 100644 --- a/solidata_api/_core/pandas_ops/__init__.py +++ b/solidata_api/_core/pandas_ops/__init__.py @@ -0,0 +1 @@ +from .pd_read_files import * \ No newline at end of file diff --git a/solidata_api/_core/pandas_ops/pd_read_files.py b/solidata_api/_core/pandas_ops/pd_read_files.py new file mode 100644 index 0000000..528c826 --- /dev/null +++ b/solidata_api/_core/pandas_ops/pd_read_files.py @@ -0,0 +1,32 @@ +# -*- encoding: utf-8 -*- + +""" +pd_read_files.py +""" + + +from log_config import log, pformat + +log.debug("... loading pd_read_files.py ...") + + +import pandas as pd + + +def read_file_with_pd ( uploaded_file, file_extension ) : + + if file_extension == "csv" : + df = pd.read_csv(uploaded_file) + + elif file_extension in ["xls","xlsx"] : + df = pd.read_excel(uploaded_file) + + elif file_extension == "xml" : + ### TO DO !!! + df = pd.read_excel(uploaded_file) + + + + + return df + diff --git a/solidata_api/_core/queries_db/__init__.py b/solidata_api/_core/queries_db/__init__.py index adf255f..e430f20 100644 --- a/solidata_api/_core/queries_db/__init__.py +++ b/solidata_api/_core/queries_db/__init__.py @@ -25,6 +25,7 @@ mongo_datamodels_fields = mongo.db[ app.config["MONGO_COLL_DATAMODELS_FIELDS"] ] # mongo_connectors = mongo.db[ app.config["MONGO_COLL_CONNECTORS"] ] mongo_datasets_inputs = mongo.db[ app.config["MONGO_COLL_DATASETS_INPUTS"] ] +mongo_datasets_raws = mongo.db[ app.config["MONGO_COLL_DATASETS_RAWS"] ] mongo_recipes = mongo.db[ app.config["MONGO_COLL_RECIPES"] ] # mongo_corr_dicts = mongo.db[ app.config["MONGO_COLL_CORR_DICTS"] ] mongo_datasets_outputs = mongo.db[ app.config["MONGO_COLL_DATASETS_OUTPUTS"] ] @@ -35,14 +36,16 @@ # mongo_licences = mongo.db[ app.config["MONGO_COLL_LICENCES"] ] -db = { +db_dict = { "mongo_tags" : mongo_tags, "mongo_users" : mongo_users, + "mongo_users" : mongo_users, "mongo_projects" : mongo_projects, "mongo_datamodels_templates" : mongo_datamodels_templates, "mongo_datamodels_fields" : mongo_datamodels_fields, # "mongo_connectors" : mongo_connectors, "mongo_datasets_inputs" : mongo_datasets_inputs, + "mongo_datasets_raws" : mongo_datasets_raws, "mongo_recipes" : mongo_recipes, # "mongo_corr_dicts" : mongo_corr_dicts, @@ -52,7 +55,7 @@ } def select_collection(coll_name): - coll = db[coll_name] + coll = db_dict[coll_name] return coll diff --git a/solidata_api/_core/utils/app_logs.py b/solidata_api/_core/utils/app_logs.py index 9d788ae..094c5e1 100644 --- a/solidata_api/_core/utils/app_logs.py +++ b/solidata_api/_core/utils/app_logs.py @@ -9,8 +9,10 @@ log.debug("... loading app_logs.py ...") +from bson.objectid import ObjectId +from datetime import datetime, timedelta +from solidata_api._core.queries_db import db_dict -from datetime import datetime, timedelta def create_modif_log( doc, action, @@ -31,8 +33,32 @@ def create_modif_log( doc, modif["modif_by"] = by if val != None : - modif["modif_val"] = val + modif["modif_val"] = val doc["modif_log"].insert(0, modif) - return doc \ No newline at end of file + return doc + + +def add_to_datasets(coll, target_doc_oid, doc_type, oid_by, oid_to_add, include_is_fav=False) : + """ + expects all values as already stringified + """ + + ### select mongo collection + mongo_coll = db_dict[coll] + + ### add dsi ref to user + doc_ = mongo_coll.find_one( {"_id" : ObjectId(target_doc_oid) } ) + + ### create ref to add to doc datasets + new_ref_ = { "oid_"+doc_type : oid_to_add, + "added_by" : oid_by , + "added_at" : datetime.utcnow(), + } + if include_is_fav == True : + new_ref_["is_fav"] = True + + doc_["datasets"][doc_type+"_list"].append(new_ref_) + + mongo_coll.save(doc_) \ No newline at end of file diff --git a/solidata_api/_models/models_dataset_input.py b/solidata_api/_models/models_dataset_input.py index b487f6f..19f9e84 100644 --- a/solidata_api/_models/models_dataset_input.py +++ b/solidata_api/_models/models_dataset_input.py @@ -13,9 +13,9 @@ from flask_restplus import fields ### import data serializers -from solidata_api._serializers.schema_logs import * -from solidata_api._serializers.schema_generic import * -from solidata_api._serializers.schema_projects import * +from solidata_api._serializers.schema_logs import * +from solidata_api._serializers.schema_generic import * +from solidata_api._serializers.schema_projects import * ### import generic models functions from solidata_api._models.models_generic import * @@ -36,51 +36,50 @@ def __init__(self, ns_) : ### SELF MODULES self.basic_infos = create_model_basic_infos(ns_, model_name="Dsi_infos") - self.log = create_model_log(ns_, model_name="Dsi_log",include_is_running=True, include_is_loaded=True ) + self.public_auth = create_model_public_auth(ns_, model_name="Dsi_public_auth") + self.log = create_model_log(ns_, model_name="Dsi_log", include_is_running=True, include_is_loaded=True ) self.modif_log = create_model_modif_log(ns_, model_name="Dsi_modif_log") - self.specs = create_model_specs(ns_, model_name="Dsi_specs", include_src_link=True) + self.specs = create_model_specs(ns_, model_name="Dsi_specs", include_src_link=True) self.team = create_model_team(ns_, model_name="Dsi_team") - self.uses = create_uses(ns_, model_name="Dsi_uses", schema_list=["usr","prj"]) - + + self.uses = create_uses(ns_, model_name="Dsi_uses", schema_list=["usr","prj"]) + self.datasets = create_model_datasets(ns_, model_name="Dsi_datasets", schema_list=["dsr","tag"]) + ### IN / complete data to enter in DB self.mod_complete_in = ns_.model('Project_in', { - 'infos' : self.basic_infos, - 'specs' : self.specs , - 'log' : self.log , - 'modif_log' : self.modif_log , - 'uses' : self.uses, + 'infos' : self.basic_infos, + 'public_auth' : self.public_auth, + 'specs' : self.specs , + 'log' : self.log , + 'modif_log' : self.modif_log , + ### uses of the document + 'uses' : self.uses, + ### team and edition levels - 'team' : self.team , + 'team' : self.team , - }) + ### datasets + 'datasets' : self.datasets, - ### OUT / complete data to enter in DB - # self.mod_complete_out = ns_.model('Project_out', { + }) - # 'infos' : self.basic_infos, - # 'log' : self.project_log , - - # ### team and edition levels - # # 'project_team' : fields.List(self.collaborator) , - # 'proj_team' : self.collaborators , - # ### datasets - # 'datamodel' : oid, - # 'dataset_inputs' : fields.List(oid), - # 'correspondance_dicts' : fields.List(oid), - # 'recipes' : fields.List(oid), + ### IN / complete data to enter in DB + self.mod_minimum = ns_.model('Project_minimum', { - # }) + 'infos' : self.basic_infos, + 'public_auth' : self.public_auth, + }) @property def model_complete_in(self): return self.mod_complete_in - # @property - # def model_complete_out(self): - # return self.mod_complete_out + @property + def model_minimum(self): + return self.mod_minimum diff --git a/solidata_api/_models/models_dataset_raw.py b/solidata_api/_models/models_dataset_raw.py new file mode 100644 index 0000000..49300c0 --- /dev/null +++ b/solidata_api/_models/models_dataset_raw.py @@ -0,0 +1,67 @@ +# -*- encoding: utf-8 -*- + +""" +_models/models_dataset_raw.py +""" + +from log_config import log, pformat + +log.debug("... loading models_dataset_raw.py ...") + + +from flask_restplus import fields + +### import data serializers +from solidata_api._serializers.schema_logs import * +from solidata_api._serializers.schema_generic import * +# from solidata_api._serializers.schema_projects import * + +### import generic models functions +from solidata_api._models.models_generic import * + +### create models from serializers +# nested models : https://github.com/noirbizarre/flask-restplus/issues/8 +# model_user_infos = ns.model( "User model", user_infos) #, mask="{name,surname,email}" ) + + + +class Dsr_infos : + """ + Model to display / marshal + dataset raw + """ + + def __init__(self, ns_) : + + ### SELF MODULES + self.basic_infos = create_model_basic_infos(ns_, model_name="Dsr_infos") + self.public_auth = create_model_public_auth(ns_, model_name="Dsr_public_auth") + self.log = create_model_log(ns_, model_name="Dsr_log" ) + self.modif_log = create_model_modif_log(ns_, model_name="Dsr_modif_log") + self.specs = create_model_specs(ns_, model_name="Dsr_specs") + + self.uses = create_uses(ns_, model_name="Dsr_uses", schema_list=["dsi","dso"]) + + + ### IN / complete data to enter in DB + self.mod_complete_in = ns_.model('Dsr_in', { + + 'infos' : self.basic_infos, + 'public_auth' : self.public_auth, + 'log' : self.log , + 'modif_log' : self.modif_log , + 'specs' : self.specs , + + ### uses of the document + 'uses' : self.uses, + + }) + + + @property + def model_complete_in(self): + return self.mod_complete_in + + + + diff --git a/solidata_api/_models/models_generic.py b/solidata_api/_models/models_generic.py index 19025f9..52ebc05 100644 --- a/solidata_api/_models/models_generic.py +++ b/solidata_api/_models/models_generic.py @@ -25,7 +25,6 @@ def create_model_basic_infos(ns_, model_name = "Basic_infos", schema = doc_basics, is_user_infos = False, - include_tags = True, ) : """ """ @@ -35,15 +34,28 @@ def create_model_basic_infos(ns_, model_name = "Basic_infos", if is_user_infos == True : schema = user_basics - if include_tags == True : - schema["tags_list"] = tags_list - basic_infos = fields.Nested( - ns_.model( model_name , schema ) + ns_.model( model_name , schema ), + description = "basic infos about the document" ) return basic_infos +### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### +### MODEL / PUBLIC AUTH +### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### +def create_model_public_auth(ns_, model_name="Public_auth"): + + """ + """ + + public_authorizations = fields.Nested( + ns_.model( model_name, public_auth ), + description = "public authorization levels on this document" + ) + + return public_authorizations + ### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### ### MODEL / TEAM @@ -100,7 +112,7 @@ def create_professional_infos(ns_, model_name="Structures"): def create_uses(ns_, model_name = "Uses", include_used_as = False, used_as = "tax", - schema_list = ["prj", "dmt", "dmf", "dsi", "usr","rec"], + schema_list = ["prj","dmt","dmf","dsi","dsr","usr","rec"], ): """ @@ -112,12 +124,19 @@ def create_uses(ns_, model_name = "Uses", doc_uses = { "used_by" : used_by, - "used_at" : used_at, } if include_used_as == True and schema == "prj" : doc_uses["used_as"] = used_as + uses_dates = fields.List ( + used_at , + description = "Uses dates", + default = [] + ) + + doc_uses["used_at"] = uses_dates + uses_infos = fields.Nested( ns_.model( "Used_by_"+schema, doc_uses ) ) @@ -140,10 +159,37 @@ def create_uses(ns_, model_name = "Uses", ### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### ### MODEL / DATASETS ### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### -def create_model_datasets(ns_, model_name = "Datasets" , - include_fav = False, - display_fullname = False , - schema_list = ["prj", "dmt", "dmf", "dsi", "rec", "dso"], + +def create_model_dataset(ns_, model_name = "Datasets" , + include_fav = False, + include_dmf_open_level = False , + display_fullname = False , + schema = "prj", + ) : + model_dataset = { + "oid_"+schema : oid_dict[schema]["field"], + 'added_at' : added_at, + 'added_by' : added_by, + } + + if include_fav == True : + model_dataset["is_fav"] = is_fav + + if include_dmf_open_level == True and schema in ["dmf","dsr"] : + model_dataset["open_level"] = open_level + + dataset = fields.Nested( + ns_.model( schema.title()+"_ref", model_dataset ) + ) + + return dataset + + +def create_model_datasets(ns_, model_name = "Datasets" , + include_fav = False, + include_dmf_open_level = False , + display_fullname = False , + schema_list = ["prj","dmt","dmf","dsi","dsr","rec","dso","tag","func"], ) : """ """ @@ -151,19 +197,14 @@ def create_model_datasets(ns_, model_name = "Datasets" , datasets_dict = {} for schema in schema_list : - - model_dataset = { - schema+"_oid" : oid_dict[schema]["field"], - 'added_at' : added_at, - 'added_by' : added_by, - } - - if include_fav == True : - model_dataset["is_fav"] = is_fav - - dataset = fields.Nested( - ns_.model( schema.title()+"_ref", model_dataset ) - ) + + dataset = create_model_dataset( ns_, + model_name = model_name, + include_fav = include_fav, + include_dmf_open_level = include_dmf_open_level, + display_fullname = display_fullname, + schema = schema + ) dataset_list = fields.List( dataset, @@ -180,6 +221,50 @@ def create_model_datasets(ns_, model_name = "Datasets" , return datasets +### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### +### MODEL / MAPPING +### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### +def create_model_mappings(ns_, model_name = "Mapping" , + schema_list = ["dsi_to_dmt", "rec_to_func", "rec_to_dmt"], + ) : + """ + mapping the relation between a document and another + """ + + mapping_dict = {} + + for schema in schema_list : + + ### TO DO + + model_mapping = mapping_oid_dict[schema] + model_mapping['added_at'] = added_at + model_mapping['added_by'] = added_by + + if schema == "dsi_to_dmf" : + model_mapping['visible_dmf_list'] = fields.List( + oid_dmf, + description = "visible dmf list" + ) + + mapping = fields.Nested( + ns_.model( schema.title(), model_mapping ), + description = "mapping between {}".format(schema), + ) + + mapping_list = fields.List( + mapping, + description = "List of {}s on this document".format(schema), + default = [] + ) + + mapping_dict[schema] = mapping_list + + mappings = fields.Nested( + ns_.model( model_name, mapping_dict ) + ) + + return mappings ### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### ### MODEL / MODIFICATIONS LOG @@ -225,13 +310,13 @@ def create_model_log(ns_, model_name = "Log", } if include_counts == True : - log_base[ counts_name ] = count + log_base[ counts_name ] = count if include_is_running == True : - log_base[ "is_running" ] = is_running + log_base[ "is_running" ] = is_running if include_is_loaded == True : - log_base[ "is_loaded" ] = is_loaded + log_base[ "is_loaded" ] = is_loaded ### create the log logs = fields.Nested( @@ -266,7 +351,7 @@ def create_model_specs(ns_, model_name = "Specs", pass if include_child_of_tag == True : - pass + pass ### compile the document's log doc_specs = fields.Nested( diff --git a/solidata_api/_models/models_project.py b/solidata_api/_models/models_project.py index 6c30fc3..97aeec3 100644 --- a/solidata_api/_models/models_project.py +++ b/solidata_api/_models/models_project.py @@ -15,7 +15,7 @@ ### import data serializers from solidata_api._serializers.schema_logs import * from solidata_api._serializers.schema_generic import * -from solidata_api._serializers.schema_projects import * +# from solidata_api._serializers.schema_projects import * ### import generic models functions from solidata_api._models.models_generic import * @@ -36,58 +36,53 @@ def __init__(self, ns_) : ### SELF MODULES self.basic_infos = create_model_basic_infos(ns_, model_name="Project_infos") - self.log = create_model_log(ns_, model_name="Project_log", include_is_running=True ) + self.log = create_model_log(ns_, model_name="Project_log", include_is_running=True ) self.modif_log = create_model_modif_log(ns_, model_name="Project_modif_log") self.specs = create_model_specs(ns_, model_name="Project_specs") self.team = create_model_team(ns_, model_name="Project_team") - self.datasets = create_model_datasets(ns_, model_name="Project_datasets", schema_list=["dmt","dsi","rec","dso"]) + self.public_auth = create_model_public_auth(ns_, model_name="Project_public_auth") + self.uses = create_uses(ns_, model_name="Project_uses", schema_list=["usr"]) - + + self.datasets = create_model_datasets(ns_, model_name="Project_datasets", schema_list=["dmt","dsi","rec","dso","tag"]) + self.mapping = create_model_mappings(ns_, model_name="Project_mapping", schema_list=[ "dsi_to_dmf", "rec_to_dmf"]) + ### IN / complete data to enter in DB self.mod_complete_in = ns_.model('Project_in', { - 'infos' : self.basic_infos, - 'specs' : self.specs , - 'log' : self.log , - 'modif_log' : self.modif_log , - 'uses' : self.uses, + 'infos' : self.basic_infos, + 'public_auth' : self.public_auth, + 'specs' : self.specs , + 'log' : self.log , + 'modif_log' : self.modif_log , + + ### uses of the document + 'uses' : self.uses, ### team and edition levels - 'team' : self.team , + 'team' : self.team , ### datasets "datasets" : self.datasets , - # 'dmt' : oid_dmt, - # 'dsi' : fields.List(oid_dsi), - # 'rec' : fields.List(oid_rec), - # 'dso' : fields.List(oid_dso), + 'mapping' : self.mapping, }) - ### OUT / complete data to enter in DB - # self.mod_complete_out = ns_.model('Project_out', { - - # 'infos' : self.basic_infos, - # 'log' : self.project_log , - - # ### team and edition levels - # # 'project_team' : fields.List(self.collaborator) , - # 'proj_team' : self.collaborators , - # ### datasets - # 'datamodel' : oid, - # 'dataset_inputs' : fields.List(oid), - # 'correspondance_dicts' : fields.List(oid), - # 'recipes' : fields.List(oid), + ### MIN / minimum data to create a project in DB + self.mod_minimum = ns_.model('Project_minimum', { - # }) + 'infos' : self.basic_infos, + 'public_auth' : self.public_auth, + }) + @property def model_complete_in(self): return self.mod_complete_in - # @property - # def model_complete_out(self): - # return self.mod_complete_out + @property + def model_minimum(self): + return self.mod_minimum diff --git a/solidata_api/_models/models_user.py b/solidata_api/_models/models_user.py index 244fd80..aff6836 100644 --- a/solidata_api/_models/models_user.py +++ b/solidata_api/_models/models_user.py @@ -144,17 +144,14 @@ def __init__(self, ns_) : ### SELF MODULES - ### basic infos self.basic_infos = create_model_basic_infos(ns_, model_name="User_infos", is_user_infos=True) self.log = create_model_log(ns_, model_name="User_log", include_counts=True, counts_name="login_count") self.modif_log = create_model_modif_log(ns_, model_name="User_modif_log") - self.datasets = create_model_datasets(ns_, model_name="User_datasets", include_fav=True, schema_list=["prj","dmt", "dmf","dsi","rec"]) + self.datasets = create_model_datasets(ns_, model_name="User_datasets", include_fav=True, schema_list=["prj","dmt", "dmf","dsi","rec","tag"]) self.specs = create_model_specs(ns_, model_name="User_specs") self.professional_infos = create_professional_infos(ns_, model_name="User_professionnal_infos") self.team = create_model_team(ns_, model_name="User_team") - - ### profile - self.profile = fields.Nested( + self.profile = fields.Nested( ns_.model("User_profile", usr_profile_ ) ) @@ -164,13 +161,18 @@ def __init__(self, ns_) : 'infos' : self.basic_infos, 'profile' : self.profile, + 'professional_infos' : self.professional_infos, 'log' : self.log , 'specs' : self.specs , 'modif_log' : self.modif_log , - "datasets" : self.datasets , - 'professional_infos' : self.professional_infos, + + ### team (as favorites) 'team' : self.team , + ### datasets + "datasets" : self.datasets , + + ### auth level of current user 'auth': fields.Nested( ns_.model('User_authorizations', user_auth_in ) ), @@ -182,13 +184,19 @@ def __init__(self, ns_) : 'infos' : self.basic_infos, 'profile' : self.profile, + 'professional_infos' : self.professional_infos, + 'specs' : self.specs , 'log' : self.log , 'modif_log' : self.modif_log , - "datasets" : self.datasets , - 'professional_infos' : self.professional_infos, + + ### team (as favorites) 'team' : self.team , + ### datasets + "datasets" : self.datasets , + + ### auth level of current user 'auth' : fields.Nested( ns_.model('User_authorizations', user_auth_out ) ), @@ -219,10 +227,6 @@ def model_complete_in(self): def model_complete_out(self): return self.mod_complete_out - # @property - # def model_update(self): - # return self.mod_update - @property def model_access(self): return self.mod_access diff --git a/solidata_api/_serializers/schema_generic.py b/solidata_api/_serializers/schema_generic.py index a84f3e6..26c68c9 100644 --- a/solidata_api/_serializers/schema_generic.py +++ b/solidata_api/_serializers/schema_generic.py @@ -30,10 +30,6 @@ required = False, ) -doc_src_type_list = ["api","xls","xlsx","xml","csv"] - -doc_type_list = ["usr","prj","dmt","dmf","dsi","rec","dso"] - ### basic informations about a document : project / licence / oid ... title = fields.String( @@ -58,6 +54,14 @@ enum = licences_options, required = True, ) +open_level = fields.String( + description = "open level of the document", + attribute = "open_level", + example = "commons", + default = 'open_data', + enum = open_level_choices, + required = False, + ) src_link = fields.String( description = "source link of the document", attribute = "src_link", @@ -107,7 +111,7 @@ required = True, ) used_by = fields.String( - description = "oid of an user", + description = "oid of a document", attribute = "used_by", example = "5b461ed90a82867e7b114f44", required = True, @@ -134,31 +138,37 @@ oid_prj = fields.String( description = "oid of a project", - attribute = "oid_proj", + attribute = "oid_prj", example = "5b461ed90a82867e7b114f44", required = True, ) oid_dmf = fields.String( description = "oid of a datamodel field", - attribute = "oid_dm_f", + attribute = "oid_dmf", example = "5b461ed90a82867e7b114f44", required = True, ) oid_dmt = fields.String( description = "oid of a datamodel template", - attribute = "oid_dm_t", + attribute = "oid_dmt", example = "5b461ed90a82867e7b114f44", required = True, ) oid_dsi = fields.String( description = "oid of a dataset input", - attribute = "oid_ds_i", + attribute = "oid_dsi", + example = "5b461ed90a82867e7b114f44", + required = True, + ) +oid_dsr = fields.String( + description = "oid of a dataset raw", + attribute = "oid_dsr", example = "5b461ed90a82867e7b114f44", required = True, ) oid_dso = fields.String( description = "oid of a dataset output", - attribute = "oid_ds_o", + attribute = "oid_dso", example = "5b461ed90a82867e7b114f44", required = True, ) @@ -180,6 +190,12 @@ example = "5b461ed90a82867e7b114f44", required = True, ) +oid_func = fields.String( + description = "oid of a function", + attribute = "oid_func", + example = "5b461ed90a82867e7b114f44", + required = True, + ) ### store a correspondance dict of oids... oid_dict = { @@ -189,12 +205,70 @@ "dmt" : { "field" : oid_dmt , "fullname" : "datamodel_template" } , "dmf" : { "field" : oid_dmf , "fullname" : "datamodel_field" } , "dsi" : { "field" : oid_dsi , "fullname" : "dataset_input" } , + "dsr" : { "field" : oid_dsr , "fullname" : "dataset_raw" } , "rec" : { "field" : oid_rec , "fullname" : "recipe" } , "dso" : { "field" : oid_dso , "fullname" : "dataset_output" } , - "dso" : { "field" : oid_fld , "fullname" : "field" } , - "dso" : { "field" : oid_tag , "fullname" : "tag" } , + "fld" : { "field" : oid_fld , "fullname" : "field" } , + "tag" : { "field" : oid_tag , "fullname" : "tag" } , +} + + +### for mappings +col_index = fields.String( + description = "index of a column within a dataset input", + attribute = "col_index", + example = "my_col_index", + required = True, + ) + +mapping_oid_dict = { + + "dsi_to_dmf" : { + ### src dsi + "oid_dsi" : oid_dsi, + "col_index" : col_index, + ### target dmf + "oid_dmt" : oid_dmt, + "oid_dmf" : oid_dmf, + } , + + "rec_to_dmf" : { + ### src rec + "oid_rec" : oid_rec, + "oid_func" : oid_func, + ### target dmf + "oid_dmt" : oid_dmt, + "oid_dmf" : oid_dmf, + ### rec_params : {} + } , + "rec_to_func" : { + "oid_func" : oid_func, + ### rec_params : {} + } +} + +### edit auth for a document +guests_can_see = fields.Boolean( + description = "guests can see the document ?", + attribute = "guests_can_see", + example = True, + required = True, + default = True, + ) +guests_can_edit = fields.Boolean( + description = "guests can edit the document ?", + attribute = "guests_can_edit", + example = False, + required = True, + default = False, + ) + +public_auth = { + "guests_can_see" : guests_can_see, + "guests_can_edit" : guests_can_edit } + ### data_raw for dsi - dso - dmf f_cell = fields.String( description = "content of a cell", @@ -221,8 +295,8 @@ ) is_linked_to_src = fields.Boolean( - description = "is the project currently loaded ?", - attribute = "is_loaded", + description = "is the project currently linked to the source ?", + attribute = "is_linked_to_src", example = False, required = False, default = False, @@ -230,13 +304,12 @@ - doc_type = fields.String( description = "category of a document", - attribute = "doc_categ", + attribute = "doc_type", enum = doc_type_list, - example = "usr", - default = 'usr', + # example = "usr", + # default = 'usr', required = True, ) diff --git a/solidata_api/_serializers/schema_users.py b/solidata_api/_serializers/schema_users.py index 8438efc..a1cfd4e 100644 --- a/solidata_api/_serializers/schema_users.py +++ b/solidata_api/_serializers/schema_users.py @@ -90,7 +90,7 @@ description = "edit auth of an user", enum = user_actions_proj, ), - required = True, + required = False, attribute = "edit_auth", default = [] ) @@ -112,19 +112,26 @@ ) ### professional infos -structure = fields.String( - description = "structures / organisations the user" +struct_name = fields.String( + description = "name of the user's structure", + attribute = "struct_name", + required = True, ) - # example = ["my structure A", "my structure B"], - # attribute = "struct", - # default = [] struct_profile = fields.String( - description = "structures / organisations profile", + description = "profile of the structure", + attribute = "struct_profile", example = "public_state", enum = user_structure, + required = False, ) - # attribute = "struct_profiles", - # default = [] +struct_url = fields.String( + description = "structure url link", + attribute = "struct_url", + example = "my-url-link", + default = '', + required = False, + ) + usr_profile = fields.String( description = "profiles of the user", enum = user_profiles, @@ -200,7 +207,7 @@ user_struct = { - "structure" : structure, - "structure_profile" : struct_profile, - "structure_url" : url_link, + "struct_name" : struct_name, + "struct_profile" : struct_profile, + "struct_url" : struct_url, } diff --git a/solidata_api/api/__init__.py b/solidata_api/api/__init__.py index 61e4588..8efc1a6 100644 --- a/solidata_api/api/__init__.py +++ b/solidata_api/api/__init__.py @@ -48,7 +48,7 @@ from solidata_api._choices import bad_passwords, authorized_filetypes, authorized_mimetype from solidata_api._core.utils import * # create_modif_log, secure_filename, allowed_file - +from solidata_api._core.pandas_ops import * # create_modif_log, secure_filename, allowed_file from solidata_api._core.emailing import send_email ### import mongo utils diff --git a/solidata_api/api/api_auth/endpoint_user_login.py b/solidata_api/api/api_auth/endpoint_user_login.py index 0a8bb93..52fa655 100644 --- a/solidata_api/api/api_auth/endpoint_user_login.py +++ b/solidata_api/api/api_auth/endpoint_user_login.py @@ -152,7 +152,7 @@ def post(self): ### update user log in db - user = create_modif_log(doc=user, action="login") + # user = create_modif_log(doc=user, action="login") user["log"]["login_count"] += 1 mongo_users.save(user) diff --git a/solidata_api/api/api_dataset_inputs/endpoint_dsi_create.py b/solidata_api/api/api_dataset_inputs/endpoint_dsi_create.py index 4710dbd..bfab939 100644 --- a/solidata_api/api/api_dataset_inputs/endpoint_dsi_create.py +++ b/solidata_api/api/api_dataset_inputs/endpoint_dsi_create.py @@ -15,8 +15,10 @@ ### import models from solidata_api._models.models_dataset_input import * +from solidata_api._models.models_dataset_raw import * model_dsi_in = Dsi_infos(ns).mod_complete_in - +model_dsr_in = Dsr_infos(ns).mod_complete_in +# model_dsi_ref = create_model_dataset(ns, model_name="Dsi_ref", include_fav=True,schema="dsi") ### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### ### ROUTES @@ -31,8 +33,8 @@ class DsiCreate(Resource): # @api.marshal_with(project_model, envelope="projects_list") - @guest_required # @current_user_required + @guest_required @ns.expect(file_parser) def post(self): """ @@ -52,6 +54,7 @@ def post(self): ### check if client is an admin or if is the current user claims = get_jwt_claims() log.debug("claims : \n %s", pformat(claims) ) + oid_usr_ = claims["_id"] ### check extension and mimetype if uploaded_file and allowed_file(uploaded_file.filename) : @@ -61,67 +64,150 @@ def post(self): log.debug("filename : %s", filename) - ### read file with pandas - df = pd.read_csv(uploaded_file) - print (df.head(5)) - - - ### trim columns' titles and save them as col_code + col_oid - df_col_dict = df.to_dict() - - ### check if file already exists in uploads - - - ### save file in uploads folder if file doesn't exist - # destination = os.path.join(app.config["UPLOADS_DATA"]) - destination = app.config["UPLOADS_DATA"] - log.debug("destination : %s", destination) - if not os.path.exists(destination): - os.makedirs(destination) - - file_path = "{}{}".format( destination, filename ) - log.debug("file_path : %s", file_path) - - uploaded_file.save(file_path) - log.debug("uploaded_file '{}' saved at file_path : {}".format(uploaded_file, file_path) ) - - - - - ### create new dsi object - new_dsi_info = { - "log" : { - "created_at" : datetime.utcnow(), - "created_by" : claims["_id"], - }, - "specs" : { - "doc_type" : "dsi", - "src_type" : get_file_extension(filename), - "src_link" : filename, + ### check if file already exists in db + existing_dsi = mongo_datasets_inputs.find_one({"specs.src_link" : filename}) + + if existing_dsi is None and allowed_file(filename) : + + file_extension = get_file_extension(filename) + + + ### read file with pandas + df = read_file_with_pd(uploaded_file,file_extension ) + + ### drop rows where all values are nan + df = df.dropna(how="all") + print (df.head(5)) + + ### trim columns' titles + df_col_dict = df.to_dict(orient="list") + log.debug("df.to_dict(orient='list') : \n %s", pformat(df.head(5).to_dict(orient="list")) ) + + ### save file in uploads folder if file doesn't exist + destination = app.config["UPLOADS_DATA"] + log.debug("destination : %s", destination) + if not os.path.exists(destination): + os.makedirs(destination) + + file_path = "{}{}".format( destination, filename ) + log.debug("file_path : %s", file_path) + + ### save file in uploads + uploaded_file.save(file_path) + log.debug("uploaded_file '{}' saved at file_path : {}".format(uploaded_file, file_path) ) + + + ### create new empty dsi object + new_dsi_infos = { + "infos" : { + "title" : filename, + }, + "log" : { + "created_at" : datetime.utcnow(), + "created_by" : oid_usr_, + }, + "specs" : { + "doc_type" : "dsi", + "src_type" : file_extension, + "src_link" : filename, + }, + } + log.debug("new_dsi_infos : \n %s" , pformat(new_dsi_infos)) + + new_dsi = marshal( new_dsi_infos , model_dsi_in) + log.debug("new_dsi : \n %s" , pformat(new_dsi)) + + ### save dsi object to db + new_dsi_doc = mongo_datasets_inputs.insert(new_dsi) + log.info("new_dsi is saved in db... ") + + ### keep track of new_dsi_doc oid + oid_dsi_ = str(new_dsi_doc) + log.info("oid_dsi_ : %s ", oid_dsi_ ) + + + + ### loop through df_col_dict to save each column in mongo_datasets_raws + for col_key, col_values in df_col_dict.items() : + + ### create new empty dsr object + new_dsr_infos = { + "infos" : { + "title" : col_key, + "licence" : "private_use_only", + "description" : "column '{}' of '{}' file".format(col_key,filename) + }, + "public_auth" : { + "guests_can_see" : False, + }, + "log" : { + "created_at" : datetime.utcnow(), + "created_by" : oid_usr_, + }, + "specs" : { + "doc_type" : "dsr", + "src_type" : get_file_extension(filename), + "src_link" : filename, + }, + "uses" : { + "by_dsi" : { + "used_by" : oid_dsi_ + } + } } - } - log.debug("new_dsi_info : \n %s" , pformat(new_dsi_info)) - - new_dsi = marshal( new_dsi_info , model_dsi_in) - log.debug("new_dsi : \n %s" , pformat(new_dsi)) - - - - ### save dsi object to db - mongo_datasets_inputs.save(new_dsi) - log.info("new_dsi is saved in db ") - - - # args = file_parser.parse_args() - # if args['data_file'].mimetype in authorized_mimetype : - # destination = os.path.join(app.config.get('DATA_FOLDER'), 'uploads/data_files/') - # if not os.path.exists(destination): - # os.makedirs(destination) - # xls_file = '%s%s' % (destination, 'data_file.xls') - # args['data_file'].save(xls_file) - # else: - # abort(404) - - return { - "msg" : "your file has been correctly uploaded...", - }, 200 \ No newline at end of file + ### fill new_dsi_info with the data + new_dsr = marshal( new_dsr_infos , model_dsr_in) + new_dsr["data_raw"] = col_values + + ### save dsr object to db + new_dsr_doc = mongo_datasets_raws.insert(new_dsr) + + ### keep track of new_dsr_doc oid + oid_dsr_ = str(new_dsr_doc) + + log.info("new_dsr '{}' is saved in db... / oid_dsr_ : {} ".format(col_key, oid_dsr_) ) + + + ### add dsr ref to dsi + add_to_datasets( coll = "mongo_datasets_inputs", + target_doc_oid = oid_dsi_, + doc_type = "dsr", + oid_by = oid_usr_, + oid_to_add = oid_dsr_, + ) + + # args = file_parser.parse_args() + # if args['data_file'].mimetype in authorized_mimetype : + # destination = os.path.join(app.config.get('DATA_FOLDER'), 'uploads/data_files/') + # if not os.path.exists(destination): + # os.makedirs(destination) + # xls_file = '%s%s' % (destination, 'data_file.xls') + # args['data_file'].save(xls_file) + # else: + # abort(404) + + + + ### add dsi ref to user + add_to_datasets( coll = "mongo_users", + target_doc_oid = oid_usr_, + doc_type = "dsi", + oid_by = oid_usr_, + oid_to_add = oid_dsi_, + include_is_fav = True + ) + + + return { + "msg" : "your file '{}' has been correctly uploaded...".format(filename), + "filename" : filename, + "oid_dsi" : str(oid_dsi_) + }, 200 + + else : + existing_dsi_oid = existing_dsi["_id"] + return { + "msg" : "your file '{}' already exists in dsi db / try update instead ...".format(filename), + "filename" : filename, + "oid_dsi" : str(existing_dsi_oid) + }, 401 \ No newline at end of file diff --git a/solidata_api/api/api_projects/endpoint_prj_create.py b/solidata_api/api/api_projects/endpoint_prj_create.py index c60fa8c..da0390a 100644 --- a/solidata_api/api/api_projects/endpoint_prj_create.py +++ b/solidata_api/api/api_projects/endpoint_prj_create.py @@ -15,8 +15,11 @@ ### import models from solidata_api._models.models_project import * -model_project_in = Project_infos(ns).mod_complete_in -# model_project_out = Project_infos(ns).mod_complete_out +mod_prj = Project_infos(ns) +# model_project_in = Project_infos(ns).model_complete_in +model_project_in = mod_prj.mod_complete_in +# model_project_min = Project_infos(ns).model_min +model_project_min = mod_prj.mod_minimum ### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ### @@ -34,12 +37,33 @@ class ProjectCreate(Resource): # @api.marshal_with(project_model, envelope="projects_list") - @jwt_required - @ns.expect(model_project_in) + # @jwt_required + @guest_required + # @ns.expect(model_project_in) + # @ns.expect(model_project_min) def post(self): """ Create a new project in db """ + + ### DEBUGGING + print() + print("-+- "*40) + log.debug( "ROUTE class : %s", self.__class__.__name__ ) + log.debug ("payload : \n{}".format(pformat(ns.payload))) + + ### get data from form + new_prj_infos = ns.payload + + ### marshall infos with prj complete model + # new_prj = marshal( new_prj_infos , model_project_in) + + ### save new_prj in db + # mongo_projects.insert(new_prj) + + + return { - "msg" : "nananana" + "msg" : "nananana", + } \ No newline at end of file diff --git a/solidata_api/api/api_users/endpoint_usr_register.py b/solidata_api/api/api_users/endpoint_usr_register.py index cc59e48..cb305bd 100644 --- a/solidata_api/api/api_users/endpoint_usr_register.py +++ b/solidata_api/api/api_users/endpoint_usr_register.py @@ -71,28 +71,29 @@ def post(self): new_user_infos = {"infos" : ns.payload, "auth" : ns.payload } new_user = marshal( new_user_infos , model_user_complete_in) new_user["auth"]["pwd"] = hashpass + new_user["specs"]["doc_type"] = "usr" new_user["log"]["created_at"] = datetime.utcnow() new_user["log"]["created_by"] = payload_email ### temporary save new user in db - mongo_users.insert( new_user ) + _id = mongo_users.insert( new_user ) log.info("new user is being created : \n%s", pformat(new_user)) + log.info("_id : \n%s", pformat(_id)) - ### get back user from db to add its - user_created = mongo_users.find_one({"infos.email" : payload_email}) - new_user["_id"] = str(user_created["_id"]) + + new_user["_id"] = str(_id) # str(user_created["_id"]) - ### create access and refresh tokens + ### create access tokens log.debug("... create_access_token") access_token = create_access_token(identity=new_user) + ### create refresh tokens log.debug("... refresh_token") - ### just create refresh token once / so it could be blacklisted - # refresh_token = create_refresh_token(identity=new_user) + ### just create a temporary refresh token once / so it could be blacklisted expires = app.config["JWT_CONFIRM_EMAIL_REFRESH_TOKEN_EXPIRES"] # timedelta(days=7) refresh_token = create_refresh_token(identity=new_user, expires_delta=expires) - ### add confirm_email claim + ### add confirm_email to claims for access_token_confirm_email new_user["confirm_email"] = True access_token_confirm_email = create_access_token(identity=new_user, expires_delta=expires) log.debug("access_token_confirm_email : \n %s", access_token_confirm_email ) @@ -105,8 +106,8 @@ def post(self): log.info("tokens : \n %s", pformat(tokens)) ### update new user in db - # new_user["auth"]["refr_tok"] = refresh_token - # new_user["auth"]["refr_tok"] = user_created["auth"]["refr_tok"] = refresh_token + # user_created = mongo_users.find_one({"infos.email" : payload_email}) + user_created = mongo_users.find_one({"_id" : _id}) user_created["auth"]["refr_tok"] = refresh_token mongo_users.save(user_created) log.info("new user is updated with its tokens : \n%s", pformat(new_user)) diff --git a/solidata_api/application.py b/solidata_api/application.py index 509c314..84d76ee 100644 --- a/solidata_api/application.py +++ b/solidata_api/application.py @@ -98,7 +98,7 @@ def create_app( app_name='SOLIDATA_API', run_mode="dev" ): from solidata_api._core.async_tasks import async # access mongodb collections - from solidata_api._core.queries_db import ( db, + from solidata_api._core.queries_db import ( db_dict, mongo_tags, mongo_users, mongo_projects, @@ -106,6 +106,7 @@ def create_app( app_name='SOLIDATA_API', run_mode="dev" ): mongo_datamodels_fields, # mongo_connectors, ### all cd are treated as ds_i mongo_datasets_inputs, + mongo_datasets_raws, mongo_datasets_outputs, mongo_recipes, mongo_jwt_blacklist, diff --git a/solidata_api/config.py b/solidata_api/config.py index 2ee7956..fdd6226 100644 --- a/solidata_api/config.py +++ b/solidata_api/config.py @@ -62,6 +62,7 @@ class BaseConfig(object): MONGO_COLL_DATAMODELS_FIELDS = "datamodels_fields" # MONGO_COLL_CONNECTORS = "connectors" MONGO_COLL_DATASETS_INPUTS = "datasets_inputs" + MONGO_COLL_DATASETS_RAWS = "datasets_raws" MONGO_COLL_DATASETS_OUTPUTS = "datasets_outputs" MONGO_COLL_RECIPES = "recipes" # MONGO_COLL_CORR_DICTS = "corr_dicts" diff --git a/uploads/data_sources/2017_communes_fr_com_code.csv b/uploads/data_sources/2017_communes_fr_com_code.csv new file mode 100644 index 0000000..e69de29