diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13b0ffba..876c6744 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,9 +11,9 @@ jobs: - uses: actions/checkout@v2 - name: "Set up Python" - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' - name: "python backend - install requirements" working-directory: srv/python diff --git a/config-php-sample.json b/config-php-sample.json index 2ed7b236..14c41c7e 100644 --- a/config-php-sample.json +++ b/config-php-sample.json @@ -1,8 +1,8 @@ { "app_conf": { "studio_title": "GéoBretagne mviewer studio", - "mviewer_version": "3.13", - "mviewerstudio_version": "4.2", + "mviewer_version": "4", + "mviewerstudio_version": "4.2.1", "is_php": "true", "php": { "upload_service": "srv/php/store.php", @@ -83,7 +83,7 @@ "type": "OSM", "url": "https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png", "maxzoom": "20", - "attribution": "Map tiles by CartoDb, under CC BY 3.0 " + "attribution": "Map tiles by CartoDb, under CC BY 3.0 " }, "esriworldimagery": { "id": "esriworldimagery", @@ -142,14 +142,14 @@ "osm_grey": { "id": "osm_grey", "thumbgallery": "img/basemap/osm_grey.png", - "title": "GéoBretagne - GéoGrandEst", + "title": "GéoBretagne - DataGrandEst", "label": "OpenStreetMap style noir et blanc", "type": "WMTS", "url": "https://tile.geobretagne.fr/osm/service", "layers": "osm:grey", "format": "image/png", "fromcapacity": "false", - "attribution": "GéoBretagne - GéoGrandEst. Données : les contributeurs d'OpenStreetMap , ODbL ", + "attribution": "GéoBretagne - DataGrandEst. Données : les contributeurs d'OpenStreetMap , ODbL ", "style": "normal", "matrixset": "PM", "maxzoom": "22" @@ -181,12 +181,7 @@ "baseref": "https://geobretagne.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" }, { - "title": "Catalogue Région Bretagne", - "url": "https://kartenn.region-bretagne.fr/geonetwork/srv/fre/csw", - "baseref": "https://kartenn.region-bretagne.fr/geonetwork/srv/fre/catalog.search#/metadata/" - }, - { - "title": "Catalogue de la Région Grand Est", + "title": "Catalogue DataGrandEst", "url": "https://datagrandest.fr/geonetwork/srv/fre/csw", "baseref": "https://datagrandest.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" } diff --git a/config-python-sample.json b/config-python-sample.json index 95cdf22c..0786e50d 100644 --- a/config-python-sample.json +++ b/config-python-sample.json @@ -1,15 +1,15 @@ { "app_conf": { "studio_title": "Mviewer Studio", - "mviewer_version": "3.13", - "mviewerstudio_version": "4.2", + "mviewer_version": "4", + "mviewerstudio_version": "4.2.1", "api": "api/app", "store_style_service": "api/style", "mviewer_instance": "/mviewer/", "publish_url": "/mviewer/?config=apps/public/{{config}}.xml", "conf_path_from_mviewer": "apps/store/", "mviewer_short_url": { - "used": true, + "used": false, "apps_folder": "store", "public_folder": "public" }, @@ -77,7 +77,7 @@ "type": "OSM", "url": "https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png", "maxzoom": "20", - "attribution": "Map tiles by CartoDb, under CC BY 3.0 " + "attribution": "Map tiles by CartoDb, under CC BY 3.0 " }, "esriworldimagery": { "id": "esriworldimagery", @@ -136,14 +136,14 @@ "osm_grey": { "id": "osm_grey", "thumbgallery": "img/basemap/osm_grey.png", - "title": "GéoBretagne - GéoGrandEst", + "title": "GéoBretagne - DataGrandEst", "label": "OpenStreetMap style noir et blanc", "type": "WMTS", "url": "https://tile.geobretagne.fr/osm/service", "layers": "osm:grey", "format": "image/png", "fromcapacity": "false", - "attribution": "GéoBretagne - GéoGrandEst. Données : les contributeurs d'OpenStreetMap , ODbL ", + "attribution": "GéoBretagne - DataGrandEst. Données : les contributeurs d'OpenStreetMap , ODbL ", "style": "normal", "matrixset": "PM", "maxzoom": "22" @@ -175,15 +175,10 @@ "baseref": "https://geobretagne.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" }, { - "title": "Catalogue Région Bretagne", - "url": "https://kartenn.region-bretagne.fr/geonetwork/srv/fre/csw", - "baseref": "https://kartenn.region-bretagne.fr/geonetwork/srv/fre/catalog.search#/metadata/" - }, - { - "title": "Catalogue de la Région Grand Est", + "title": "Catalogue DataGrandEst", "url": "https://datagrandest.fr/geonetwork/srv/fre/csw", "baseref": "https://datagrandest.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" - } + } ], "wms": [{ "title": "Serveur WMS de la Région", diff --git a/docker/Dockerfile-python-backend b/docker/Dockerfile-python-backend index 4340adba..7413051a 100644 --- a/docker/Dockerfile-python-backend +++ b/docker/Dockerfile-python-backend @@ -44,4 +44,4 @@ ENV EXPORT_CONF_FOLDER=/home/apprunner/apps/store \ # You will probably have to override this one on runtime with your custom config COPY config-python-sample.json mviewerstudio_backend/static/apps/config.json -CMD ["gunicorn", "-w 4", "-b 0.0.0.0:8000", "mviewerstudio_backend.app:app"] +CMD ["gunicorn", "-w 1", "-b 0.0.0.0:8000", "mviewerstudio_backend.app:app"] diff --git a/docker/nginx/default.conf.template b/docker/nginx/default.conf.template index d12b3f75..c0e216ac 100644 --- a/docker/nginx/default.conf.template +++ b/docker/nginx/default.conf.template @@ -3,7 +3,7 @@ server { server_name ${NGINX_HOST}; location /mviewer/ { - proxy_pass http://mviewer:80/; + proxy_pass http://mviewer:8080/; } location /${MVIEWERSTUDIO_URL_PATH_PREFIX}/ { diff --git a/docs/doc_tech/dev_corner.rst b/docs/doc_tech/dev_corner.rst index 052c51fd..c8c01d2b 100644 --- a/docs/doc_tech/dev_corner.rst +++ b/docs/doc_tech/dev_corner.rst @@ -21,8 +21,8 @@ Prérequis - Disposer des droits d'exécution en local (via Flask) - Mviewer doit être installé et accessible en local (avec droit d'écriture) -Généralités ------------ +Généralités +----------- Le debugger VS Code permet d'utiliser le virtualenv (répertoire .venv) installé dans le répertoire `srv/python`. @@ -33,11 +33,33 @@ https://docs.posit.co/ide/server-pro/user/vs-code/guide/python-environments.html Vous pouvez également consultez cette documentation sur le debugger Python dans VS code : -https://code.visualstudio.com/docs/python/tutorial-flask - - -Configuration du debugger VS Code ---------------------------------- +https://code.visualstudio.com/docs/python/tutorial-flask + + +Synchronisation API / Swagger +----------------------------- + +Toute modification des routes backend (fichier ``srv/python/mviewerstudio_backend/route.py``) +doit être synchronisée dans la spécification Swagger/OpenAPI +(``srv/python/mviewerstudio_backend/swagger.yaml``). + +Accès à Swagger +--------------- + +Une fois le backend démarré, vous pouvez accéder à la documentation API : + +- Swagger UI : ``http://localhost:5007/swagger`` (ou ``/swagger/``) +- Spécification OpenAPI : ``http://localhost:5007/swagger.yaml`` + +Si un préfixe d'URL est configuré (ex: ``MVIEWERSTUDIO_URL_PATH_PREFIX=mviewerstudio``), +les URLs deviennent : + +- Swagger UI : ``http://localhost:5007/mviewerstudio/swagger`` +- Spécification OpenAPI : ``http://localhost:5007/mviewerstudio/swagger.yaml`` + + +Configuration du debugger VS Code +--------------------------------- 1. Ouvrir le répertoire /srv/python/ dans VS Code. 2. Ouvrir le fichier .vscode/launch.json (voir la section suivante si non existant) diff --git a/docs/doc_tech/install_python.rst b/docs/doc_tech/install_python.rst index dac79c6e..6e3f75c7 100644 --- a/docs/doc_tech/install_python.rst +++ b/docs/doc_tech/install_python.rst @@ -24,7 +24,7 @@ Vous aurez besoin : sudo apt install libxslt1-dev libxml2-dev python3 python3-pip python3-venv pip install virtualenv -- d'une version Python >= 3.9 +- d'une version Python >= 3.11 - d'une instance mviewer fonctionnelle (/mviewer) Procédures d'installation @@ -150,6 +150,24 @@ Lancement de l'application avec Flask flask run -p 5007 +Documentation Swagger (API) +=========================== + +Le backend Python expose une interface Swagger UI ainsi que le fichier OpenAPI : + +- Swagger UI : ``/swagger`` (ou ``/swagger/``) +- Spécification OpenAPI : ``/swagger.yaml`` + +Exemples : + +- sans préfixe d'URL : ``http://localhost:5007/swagger`` +- avec ``MVIEWERSTUDIO_URL_PATH_PREFIX=mviewerstudio`` : ``http://localhost:5007/mviewerstudio/swagger`` + +.. note:: + Ces routes sont servies directement par Flask via ``mviewerstudio_backend/route.py``. + Le fichier de spécification est ``srv/python/mviewerstudio_backend/swagger.yaml``. + + Mise en production ****************** @@ -208,6 +226,9 @@ Ajoutez ensuite ce contenu en adaptant les valeurs (chemin, user...) selon votre fichier `mviewerstudio.service` + .. warning:: + Nous conseillons de laisser la valeur du worker à 1 (voir issue #389) + .. code-block:: sh [Unit] @@ -225,6 +246,7 @@ fichier `mviewerstudio.service` WorkingDirectory=/home/monuser/mviewerstudio/srv/python ExecStart=/home/monuser/mviewerstudio/srv/python/.venv/bin/gunicorn \ -b 127.0.0.1:5007 \ + --workers=1 \ --access-logfile /var/log/mviewerstudio/gunicorn-access.log \ --log-level info \ --error-logfile /var/log/mviewerstudio/gunicorn-error.log \ diff --git a/lib/mv.js b/lib/mv.js index 24f12657..04cd4279 100755 --- a/lib/mv.js +++ b/lib/mv.js @@ -71,7 +71,7 @@ var mv = (function () { function uuid() { var dt = new Date().getTime(); var uuid = "xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - var r = (dt + Math.random() * 16) % 16 | 0; + var r = ((dt + Math.random() * 16) % 16) | 0; dt = Math.floor(dt / 16); return (c == "x" ? r : (r & 0x3) | 0x8).toString(16); }); @@ -321,12 +321,31 @@ var mv = (function () { $("#frm-bl").append(html); $("#frm-bl-visible").append(html2); $(".bl." + classe + " input").bind("change", function (e) { + var checkbox = $(e.currentTarget); + var allChecked = $(".bl." + classe + " input:checked"); + if (!checkbox.prop("checked") && allChecked.length === 0) { + checkbox.prop("checked", true); + return; + } var id = $(this).parent().parent().attr("data-layerid"); var value = $(e.currentTarget).prop("checked"); + var select = $("#frm-bl-visible"); + var currentValue = select.val(); if (value === true) { - $("#frm-bl-visible option[value='" + id + "']").removeAttr("disabled"); + select.find("option[value='" + id + "']").removeAttr("disabled"); + if (!currentValue) { + select.val(id).trigger("change"); + } } else { - $("#frm-bl-visible option[value='" + id + "']").attr("disabled", "disabled"); + select.find("option[value='" + id + "']").attr("disabled", "disabled"); + if (id === currentValue) { + var first = select.find("option:not(:disabled)").first(); + if (first.length) { + select.val(first.val()).trigger("change"); + } else { + select.val(null).trigger("change"); + } + } } }); }, @@ -1311,7 +1330,7 @@ var mv = (function () { $("#opt-searchl-attribution").show(); $("#opt-searchl-inputlabel").show(); // TODO : need to get values from config - $("#opt-searchlocalities-url").val("https://api-adresse.data.gouv.fr/search/"); + $("#opt-searchlocalities-url").val("https://data.geopf.fr/geocodage/search/"); $("#opt-searchlocalities-attribution").val("Base adresse nationale (BAN)"); $("#opt-searchlocalities-inputlabel").val(""); $("#search-querymaponclick").show(); diff --git a/lib/ogc.js b/lib/ogc.js index ce73e85b..b443203b 100755 --- a/lib/ogc.js +++ b/lib/ogc.js @@ -241,28 +241,38 @@ var ogc = (function () { mv.showCSWResults(results); }; + var _parseXml = function (text) { + var parser = new DOMParser(); + return parser.parseFromString(text, "text/xml"); + }; + var _cswAjax = function (url, body, metadataUrl, typeNames) { - $.ajax({ - type: "POST", - url: mv.ajaxURL(url), - crossDomain: true, - data: body, - dataType: "xml", - contentType: "application/xml", - success: function (data) { - _cswParse(data, url, metadataUrl, typeNames); + fetch(mv.ajaxURL(url), { + method: "POST", + headers: { + "Content-Type": "application/xml", }, - error: function (xhr, ajaxOptions, thrownError) { + body: body, + }) + .then((response) => { + if (!response.ok) { + throw new Error( + "CSW request failed: " + response.status + " " + response.statusText + ); + } + return response.text(); + }) + .then((data) => { + _cswParse(_parseXml(data), url, metadataUrl, typeNames); + }) + .catch((error) => { console.error("CSW request failed", { url: url, body: body, - xhr: xhr, - ajaxOptions: ajaxOptions, - thrownError: thrownError, + error: error, }); alert("Echec de la requête CSW :\n" + url); - }, - }); + }); }; var _wmsCapabilitiesParse = function (data, layerid) { @@ -405,25 +415,28 @@ var ogc = (function () { ? getCapabilitiesUrl + "?" + getCapabilitiesParams : getCapabilitiesUrl + "&" + getCapabilitiesParams; - $.ajax({ - type: "GET", - url: mv.ajaxURL(getCapabilitiesUrl), - crossDomain: true, - dataType: "xml", - contentType: "application/xml", - success: function (data) { - _wmsParse(data, keyword); - }, - error: function (xhr, ajaxOptions, thrownError) { + fetch(mv.ajaxURL(getCapabilitiesUrl)) + .then((response) => { + if (!response.ok) { + throw new Error( + "WMS GetCapabilities request failed: " + + response.status + + " " + + response.statusText + ); + } + return response.text(); + }) + .then((data) => { + _wmsParse(_parseXml(data), keyword); + }) + .catch((error) => { console.error("WMS GetCapabilities request failed", { url: getCapabilitiesUrl, - xhr: xhr, - ajaxOptions: ajaxOptions, - thrownError: thrownError, + error: error, }); alert("Echec de la requête WMS GetCapabilities :\n" + getCapabilitiesUrl); - }, - }); + }); }; var _DescribeFeatureTypeParse = function (data, typeName, layerid, url) { @@ -465,12 +478,7 @@ var ogc = (function () { ? descFeatTypeUrl + "?" + descFeatTypeParams : descFeatTypeUrl + "&" + descFeatTypeParams; - return fetch(mv.ajaxURL(descFeatTypeUrl), { - mode: "cors", - headers: { - "Content-Type": "application/xml", - }, - }) + return fetch(descFeatTypeUrl) .then((response) => response.text()) .then((data) => { return _DescribeFeatureTypeParse(data, typeName, layerid, url); @@ -488,25 +496,28 @@ var ogc = (function () { ? getCapabilitiesUrl + "?" + getCapabilitiesParams : getCapabilitiesUrl + "&" + getCapabilitiesParams; - $.ajax({ - type: "GET", - url: mv.ajaxURL(getCapabilitiesUrl), - crossDomain: true, - dataType: "xml", - contentType: "application/xml", - success: function (data) { - _wmsCapabilitiesParse(data, layerid); - }, - error: function (xhr, ajaxOptions, thrownError) { + fetch(getCapabilitiesUrl) + .then((response) => { + if (!response.ok) { + throw new Error( + "WMS GetCapabilities request failed: " + + response.status + + " " + + response.statusText + ); + } + return response.clone().text(); + }) + .then((data) => { + _wmsCapabilitiesParse(_parseXml(data), layerid); + }) + .catch((error) => { console.error("WMS GetCapabilities request failed", { url: getCapabilitiesUrl, - xhr: xhr, - ajaxOptions: ajaxOptions, - thrownError: thrownError, + error: error, }); alert("Echec de la requête WMS GetCapabilities :\n" + getCapabilitiesUrl); - }, - }); + }); }; var _describeLayerParse = function (xml, layerid) { @@ -524,12 +535,7 @@ var ogc = (function () { ? descLayerUrl + "?" + descLayerParams : descLayerUrl + "&" + descLayerParams; - return fetch(mv.ajaxURL(descLayerUrl), { - mode: "cors", - headers: { - "Content-Type": "application/xml", - }, - }) + return fetch(mv.ajaxURL(descLayerUrl)) .then((response) => response.text()) .then((data) => { return { @@ -613,25 +619,28 @@ var ogc = (function () { let newUrl = ogc.urlToObject(url, queryParams); let stringUrl = newUrl.toString(); - $.ajax({ - type: "GET", - url: mv.ajaxURL(stringUrl), - crossDomain: true, - dataType: "json", - contentType: "application/json", - success: function (data) { + fetch(stringUrl) + .then((response) => { + if (!response.ok) { + throw new Error( + "WFS GetFeature request failed: " + + response.status + + " " + + response.statusText + ); + } + return response.json(); + }) + .then((data) => { onSuccess(data); - }, - error: function (xhr, ajaxOptions, thrownError) { + }) + .catch((error) => { console.error("WFS GetFeature request failed", { url: stringUrl, - xhr: xhr, - ajaxOptions: ajaxOptions, - thrownError: thrownError, + error: error, }); alert("Echec de la requête WFS GetFeature :\n" + stringUrl); - }, - }); + }); }, }; })(); diff --git a/srv/python/README.md b/srv/python/README.md index b0ebe084..b005802d 100644 --- a/srv/python/README.md +++ b/srv/python/README.md @@ -1,7 +1,7 @@ # MViewerStudio Backend v2 Ce dossier contient la version 2 du backend de mviewerstudio. Cette version est -écrite en python (3.7+) et utilise le framework Flask. Il n'y aucune base de +écrite en python (3.11+) et utilise le framework Flask. Il n'y aucune base de données, les données sont stockées dans des fichiers json. ## Installation @@ -19,4 +19,3 @@ Vous pouvez utiliser la composition docker présente à la racine du dépot. Le * Lancer les tests unitaires : `pytest mviewerstudio_backend/test.py` * Vérifier les types : `mypy --ignore-missing mviewerstudio_backend` - diff --git a/srv/python/mviewerstudio_backend/error_handlers.py b/srv/python/mviewerstudio_backend/error_handlers.py index 6bd3c257..31675e00 100644 --- a/srv/python/mviewerstudio_backend/error_handlers.py +++ b/srv/python/mviewerstudio_backend/error_handlers.py @@ -4,7 +4,6 @@ import json import logging - logger = logging.getLogger(__name__) diff --git a/srv/python/mviewerstudio_backend/register_utils.py b/srv/python/mviewerstudio_backend/register_utils.py index 0db4e4ba..15c850b8 100644 --- a/srv/python/mviewerstudio_backend/register_utils.py +++ b/srv/python/mviewerstudio_backend/register_utils.py @@ -7,7 +7,6 @@ import git - logger = logging.getLogger(__name__) diff --git a/srv/python/mviewerstudio_backend/route.py b/srv/python/mviewerstudio_backend/route.py index b87366e6..1f35f0f2 100644 --- a/srv/python/mviewerstudio_backend/route.py +++ b/srv/python/mviewerstudio_backend/route.py @@ -1,4 +1,13 @@ -from flask import Blueprint, jsonify, Response, request, current_app, redirect +from flask import ( + Blueprint, + jsonify, + Response, + request, + current_app, + redirect, + render_template_string, + send_from_directory, +) from .utils.login_utils import current_user from .utils.config_utils import ( Config, @@ -48,6 +57,43 @@ def default_doc(): return redirect("index.html") +@basic_store.route("/swagger", methods=["GET"]) +@basic_store.route("/swagger/", methods=["GET"]) +def swagger_ui() -> Response: + """ + Serve Swagger UI for backend API. + """ + return render_template_string(""" + + + + + + MviewerStudio API - Swagger + + + +
+ + + + + """) + + +@basic_store.route("/swagger.yaml", methods=["GET"]) +def swagger_spec() -> Response: + """ + Serve OpenAPI specification file. + """ + return send_from_directory(path.dirname(__file__), "swagger.yaml") + + @basic_store.route("/api/user", methods=["GET"]) def user() -> Response: """ diff --git a/srv/python/mviewerstudio_backend/swagger.yaml b/srv/python/mviewerstudio_backend/swagger.yaml index 541cefdb..e74506b9 100644 --- a/srv/python/mviewerstudio_backend/swagger.yaml +++ b/srv/python/mviewerstudio_backend/swagger.yaml @@ -1,207 +1,661 @@ -openapi: 3.0.0 +openapi: 3.0.3 info: - title: Mviewerstudio API - description: Description de l'API de [mviewerstudio](https://github.com/mviewer/mviewerstudio). + title: MviewerStudio Backend API + description: API HTTP du backend MviewerStudio. version: 1.0.0 servers: - - url: https://kartenn.region-bretagne.fr/mviewerstudio/ - description: GéoBretagne instance - - url: https://gis.jdev.fr/mviewerstudio/# - description: JDev Instance (tests, développements) + - url: / + +tags: + - name: User + - name: Apps + - name: Versions + - name: Publication + - name: Templates + - name: Files + - name: Proxy + paths: - /proxy: + /api/user: get: - summary: Dev proxy path - description: Use as proxy for OGC URL and avoid local CORS error + tags: [User] + summary: Return current authenticated user + description: Return informations from headers (sec-proxy / geOrchestra style). + responses: + '200': + description: User payload + content: + application/json: + schema: + $ref: '#/components/schemas/User' + + /api/app: + post: + tags: [Apps] + summary: Create XML configuration + description: Create a new app configuration from XML payload. + requestBody: + required: true + content: + text/xml: + schema: + type: string + example: "..." + responses: + '200': + description: App created + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOrUpdateAppResponse' + '400': + $ref: '#/components/responses/BadRequest' + put: + tags: [Apps] + summary: Update XML configuration + description: Read XML UUID and update register and local filesystem. parameters: - - name: url + - name: message in: query - description: 'Target URL' - required: true + required: false schema: type: string + description: Custom commit message. + requestBody: + required: true + content: + text/xml: + schema: + type: string + example: "..." responses: - '200': # status code - description: URL Content - '405': # status code - description: not allowed + '200': + description: App updated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/CreateOrUpdateAppResponse' + - type: object + properties: + diff: + type: string '400': - description: Bad request - url param missing - /api/user: + $ref: '#/components/responses/BadRequest' get: - summary: User informations. - description: Return informations from header (e.g georchestra header CAS informations) + tags: [Apps] + summary: List stored configurations + description: Return all mviewer configs for current user org, optionally filtered by search. + parameters: + - name: search + in: query + required: false + schema: + type: string responses: - '200': # status code - description: SUCCESS + '200': + description: List of configs content: application/json: - schema: + schema: type: array - items: - type: string - /api/app: + items: + $ref: '#/components/schemas/ConfigSummary' + + /api/app/{id}: + delete: + tags: [Apps] + summary: Delete one mviewer config + parameters: + - $ref: '#/components/parameters/AppId' + responses: + '200': + description: Delete result + content: + application/json: + schema: + type: object + properties: + deleted_files: + type: integer + success: + type: boolean + deleted_publish: + nullable: true + oneOf: + - type: boolean + - type: string + '400': + $ref: '#/components/responses/BadRequest' + '405': + $ref: '#/components/responses/MethodNotAllowed' + + /api/app/{id}/publish/{name}: post: - summary: Create new app + tags: [Publication] + summary: Publish configuration + description: Copy XML and assets from draft workspace to publication directory. + parameters: + - $ref: '#/components/parameters/AppId' + - $ref: '#/components/parameters/PublishName' + - name: instance + in: query + required: false + schema: + type: string + description: Optional instance root path used to rewrite templates URLs. requestBody: - description: Will save new app in dedicated directory + required: true content: text/xml: schema: - $ref: '#/components/schemas/Config' - required: true + type: string + responses: + '200': + description: Publish result + content: + application/json: + schema: + type: object + properties: + online_file: + type: string + draft_file: + type: string + '400': + $ref: '#/components/responses/BadRequest' + '409': + description: Conflict (already exists with inconsistent relation) + delete: + tags: [Publication] + summary: Unpublish configuration + parameters: + - $ref: '#/components/parameters/AppId' + - $ref: '#/components/parameters/PublishName' responses: '200': - description: Successful operation + description: Unpublish result content: application/json: - examples: - config: - value: - config: - creator: anonymous - date: '2023-02-23T14:18:25.727427' - id: e507378c1fe7 - keywords: a,b,c,d - subject: '' - title: My new project title - url: /e507378c1fe7/My_new_project_title.xml - versions: - - '1' - - '2' - - '3' - filepath: /e507378c1fe7/My_new_project_title.xml - success: true + schema: + type: object + properties: + online_file: + type: string + draft_file: + type: string + '400': + $ref: '#/components/responses/BadRequest' + /api/app/{id}/versions: + get: + tags: [Versions] + summary: Get all app versions + description: Gets all tags and commits for a given UUID application. + parameters: + - $ref: '#/components/parameters/AppId' + responses: + '200': + description: Versions payload + content: + application/json: + schema: + type: object + properties: + versions: + type: object + properties: + tags: + type: array + items: + $ref: '#/components/schemas/VersionTag' + commits: + type: array + items: + $ref: '#/components/schemas/VersionCommit' + config: + type: object + additionalProperties: true '400': - description: Bad request - XML missing + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/version/{version}: + put: + tags: [Versions] + summary: Switch app version + description: Switch current app workspace to a commit or tag. + parameters: + - $ref: '#/components/parameters/AppId' + - $ref: '#/components/parameters/Version' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + as_new: + type: boolean + responses: + '200': + description: Switch result content: application/json: - examples: - badRequest: - value: - name: "Bad Request" - description: "No XML found in the request body !" + schema: + type: object + properties: + success: + type: boolean + message: + type: string + detached: + type: boolean + '400': + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/version/{version}/preview: get: - summary: Return all available apps + tags: [Versions] + summary: Preview a specific version + description: Copy XML and assets of a specific version into preview directory and return preview URL. + parameters: + - $ref: '#/components/parameters/AppId' + - $ref: '#/components/parameters/Version' responses: '200': - description: successful operation + description: Preview URL + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + file: + type: string + '400': + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/preview: + post: + tags: [Versions] + summary: Preview uncommitted XML + description: Create a temporary preview XML from request body without saving changes. + parameters: + - $ref: '#/components/parameters/AppId' + requestBody: + required: true + content: + text/xml: + schema: + type: string + responses: + '200': + description: Preview file path + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + file: + type: string + + /api/app/{id}/version: + post: + tags: [Versions] + summary: Create a tag for app + parameters: + - $ref: '#/components/parameters/AppId' + responses: + '200': + description: Tag created + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + '400': + $ref: '#/components/responses/BadRequest' delete: - summary: To delete apps - description: 'Pass a list of apps ids to delete' + tags: [Versions] + summary: Delete app versions + description: Delete each app versions except master branch based on request payload. parameters: - - name: ids - in: query - description: List of apps to delete - required: true - schema: - $ref: '#/components/schemas/ArrayOfInt' + - $ref: '#/components/parameters/AppId' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [versions] + properties: + versions: + type: array + items: + type: string responses: + '200': + description: Number of deleted versions + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + deleted: + type: integer '400': - description: ids param missing + $ref: '#/components/responses/BadRequest' - /api/app/{id}/version: + /api/style: post: - summary: Create a new app version + tags: [Files] + summary: Store SLD style + description: Store SLD style content locally and return generated filepath. + requestBody: + required: true + content: + text/plain: + schema: + type: string + responses: + '200': + description: Stored style path + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + filepath: + type: string + + /api/app/{id}/template/{file_name}: + post: + tags: [Templates] + summary: Add layer template parameters: - - name: id + - $ref: '#/components/parameters/AppId' + - name: file_name in: path - description: Target app ID required: true schema: type: string + requestBody: + required: true + content: + text/plain: + schema: + type: string responses: '200': - description: successful operation - '400': - description: Bad request - Incorrect version name + description: Template stored path content: application/json: - examples: - badRequest: - value: - name: "Bad Request" - description: "This version ID does not exists !'" + schema: + type: object + properties: + success: + type: boolean + filepath: + type: string + '400': + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/template/{id_layer}: delete: - summary: Delete versions from list - description: 'Will delete all release excepted latest' + tags: [Templates] + summary: Delete layer template parameters: - - name: id + - $ref: '#/components/parameters/AppId' + - name: id_layer in: path - description: Target app ID required: true schema: type: string - - name: versions - in: query - description: List of versions to delete - required: true - schema: - $ref: '#/components/schemas/ArrayOfInt' responses: '200': - description: successful operation + description: Template removed + content: + application/json: + schema: + type: object + properties: + success: + type: boolean '400': - description: Bad request - Incorrect version name + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/exists: + get: + tags: [Apps] + summary: Check if app exists + parameters: + - $ref: '#/components/parameters/AppId' + responses: + '200': + description: Existence check content: application/json: - examples: - badRequest: - value: - name: "Bad Request" - description: "Version does not exists!" - /api/app/{id}/version/{version}: - put: - summary: Change current app version + schema: + type: object + properties: + success: + type: boolean + exists: + type: boolean + + /api/download/{id}: + get: + tags: [Files] + summary: Generate zip download link for app parameters: - - name: id - in: path - description: IDs of config to use - required: true - schema: - type: string - - name: version - in: path - description: version number to delete - required: true - schema: - type: integer + - $ref: '#/components/parameters/AppId' responses: '200': - description: successful operation - '400': - description: Bad request - Incorrect version name + description: Download link content: application/json: - examples: - badRequest: - value: - name: "Bad Request" - description: "Version does not exists!" - /api/app/all: - delete: - summary: Delete all apps metadata - description: Will clean store + schema: + type: object + properties: + success: + type: boolean + url: + type: string + name: + type: string + '400': + $ref: '#/components/responses/BadRequest' + + /proxy/: + get: + tags: [Proxy] + summary: Proxy service (GET) + description: Proxy content from a whitelisted origin. + parameters: + - $ref: '#/components/parameters/ProxyUrl' responses: '200': - description: successful operation + description: Proxied raw response + content: + '*/*': + schema: + type: string + format: binary + '400': + $ref: '#/components/responses/BadRequest' + '405': + $ref: '#/components/responses/MethodNotAllowed' + post: + tags: [Proxy] + summary: Proxy service (POST) + description: Forward XML body to a whitelisted origin. + parameters: + - $ref: '#/components/parameters/ProxyUrl' + requestBody: + required: true + content: + application/xml: + schema: + type: string + responses: + '200': + description: Proxied raw response + content: + '*/*': + schema: + type: string + format: binary + '400': + $ref: '#/components/responses/BadRequest' + '405': + $ref: '#/components/responses/MethodNotAllowed' components: + parameters: + AppId: + name: id + in: path + required: true + schema: + type: string + description: Application UUID. + Version: + name: version + in: path + required: true + schema: + type: string + description: Git tag or commit SHA. + PublishName: + name: name + in: path + required: true + schema: + type: string + description: Publication name. + ProxyUrl: + name: url + in: query + required: true + schema: + type: string + format: uri + description: Target URL to proxy. + + responses: + BadRequest: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + name: Bad Request + description: Invalid request payload + MethodNotAllowed: + description: Method not allowed + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + name: Method Not Allowed + description: Not allowed ! + schemas: - ArrayOfInt: - type: array - items: - type: integer - format: int64 - example: 1 - Config: + Error: + type: object + properties: + name: + type: string + description: + type: string + + User: + type: object + properties: + user_name: + type: string + first_name: + type: string + last_name: + type: string + organisation: + type: object + properties: + legal_name: + nullable: true + type: string + roles: + type: array + items: + type: string + + ConfigSummary: + type: object + additionalProperties: true + properties: + id: + type: string + title: + type: string + description: + type: string + publisher: + type: string + creator: + type: string + url: + type: string + link: + type: string + + CreateOrUpdateAppResponse: + type: object + properties: + success: + type: boolean + filepath: + type: string + config: + type: object + additionalProperties: true + + VersionTag: type: object - xml: - name: config properties: - mviewerstudioversion: + name: + type: string + message: + type: string + commit: + type: string + + VersionCommit: + type: object + properties: + message: + type: string + id: + type: string + author: + type: string + date: + type: string + current: + type: boolean + publication: + type: string + tag: type: string - example: 3.2 - xml: - attribute: true \ No newline at end of file diff --git a/srv/python/requirements.txt b/srv/python/requirements.txt index 0a397685..f10a22c0 100644 --- a/srv/python/requirements.txt +++ b/srv/python/requirements.txt @@ -1,5 +1,5 @@ flask>=2 -gunicorn>20,<21 +gunicorn>20 lxml>=4.9.2 GitPython>=3 requests diff --git a/srv/python/setup.py b/srv/python/setup.py index eb1c20d8..0c94bcda 100644 --- a/srv/python/setup.py +++ b/srv/python/setup.py @@ -12,11 +12,11 @@ url="https://github.com/mviewer/mviewerstudio", packages=find_packages(), install_requires=REQUIREMENTS, + python_requires=">=3.11", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.11", "Framework :: Flask", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", diff --git a/themes.json b/themes.json index e5723f70..6a2719ea 100644 --- a/themes.json +++ b/themes.json @@ -12,7 +12,7 @@ }, { "mviewer": "Démo Isochrones", - "url": "http://kartenn.region-bretagne.fr/kartoviz/demo/isochrones.xml", + "url": "http://kartenn.region-bretagne.fr/kartoviz/demo/isochroneAddon.xml", "themes": [ { "id": "isochrones", "label": "Calcul isochrones"