From ca271b42e5caee46efa4f605a1d3136caaade4fa Mon Sep 17 00:00:00 2001 From: Jesper Zedlitz <jesper@zedlitz.de> Date: Fri, 27 Dec 2024 08:52:13 +0100 Subject: [PATCH] new tests for ODS, RDF, and WMTS --- formats/gml_format.py | 8 - formats/ods_format.py | 27 +++ formats/pdf_format.py | 4 +- formats/png_format.py | 5 +- formats/rdf_format.py | 19 ++ formats/shp_format.py | 12 +- formats/wfs_srvc_format.py | 22 ++- formats/wms_srvc_format.py | 22 ++- formats/wmts_srvc_format.py | 53 +++++ formats/zip_format.py | 11 ++ tests/data/WMTSCapabilities.xml | 96 +++++++++ tests/data/rdf.json | 273 ++++++++++++++++++++++++++ tests/data/rdf.xml | 80 ++++++++ tests/data/valid.docx | Bin 0 -> 4997 bytes tests/data/valid.ods | Bin 0 -> 9575 bytes tests/data/valid.odt | Bin 0 -> 9939 bytes tests/data/valid.xlsx | Bin 0 -> 5515 bytes tests/test_all_formats.py | 26 +++ tests/test_format_fidelity_checker.py | 58 +++--- tests/test_gml_format.py | 2 + tests/test_ods_format.py | 36 ++++ tests/test_rdf_format.py | 32 +++ tests/test_wmts_format.py | 26 +++ tests/test_xml_format.py | 20 ++ tests/test_zip_format.py | 21 ++ 25 files changed, 791 insertions(+), 62 deletions(-) create mode 100644 formats/ods_format.py create mode 100644 formats/rdf_format.py create mode 100644 formats/wmts_srvc_format.py create mode 100644 formats/zip_format.py create mode 100644 tests/data/WMTSCapabilities.xml create mode 100644 tests/data/rdf.json create mode 100644 tests/data/rdf.xml create mode 100644 tests/data/valid.docx create mode 100644 tests/data/valid.ods create mode 100644 tests/data/valid.odt create mode 100644 tests/data/valid.xlsx create mode 100644 tests/test_all_formats.py create mode 100644 tests/test_ods_format.py create mode 100644 tests/test_rdf_format.py create mode 100644 tests/test_wmts_format.py create mode 100644 tests/test_xml_format.py create mode 100644 tests/test_zip_format.py diff --git a/formats/gml_format.py b/formats/gml_format.py index c74e401..b0dc4f9 100644 --- a/formats/gml_format.py +++ b/formats/gml_format.py @@ -1,6 +1,4 @@ import geopandas -from pyogrio.errors import DataSourceError -from shapely.errors import GEOSException def is_valid(resource, file): @@ -10,12 +8,6 @@ def is_valid(resource, file): try: geopandas.read_file(f) return True - except DataSourceError as e: - resource["error"] = str(e) - return False - except GEOSException as e: - resource["error"] = str(e) - return False except Exception as e: resource["error"] = str(e) return False diff --git a/formats/ods_format.py b/formats/ods_format.py new file mode 100644 index 0000000..0ff033b --- /dev/null +++ b/formats/ods_format.py @@ -0,0 +1,27 @@ +import zipfile + + +def is_valid(resource, file): + """Check if the content is a ODS file.""" + + if not zipfile.is_zipfile(file.name): + resource["error"] = "Not a ZIP file." + return False + + with zipfile.ZipFile(file.name, "r") as zip_ref: + zip_contents = zip_ref.namelist() + + required_files = ["mimetype", "content.xml", "meta.xml", "styles.xml"] + + if not all(file in zip_contents for file in required_files): + resource["error"] = "That does not look like an ODS file." + return False + + with zip_ref.open("mimetype") as mimetype_file: + mimetype_content = mimetype_file.read().decode("utf-8").strip() + + if mimetype_content != "application/vnd.oasis.opendocument.spreadsheet": + resource["error"] = f"Incorrect MIME type: {mimetype_content}" + return False + + return True diff --git a/formats/pdf_format.py b/formats/pdf_format.py index 4a7ee69..2c7e933 100644 --- a/formats/pdf_format.py +++ b/formats/pdf_format.py @@ -1,5 +1,4 @@ from pypdf import PdfReader -from pypdf.errors import PyPdfError def is_valid(resource, file): @@ -9,5 +8,6 @@ def is_valid(resource, file): try: PdfReader(f) return True - except PyPdfError: + except Exception as e: + resource["error"] = str(e) return False diff --git a/formats/png_format.py b/formats/png_format.py index ec3a734..c7a9efb 100644 --- a/formats/png_format.py +++ b/formats/png_format.py @@ -1,4 +1,4 @@ -from PIL import Image, UnidentifiedImageError +from PIL import Image def is_valid(resource, file): @@ -7,5 +7,6 @@ def is_valid(resource, file): try: with Image.open(file.name, formats=["PNG"]): return True - except UnidentifiedImageError: + except Exception as e: + resource["error"] = str(e) return False diff --git a/formats/rdf_format.py b/formats/rdf_format.py new file mode 100644 index 0000000..27de8ee --- /dev/null +++ b/formats/rdf_format.py @@ -0,0 +1,19 @@ +from rdflib import Graph + + +def is_valid(resource, file): + """Check if file is a valid RDF document.""" + + try: + graph = Graph() + graph.parse(file.name) + + # even an empty RDF document contains two statements + if len(graph) > 2: + return True + else: + resource["error"] = "RDF document does not contain any statements." + return False + except Exception as e: + resource["error"] = str(e) + return False diff --git a/formats/shp_format.py b/formats/shp_format.py index de42333..a133299 100644 --- a/formats/shp_format.py +++ b/formats/shp_format.py @@ -1,6 +1,4 @@ import geopandas -from pyogrio.errors import DataSourceError -from shapely.errors import GEOSException import zipfile @@ -24,10 +22,7 @@ def is_valid(resource, file): with open(file.name, "rb") as f: try: geopandas.read_file(f) - except DataSourceError as e: - resource["error"] = str(e) - return False - except GEOSException as e: + except Exception as e: resource["error"] = str(e) return False return True @@ -37,10 +32,7 @@ def is_valid(resource, file): with z.open(shp) as f: try: geopandas.read_file(f"zip://{file.name}!{shp}") - except DataSourceError as e: - resource["error"] = str(e) - return False - except GEOSException as e: + except Exception as e: resource["error"] = str(e) return False return True diff --git a/formats/wfs_srvc_format.py b/formats/wfs_srvc_format.py index bdf788e..9ded4c2 100644 --- a/formats/wfs_srvc_format.py +++ b/formats/wfs_srvc_format.py @@ -12,21 +12,26 @@ def _load_into_file(url): return temp_file -def _is_capabilites_response(file): +def _is_capabilites_response(resource, file): with open(file.name, "rb") as f: try: xml = ET.parse(f).getroot() - return ( + if ( xml.tag == "{http://www.opengis.net/wfs/2.0}WFS_Capabilities" or xml.tag == "{http://www.opengis.net/wfs}WFS_Capabilities" - ) - except ET.ParseError: + ): + return True + else: + resource["error"] = "Root element is not WFS_Capabilities" + return False + except Exception as e: + resource["error"] = str(e) return False def is_valid(resource, file): - if _is_capabilites_response(file): + if _is_capabilites_response(resource, file): return True # The response is not a capabilites XML files. That is allowed. @@ -38,7 +43,12 @@ def is_valid(resource, file): url = url + "?" url = url + "service=WFS&request=GetCapabilities" - return _is_capabilites_response(_load_into_file(url)) + + try: + return _is_capabilites_response(resource, _load_into_file(url)) + except Exception as e: + resource["error"] = str(e) + return False else: # The URL already contains a getCapabilites request but the result was not a correct answer. return False diff --git a/formats/wms_srvc_format.py b/formats/wms_srvc_format.py index 7221d62..6263452 100644 --- a/formats/wms_srvc_format.py +++ b/formats/wms_srvc_format.py @@ -12,18 +12,25 @@ def _load_into_file(url): return temp_file -def _is_capabilites_response(file): +def _is_capabilites_response(resource, file): with open(file.name, "rb") as f: try: xml = ET.parse(f).getroot() - return xml.tag == "{http://www.opengis.net/wms}WMS_Capabilities" - except ET.ParseError: + if xml.tag == "{http://www.opengis.net/wms}WMS_Capabilities": + return True + else: + resource["error"] = ( + "Root element is not {http://www.opengis.net/wmts/1.0}WMS_Capabilities" + ) + return False + except Exception as e: + resource["error"] = str(e) return False def is_valid(resource, file): - if _is_capabilites_response(file): + if _is_capabilites_response(resource, file): return True # The response is not a capabilites XML files. That is allowed. @@ -35,7 +42,12 @@ def is_valid(resource, file): url = url + "?" url = url + "service=WMS&request=GetCapabilities" - return _is_capabilites_response(_load_into_file(url)) + try: + return _is_capabilites_response(resource, _load_into_file(url)) + except Exception as e: + resource["error"] = str(e) + return False + else: # The URL already contains a getCapabilites request but the result was not a correct answer. return False diff --git a/formats/wmts_srvc_format.py b/formats/wmts_srvc_format.py new file mode 100644 index 0000000..a27c093 --- /dev/null +++ b/formats/wmts_srvc_format.py @@ -0,0 +1,53 @@ +import xml.etree.ElementTree as ET +import requests +import tempfile + + +def _load_into_file(url): + response = requests.get(url) + response.raise_for_status() + + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file.write(response.content) + return temp_file + + +def _is_capabilites_response(resource, file): + with open(file.name, "rb") as f: + try: + xml = ET.parse(f).getroot() + + if xml.tag == "{http://www.opengis.net/wmts/1.0}Capabilities": + return True + else: + resource["error"] = ( + "Root element is not {http://www.opengis.net/wmts/1.0}WMS_Capabilities" + ) + return False + except Exception as e: + resource["error"] = str(e) + return False + + +def is_valid(resource, file): + if _is_capabilites_response(resource, file): + return True + + # The response is not a capabilites XML files. That is allowed. + # Let's add the request parameters to the URL and try again. + + url = resource["url"] + if "request=" not in url.lower(): + if not url.endswith("?"): + url = url + "?" + + url = url + "service=WMTS&request=GetCapabilities" + try: + return _is_capabilites_response(resource, _load_into_file(url)) + except Exception as e: + resource["error"] = str(e) + return False + + else: + # The URL already contains a getCapabilites request but the result was not a correct answer. + return False diff --git a/formats/zip_format.py b/formats/zip_format.py new file mode 100644 index 0000000..8de8b6a --- /dev/null +++ b/formats/zip_format.py @@ -0,0 +1,11 @@ +import zipfile + + +def is_valid(resource, file): + """Check if the file is a ZIP file.""" + + if not zipfile.is_zipfile(file.name): + resource["error"] = "Not a ZIP file." + return False + + return True diff --git a/tests/data/WMTSCapabilities.xml b/tests/data/WMTSCapabilities.xml new file mode 100644 index 0000000..a9d19df --- /dev/null +++ b/tests/data/WMTSCapabilities.xml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<Capabilities xmlns="http://www.opengis.net/wmts/1.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xsi:schemaLocation="http://www.opengis.net/wmts/1.0 http://schemas.opengis.net/wmts/1.0/wmtsGetCapabilities_response.xsd" version="1.0.0"> + <ows:ServiceIdentification> + <ows:Title>WMTS_SH_ALKIS</ows:Title> + <ows:Abstract>Flächendeckende Beschreibung der Angaben zu den Layern "Flurstücke", "Gebäude" sowie zu den Gruppierungen "Tatsächliche Nutzung" und "Gesetzliche Festlegungen" gemäß der entsprechenden Objektbereiche im ALKIS-Objektartenkatalog. Die Gruppierung "Weiteres" ist optional und enthält die Objektbereiche "Bauwerke und Einrichtungen" sowie "Relief". Alle ALKIS-Objekte des Grunddatenbestandes (ausser Grenzpunkte und Netzpunkte) sind Pflichtinhalte. Alle weiteren ALKIS-Objekte können optional geführt werden. Die Präsentation der ALKIS-Daten erfolgt grundsätzlich nach dem ALKIS-Signaturenkatalog für AdV-Standardausgaben. Soweit im Signaturenkatalog festgelegt, stehen für alle Layer Darstellungen in Farbe zur Verfügung. Für "Flurstücke" und "Gebäude" werden zusätzlich Darstellungen in Grausstufen (entsprechend Signaturenkatalog) und in Gelb (keine Flächendarstellung, nur Konturen) angeboten.</ows:Abstract> + <ows:Keywords> + <ows:Keyword>WMS</ows:Keyword> + <ows:Keyword>Landesamt für Vermessung ung Geoinformation Schleswig-Holstein</ows:Keyword> + <ows:Keyword>LVermGeo SH</ows:Keyword> + <ows:Keyword>AdV</ows:Keyword> + <ows:Keyword>ALKIS</ows:Keyword> + <ows:Keyword>opendata</ows:Keyword> + </ows:Keywords> + <ows:ServiceType>OGC WMTS</ows:ServiceType> + <ows:ServiceTypeVersion>1.0.0</ows:ServiceTypeVersion> + <ows:Fees>Für die Nutzung der Daten ist die Creative Commons (CC BY 4.0) – Namensnennung 4.0 International anzuwenden. Die Lizenz ist über http://creativecommons.org/licenses/by/4.0 abrufbar. Der Quellenvermerk lautet "© GeoBasis-DE/LVermGeo SH/CC BY 4.0" ||{"id":"cc-by/4.0","name":"Creative Commons Namensnennung – 4.0 International (CC BY 4.0)","url":"http://creativecommons.org/licenses/by/4.0/","quelle":"© GeoBasis-DE/LVermGeo SH/CC BY 4.0"}</ows:Fees> + <ows:AccessConstraints>NONE</ows:AccessConstraints> + </ows:ServiceIdentification> + <ows:ServiceProvider> + <ows:ProviderName>Landesamt für Vermessung und Geoinformation Schleswig-Holstein (LVermGeo SH)</ows:ProviderName> + <ows:ProviderSite xlink:href="http://www.schleswig-holstein.de/DE/Landesregierung/LVERMGEOSH/lvermgeosh_node.html"/> + <ows:ServiceContact> + <ows:IndividualName>Servicestelle Geoserver</ows:IndividualName> + <ows:PositionName></ows:PositionName> + <ows:ContactInfo> + <ows:Phone> + <ows:Voice>+49 (0)431 383-2019</ows:Voice> + <ows:Facsimile>+49 (0)431 988624-2019</ows:Facsimile> + </ows:Phone> + <ows:Address> + <ows:DeliveryPoint>Landesamt für Vermessung und Geoinformation Schleswig-Holstein (LVermGeo SH)</ows:DeliveryPoint> + <ows:City>Kiel</ows:City> + <ows:PostalCode>24106</ows:PostalCode> + <ows:Country>Germany</ows:Country> + <ows:ElectronicMailAddress>Geoserver@LVermGeo.landsh.de</ows:ElectronicMailAddress> + </ows:Address> + </ows:ContactInfo> + </ows:ServiceContact> + </ows:ServiceProvider> + <Contents> + <Layer> + <ows:Title>SH_ALKIS</ows:Title> + <ows:Abstract></ows:Abstract> + <ows:WGS84BoundingBox> + <ows:LowerCorner>0.10594674240568917 45.237542736025574</ows:LowerCorner> + <ows:UpperCorner>20.448891294525673 56.84787345153812</ows:UpperCorner> + </ows:WGS84BoundingBox> + <ows:Identifier>SH_ALKIS</ows:Identifier> + <Style> + <ows:Identifier>default</ows:Identifier> + <LegendURL + format="image/png" + xlink:href="https://dienste.gdi-sh.de//WMTS_SH_ALKIS_OpenGBD/service?service=WMS&request=GetLegendGraphic&version=1.3.0&format=image%2Fpng&layer=SH_ALKIS" + /> + </Style> + <Format>image/png</Format> + <TileMatrixSetLink> + <TileMatrixSet>DE_EPSG_25832_ADV</TileMatrixSet> + </TileMatrixSetLink> + <ResourceURL format="image/png" resourceType="tile" + template="https://dienste.gdi-sh.de//WMTS_SH_ALKIS_OpenGBD/wmts/SH_ALKIS/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png"/> + </Layer> + <TileMatrixSet> + <ows:Identifier>DE_EPSG_25832_ADV</ows:Identifier> + <ows:SupportedCRS>EPSG:25832</ows:SupportedCRS> + <TileMatrix> + <ows:Identifier>00</ows:Identifier> + <ScaleDenominator>4265.4591676995715</ScaleDenominator> + <TopLeftCorner>-46133.17 6301219.54</TopLeftCorner> + <TileWidth>256</TileWidth> + <TileHeight>256</TileHeight> + <MatrixWidth>4096</MatrixWidth> + <MatrixHeight>4096</MatrixHeight> + </TileMatrix> + <TileMatrix> + <ows:Identifier>01</ows:Identifier> + <ScaleDenominator>2132.729583849782</ScaleDenominator> + <TopLeftCorner>-46133.17 6301219.54</TopLeftCorner> + <TileWidth>256</TileWidth> + <TileHeight>256</TileHeight> + <MatrixWidth>8192</MatrixWidth> + <MatrixHeight>8192</MatrixHeight> + </TileMatrix> + <TileMatrix> + <ows:Identifier>02</ows:Identifier> + <ScaleDenominator>1066.3647919248929</ScaleDenominator> + <TopLeftCorner>-46133.17 6301219.54</TopLeftCorner> + <TileWidth>256</TileWidth> + <TileHeight>256</TileHeight> + <MatrixWidth>16384</MatrixWidth> + <MatrixHeight>16384</MatrixHeight> + </TileMatrix> + </TileMatrixSet> + </Contents> + <ServiceMetadataURL xlink:href="https://dienste.gdi-sh.de//WMTS_SH_ALKIS_OpenGBD/wmts/1.0.0/WMTSCapabilities.xml"/> +</Capabilities> \ No newline at end of file diff --git a/tests/data/rdf.json b/tests/data/rdf.json new file mode 100644 index 0000000..21a9c10 --- /dev/null +++ b/tests/data/rdf.json @@ -0,0 +1,273 @@ +[ + { + "@id": "https://example.org/dataset/87e42608-769f-4ca8-8593-7546a027b2b8", + "@type": [ + "http://www.w3.org/ns/dcat#Dataset" + ], + "http://purl.org/dc/terms/accessRights": [ + { + "@id": "http://publications.europa.eu/resource/authority/access-right/PUBLIC" + } + ], + "http://purl.org/dc/terms/description": [ + { + "@value": "Anzahl täglicher Landungen und Starts unbekannter Flugobjekte (UFOs) in Schleswig-Holstein. 🛸👽\n##Methodik\nGezählt werden nur die Landungen und Starts von UFOs, die gemeldet und zusätzlich offiziell bestätigt wurden. Sichtungen, die zu keinem Bodenkontakt führen, werden nicht gezählt.\n##Attribute\n- `datum` - Datum\n- `ufo_landungen` - Anzahl UFO-Landungen\n- `ufo_starts` - Anzahl UFO-Starts\n" + } + ], + "http://purl.org/dc/terms/identifier": [ + { + "@value": "87e42608-769f-4ca8-8593-7546a027b2b8" + } + ], + "http://purl.org/dc/terms/issued": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2024-06-18T07:20:05.693344" + } + ], + "http://purl.org/dc/terms/license": [ + { + "@id": "http://dcat-ap.de/def/licenses/cc-zero" + } + ], + "http://purl.org/dc/terms/modified": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2024-06-18T07:20:05.693344" + } + ], + "http://purl.org/dc/terms/publisher": [ + { + "@id": "https://example.org/organization/ufo-kontrolle" + } + ], + "http://purl.org/dc/terms/spatial": [ + { + "@id": "http://dcat-ap.de/def/politicalGeocoding/stateKey/01" + } + ], + "http://purl.org/dc/terms/temporal": [ + { + "@id": "_:n1fa3c2476143497285348e0c39705837b1" + } + ], + "http://purl.org/dc/terms/title": [ + { + "@value": "Bestätigte UFO-Landungen und -Starts" + } + ], + "http://www.w3.org/ns/dcat#distribution": [ + { + "@id": "_:n1fa3c2476143497285348e0c39705837b4" + }, + { + "@id": "_:n1fa3c2476143497285348e0c39705837b2" + } + ], + "http://www.w3.org/ns/dcat#keyword": [ + { + "@value": "Weltall" + }, + { + "@value": "Start" + }, + { + "@value": "Landung" + }, + { + "@value": "Raumschiff" + }, + { + "@value": "UFO" + }, + { + "@value": "Testdaten" + } + ], + "http://www.w3.org/ns/dcat#theme": [ + { + "@id": "http://publications.europa.eu/resource/authority/data-theme/INTL" + } + ] + }, + { + "@id": "_:n1fa3c2476143497285348e0c39705837b4", + "@type": [ + "http://www.w3.org/ns/dcat#Distribution" + ], + "http://purl.org/dc/terms/format": [ + { + "@id": "http://publications.europa.eu/resource/authority/file-type/JSON" + } + ], + "http://purl.org/dc/terms/issued": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2024-06-18T05:20:07.232559" + } + ], + "http://purl.org/dc/terms/license": [ + { + "@id": "http://dcat-ap.de/def/licenses/cc-zero" + } + ], + "http://purl.org/dc/terms/modified": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2024-06-18T05:20:07.191976" + } + ], + "http://purl.org/dc/terms/rights": [ + { + "@id": "http://dcat-ap.de/def/licenses/cc-zero" + } + ], + "http://purl.org/dc/terms/title": [ + { + "@value": "Frictionless Data Resource" + } + ], + "http://spdx.org/rdf/terms#checksum": [ + { + "@id": "_:n1fa3c2476143497285348e0c39705837b5" + } + ], + "http://www.w3.org/ns/dcat#accessURL": [ + { + "@id": "http://localhost:8000/ufo-resource.json" + } + ], + "http://www.w3.org/ns/dcat#byteSize": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#integer", + "@value": 487 + } + ], + "http://www.w3.org/ns/dcat#downloadURL": [ + { + "@id": "http://localhost:8000/ufo-resource.json" + } + ], + "http://www.w3.org/ns/dcat#mediaType": [ + { + "@id": "https://www.iana.org/assignments/media-types/application/csv" + } + ] + }, + { + "@id": "_:n1fa3c2476143497285348e0c39705837b5", + "@type": [ + "http://spdx.org/rdf/terms#Checksum" + ], + "http://spdx.org/rdf/terms#algorithm": [ + { + "@id": "http://spdx.org/rdf/terms#checksumAlgorithm_md5" + } + ], + "http://spdx.org/rdf/terms#checksumValue": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#hexBinary", + "@value": "8dca8b179bbe0d46c5004da5112f6c4c" + } + ] + }, + { + "@id": "_:n1fa3c2476143497285348e0c39705837b2", + "@type": [ + "http://www.w3.org/ns/dcat#Distribution" + ], + "http://purl.org/dc/terms/format": [ + { + "@id": "http://publications.europa.eu/resource/authority/file-type/CSV" + } + ], + "http://purl.org/dc/terms/issued": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2024-06-18T05:20:07.232559" + } + ], + "http://purl.org/dc/terms/license": [ + { + "@id": "http://dcat-ap.de/def/licenses/cc-zero" + } + ], + "http://purl.org/dc/terms/modified": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2024-06-18T05:20:07.191976" + } + ], + "http://purl.org/dc/terms/rights": [ + { + "@id": "http://dcat-ap.de/def/licenses/cc-zero" + } + ], + "http://purl.org/dc/terms/title": [ + { + "@value": "ufo.csv" + } + ], + "http://spdx.org/rdf/terms#checksum": [ + { + "@id": "_:n1fa3c2476143497285348e0c39705837b3" + } + ], + "http://www.w3.org/ns/dcat#accessURL": [ + { + "@id": "http://localhost:8000/ufo.csv" + } + ], + "http://www.w3.org/ns/dcat#byteSize": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#integer", + "@value": 151 + } + ], + "http://www.w3.org/ns/dcat#downloadURL": [ + { + "@id": "http://localhost:8000/ufo.csv" + } + ], + "http://www.w3.org/ns/dcat#mediaType": [ + { + "@id": "https://www.iana.org/assignments/media-types/application/csv" + } + ] + }, + { + "@id": "_:n1fa3c2476143497285348e0c39705837b3", + "@type": [ + "http://spdx.org/rdf/terms#Checksum" + ], + "http://spdx.org/rdf/terms#algorithm": [ + { + "@id": "http://spdx.org/rdf/terms#checksumAlgorithm_sha1" + } + ], + "http://spdx.org/rdf/terms#checksumValue": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#hexBinary", + "@value": "3ffba0a43d3497a7918b376a335c31fbecc9325b" + } + ] + }, + { + "@id": "_:n1fa3c2476143497285348e0c39705837b1", + "@type": [ + "http://purl.org/dc/terms/PeriodOfTime" + ], + "http://www.w3.org/ns/dcat#endDate": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#date", + "@value": "2024-06-17" + } + ], + "http://www.w3.org/ns/dcat#startDate": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#date", + "@value": "2024-06-10" + } + ] + } +] \ No newline at end of file diff --git a/tests/data/rdf.xml b/tests/data/rdf.xml new file mode 100644 index 0000000..3e6690e --- /dev/null +++ b/tests/data/rdf.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="utf-8"?> +<rdf:RDF + xmlns:dcat="http://www.w3.org/ns/dcat#" + xmlns:dcterms="http://purl.org/dc/terms/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:spdx="http://spdx.org/rdf/terms#" +> + <rdf:Description rdf:about="https://example.org/dataset/87e42608-769f-4ca8-8593-7546a027b2b8"> + <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Dataset"/> + <dcterms:accessRights rdf:resource="http://publications.europa.eu/resource/authority/access-right/PUBLIC"/> + <dcterms:description>Anzahl täglicher Landungen und Starts unbekannter Flugobjekte (UFOs) in Schleswig-Holstein. 🛸👽 +##Methodik +Gezählt werden nur die Landungen und Starts von UFOs, die gemeldet und zusätzlich offiziell bestätigt wurden. Sichtungen, die zu keinem Bodenkontakt führen, werden nicht gezählt. +##Attribute +- `datum` - Datum +- `ufo_landungen` - Anzahl UFO-Landungen +- `ufo_starts` - Anzahl UFO-Starts +</dcterms:description> + <dcterms:identifier>87e42608-769f-4ca8-8593-7546a027b2b8</dcterms:identifier> + <dcterms:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T07:20:05.693344</dcterms:issued> + <dcterms:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T07:20:05.693344</dcterms:modified> + <dcterms:license rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/> + <dcterms:publisher rdf:resource="https://example.org/organization/ufo-kontrolle"/> + <dcterms:spatial rdf:resource="http://dcat-ap.de/def/politicalGeocoding/stateKey/01"/> + <dcterms:temporal rdf:nodeID="n6747fd43db2143cca14c39970555b181b1"/> + <dcterms:title>Bestätigte UFO-Landungen und -Starts</dcterms:title> + <dcat:distribution rdf:nodeID="n6747fd43db2143cca14c39970555b181b2"/> + <dcat:distribution rdf:nodeID="n6747fd43db2143cca14c39970555b181b4"/> + <dcat:keyword>UFO</dcat:keyword> + <dcat:keyword>Landung</dcat:keyword> + <dcat:keyword>Start</dcat:keyword> + <dcat:keyword>Raumschiff</dcat:keyword> + <dcat:keyword>Weltall</dcat:keyword> + <dcat:keyword>Testdaten</dcat:keyword> + <dcat:theme rdf:resource="http://publications.europa.eu/resource/authority/data-theme/INTL"/> + </rdf:Description> + <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b4"> + <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/> + <dcterms:format rdf:resource="http://publications.europa.eu/resource/authority/file-type/JSON"/> + <dcterms:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T05:20:07.232559</dcterms:issued> + <dcterms:license rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/> + <dcterms:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T05:20:07.191976</dcterms:modified> + <dcterms:rights rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/> + <dcterms:title>Frictionless Data Resource</dcterms:title> + <spdx:checksum rdf:nodeID="n6747fd43db2143cca14c39970555b181b5"/> + <dcat:accessURL rdf:resource="http://localhost:8000/ufo-resource.json"/> + <dcat:byteSize rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">487</dcat:byteSize> + <dcat:downloadURL rdf:resource="http://localhost:8000/ufo-resource.json"/> + <dcat:mediaType rdf:resource="https://www.iana.org/assignments/media-types/application/csv"/> + </rdf:Description> + <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b2"> + <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/> + <dcterms:format rdf:resource="http://publications.europa.eu/resource/authority/file-type/CSV"/> + <dcterms:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T05:20:07.232559</dcterms:issued> + <dcterms:license rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/> + <dcterms:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T05:20:07.191976</dcterms:modified> + <dcterms:rights rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/> + <dcterms:title>ufo.csv</dcterms:title> + <spdx:checksum rdf:nodeID="n6747fd43db2143cca14c39970555b181b3"/> + <dcat:accessURL rdf:resource="http://localhost:8000/ufo.csv"/> + <dcat:byteSize rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">151</dcat:byteSize> + <dcat:downloadURL rdf:resource="http://localhost:8000/ufo.csv"/> + <dcat:mediaType rdf:resource="https://www.iana.org/assignments/media-types/application/csv"/> + </rdf:Description> + <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b5"> + <rdf:type rdf:resource="http://spdx.org/rdf/terms#Checksum"/> + <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md5"/> + <spdx:checksumValue rdf:datatype="http://www.w3.org/2001/XMLSchema#hexBinary">8dca8b179bbe0d46c5004da5112f6c4c</spdx:checksumValue> + </rdf:Description> + <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b3"> + <rdf:type rdf:resource="http://spdx.org/rdf/terms#Checksum"/> + <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/> + <spdx:checksumValue rdf:datatype="http://www.w3.org/2001/XMLSchema#hexBinary">3ffba0a43d3497a7918b376a335c31fbecc9325b</spdx:checksumValue> + </rdf:Description> + <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b1"> + <rdf:type rdf:resource="http://purl.org/dc/terms/PeriodOfTime"/> + <dcat:endDate rdf:datatype="http://www.w3.org/2001/XMLSchema#date">2024-06-17</dcat:endDate> + <dcat:startDate rdf:datatype="http://www.w3.org/2001/XMLSchema#date">2024-06-10</dcat:startDate> + </rdf:Description> +</rdf:RDF> diff --git a/tests/data/valid.docx b/tests/data/valid.docx new file mode 100644 index 0000000000000000000000000000000000000000..2fc6b99e13b6734528592b091b1fa76f3dbd8b2c GIT binary patch literal 4997 zcmWIWW@Zs#;Nak3@Ufj8$$$j785kJii&Arn_4PpH+DX3N%#J*5@BfNAzq^~G-ErdF zHzlWhrLXKQGvB<saE{$LbM60oj$FF}x)vPYQ~CLW%%$9O{}%Yo>F#zsyeiYN(BR@O zgRF|%w)*l3m-Tmr+fBIQH8VzQ)xDNQi9(+J9Y=lMtTZt!TrpcQ%|Gb4O_I{)<r53< zZu>r?-e|#m!>iY_PDJl!+WOn$kGWZu*d~=0wY0cpG3z&5=!yDOrd&)|T)DFFXO_uS zo{HFvztsyZc+PzYHgP}ixUTYJq(#KG?Nc?DM!S{&&os%eIlbq2&c(P#-DejazA61> zet<VS$BPS$$~PGp7;Kps7;uM(AOizKN`7)cQGP+OesX?Ms$NBI&eWhl|3d}>b>G8v zYUS^;Id1NmsPQR5#ZYkZ!?oMkL$<HW<Y)U=wVPLqOMl<pyWhX={=IH`Z+WU<&cTSt zm4cnsf}9Q?y*#!`O`QJIWRGH_>oOK?Emq}C?B2(Ih8`<DrOoWW>{3EN6Yrb}n>4R0 zZ*se$xpdn4g(=&fOPp#veQF9%Y*tu^@xPN{4wE&cE(uNGZ0j*=TKKK&iGt>i=~twK zw=gvYYb;=Copx;RjDH1!KAHM<854!mGz-^1RQk$M_3fOO|C-mk!oNqanr*)<fAjI@ z*RnK1H*4{!oabv^a)-fLpi$UkcbWT>t@Y+J{XOTooK)zUY_M~lyZK86&&3aw7T4c1 zZ;FkX`G;{$bl?VtK#xCJJLIx5m~_m87XQAuhqYcbCeC2%tsP&VGaWiv>-}-*vt;hH z!e@R7nvQqgoxe5Lc+c%m0l)k2Rum+C*SK&lF+qFw?lX_Kt@M}XFZ=wDQO3I2uF%j+ zXMwi-;my%!_cMYb>t^EBhk1+)4BzoamH={OB^DHb;%e^1vw4RM1X|wLcAa?SHD`&5 z(+R^@{Z&p{LBTJ0cb=QH!z<@>oWP!1Ud_yNUo+FwzpUMxb~^aFn|nc=Wb6bJKM$6u zSza%9Dab#4UE-(I?-ahsREn#J&41dTzO{2VJ_u;+XL9`AWYSUQxN<sg$)V#1uN}N8 zeVvo%($y<(zIz?&p4iT3a!JRth)bO%*3on;^Mo%)jtc(gUfZ+h&iVXPn|W={dA-Qd z7O+@cpPCu4W$CL1^@X+^-!gJnFFCM5a9>b7-@SFp8D`y^E&4Z~^m=(X^SkBw@}D|V zs$~y(?<-6^w<+aQNkFdYSqa(4dT)I9=ghM^;%RaHz}n;27O|ZVcKh9w+q>FkfBF{v z8^y|VCP+N8IV&l9a^lv|8+)P^^_Tp#Fflr$qMoIm^VvEtz>z2ZU_ApUxa;(k{u?qf zFeu;+ZW#s!hVuNP6n#jMk&>TWnwy$e0t$6V@ey>I>yUxKp5LO0@6FbBb(nlrY6@Q| zFL3!S^KBKC)i3T#hiqx;XqQVjvo`iIi;dkiZSKXN$1)f_MVOt}`dobBb-?a^kNAdD zZZG+wJs)<imwp|2<6@kHZ_4yJ)uGAiwJAEU3^aRRtFXS%%3Er6itDFJ-}5_PO%;A$ zNV)CQa9fOX!=J2=^0~p&dJk<4T+U}&yYa!ib-KS6XvDUvJZN0JfcekvzbPKSFJ+v1 zy6|XullWKdBPAF3CTRRg-QTMIS^H4V2@a96YS}vvn|<a<x@rEm*zkr86gp)ot8%X} zFfdHV8#<tp0~|WY;j%R3blz<Pfj!@~S?=X}dP?nZda_`}#YOFGY;S59Tmyxq_ib3R zW=H(}rC*DL7ZgnD)_8aB+&LZByLauiC1ouv5=;Cpr8+6Co2l8luK3fA|N9~)o~T^q zt=0R?(z3|?-SzqJcC9g46B_fILvqV$AHS#zGn0A4ujo!mNSL$oPm|=-nX80)__-yu z6x4gan{_{4n31V-ea31YjaP--rFW+1w8ec4opk5e0oAW7uA3g(HSz6-{r}ii-DAo< zub*?sJZSJvhR^BbzV(Zo{7o{88Xq1o4!z-RHQizN=4t=AZhkpA^Wxq`b5e8yj~VP0 z=Ki$wB=^2pXWjWXU-fVN%W(PDMBk0xceJ?t*YXN)u2|GM;gG!lvGl-~XOD;MopbZ| zk8}IxfBAfyW1Sk)(M|KWnmKTHnsZKdIT+W{lhP}`%HXNtnr~lj9Ll`;bMceF>9Q-% zoOG6$tMZXk*URwOWV<i3dl^q^-#@PK;mBvJ4~JIAO#k`y+3oAKM^+r)9q0a{*S2-b zd&k;J2CH*_0*bawwm3Wg@3d=s-e<qQD}UxeuJ>jAhc>&{9Q=O!>enomKZ_bJDwsB` z%V%P9xjQHK^|flgg9$61Z<%9u?}lWUr14>{$9n^vi!#1mSXHaPHOM2rNNt&8+xtT9 zWBqnoT6Yr;EneEou%)|xcILJX`c78!*aiFB#n10Q`ZXSucIM8FKlYcAfgzd|U#-Ir z%UQ)Gl{u-!pd58J<aW?)1A*H2;T2!xWF*>*BzY#!GFqB?$!}rp{S&VvnsruA^5M9D zY5&Xq(x*2Pin2XsX2vJI&nqkaHYa)RoV(MFb~(Rp3FEwd|Mg}L(d#n<Cb!h?-uXgy z^NGp;NsqM#iN+;w-h4TGvgE-ZpGDm(;&&c2No@U|CUCq@Jfi9LWj4{PMY-M|L~eQ7 zTs1QeD=z6fHr>eX#rOV4LM=&eXZ$_9)N$#9FzdO$ZP%TSc)s&NME|01f9w8+ohoxU zxt%enY42K}{%4XOl601Q*eu1<?Pa%Pjz(^xPG<C>!+x39eLfeTSvcul>xH_FzBgRH zK6cW6zvv}{jJQ%_cv8=_W7(pyi$yIL2N{Os`Enloxlnk*hKb**=Jj9dpTFp}$&{Vh z+zVn#`pOq1FokWfm{Z#wc5(hY2emf|^NRTUI}e*p-gR^ii)m)D<hOJ8?b|;Ee7O|p z(d+8AgZtucnbu=FZd%1XIi%zE&`@;dyk$rFt`_wzF*-JhyKU-q`LtyYUpda)=9(?+ zQ|wfpwr7cJuJfyJ#?DQW8w@rkbTRDGi%n~^xD<EIOG{&;?P|~K6Y65F{Od43$fvSp z?_a$W{|_<8c;`k}IGp)coPFg_{%XOEj`2FWZ>E?0`noxIjy5CT=JQ!oCvd&5on60t z^G)8P*{b_oO&0q(zqUJ++R#0#!SK@6#>VM&3_LsCz0IE<JT2?6YOSF|aO4&H6MNSF zkqzt*@2L7?e#_?5juk!{F8)0E6P6~v><rTssatzUHM)B1Nr6k--PaucWqgi5yl~&^ zpzuSw`XTD!Z$I7fn!7r4>bKcbZi(h`sa2$M7S8@ZnL+!`{kX&7E`L^N9d_S#?DxU9 z`XBzKE${l7|7zR5p0cNJtZ%W4><Kue`|;Y^y;9}tQ#-$%o%;`5Chj_>@?|y?1H(d2 zeCb*UmafzC^GZSzlXAe#skH%)euoVN_I&0#UH)ndd&{C_qC$^asunzDj84BCvFZB6 z>ndNq@9@)BJh5=Wy*G1j&b%49&VH__Et}aqEz9|$K?<4i(pDj#YQGo#*I;#DAhkw) z%@e`Mb>Zcw7v|Wwie=8-A)|Hk*o~=P8}lwQp08=2vf&szyJ78NM_#)tu^*&nPTsXS zD^P_0RHI_$G|uZMek?PTn!op7fk&YTXNKZ3?j5GGr>|PhmGEMk?)1LLSxj2y$n0{5 znG2#hder^+w@q?Y486yk{^p|*@636P=Unc_J+ozDuI^U%=uO-2v)z6B_p(dbh1u(u zyUg4+%ch(C@E`xx;hT?7I4u^<v+U%$gvV+N#plkiUA;2pbM*8Nwvqz=tkTuH?9R>a zva+xE$_gqp4j#YBA<M|X5Xy`%aN!kBacW6PW?nkD*hrmVn|IiNr}g{1Ll5Fkb9Vic z*tl6V)=JfuLHY7V25%7+tGudTMy5>scQxwzYie%2-TyZtMkDq3!baanN5_Lj&kyZv zT>tp@V{MU0gA$9P1#Ii4B;L1s^UP{;$Eu=*(oXB1v$0K#ke>Y3L9Y1Z#HLO2Cc9ru zx0*dOSX?;zRHXagjBTM`b8L1otX=mf@!QcDcduzpFD(5<XGp(TaXfR1@;tsnfvVy% z(iJQ3iS<nWaNLwnB*5hOT<=*Kf)U>YQ`hpHeyhdv!u0B;Qgw5yopnX-3o^Xk#jCx~ zd30v)6sx2i@8>SvA5vg`Hz$8u+E<g;;am5E!fP|jh0h-u7#Q*y@r9QtEHRd3q~@mT zgGobBQG0i)Z~koqp0@YDMVsEOezb%0l7gS14tJTx2Ip;eC1-BC>aj_2<=ap1m!I%^ zw0_}XgM=3~ogTI|u3bHQcl}?eQt6iLx>8u`u8)S}+0!8}Y!=?%mv>)ZV<TS)*QP5o zE+|J;?XTP3v{XAl@#Di=dWs8Mi?2>HJiS-?jc??_Z`F~RA{P0x<}784+p}g;j@8UF zGnp1<3g_3U?>l33urT<Odh4yLN^GhY^LOdGq-tJVP}3LB<Z=DEioK_e>y63H-kTpQ zex6k;P~E(?;fuiBx-}=i%P&<tuOMv2$)EaKbT`8#-kmx&yw@3CCjOdrZ_735BHO(T z4A1vXd3f_8(<9G28+2F1^*s?d|9-*g?Oz@<@)!2l{{3<4KHrbl<<-Bohu4`s_<2`$ z{~b^6RntEU)=9bEc*q>$v5<eUd0+akrj(_BY`V^;3F>^~ty>zAUGu8qt<~Wxvy@r; zm)@*WFu8tyh1$D_M7BG{@;wZmo~knX+ZMFg{1VZ8$STo&&ir$UiH=~Dp>$2ld;b*n zTl?frzMb#7DejTi3-hR>riIt?LNaD&NjsgIYwcCDVWCH~*TlTN#uALXdt5_*sPcAA zpL^>S%im&FGac_&>s;f6toqZJuWmlIKeHzPZ%}Ba)7o>Ij5B|qXDnCaDNbE;yFvGr z$RW?t_9+D!v%XKRn|*3cz2%J56L<A(!Y{wSs<e1_y?k2o%(tK4{<qx!_fW+1{L^gm z-!`ZJ6$T}+ZyP=~$uTi7T;#x)z=RnX7^0o?^GZ_lO5#H*3sQ??LCI_Hl+&Q%&h`7> zt`pHycBD26c->;{S1k#=cH+k5D+2Z@yO;j0KIR#+P|KuM%xvPMnbqg#dOoVz7FfE8 z?fKG+tTR)RCpA2C^N1CFQu%R0+~k`jQ}nlq)imwBC7vSLw0Gf^&#D^FjFTOcmzwx( zY}meT(X@}{VT^la`qyiiTQ%@ycF3j7y6<>0tkmP3`Hvk>7tK-Go*VBX+7cP<eU52b z=9)`!bDM9;3#{9E!+Uz!Nuv-m+d9p)`+v+!DBa3FLCa5U{&($rI$ykJF=fpwxa-H| z@$5{i`?3v<`;K^Kls&8b<#Fi4PUCBjvkNE4#{FT83t2AKdw`YmxZuTK&mDrNO=7NE zBjRP$(JiLXmr^gGzU%2boeh8O#qKWH@8mzrde_4fGJB543#;D<6`5RR(9VBb>d(*M zE&G~(2u<Yv7WMy8v$WTS&2sk`TN-BVzv1-UMndsHC+FMJqN1K-%bfd)ZpmD{><@~e zpC^hP6B!v8E`r-xj7%a7h!GFuo<C^B18D#Rb^Ig18&xB6FBDYTA~XsyVi_ht*N)t) z0(Dgp+V?XdX@_=P(RCyDIzT-ygl;_+{QWR=laSk>sNV2lg_?xiXF=DFT-AdbstAX4 zutT*Ynyu)Xk*hLLYXG4+feWe`rEP$&AGv5pb^Bg!sD9+?4_!NQ#s!uC2<zYSK(&MG o0i@ah-3a8A1<K(FBmDTW=Jx<^RyL3#E(R`!dL{;j58${504KvmA^-pY literal 0 HcmV?d00001 diff --git a/tests/data/valid.ods b/tests/data/valid.ods new file mode 100644 index 0000000000000000000000000000000000000000..3726fbeac976ef3a6a7399dd2db5682b88c513b4 GIT binary patch literal 9575 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 Y#dLr-D;r3eAcG)7DhmU{8wC�H2vNrvLx| literal 0 HcmV?d00001 diff --git a/tests/data/valid.odt b/tests/data/valid.odt new file mode 100644 index 0000000000000000000000000000000000000000..f4802adff3a9191037421a3ccd64151e95d8fe1f GIT binary patch literal 9939 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 F@c=a-Bz^z@ literal 0 HcmV?d00001 diff --git a/tests/data/valid.xlsx b/tests/data/valid.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5f1a3bcdf0411c4b605d5fa51708daf42dc48ae7 GIT binary patch literal 5515 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 XAK=Z(2C{;Sfs3JmiGks(5Qql=R2NH5 literal 0 HcmV?d00001 diff --git a/tests/test_all_formats.py b/tests/test_all_formats.py new file mode 100644 index 0000000..b2b8fd9 --- /dev/null +++ b/tests/test_all_formats.py @@ -0,0 +1,26 @@ +import unittest +import importlib +import pkgutil +import tempfile + + +class TestAllFormats(unittest.TestCase): + def test_load_all_modules(self): + """Make sure that every format module has been loaded at least once. + Otherwise, the code coverage will not know about the file.""" + package = importlib.import_module("formats") + modules = [module.name for module in pkgutil.iter_modules(package.__path__)] + for module in modules: + format_check_module = importlib.import_module("formats." + module) + with tempfile.NamedTemporaryFile(delete=True) as temp_file: + resource = {} + resource["url"] = "https://test.invalid/data" + try: + format_check_module.is_valid(resource, temp_file) + except Exception as e: + print(f"Module for format {module} failed.") + raise (e) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_format_fidelity_checker.py b/tests/test_format_fidelity_checker.py index 6c21860..f5798a7 100644 --- a/tests/test_format_fidelity_checker.py +++ b/tests/test_format_fidelity_checker.py @@ -11,10 +11,9 @@ from rdflib.namespace import RDF, DCAT class TestDcatCatalogCheck(unittest.TestCase): def setUp(self): - self.dcc = DcatCatalogCheck( - "http://localhost:8000/", "my_api_key") + self.dcc = DcatCatalogCheck("http://test.invalid:8000/", "my_api_key") # Mock the logger to capture log messages - self.logger_patch = patch.object(self.dcc, 'logger', MagicMock()) + self.logger_patch = patch.object(self.dcc, "logger", MagicMock()) self.mock_logger = self.logger_patch.start() def tearDown(self): @@ -30,13 +29,10 @@ class TestDcatCatalogCheck(unittest.TestCase): "XML": ["application/xml"], } - self.assertTrue(self.dcc.is_mime_type_compatible( - "JSON", "application/json")) - self.assertFalse(self.dcc.is_mime_type_compatible( - "JSON", "application/xml")) + self.assertTrue(self.dcc.is_mime_type_compatible("JSON", "application/json")) + self.assertFalse(self.dcc.is_mime_type_compatible("JSON", "application/xml")) self.assertFalse( - self.dcc.is_mime_type_compatible( - "UnknownFormat", "application/json") + self.dcc.is_mime_type_compatible("UnknownFormat", "application/json") ) def test_read_allowed_file_formats(self): @@ -48,8 +44,7 @@ class TestDcatCatalogCheck(unittest.TestCase): ): formats = self.dcc.read_allowed_file_formats() self.assertEqual( - formats, {"JSON": ["application/json"], - "XML": ["application/xml"]} + formats, {"JSON": ["application/json"], "XML": ["application/xml"]} ) def test_load_uri_replacements(self): @@ -59,10 +54,8 @@ class TestDcatCatalogCheck(unittest.TestCase): read_data='[{"regex": "old", "replaced_by": "new"}]' ), ): - replacements = self.dcc.load_uri_replacements() - self.assertEqual( - replacements, [{"regex": "old", "replaced_by": "new"}]) + self.assertEqual(replacements, [{"regex": "old", "replaced_by": "new"}]) # Simulate that the file does not exist @@ -111,7 +104,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "JSON" self.dcc.check_resource(resource) self.assertEqual(resource["accessible"], True) @@ -128,7 +121,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "JSON" self.dcc.check_resource(resource) self.assertEqual(resource["accessible"], True) @@ -146,7 +139,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "JSON" self.dcc.check_resource(resource) self.assertEqual(resource["accessible"], True) @@ -164,7 +157,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "JSON" self.dcc.check_resource(resource) self.assertEqual(resource["accessible"], True) @@ -182,7 +175,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "JSON" self.dcc.check_resource(resource) self.assertEqual(resource["accessible"], True) @@ -198,7 +191,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "XML" self.dcc.check_resource(resource) self.assertEqual(resource["accessible"], True) @@ -214,7 +207,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "PNG" resource["checksum_algorithm"] = ( "http://spdx.org/rdf/terms#checksumAlgorithm_sha1" @@ -247,7 +240,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "JSON" self.dcc.check_resource(resource) self.assertEqual(resource.get("accessible"), True) @@ -266,7 +259,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "JSON" self.dcc.check_resource(resource) self.assertEqual(resource.get("accessible"), True) @@ -285,7 +278,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/data" + resource["url"] = "http://test.invalid/data" resource["format"] = "JSON" self.dcc.check_resource(resource) self.assertEqual(resource.get("accessible"), True) @@ -312,7 +305,7 @@ class TestDcatCatalogCheck(unittest.TestCase): self.dcc.load_http_complete = MagicMock(return_value=mock_response) resource = {} - resource["url"] = "http://localhost/zos116.zip" + resource["url"] = "http://test.invalid/zos116.zip" resource["format"] = "SHP" self.dcc.check_resource(resource) @@ -326,7 +319,7 @@ class TestDcatCatalogCheck(unittest.TestCase): # Test data to simulate the contents of previous_results.json test_data = [ {"url": "http://example.com", "status": "valid", "format": "JSON"}, - {"url": "http://example.org", "status": "invalid", "format": "XML"} + {"url": "http://example.org", "status": "invalid", "format": "XML"}, ] # Write test data to a file 'previous_results.json' @@ -342,9 +335,11 @@ class TestDcatCatalogCheck(unittest.TestCase): self.assertIn("http://example.com", self.dcc.previous_results) self.assertIn("http://example.org", self.dcc.previous_results) self.assertEqual( - self.dcc.previous_results["http://example.com"]["status"], "valid") + self.dcc.previous_results["http://example.com"]["status"], "valid" + ) self.assertEqual( - self.dcc.previous_results["http://example.org"]["status"], "invalid") + self.dcc.previous_results["http://example.org"]["status"], "invalid" + ) @patch("os.path.exists", return_value=False) def test_read_previous_results_file_not_exist(self, mock_exists): @@ -365,7 +360,12 @@ class TestDcatCatalogCheck(unittest.TestCase): "Invalid JSON at line 1: Expecting value: line 1 column 1 (char 0)" ) - @patch("builtins.open", mock_open(read_data='{"status": "valid", "format": "JSON"}\n{"url": "http://example.com", "status": "valid", "format": "JSON"}')) + @patch( + "builtins.open", + mock_open( + read_data='{"status": "valid", "format": "JSON"}\n{"url": "http://example.com", "status": "valid", "format": "JSON"}' + ), + ) @patch("os.path.exists", return_value=True) def test_read_previous_results_missing_url(self, mock_exists): """Test when the file has a line with missing 'url'.""" diff --git a/tests/test_gml_format.py b/tests/test_gml_format.py index a2e40a7..e63638c 100644 --- a/tests/test_gml_format.py +++ b/tests/test_gml_format.py @@ -7,11 +7,13 @@ class TestGmlFormat(unittest.TestCase): resource = {} with open("tests/data/bermuda.gml", "r") as file: self.assertTrue(is_valid(resource, file)) + self.assertIsNone(resource.get("error")) def test_is_valid__invalid(self): resource = {} with open("tests/data/correct.xml", "r") as file: self.assertFalse(is_valid(resource, file)) + self.assertIsNotNone(resource.get("error")) if __name__ == "__main__": diff --git a/tests/test_ods_format.py b/tests/test_ods_format.py new file mode 100644 index 0000000..e152d5e --- /dev/null +++ b/tests/test_ods_format.py @@ -0,0 +1,36 @@ +import unittest +from formats.ods_format import is_valid + + +class TestOdsFormat(unittest.TestCase): + def test_is_valid__valid(self): + resource = {} + with open("tests/data/valid.ods", "r") as file: + self.assertTrue(is_valid(resource, file)) + self.assertIsNone(resource.get("error")) + + def test_is_valid__invalid_no_zip(self): + resource = {} + with open("tests/data/correct.json", "r") as file: + self.assertFalse(is_valid(resource, file)) + self.assertIsNotNone(resource.get("error")) + + def test_is_valid__invalid_no_odt(self): + resource = {} + with open("tests/data/valid.odt", "r") as file: + self.assertFalse(is_valid(resource, file)) + self.assertIsNotNone(resource.get("error")) + self.assertEqual( + "Incorrect MIME type: application/vnd.oasis.opendocument.text", + resource["error"], + ) + + def test_is_valid__invalid_zip(self): + resource = {} + with open("tests/data/valid.xlsx", "r") as file: + self.assertFalse(is_valid(resource, file)) + self.assertIsNotNone(resource.get("error")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_rdf_format.py b/tests/test_rdf_format.py new file mode 100644 index 0000000..8a313f3 --- /dev/null +++ b/tests/test_rdf_format.py @@ -0,0 +1,32 @@ +import unittest +from formats.rdf_format import is_valid + + +class TestRdfFormat(unittest.TestCase): + def test_is_valid__valid_turtle(self): + resource = {} + with open("tests/data/ufo.ttl", "r") as file: + self.assertTrue(is_valid(resource, file)) + self.assertIsNone(resource.get("error")) + + def test_is_valid__valid_xml(self): + resource = {} + with open("tests/data/rdf.xml", "r") as file: + self.assertTrue(is_valid(resource, file)) + self.assertIsNone(resource.get("error")) + + def test_is_valid__valid_jsonld(self): + resource = {} + with open("tests/data/rdf.json", "r") as file: + self.assertTrue(is_valid(resource, file)) + self.assertIsNone(resource.get("error")) + + def test_is_valid__invalid(self): + resource = {} + with open("tests/data/correct.json", "r") as file: + self.assertFalse(is_valid(resource, file)) + self.assertIsNotNone(resource.get("error")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_wmts_format.py b/tests/test_wmts_format.py new file mode 100644 index 0000000..9301e22 --- /dev/null +++ b/tests/test_wmts_format.py @@ -0,0 +1,26 @@ +import unittest +from formats.wmts_srvc_format import is_valid + + +class TestWmtsSrvcFormat(unittest.TestCase): + def test_is_valid__valid(self): + resource = {} + resource["url"] = ( + "https://dienste.gdi-sh.invalid/WMTS_SH_ALKIS_OpenGBD/wmts/1.0.0/WMTSCapabilities.xml" + ) + with open("tests/data/WMTSCapabilities.xml", "r") as file: + self.assertTrue(is_valid(resource, file)) + self.assertIsNone(resource.get("error")) + + def test_is_valid__invalid(self): + resource = {} + resource["url"] = ( + "https://dienste.gdi-sh.invalid/WMTS_SH_ALKIS_OpenGBD/wmts/1.0.0/WMTSCapabilities.xml" + ) + with open("tests/data/correct.xml", "r") as file: + self.assertFalse(is_valid(resource, file)) + self.assertIsNotNone(resource.get("error")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_xml_format.py b/tests/test_xml_format.py new file mode 100644 index 0000000..5672f9f --- /dev/null +++ b/tests/test_xml_format.py @@ -0,0 +1,20 @@ +import unittest +from formats.xml_format import is_valid + + +class TestXmlFormat(unittest.TestCase): + def test_is_valid__valid(self): + resource = {} + with open("tests/data/correct.xml", "r") as file: + self.assertTrue(is_valid(resource, file)) + self.assertIsNone(resource.get("error")) + + def test_is_valid__invalid(self): + resource = {} + with open("tests/data/incorrect.xml", "r") as file: + self.assertFalse(is_valid(resource, file)) + self.assertIsNotNone(resource.get("error")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_zip_format.py b/tests/test_zip_format.py new file mode 100644 index 0000000..6161737 --- /dev/null +++ b/tests/test_zip_format.py @@ -0,0 +1,21 @@ +import unittest +from formats.zip_format import is_valid + + +class TestZipFormat(unittest.TestCase): + def test_is_valid__valid(self): + resource = {} + with open("tests/data/bermuda.zip", "r") as file: + self.assertTrue(is_valid(resource, file)) + self.assertIsNone(resource.get("error")) + + def test_is_valid__invalid(self): + resource = {} + with open("tests/data/correct.xml", "r") as file: + self.assertFalse(is_valid(resource, file)) + self.assertIsNotNone(resource.get("error")) + self.assertEqual("Not a ZIP file.", resource["error"]) + + +if __name__ == "__main__": + unittest.main() -- GitLab