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" },