diff --git a/algorithm_catalog/dhi/record.json b/algorithm_catalog/dhi/record.json new file mode 100644 index 00000000..24b5cdfc --- /dev/null +++ b/algorithm_catalog/dhi/record.json @@ -0,0 +1,43 @@ +{ + "id": "dhi", + "type": "Feature", + "conformsTo": [ + "http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core" + ], + "properties": { + "created": "2025-12-03T13:00:00Z", + "updated": "2025-12-03T13:00:00Z", + "type": "algoritm_provider", + "title": "DHI Group", + "description": "", + "keywords": [], + "language": { + "code": "en-US", + "name": "English (United States)" + }, + "languages": [ + { + "code": "en-US", + "name": "English (United States)" + } + ], + "contacts": [], + "themes": [], + "license": "other" + }, + "linkTemplates": [], + "links": [ + { + "rel": "website", + "type": "text/html", + "title": "DHI", + "href": "https://www.dhigroup.com/" + }, + { + "rel": "logo", + "type": "image/png", + "title": "Logo", + "href": "https://cms.dhigroup.com/media/a0lpraol/dhi-white-logo.svg?v=1d9818c2cbe8bf0" + } + ] +} \ No newline at end of file diff --git a/algorithm_catalog/eurac/record.json b/algorithm_catalog/eurac/record.json new file mode 100644 index 00000000..968dcbf7 --- /dev/null +++ b/algorithm_catalog/eurac/record.json @@ -0,0 +1,43 @@ +{ + "id": "eurac", + "type": "Feature", + "conformsTo": [ + "http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core" + ], + "properties": { + "created": "2025-12-03T13:00:00Z", + "updated": "2025-12-03T13:00:00Z", + "type": "algoritm_provider", + "title": "Eurac Research", + "description": "", + "keywords": [], + "language": { + "code": "en-US", + "name": "English (United States)" + }, + "languages": [ + { + "code": "en-US", + "name": "English (United States)" + } + ], + "contacts": [], + "themes": [], + "license": "other" + }, + "linkTemplates": [], + "links": [ + { + "rel": "website", + "type": "text/html", + "title": "EURAC", + "href": "https://www.eurac.edu" + }, + { + "rel": "logo", + "type": "image/png", + "title": "Logo", + "href": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Eurac_Research_logo.svg/2560px-Eurac_Research_logo.svg.png" + } + ] +} \ No newline at end of file diff --git a/algorithm_catalog/terradue/record.json b/algorithm_catalog/terradue/record.json new file mode 100644 index 00000000..88a34c52 --- /dev/null +++ b/algorithm_catalog/terradue/record.json @@ -0,0 +1,43 @@ +{ + "id": "terradue", + "type": "Feature", + "conformsTo": [ + "http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core" + ], + "properties": { + "created": "2025-12-03T13:00:00Z", + "updated": "2025-12-03T13:00:00Z", + "type": "algoritm_provider", + "title": "Terradue Srl", + "description": "", + "keywords": [], + "language": { + "code": "en-US", + "name": "English (United States)" + }, + "languages": [ + { + "code": "en-US", + "name": "English (United States)" + } + ], + "contacts": [], + "themes": [], + "license": "other" + }, + "linkTemplates": [], + "links": [ + { + "rel": "website", + "type": "text/html", + "title": "Terradue", + "href": "https://www.terradue.com/portal/" + }, + { + "rel": "logo", + "type": "image/png", + "title": "Logo", + "href": "https://www.terradue.com/wp-content/uploads/2017/02/logo-03-01-1.svg" + } + ] +} \ No newline at end of file diff --git a/algorithm_catalog/vito/record.json b/algorithm_catalog/vito/record.json new file mode 100644 index 00000000..2834f40f --- /dev/null +++ b/algorithm_catalog/vito/record.json @@ -0,0 +1,43 @@ +{ + "id": "vito", + "type": "Feature", + "conformsTo": [ + "http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core" + ], + "properties": { + "created": "2025-12-03T13:00:00Z", + "updated": "2025-12-03T13:00:00Z", + "type": "algoritm_provider", + "title": "Vlaams Instituut voor Technologisch Onderzoek", + "description": "", + "keywords": [], + "language": { + "code": "en-US", + "name": "English (United States)" + }, + "languages": [ + { + "code": "en-US", + "name": "English (United States)" + } + ], + "contacts": [], + "themes": [], + "license": "other" + }, + "linkTemplates": [], + "links": [ + { + "rel": "website", + "type": "text/html", + "title": "VITO", + "href": "https://vito.be" + }, + { + "rel": "logo", + "type": "image/png", + "title": "Logo", + "href": "https://vito.be/themes/custom/vitotheme/logo.svg" + } + ] +} \ No newline at end of file diff --git a/algorithm_catalog/wur/record.json b/algorithm_catalog/wur/record.json new file mode 100644 index 00000000..2436ef6c --- /dev/null +++ b/algorithm_catalog/wur/record.json @@ -0,0 +1,43 @@ +{ + "id": "wur", + "type": "Feature", + "conformsTo": [ + "http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core" + ], + "properties": { + "created": "2025-12-03T13:00:00Z", + "updated": "2025-12-03T13:00:00Z", + "type": "algoritm_provider", + "title": "Wageningen University & Research", + "description": "", + "keywords": [], + "language": { + "code": "en-US", + "name": "English (United States)" + }, + "languages": [ + { + "code": "en-US", + "name": "English (United States)" + } + ], + "contacts": [], + "themes": [], + "license": "other" + }, + "linkTemplates": [], + "links": [ + { + "rel": "website", + "type": "text/html", + "title": "Wageningen University & Research", + "href": "https://www.wur.nl/" + }, + { + "rel": "logo", + "type": "image/png", + "title": "Logo", + "href": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRng0u0PfPV21hj3R-y9VRh1z_Sj5b61g6Djo4p2mMoJ4GjHkVd3Mm9o0ig9Ubldgk-b0E&usqp=CAU" + } + ] +} \ No newline at end of file diff --git a/qa/tools/apex_algorithm_qa_tools/records.py b/qa/tools/apex_algorithm_qa_tools/records.py index 37af6be2..f269b0b6 100644 --- a/qa/tools/apex_algorithm_qa_tools/records.py +++ b/qa/tools/apex_algorithm_qa_tools/records.py @@ -22,6 +22,10 @@ def get_platform_ogc_record_schema() -> dict: return _get_ogc_record_schema("platform.json") +def get_provider_ogc_record_schema() -> dict: + return _get_ogc_record_schema("provider.json") + + def _get_ogc_records(folder: str, glob: str) -> List[Any]: records = [] for path in (get_project_root() / folder).glob(glob): @@ -37,3 +41,7 @@ def get_service_ogc_records() -> List[Any]: def get_platform_ogc_records() -> List[Any]: return _get_ogc_records("platform_catalog", "*.json") + + +def get_provider_ogc_records() -> List[Any]: + return _get_ogc_records("algorithm_catalog", "*/record.json") diff --git a/qa/unittests/tests/test_records.py b/qa/unittests/tests/test_records.py index e03d685e..0a7f3150 100644 --- a/qa/unittests/tests/test_records.py +++ b/qa/unittests/tests/test_records.py @@ -1,11 +1,15 @@ import jsonschema import pytest +from apex_algorithm_qa_tools.common import get_project_root +from pathlib import Path from apex_algorithm_qa_tools.records import ( get_service_ogc_records, get_service_ogc_record_schema, get_platform_ogc_records, get_platform_ogc_record_schema, + get_provider_ogc_records, + get_provider_ogc_record_schema, ) @@ -44,3 +48,26 @@ def test_service_record_validation(record): ) def test_platform_record_validation(record): jsonschema.validate(instance=record, schema=get_platform_ogc_record_schema()) + + +def test_algorithm_provider_records_(): + # Test that there is at least one provider record based on the folder structure in the algorithm_repo directory. + # For each subfolder in the `algorithm_catalog` folder, there should be exactly one provider record with a matching `record.json`. + algorithm_catalog_dir: Path = get_project_root() / "algorithm_catalog" + subdirs = [p.name for p in algorithm_catalog_dir.iterdir() if p.is_dir()] + assert len(subdirs) > 0, "No subfolders found under algorithm_catalog" + + for subdir in subdirs: + assert (algorithm_catalog_dir / subdir / "record.json").exists(), f"Missing record.json for provider '{subdir}'" + + +@pytest.mark.parametrize( + "record", + [ + # Use scenario id as parameterization id to give nicer test names. + pytest.param(record, id=record["id"]) + for record in get_provider_ogc_records() + ], +) +def test_provider_record_validation(record): + jsonschema.validate(instance=record, schema=get_provider_ogc_record_schema()) diff --git a/schemas/provider.json b/schemas/provider.json new file mode 100644 index 00000000..e0907195 --- /dev/null +++ b/schemas/provider.json @@ -0,0 +1,410 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "APEx Platform - OGC API Record", + "type": "object", + "required": [ + "id", + "type", + "conformsTo", + "properties", + "links" + ], + "properties": { + "id": { + "type": "string", + "description": "The unique identifier for the APEx Service." + }, + "type": { + "type": "string", + "description": "The type of the OGC API Record.", + "enum": [ + "Feature" + ] + }, + "conformsTo": { + "type": "array", + "description": "The extensions/conformance classes used in this record.", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "anyOf": [ + { + "contains": { + "const": "http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core" + } + } + ] + }, + "geometry": { + "type": "null" + }, + "properties": { + "type": "object", + "required": [ + "created", + "updated", + "type", + "title", + "description", + "contacts", + "themes" + ], + "properties": { + "created": { + "type": "string", + "description": "Create date/time of the record." + }, + "updated": { + "type": "string", + "description": "Update date/time of the record." + }, + "type": { + "type": "string", + "description": "Type of the OGC API Record", + "enum": [ + "algoritm_provider" + ] + }, + "title": { + "type": "string", + "description": "Title of the Platform" + }, + "description": { + "type": "string", + "description": "Description of the Platform" + }, + "keywords": { + "type": "array", + "description": "The topic or topics of the Platform. Typically represented using free-form keywords, tags, key phrases, or classification codes.", + "items": { + "type": "string" + } + }, + "language": { + "description": "The language used for textual values in this record representation.", + "$ref": "#/properties/properties/properties/languages/items" + }, + "languages": { + "type": "array", + "description": "This list of languages in which this record is available.", + "items": { + "type": "object", + "description": "The language used for textual values in this record.", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "description": "The language tag as per RFC-5646." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "The untranslated name of the language." + }, + "alternate": { + "type": "string", + "description": "The name of the language in another well-understood language, usually English." + }, + "dir": { + "type": "string", + "description": "The direction for text in this language. The default, `ltr` (left-to-right), represents the most common situation. However, care should be taken to set the value of `dir` appropriately if the language direction is not `ltr`. Other values supported are `rtl` (right-to-left), `ttb` (top-to-bottom), and `btt` (bottom-to-top).", + "enum": [ + "ltr", + "rtl", + "ttb", + "btt" + ], + "default": "ltr" + } + } + } + }, + "contacts": { + "type": "array", + "description": "A list of contacts qualified by their role(s) in association to the Platform described by the record.", + "items": { + "type": "object", + "description": "Identification of, and means of communication with, person responsible\nfor the Platform.", + "properties": { + "name": { + "type": "string", + "description": "The name of the responsible person or team." + }, + "position": { + "type": "string", + "description": "The name of the role or position of the responsible person or team taken from the organization's formal organizational hierarchy or chart." + }, + "organization": { + "type": "string", + "description": "Organization/affiliation of the contact." + }, + "logo": { + "description": "Graphic identifying a contact. The link relation should be `icon` and the media type should be an image media type.", + "allOf": [ + { + "$ref": "#/properties/links/items" + }, + { + "type": "object", + "required": [ + "rel", + "type" + ], + "properties": { + "rel": { + "enum": [ + "icon" + ] + } + } + } + ] + }, + "phones": { + "type": "array", + "description": "Telephone numbers at which contact can be made.", + "items": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string", + "description": "The value is the phone number itself.", + "pattern": "^\\+[1-9]{1}[0-9]{3,14}$" + }, + "roles": { + "description": "The type of phone number (e.g. home, work, fax, etc.).", + "$ref": "#/properties/properties/properties/contacts/items/properties/roles" + } + } + } + }, + "emails": { + "type": "array", + "description": "Email addresses at which contact can be made.", + "items": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string", + "description": "The value is the email number itself.", + "format": "email" + }, + "roles": { + "description": "The type of email (e.g. home, work, etc.).", + "$ref": "#/properties/properties/properties/contacts/items/properties/roles" + } + } + } + }, + "addresses": { + "type": "array", + "description": "Physical location at which contact can be made.", + "items": { + "type": "object", + "properties": { + "deliveryPoint": { + "type": "array", + "description": "Address lines for the location.", + "items": { + "type": "string" + } + }, + "city": { + "type": "string", + "description": "City for the location." + }, + "administrativeArea": { + "type": "string", + "description": "State or province of the location." + }, + "postalCode": { + "type": "string", + "description": "ZIP or other postal code." + }, + "country": { + "type": "string", + "description": "Country of the physical address. ISO 3166-1 is recommended." + }, + "roles": { + "description": "The type of address (e.g. office, home, etc.).", + "$ref": "#/properties/properties/properties/contacts/items/properties/roles" + } + } + } + }, + "links": { + "type": "array", + "description": "On-line information about the contact.", + "items": { + "allOf": [ + { + "$ref": "#/properties/links/items" + }, + { + "type": "object", + "required": [ + "type" + ] + } + ] + } + }, + "hoursOfService": { + "type": "string", + "description": "Time period when the contact can be contacted." + }, + "contactInstructions": { + "type": "string", + "description": "Supplemental instructions on how or when to contact the\nresponsible party." + }, + "roles": { + "description": "The set of named duties, job functions and/or permissions associated with this contact. (e.g. developer, administrator, etc.).", + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + } + }, + "themes": { + "type": "array", + "description": "A knowledge organization system used to classify the Platform.", + "items": { + "type": "object", + "required": [ + "concepts", + "scheme" + ], + "properties": { + "concepts": { + "type": "array", + "description": "One or more entity/concept identifiers from this knowledge system. it is recommended that a resolvable URI be used for each entity/concept identifier.", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "An identifier for the concept." + }, + "title": { + "type": "string", + "description": "A human readable title for the concept." + }, + "description": { + "type": "string", + "description": "A human readable description for the concept." + }, + "url": { + "type": "string", + "format": "uri", + "description": "A URI providing further description of the concept." + } + } + } + }, + "scheme": { + "type": "string", + "description": "An identifier for the knowledge organization system used to classify the APEx service. It is recommended that the identifier be a resolvable URI. The list of schemes used in a searchable catalog can be determined by inspecting the server's OpenAPI document or, if the server implements CQL2, by exposing a queryable (e.g. named `scheme`) and enumerating the list of schemes in the queryable's schema definition." + } + } + } + } + } + }, + "linkTemplates": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/properties/links/items" + }, + { + "type": "object", + "required": [ + "uriTemplate" + ], + "properties": { + "uriTemplate": { + "type": "string", + "description": "Supplies a resolvable URI to a remote resource (or resource fragment)." + }, + "varBase": { + "type": "string", + "description": "The base URI to which the variable name can be appended to retrieve the definition of the variable as a JSON Schema fragment.", + "format": "uri" + }, + "variables": { + "type": "object", + "description": "This object contains one key per substitution variable in the templated URL. Each key defines the schema of one substitution variable using a JSON Schema fragment and can thus include things like the data type of the variable, enumerations, minimum values, maximum values, etc." + } + } + } + ] + } + }, + "links": { + "type": "array", + "items": { + "type": "object", + "properties": { + "rel": { + "type": "string", + "description": "The type or semantics of the relation.", + "enum": [ + "logo", + "website" + ] + }, + "type": { + "type": "string", + "description": "A hint indicating what the media type of the result of dereferencing the link should be." + }, + "title": { + "type": "string", + "description": "Used to label the destination of a link such that it can be used as a human-readable identifier." + }, + "href": { + "type": "string", + "format": "uri", + "description": "URL pointing to an external resource." + } + }, + "required": [ + "rel", + "type", + "title", + "href" + ] + }, + "minItems": 1, + "allOf": [ + { + "contains": { + "type": "object", + "properties": { + "rel": { + "const": "logo" + } + } + } + } + ] + } + } +} \ No newline at end of file