diff --git a/install.md b/install.md index 3aff82fc3..0c972b8ba 100644 --- a/install.md +++ b/install.md @@ -25,7 +25,7 @@ that we are using to set up Debian servers for OpenAtlas installations. ### Python 3.13 and Flask 3.1.1 sudo apt install python3 python3-flask python3-psycopg2 python3-flask-babel python3-flask-login sudo apt install python3-jwt python3-python-flask-jwt-extended python3-flaskext.wtf python3-flask-cors - sudo apt install python3-rdflib python3-shapely python3-flasgger python3-flask-restful python3-pandas + sudo apt install python3-rdflib python3-shapely python3-flask-restful python3-pandas sudo apt install python3-validators python3-email-validator python3-wand python3-svgwrite sudo apt install python3-xmltodict python3-markdown exiftran python3-bcrypt python3-dateutil p7zip-full sudo apt install python3-requests python3-bs4 python3-unidecode python3-lxml python3-unidecode python3-numpy diff --git a/install/container/Containerfile b/install/container/Containerfile index d7fe65578..10a4da907 100644 --- a/install/container/Containerfile +++ b/install/container/Containerfile @@ -6,7 +6,7 @@ RUN --mount=type=cache,target=/var/cache/apt \ apt install -y --no-install-recommends python3 python3-bcrypt python3-dateutil python3-psycopg2 python3-levenshtein python3-flask &&\ apt install -y --no-install-recommends python3-flask-babel python3-flask-login python3-flaskext.wtf python3-markdown python3-numpy &&\ apt install -y --no-install-recommends python3-pandas python3-jinja2 python3-flask-cors python3-flask-restful p7zip-full python3-flask-bcrypt &&\ - apt install -y --no-install-recommends python3-wand python3-rdflib python3-requests python3-flasgger python3-fiona python3-magic &&\ + apt install -y --no-install-recommends python3-wand python3-rdflib python3-requests python3-fiona python3-magic &&\ apt install -y --no-install-recommends apache2 libapache2-mod-wsgi-py3 brotli python3-coverage python3-pytest python3-pytest-cov exiftran &&\ apt install -y --no-install-recommends iipimage-server libvips-tools python3-email-validator python3-svgwrite python3-shapely &&\ apt install -y --no-install-recommends python3-validators python3-jwt python3-python-flask-jwt-extended python3-bs4 python3-unidecode &&\ diff --git a/install/upgrade/upgrade.md b/install/upgrade/upgrade.md index f502d6f64..8b6e9e9a1 100644 --- a/install/upgrade/upgrade.md +++ b/install/upgrade/upgrade.md @@ -26,6 +26,16 @@ their specific directories and execute: sudo python3 database_upgrade.py +### 9.4.x to 9.5.0 +New NPM packages and security updates are available: + + cd openatlas/static + npm install + +One Python library is not needed by OpenAtlas anymore and may be removed: + + apt purge python3-flasgger + ### 9.3.x to 9.4.0 9.4.0.sql is needed but will be taken care of by the database upgrade script. diff --git a/openatlas/__init__.py b/openatlas/__init__.py index 6f9056da6..28c88c26e 100644 --- a/openatlas/__init__.py +++ b/openatlas/__init__.py @@ -1,6 +1,8 @@ import datetime +import json import locale import os +import shutil from typing import Optional from flask import Flask, g, redirect, request, session, url_for @@ -72,6 +74,11 @@ def before_request() -> Response | None: if request.path.startswith('/display'): return None # Avoid overheads for file display + if request.path.startswith('/swagger') or \ + request.path.startswith('/openapi.json'): + write_openapi_instance() + return None # Avoid overheads for swagger + session['language'] = get_locale() g.admins_available = admins_available() if not g.admins_available \ @@ -98,8 +105,8 @@ def before_request() -> Response | None: app.config['UPLOAD_PATH'], app.config['TMP_PATH']] g.arche_uri_rules = None - setup_files() setup_api() + setup_files() return None @@ -128,10 +135,7 @@ def setup_files() -> None: def setup_api() -> None: - from openatlas.api.resources.openapi_util import write_openapi_instance - if request.path.startswith('/swagger'): - write_openapi_instance() - elif request.path.startswith('/api/'): + if request.path.startswith('/api/'): ip = request.environ.get('HTTP_X_REAL_IP', request.remote_addr) if not current_user.is_authenticated \ and not g.settings['api_public'] \ @@ -184,3 +188,30 @@ def check_incoming_tokens( or token_['valid_until'] < datetime.datetime.now(): return True return False + + +def write_openapi_instance() -> None: + openapi = app.config['OPENAPI_FILE'] + openapi_instance = app.config['OPENAPI_INSTANCE_FILE'] + if not openapi_instance.exists(): + shutil.copy(openapi, openapi_instance) + with openapi_instance.open(mode='r+') as i, openapi.open(mode='r') as f: + original = json.load(f) + instance = json.load(i) + if original['info']['version'] != instance['info']['version']: + shutil.copy(openapi, openapi_instance) + server = { + 'url': request.host_url + 'api/{basePath}', + 'description': f'{g.settings['site_name']} Server', + 'variables': {'basePath': {'default': '0.4', 'enum': ['0.4']}}} + modified = False + if len(instance['servers']) == 2: + instance['servers'].insert(0, server) + modified = True + elif instance['servers'][0]['description'] != server['description']: + instance['servers'][0] = server + modified = True + if modified: + i.seek(0) + json.dump(instance, i, indent=4) + i.truncate() diff --git a/openatlas/api/api.py b/openatlas/api/api.py index bde3415c2..b21e150d5 100644 --- a/openatlas/api/api.py +++ b/openatlas/api/api.py @@ -1,32 +1,13 @@ -from flasgger import Swagger -from flask import Blueprint +from flask import Blueprint, render_template, send_file from flask_cors import CORS from flask_restful import Api from openatlas import app from openatlas.api.routes import routes -app.config['SWAGGER'] = { - 'openapi': '3.0.2', - 'uiversion': 3, - "swagger_version": "2.0", - "specs": [{ - "endpoint": 'openapi_04', - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html"}, - "route": '/openapi.json', - "rule_filter": lambda rule: rule.endpoint.startswith('api_04')}], - "specs_route": "/swagger/"} - app.config['PROPAGATE_EXCEPTIONS'] = True CORS(app, resources={r"/api/*": {"origins": app.config['CORS_ALLOWANCE']}}) -openapi_file = app.config['OPENAPI_FILE'] -if app.config['OPENAPI_INSTANCE_FILE'].exists(): - openapi_file = app.config['OPENAPI_INSTANCE_FILE'] -Swagger(app, parse=False, template_file=str(openapi_file)) - blueprint = Blueprint('api', __name__, url_prefix='/api') api = Api(blueprint) for route in routes: @@ -38,3 +19,16 @@ for route in routes: api_04.add_resource(route[0], route[1], endpoint=route[2]) app.register_blueprint(blueprint_04) + + +@app.route('/openapi.json') +def get_openapi_json(): + openapi_file = app.config['OPENAPI_FILE'] + if app.config['OPENAPI_INSTANCE_FILE'].exists(): + openapi_file = app.config['OPENAPI_INSTANCE_FILE'] + return send_file(openapi_file, mimetype='application/json') + + +@app.route('/swagger') +def get_swagger_ui(): + return render_template("swagger.html") diff --git a/openatlas/api/resources/openapi_util.py b/openatlas/api/resources/openapi_util.py deleted file mode 100644 index 797590310..000000000 --- a/openatlas/api/resources/openapi_util.py +++ /dev/null @@ -1,33 +0,0 @@ -import json -import shutil - -from flask import g, request - -from openatlas import app - - -def write_openapi_instance() -> None: - openapi = app.config['OPENAPI_FILE'] - openapi_instance = app.config['OPENAPI_INSTANCE_FILE'] - if not openapi_instance.exists(): - shutil.copy(openapi, openapi_instance) - with openapi_instance.open(mode='r+') as i, openapi.open(mode='r') as f: - original = json.load(f) - instance = json.load(i) - if original['info']['version'] != instance['info']['version']: - shutil.copy(openapi, openapi_instance) - server = { - 'url': request.host_url + 'api/{basePath}', - 'description': f'{g.settings['site_name']} Server', - 'variables': {'basePath': {'default': '0.4', 'enum': ['0.4']}}} - modified = False - if len(instance['servers']) == 2: - instance['servers'].insert(0, server) - modified = True - elif instance['servers'][0]['description'] != server['description']: - instance['servers'][0] = server - modified = True - if modified: - i.seek(0) - json.dump(instance, i, indent=4) - i.truncate() diff --git a/openatlas/static/package-lock.json b/openatlas/static/package-lock.json index 598127316..786432f9b 100644 --- a/openatlas/static/package-lock.json +++ b/openatlas/static/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "static", "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@mapbox/leaflet-pip": "^1.1.0", @@ -33,6 +34,7 @@ "leaflet.markercluster": "^1.5.3", "mirador": "^4.0.0", "save-svg-as-png": "^1.4.17", + "swagger-ui-dist": "^5.32.13", "tinymce": "^8.3.2", "wellknown": "^0.5.0" } @@ -833,6 +835,12 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.3.1.tgz", "integrity": "sha512-YRCrJdhQLobGIQ8Cj1sta3nn6DrZDTSUnrIYhS2e5V590BmfVDleKoAquclAiKSBKWJwmuXTb+b4BL6rSHnahw==" }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true + }, "node_modules/@swc/helpers": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", @@ -1620,9 +1628,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.12", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", - "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -2958,6 +2966,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.32.13", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.13.tgz", + "integrity": "sha512-qQobzb3DeC2LeK0j3E8812Ef4aIq1y9flJxvZkimkqUC/w4u7wS+yCc+VakqGJLweUUBrI24effhwo8OsAvNAw==", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", diff --git a/openatlas/static/package.json b/openatlas/static/package.json index 30930ef85..1b9ce47d4 100644 --- a/openatlas/static/package.json +++ b/openatlas/static/package.json @@ -28,6 +28,7 @@ "leaflet.markercluster": "^1.5.3", "mirador": "^4.0.0", "save-svg-as-png": "^1.4.17", + "swagger-ui-dist": "^5.32.13", "tinymce": "^8.3.2", "wellknown": "^0.5.0" } diff --git a/openatlas/templates/swagger.html b/openatlas/templates/swagger.html new file mode 100644 index 000000000..baf94855d --- /dev/null +++ b/openatlas/templates/swagger.html @@ -0,0 +1,23 @@ + + + + OpenAtlas API V0.4 - Swagger UI + + + + + +
+ + + + diff --git a/pyproject.toml b/pyproject.toml index 8b2773b4f..494c3f90a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "bcrypt~=4.2.0", "beautifulsoup4~=4.13.4", "email-validator==2.2.0", - "flasgger==0.9.7.1", "flask==3.1.1", "flask-babel==4.0.0", "flask-cors==6.0.1", diff --git a/tests/base.py b/tests/base.py index bcef16242..82d7eb323 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,11 +1,13 @@ import unittest from pathlib import Path +from types import SimpleNamespace from typing import Any, Optional import psycopg2 from flask import url_for from openatlas import app +from openatlas.api.resources.api_entity import ApiEntity from openatlas.models.entity import Entity, insert as entity_insert @@ -79,6 +81,55 @@ def get_class_mapping(data: dict[str, Any], locale: str) -> bool: and data['results'][0]['icon'] and data['results'][0]['label']) + @staticmethod + def get_api_entities() -> Any: + entities = SimpleNamespace() + with app.test_request_context(): + app.preprocess_request() + for entity in ApiEntity.get_by_cidoc_classes(['all']): + match entity.name: + case 'Location of Shire': + entities.location = entity + case 'Shire': + entities.place = entity + case 'Boundary Mark': + entities.boundary_mark = entity + case 'Travel to Mordor': + entities.event = entity + case 'Exchange of the one ring': + entities.event2 = entity + case 'Economical': + entities.relation_sub = entity + case 'Austria': + entities.unit_node = entity + case 'Frodo': + entities.actor = entity + case 'Sam': + entities.actor2 = entity + case 'Home of Baggins': + entities.feature = entity + case 'The One Ring': + entities.artifact = entity + case 'Sûza': + entities.alias = entity + case 'Height': + entities.height = entity + case 'Weight': + entities.weight = entity + case 'Change of Property': + entities.change_of_property = entity + case 'File not public': + entities.file_not_public = entity + case 'File without license': + entities.file_without_licences = entity + case 'File without file': + entities.file_without_file = entity + case 'OpenAtlas logo': + entities.file = entity + case 'Public domain': + entities.open_license = entity + return entities + class ImportTestCase(TestBaseCase): def setUp(self) -> None: diff --git a/tests/test_api.py b/tests/test_api.py index bc3375053..1c7e3a9d4 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,11 +1,8 @@ -import json from pathlib import Path -from typing import Any from flask import g, url_for from openatlas import app -from openatlas.api.resources.api_entity import ApiEntity from tests.base import ApiTestCase, get_hierarchy @@ -30,75 +27,12 @@ def test_api(self) -> None: follow_redirects=True) c.get(url_for('logout')) + + e = self.get_api_entities() + with app.test_request_context(): app.preprocess_request() - - for entity in ApiEntity.get_by_cidoc_classes(['all']): - match entity.name: - case 'Location of Shire': - location = entity - case 'Shire': - place = entity - case 'Boundary Mark': - boundary_mark = entity - case 'Travel to Mordor': - event = entity - case 'Exchange of the one ring': - event2 = entity - case 'Economical': - relation_sub = entity - case 'Austria': - unit_node = entity - case 'Frodo': - actor = entity - case 'Sam': - actor2 = entity - case 'Home of Baggins': - feature = entity - case 'The One Ring': - artifact = entity - case 'Sûza': - alias = entity - case 'Height': - height = entity - case 'Weight': - weight_ = entity - case 'Change of Property': - change_of_property = entity - case 'File not public': - file_not_public = entity - case 'File without license': - file_without_licences = entity - case 'File without file': - file_without_file = entity - case 'OpenAtlas logo': - file = entity - case 'Public domain': - open_license = entity - - file.link('P2', open_license) - - # Test Swagger UI - if app.config['OPENAPI_INSTANCE_FILE'].exists(): - app.config['OPENAPI_INSTANCE_FILE'].unlink() - rv: Any = c.get(url_for('flasgger.apidocs')) - assert b'Flasgger' in rv.data - with app.config['OPENAPI_INSTANCE_FILE'].open(mode='r+') as f: - data = json.load(f) - data['servers'][0]['description'] = 'Wrong description' - f.seek(0) - json.dump(data, f) - f.truncate() - rv = c.get(url_for('flasgger.apidocs')) - assert b'Flasgger' in rv.data - with app.config['OPENAPI_INSTANCE_FILE'].open(mode='r+') as f: - data = json.load(f) - data['info']['version'] = '9.9.9' - f.seek(0) - json.dump(data, f) - f.truncate() - rv = c.get(url_for('flasgger.apidocs')) - assert b'Flasgger' in rv.data + e.file.link('P2', e.open_license) # ---Content Endpoints--- rv = c.get(url_for('api_04.classes')).get_json() @@ -127,11 +61,11 @@ def test_api(self) -> None: rv = c.get(url_for('api_04.system_class_count')).get_json() assert rv['person'] rv = c.get( - url_for('api_04.system_class_count', type_id=boundary_mark.id)) + url_for('api_04.system_class_count', type_id=e.boundary_mark.id)) assert rv.get_json()['place'] - rv = c.get(url_for('api.licensed_file_overview', file_id=file.id)) - assert rv.get_json()[str(file.id)]['license'] == 'Public domain' + rv = c.get(url_for('api.licensed_file_overview', file_id=e.file.id)) + assert rv.get_json()[str(e.file.id)]['license'] == 'Public domain' rv = c.get(url_for('api.licensed_file_overview')) assert len(rv.get_json().keys()) == 5 @@ -145,7 +79,7 @@ def test_api(self) -> None: rv = c.get( url_for( 'api_04.network_visualisation', - linked_to_ids=boundary_mark.id)) + linked_to_ids=e.boundary_mark.id)) rv = rv.get_json() assert len(rv['results']) == 16 rv = c.get(url_for('api_04.network_visualisation', download=True)) @@ -155,36 +89,36 @@ def test_api(self) -> None: rv = c.get( url_for( 'api_04.ego_network_visualisation', - id_=place.id, + id_=e.place.id, exclude_system_classes='type')) rv = rv.get_json() assert len(rv['results']) == 14 rv = c.get( url_for( 'api_04.ego_network_visualisation', - id_=height.id, + id_=e.height.id, exclude_system_classes='type')) rv = rv.get_json() assert len(rv['results']) == 0 rv = c.get( url_for( 'api_04.ego_network_visualisation', - id_=place.id, + id_=e.place.id, depth=10, - linked_to_ids=boundary_mark.id)) + linked_to_ids=e.boundary_mark.id)) rv = rv.get_json() assert len(rv['results']) == 2 rv = c.get( url_for( 'api_04.ego_network_visualisation', - id_=place.id, + id_=e.place.id, download=True)) rv = rv.get_json() assert len(rv['results']) == 18 for rv in [ - c.get(url_for('api_04.geometric_entities')), - c.get(url_for('api_04.geometric_entities', download=True))]: + c.get(url_for('api_04.geometric_entities')), + c.get(url_for('api_04.geometric_entities', download=True))]: rv = rv.get_json() assert rv['features'][0]['geometry']['coordinates'] assert rv['features'][0]['properties']['id'] @@ -205,7 +139,7 @@ def test_api(self) -> None: # ---Entity Endpoints--- # Test Entity - rv = c.get(url_for('api_04.entity', id_=place.id, download=True)) + rv = c.get(url_for('api_04.entity', id_=e.place.id, download=True)) assert 'application/json' in rv.headers.get('Content-Type') rv = rv.get_json()['features'][0] assert rv['@id'] @@ -241,7 +175,11 @@ def test_api(self) -> None: assert rv['depictions'][0]['url'] rv = c.get( - url_for('api_04.entity', id_=place.id, format='lpx', locale='de')) + url_for( + 'api_04.entity', + id_=e.place.id, + format='lpx', + locale='de')) assert 'application/json' in rv.headers.get('Content-Type') rv = rv.get_json()['features'][0] @@ -256,14 +194,15 @@ def test_api(self) -> None: 'begin_latest', 'begin_comment', 'end_earliest', 'end_latest', 'end_comment', 'types'] # Test entity in GeoJSON format - rv = c.get(url_for('api_04.entity', id_=place.id, format='geojson')) + rv = c.get(url_for('api_04.entity', id_=e.place.id, format='geojson')) assert 'application/json' in rv.headers.get('Content-Type') rv = rv.get_json()['features'][0] assert rv['geometry']['type'] assert rv['geometry']['coordinates'] for key in geojson_checklist: assert rv['properties'][key] - rv = c.get(url_for('api_04.entity', id_=place.id, format='geojson-v2')) + rv = c.get( + url_for('api_04.entity', id_=e.place.id, format='geojson-v2')) assert 'application/json' in rv.headers.get('Content-Type') rv = rv.get_json()['features'][0] assert rv['geometry']['type'] @@ -276,7 +215,7 @@ def test_api(self) -> None: assert 'Skolem URI for Linked.art.' in rv.get_json()['error'] # Test entity in Linked Open Usable Data - rv = c.get(url_for('api_04.entity', id_=place.id, format='loud')) + rv = c.get(url_for('api_04.entity', id_=e.place.id, format='loud')) assert 'application/json' in rv.headers.get('Content-Type') rv = rv.get_json() assert rv['type'] == 'Site' @@ -291,7 +230,7 @@ def test_api(self) -> None: '41.0297647897435, 28.9389559878606 41.0290525580955)),' ' POINT (16.370696110389183 48.20857123273274))') - rv = c.get(url_for('api_04.entity_uuid', uuid=place.uuid)) + rv = c.get(url_for('api_04.entity_uuid', uuid=e.place.uuid)) assert 'application/json' in rv.headers.get('Content-Type') rv = rv.get_json() assert rv['type'] == 'Site' @@ -299,11 +238,11 @@ def test_api(self) -> None: # Test Entity export and RDFS for rv in [ - c.get(url_for('api_04.entity', id_=place.id, export='csv')), + c.get(url_for('api_04.entity', id_=e.place.id, export='csv')), c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', @@ -313,11 +252,11 @@ def test_api(self) -> None: for rv in [ c.get( - url_for('api_04.entity', id_=place.id, export='csvNetwork')), + url_for('api_04.entity', id_=e.place.id, export='csvNetwork')), c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', @@ -328,16 +267,16 @@ def test_api(self) -> None: rv = c.get( url_for( 'api_04.linked_entities_by_properties_recursive', - id_=place.id, + id_=e.place.id, properties='P46')) rv = rv.get_json() - names = [place.name, feature.name, 'Bar', 'The One Ring'] + names = [e.place.name, e.feature.name, 'Bar', 'The One Ring'] for item in rv['results']: assert item['features'][0]['properties']['title'] in names rv = c.get( url_for( 'api_04.linked_entities_by_properties_recursive', - id_=place.id, + id_=e.place.id, properties='all')) rv = rv.get_json() assert rv['results'][0]['features'][0]['properties'] @@ -346,17 +285,17 @@ def test_api(self) -> None: rv = c.get( url_for( 'api_04.entity_presentation_view', - id_=place.id, + id_=e.place.id, centroid='true', place_hierarchy='true')) rv = rv.get_json() - assert rv['id'] == place.id - assert rv['systemClass'] == place.class_.name - assert rv['title'] == place.name - assert rv['description'] == place.description + assert rv['id'] == e.place.id + assert rv['systemClass'] == e.place.class_.name + assert rv['title'] == e.place.name + assert rv['description'] == e.place.description assert rv['geometries'] assert rv['when']['start']['earliest'] == "2018-01-31T00:00:00" - assert rv['types'][0]['id'] == boundary_mark.id + assert rv['types'][0]['id'] == e.boundary_mark.id assert rv['externalReferenceSystems'][0]['type'] == "closeMatch" assert rv['files'][0]['title'] == 'Picture with a License' assert rv['relations']['feature'] @@ -365,34 +304,34 @@ def test_api(self) -> None: rv = c.get( url_for( 'api_04.entity_presentation_view', - id_=feature.id, + id_=e.feature.id, place_hierarchy='true', map_overlay='true')) rv = rv.get_json() - assert rv['id'] == feature.id + assert rv['id'] == e.feature.id - rv = c.get(url_for('api_04.entity_presentation_view', id_=actor2.id)) + rv = c.get(url_for('api_04.entity_presentation_view', id_=e.actor2.id)) rv = rv.get_json() - assert rv['id'] == actor2.id - assert rv['title'] == actor2.name + assert rv['id'] == e.actor2.id + assert rv['title'] == e.actor2.name assert rv['relations']['activity'] rv = c.get( url_for( 'api_04.entity_presentation_view', - id_=event.id, + id_=e.event.id, remove_empty_values='true')) rv = rv.get_json() - assert rv['id'] == event.id - assert rv['title'] == event.name + assert rv['id'] == e.event.id + assert rv['title'] == e.event.name rv = c.get( url_for( 'api_04.entity_presentation_view', - id_=file.id)) + id_=e.file.id)) rv = rv.get_json() - assert rv['id'] == file.id - assert rv['title'] == file.name + assert rv['id'] == e.file.id + assert rv['title'] == e.file.name for rv in [ c.get(url_for('api_04.cidoc_class', class_='E21')), @@ -403,7 +342,7 @@ def test_api(self) -> None: sort='desc', column='id', relation_type='P2', - type_id=boundary_mark.id)), + type_id=e.boundary_mark.id)), c.get( url_for( 'api_04.view_class', @@ -411,37 +350,37 @@ def test_api(self) -> None: sort='desc', column='begin_from', relation_type='P2', - type_id=boundary_mark.id)), + type_id=e.boundary_mark.id)), c.get(url_for('api_04.latest', limit=2)), c.get(url_for('api_04.system_class', class_='artifact')), - c.get(url_for('api_04.entities_linked_to_entity', id_=event.id)), - c.get(url_for('api_04.type_entities', id_=boundary_mark.id)), - c.get(url_for('api_04.type_entities', id_=relation_sub.id)), - c.get(url_for('api_04.type_entities_all', id_=unit_node.id)), - c.get(url_for('api_04.type_entities_all', id_=relation_sub.id)), + c.get(url_for('api_04.entities_linked_to_entity', id_=e.event.id)), + c.get(url_for('api_04.type_entities', id_=e.boundary_mark.id)), + c.get(url_for('api_04.type_entities', id_=e.relation_sub.id)), + c.get(url_for('api_04.type_entities_all', id_=e.unit_node.id)), + c.get(url_for('api_04.type_entities_all', id_=e.relation_sub.id)), c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', sort='desc', column='cidoc_class', system_classes='person', download=True, - last=actor.id)), + last=e.actor.id)), c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', - linked_entities=place.id, + linked_entities=e.place.id, sort='desc', column='system_class', download=True, - actor=place.id))]: + actor=e.place.id))]: assert 'application/json' in rv.headers.get('Content-Type') rv = rv.get_json() assert rv['results'][0]['features'][0]['@id'] @@ -462,19 +401,19 @@ def test_api(self) -> None: rv = c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', limit=0, - first=actor2.id)).get_json() + first=e.actor2.id)).get_json() assert rv['pagination']['entities'] == 10 # Test page parameter rv = c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', @@ -495,7 +434,7 @@ def test_api(self) -> None: rv = c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', @@ -510,7 +449,7 @@ def test_api(self) -> None: c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', @@ -518,7 +457,7 @@ def test_api(self) -> None: c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', @@ -531,11 +470,11 @@ def test_api(self) -> None: for url in [ url_for( 'api_04.query', - entities=place.id, + entities=e.place.id, format='gpkg'), url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', @@ -568,7 +507,7 @@ def test_api(self) -> None: "public", "size", "begin_from", "begin_to", "end_from", "end_to", "begin", "end"] for column in columns: - checked = [place.id] if column == 'checkbox' else [] + checked = [e.place.id] if column == 'checkbox' else [] with c.get( url_for( 'api_04.table_rows', @@ -581,7 +520,7 @@ def test_api(self) -> None: rv = c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes=['E18', 'E53'], view_classes='artifact', system_classes=['person', 'type'], @@ -626,13 +565,13 @@ def test_api(self) -> None: # ---Type Endpoints--- for rv in [ - c.get(url_for('api_04.type_overview')), - c.get(url_for('api_04.type_overview', download=True))]: + c.get(url_for('api_04.type_overview')), + c.get(url_for('api_04.type_overview', download=True))]: assert 'Austria' in str(rv.get_json()) for rv in [ - c.get(url_for('api_04.type_by_view_class')), - c.get(url_for('api_04.type_by_view_class', download=True))]: + c.get(url_for('api_04.type_by_view_class')), + c.get(url_for('api_04.type_by_view_class', download=True))]: assert 'Boundary Mark' in str(rv.get_json()) rv = c.get(url_for('api_04.type_tree')) assert rv.get_json()['typeTree'] @@ -654,7 +593,7 @@ def test_api(self) -> None: "logicalOperator": "and"}]}, { "valueTypeID": [{ "operator": "lesserThanEqual", - "values": [(height.id, 1.0), (weight_.id, 1.0)], + "values": [(e.height.id, 1.0), (e.weight.id, 1.0)], "logicalOperator": "and"}]}, { "entityAliases": [{ "operator": "greaterThan", @@ -683,15 +622,15 @@ def test_api(self) -> None: (1, [{ "valueTypeID": [{ "operator": "greaterThanEqual", - "values": [(height.id, 23.0)], + "values": [(e.height.id, 23.0)], "logicalOperator": "or"}]}]), (1, [{ "valueTypeID": [{ "operator": "equal", - "values": [(height.id, 23.0)]}]}, { + "values": [(e.height.id, 23.0)]}]}, { "valueTypeID": [{ "operator": "greaterThanEqual", - "values": [(height.id, 23.0)]}]}, { + "values": [(e.height.id, 23.0)]}]}, { "typeName": [{ "operator": "equal", "values": ["Boundary Mark", "Height"], @@ -710,7 +649,7 @@ def test_api(self) -> None: "homeland"]}]}, { "valueTypeID": [{ "operator": "greaterThanEqual", - "values": [(height.id, 23.0), (weight_.id, 999.0)], + "values": [(e.height.id, 23.0), (e.weight.id, 999.0)], "logicalOperator": "and"}]}]), (5, [{ "entityCidocClass": [{ @@ -723,31 +662,31 @@ def test_api(self) -> None: "logicalOperator": "and"}]}, { "typeIDWithSubs": [{ "operator": "equal", - "values": [boundary_mark.id, height.id]}]}, { + "values": [e.boundary_mark.id, e.height.id]}]}, { "typeIDWithSubs": [{ "operator": "equal", - "values": [boundary_mark.id], + "values": [e.boundary_mark.id], "logicalOperator": "and"}]}, { "typeID": [{ "operator": "equal", - "values": [boundary_mark.id, height.id]}]}]), + "values": [e.boundary_mark.id, e.height.id]}]}]), (6, [{"entityName": [{"operator": "like", "values": ["Fr"]}]}]), (8, [{ "typeIDWithSubs": [{ "operator": "equal", - "values": [boundary_mark.id, height.id, - change_of_property.id]}]}, { + "values": [e.boundary_mark.id, e.height.id, + e.change_of_property.id]}]}, { "entityDescription": [{ "operator": "like", "values": ["FrOdO", "sam"]}]}]), (15, [{ "relationToID": [{ "operator": "equal", - "values": [place.id]}]}]), + "values": [e.place.id]}]}]), (175, [{ "typeIDWithSubs": [{ "operator": "notEqual", - "values": [boundary_mark.id], + "values": [e.boundary_mark.id], "logicalOperator": "and"}]}]), (177, [{ "typeName": [{ @@ -756,7 +695,7 @@ def test_api(self) -> None: "logicalOperator": "and"}]}, { "entityID": [{ "operator": "notEqual", - "values": [place.id], + "values": [e.place.id], "logicalOperator": "and"}]}, { "entityAliases": [{ "operator": "notEqual", @@ -776,60 +715,63 @@ def test_api(self) -> None: for rv in [ c.get( - url_for('api_04.subunits', id_=feature.id, centroid='false')), + url_for( + 'api_04.subunits', + id_=e.feature.id, + centroid='false')), c.get( - url_for('api_04.subunits', id_=place.id, download=True))]: + url_for('api_04.subunits', id_=e.place.id, download=True))]: assert 'application/json' in rv.headers.get('Content-Type') - rv = rv.get_json()[str(place.id)] + rv = rv.get_json()[str(e.place.id)] for item in rv: - if item['id'] == place.id: - assert item['id'] == place.id + if item['id'] == e.place.id: + assert item['id'] == e.place.id assert item['openatlasClassName'] == "place" - assert item['children'] == [feature.id, artifact.id] + assert item['children'] == [e.feature.id, e.artifact.id] item = item['properties'] - assert item['name'] == place.name - assert item['description'] == place.description - assert item['aliases'] == [alias.name] + assert item['name'] == e.place.name + assert item['description'] == e.place.description + assert item['aliases'] == [e.alias.name] assert item['externalReferences'] assert item['timespan'] assert item['standardType'] assert item['files'] assert item['types'] - rv = c.get(url_for('api_04.subunits', id_=place.id, count=True)) + rv = c.get(url_for('api_04.subunits', id_=e.place.id, count=True)) assert b'4' in rv.data for rv in [ - c.get(url_for('api_04.subunits', id_=place.id, format='xml')), + c.get(url_for('api_04.subunits', id_=e.place.id, format='xml')), c.get( url_for( 'api_04.subunits', - id_=place.id, + id_=e.place.id, format='xml', download=True))]: assert b'Shire' in rv.data - rv = c.get(url_for('api_04.chained_events', id_=event.id)) + rv = c.get(url_for('api_04.chained_events', id_=e.event.id)) rv = rv.get_json() - assert rv['name'] == event.name - assert rv['children'][0]['name'] == event2.name + assert rv['name'] == e.event.name + assert rv['children'][0]['name'] == e.event2.name - rv = c.get(url_for('api_04.chained_events', id_=event2.id)) + rv = c.get(url_for('api_04.chained_events', id_=e.event2.id)) rv = rv.get_json() - assert rv['name'] == event.name - assert rv['children'][0]['name'] == event2.name + assert rv['name'] == e.event.name + assert rv['children'][0]['name'] == e.event2.name # Test centroid for format_ in ['lp', 'geojson', 'geojson-v2']: rv = c.get( url_for( 'api_04.entity', - id_=feature.id, + id_=e.feature.id, format=format_, centroid='true')) assert b'(autogenerated)' in rv.data assert 'application/json' in rv.headers.get('Content-Type') rv = c.get( - url_for('api_04.subunits', id_=place.id, centroid='true')) + url_for('api_04.subunits', id_=e.place.id, centroid='true')) assert b'(autogenerated)' in rv.data assert 'application/json' in rv.headers.get('Content-Type') rv = c.get( @@ -843,13 +785,15 @@ def test_api(self) -> None: with c.get( url_for( 'api_04.display', - filename=file.id, + filename=e.file.id, image_size='table')) as rv: self.assertTrue(rv.headers['Content-Type'].startswith('image')) with c.get( - url_for('api_04.files_of_entities', entities=place.id)) as rv: - self.assertTrue(rv.get_json()[str(place.id)]) + url_for( + 'api_04.files_of_entities', + entities=e.place.id)) as rv: + self.assertTrue(rv.get_json()[str(e.place.id)]) rv = c.get(url_for('api_04.search', class_='all', term='Fro')) assert rv.get_json()['pagination']['entities'] == 2 @@ -862,28 +806,28 @@ def test_api(self) -> None: # Test Error Handling for rv in [ - c.get(url_for('api_04.entity', id_=233423424)), - c.get(url_for( - 'api_04.entity_uuid', - uuid='7b9e1c4a-5f2d-4b8a-9e3c-2d1f0a9b8c7d')), - c.get(url_for('api_04.cidoc_class', class_='E18', last=1231))]: + c.get(url_for('api_04.entity', id_=233423424)), + c.get(url_for( + 'api_04.entity_uuid', + uuid='7b9e1c4a-5f2d-4b8a-9e3c-2d1f0a9b8c7d')), + c.get(url_for('api_04.cidoc_class', class_='E18', last=1231))]: rv = rv.get_json() assert 'Entity does not exist' in rv['title'] - rv = c.get(url_for('api_04.subunits', id_=actor.id)) + rv = c.get(url_for('api_04.subunits', id_=e.actor.id)) assert 'ID is not a valid place' in rv.get_json()['title'] rv = c.get( url_for( 'api_04.query', - entities=location.id, + entities=e.location.id, cidoc_classes='E18', view_classes='artifact', system_classes='person', sort='desc', column='id', download=True, - last=place.id)) + last=e.place.id)) assert 'ID is last entity' in rv.get_json()['title'] rv = c.get(url_for('api_04.system_class', class_='Wrong')) @@ -1011,29 +955,29 @@ def test_api(self) -> None: assert 'No search value' in rv.get_json()['title'] rv = c.get( - url_for('api_04.display', filename=file_without_licences.id)) + url_for('api_04.display', filename=e.file_without_licences.id)) assert 'No license' in rv.get_json()['title'] - rv = c.get(url_for('api_04.chained_events', id_=place.id)) + rv = c.get(url_for('api_04.chained_events', id_=e.place.id)) assert 'Entity is not an event' in rv.get_json()['title'] - rv = c.get(url_for('api_04.display', filename=file_without_file.id)) + rv = c.get(url_for('api_04.display', filename=e.file_without_file.id)) assert 'File not found' in rv.get_json()['title'] - rv = c.get(url_for('api_04.iiif_manifest', version=2, id_=place.id)) + rv = c.get(url_for('api_04.iiif_manifest', version=2, id_=e.place.id)) assert 'File not found' in rv.get_json()['title'] - rv = c.get(url_for('api_04.iiif_sequence', version=2, id_=place.id)) + rv = c.get(url_for('api_04.iiif_sequence', version=2, id_=e.place.id)) assert 'File not found' in rv.get_json()['title'] - rv = c.get(url_for('api_04.display', filename=file_not_public.id)) + rv = c.get(url_for('api_04.display', filename=e.file_not_public.id)) assert 'Not public' in rv.get_json()['title'] assert b'Endpoint not found' in c.get('/api/entity2').data rv = c.get(url_for('api_04.display', filename='some_string')) assert 'Filename is not an integer' in rv.get_json()['title'] - rv = c.get(url_for('api_04.display', filename=place.id)) + rv = c.get(url_for('api_04.display', filename=e.place.id)) assert 'Entity is not a file' in rv.get_json()['title'] c.get(url_for('logout')) diff --git a/tests/test_openapi.py b/tests/test_openapi.py new file mode 100644 index 000000000..111c95fc3 --- /dev/null +++ b/tests/test_openapi.py @@ -0,0 +1,43 @@ +import json +from openatlas import app +from tests.base import TestBaseCase + + +class OpenAPI(TestBaseCase): + def test_openapi_file_generation(self): + c = self.client + instance_file = app.config['OPENAPI_INSTANCE_FILE'] + + if instance_file.exists(): + instance_file.unlink() + rv = c.get('/openapi.json') + assert rv.status_code == 200 + + with instance_file.open(mode='r+') as f: + data = json.load(f) + data['servers'][0]['description'] = 'Wrong description' + f.seek(0) + json.dump(data, f) + f.truncate() + + rv = c.get('/openapi.json') + assert rv.status_code == 200 + with instance_file.open(mode='r') as f: + data = json.load(f) + assert data['servers'][0]['description'] != 'Wrong description' + + with instance_file.open(mode='r+') as f: + data = json.load(f) + data['info']['version'] = '9.9.9' + f.seek(0) + json.dump(data, f) + f.truncate() + + rv = c.get('/openapi.json') + assert rv.status_code == 200 + with instance_file.open(mode='r') as f: + data = json.load(f) + assert data['info']['version'] != '9.9.9' + + rv = c.get('/swagger') + assert rv.status_code == 200 diff --git a/uv.lock b/uv.lock index bc10a3310..52fb2d02a 100644 --- a/uv.lock +++ b/uv.lock @@ -338,20 +338,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/14/5ef47002ef19bd5cfbc7a74b21c30ef83f22beb80609314ce0328989ceda/fiona-1.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:15751c90e29cee1e01fcfedf42ab85987e32f0b593cf98d88ed52199ef5ca623", size = 24461486, upload-time = "2024-09-16T20:15:13.399Z" }, ] -[[package]] -name = "flasgger" -version = "0.9.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "jsonschema" }, - { name = "mistune" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/e4/05e80adeadc39f171b51bd29b24a6d9838127f3aaa1b07c1501e662a8cee/flasgger-0.9.7.1.tar.gz", hash = "sha256:ca098e10bfbb12f047acc6299cc70a33851943a746e550d86e65e60d4df245fb", size = 3979409, upload-time = "2023-05-18T17:15:21.328Z" } - [[package]] name = "flask" version = "3.1.1" @@ -646,14 +632,13 @@ wheels = [ [[package]] name = "openatlas" -version = "9.3.0" +version = "9.4.0" source = { virtual = "." } dependencies = [ { name = "bcrypt" }, { name = "beautifulsoup4" }, { name = "email-validator" }, { name = "fiona" }, - { name = "flasgger" }, { name = "flask" }, { name = "flask-babel" }, { name = "flask-bcrypt" }, @@ -707,7 +692,6 @@ requires-dist = [ { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.6.0" }, { name = "email-validator", specifier = "==2.2.0" }, { name = "fiona", specifier = ">=1.10.0" }, - { name = "flasgger", specifier = "==0.9.7.1" }, { name = "flask", specifier = "==3.1.1" }, { name = "flask-babel", specifier = "==4.0.0" }, { name = "flask-bcrypt", specifier = "==1.0.1" },