diff --git a/docs/api_reference/models/files.md b/docs/api_reference/models/files.md new file mode 100644 index 000000000..e60e4ad6b --- /dev/null +++ b/docs/api_reference/models/files.md @@ -0,0 +1,5 @@ +# files + +::: optimade.models.files + options: + show_if_no_docstring: true diff --git a/docs/api_reference/server/mappers/files.md b/docs/api_reference/server/mappers/files.md new file mode 100644 index 000000000..75d3c94f2 --- /dev/null +++ b/docs/api_reference/server/mappers/files.md @@ -0,0 +1,3 @@ +# files + +::: optimade.server.mappers.files diff --git a/docs/api_reference/server/routers/files.md b/docs/api_reference/server/routers/files.md new file mode 100644 index 000000000..a987dfb69 --- /dev/null +++ b/docs/api_reference/server/routers/files.md @@ -0,0 +1,3 @@ +# files + +::: optimade.server.routers.files diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json index 2421d70c6..76077ef8b 100644 --- a/openapi/index_openapi.json +++ b/openapi/index_openapi.json @@ -506,15 +506,15 @@ "type": "string", "title": "Id", "description": "An entry's ID as defined in section Definition of Terms.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n\n- **Examples**:\n - `\"db/1234567\"`\n - `\"cod/2000000\"`\n - `\"cod/2000000@1234567\"`\n - `\"nomad/L1234567890\"`\n - `\"42\"`", - "x-optimade-queryable": "must", - "x-optimade-support": "must" + "x-optimade-support": "must", + "x-optimade-queryable": "must" }, "type": { "type": "string", "title": "Type", "description": "The name of the type of an entry.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n - MUST be an existing entry type.\n - The entry of type `` and ID `` MUST be returned in response to a request for `//` under the versioned base URL.\n\n- **Example**: `\"structures\"`", - "x-optimade-queryable": "must", - "x-optimade-support": "must" + "x-optimade-support": "must", + "x-optimade-queryable": "must" }, "links": { "anyOf": [ @@ -580,8 +580,8 @@ ], "title": "Immutable Id", "description": "The entry's immutable ID (e.g., an UUID). This is important for databases having preferred IDs that point to \"the latest version\" of a record, but still offer access to older variants. This ID maps to the version-specific record, in case it changes in the future.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n\n- **Examples**:\n - `\"8bd3e750-b477-41a0-9b11-3a799f21b44f\"`\n - `\"fjeiwoj,54;@=%<>#32\"` (Strings that are not URL-safe are allowed.)", - "x-optimade-queryable": "must", - "x-optimade-support": "optional" + "x-optimade-support": "optional", + "x-optimade-queryable": "must" }, "last_modified": { "anyOf": [ @@ -1265,8 +1265,8 @@ "type": "string", "title": "Id", "description": "An entry's ID as defined in section Definition of Terms.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n\n- **Examples**:\n - `\"db/1234567\"`\n - `\"cod/2000000\"`\n - `\"cod/2000000@1234567\"`\n - `\"nomad/L1234567890\"`\n - `\"42\"`", - "x-optimade-queryable": "must", - "x-optimade-support": "must" + "x-optimade-support": "must", + "x-optimade-queryable": "must" }, "type": { "type": "string", diff --git a/openapi/openapi.json b/openapi/openapi.json index 2520d228e..3c46ce875 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1288,324 +1288,1109 @@ } } }, - "/versions": { + "/files": { "get": { "tags": [ - "Versions" + "Files" + ], + "summary": "Get Files", + "operationId": "get_files_files_get", + "parameters": [ + { + "name": "filter", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "A filter string, in the format described in section API Filtering Format Specification of the specification.", + "default": "", + "title": "Filter" + }, + "description": "A filter string, in the format described in section API Filtering Format Specification of the specification." + }, + { + "name": "response_format", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`", + "default": "json", + "title": "Response Format" + }, + "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`" + }, + { + "name": "email_address", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "email", + "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`", + "default": "", + "title": "Email Address" + }, + "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`" + }, + { + "name": "response_fields", + "in": "query", + "required": false, + "schema": { + "type": "string", + "pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?", + "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`", + "default": "", + "title": "Response Fields" + }, + "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`" + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "type": "string", + "pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?", + "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.", + "default": "", + "title": "Sort" + }, + "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints." + }, + { + "name": "page_limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`", + "default": 20, + "title": "Page Limit" + }, + "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`" + }, + { + "name": "page_offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.", + "default": 0, + "title": "Page Offset" + }, + "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`." + }, + { + "name": "page_number", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.", + "title": "Page Number" + }, + "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`." + }, + { + "name": "page_cursor", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.", + "default": 0, + "title": "Page Cursor" + }, + "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED." + }, + { + "name": "page_above", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.", + "title": "Page Above" + }, + "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`." + }, + { + "name": "page_below", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.", + "title": "Page Below" + }, + "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED." + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.", + "default": "references", + "title": "Include" + }, + "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`." + }, + { + "name": "api_hint", + "in": "query", + "required": false, + "schema": { + "type": "string", + "pattern": "(v[0-9]+(\\.[0-9]+)?)?", + "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.", + "default": "", + "title": "Api Hint" + }, + "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0." + } ], - "summary": "Get Versions", - "description": "Respond with the text/csv representation for the served versions.", - "operationId": "get_versions_versions_get", "responses": { "200": { "description": "Successful Response", "content": { - "text/csv; header=present": { + "application/vnd.api+json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/FileResponseMany" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "553": { + "description": "Version Not Supported", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } - } + } + } + } + } + }, + "/files/{entry_id}": { + "get": { + "tags": [ + "Files" + ], + "summary": "Get Single File", + "operationId": "get_single_file_files__entry_id__get", + "parameters": [ + { + "name": "entry_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Entry Id" + } + }, + { + "name": "response_format", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`", + "default": "json", + "title": "Response Format" + }, + "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`" + }, + { + "name": "email_address", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "email", + "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`", + "default": "", + "title": "Email Address" + }, + "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`" + }, + { + "name": "response_fields", + "in": "query", + "required": false, + "schema": { + "type": "string", + "pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?", + "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`", + "default": "", + "title": "Response Fields" + }, + "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`" + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.", + "default": "references", + "title": "Include" + }, + "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`." + }, + { + "name": "api_hint", + "in": "query", + "required": false, + "schema": { + "type": "string", + "pattern": "(v[0-9]+(\\.[0-9]+)?)?", + "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.", + "default": "", + "title": "Api Hint" + }, + "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/FileResponseOne" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "553": { + "description": "Version Not Supported", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/versions": { + "get": { + "tags": [ + "Versions" + ], + "summary": "Get Versions", + "description": "Respond with the text/csv representation for the served versions.", + "operationId": "get_versions_versions_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "text/csv; header=present": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Aggregate": { + "type": "string", + "enum": [ + "ok", + "test", + "staging", + "no" + ], + "title": "Aggregate", + "description": "Enumeration of aggregate values" + }, + "Assembly": { + "properties": { + "sites_in_groups": { + "items": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "type": "array", + "title": "Sites In Groups", + "description": "Index of the sites (0-based) that belong to each group for each assembly.\n\n- **Examples**:\n - `[[1], [2]]`: two groups, one with the second site, one with the third.\n - `[[1,2], [3]]`: one group with the second and third site, one with the fourth.", + "x-optimade-queryable": "optional", + "x-optimade-support": "must" + }, + "group_probabilities": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Group Probabilities", + "description": "Statistical probability of each group. It MUST have the same length as `sites_in_groups`.\nIt SHOULD sum to one.\nSee below for examples of how to specify the probability of the occurrence of a vacancy.\nThe possible reasons for the values not to sum to one are the same as already specified above for the `concentration` of each `species`.", + "x-optimade-queryable": "optional", + "x-optimade-support": "must" + } + }, + "type": "object", + "required": [ + "sites_in_groups", + "group_probabilities" + ], + "title": "Assembly", + "description": "A description of groups of sites that are statistically correlated.\n\n- **Examples** (for each entry of the assemblies list):\n - `{\"sites_in_groups\": [[0], [1]], \"group_probabilities: [0.3, 0.7]}`: the first site and the second site never occur at the same time in the unit cell.\n Statistically, 30 % of the times the first site is present, while 70 % of the times the second site is present.\n - `{\"sites_in_groups\": [[1,2], [3]], \"group_probabilities: [0.3, 0.7]}`: the second and third site are either present together or not present; they form the first group of atoms for this assembly.\n The second group is formed by the fourth site. Sites of the first group (the second and the third) are never present at the same time as the fourth site.\n 30 % of times sites 1 and 2 are present (and site 3 is absent); 70 % of times site 3 is present (and sites 1 and 2 are absent)." + }, + "Attributes": { + "properties": {}, + "additionalProperties": true, + "type": "object", + "title": "Attributes", + "description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.\nThe keys for Attributes MUST NOT be:\n relationships\n links\n id\n type" + }, + "AvailableApiVersion": { + "properties": { + "url": { + "type": "string", + "minLength": 1, + "pattern": "^.+/v[0-1](\\.[0-9]+)*/?$", + "format": "uri", + "title": "Url", + "description": "A string specifying a versioned base URL that MUST adhere to the rules in section Base URL" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "title": "Version", + "description": "A string containing the full version number of the API served at that versioned base URL.\nThe version number string MUST NOT be prefixed by, e.g., 'v'.\nExamples: `1.0.0`, `1.0.0-rc.2`.", + "examples": [ + "0.10.1", + "1.0.0-rc.2", + "1.2.3-rc.5+develop" + ] + } + }, + "type": "object", + "required": [ + "url", + "version" + ], + "title": "AvailableApiVersion", + "description": "A JSON object containing information about an available API version" + }, + "BaseInfoAttributes": { + "properties": { + "api_version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "title": "Api Version", + "description": "Presently used full version of the OPTIMADE API.\nThe version number string MUST NOT be prefixed by, e.g., \"v\".\nExamples: `1.0.0`, `1.0.0-rc.2`.", + "examples": [ + "0.10.1", + "1.0.0-rc.2", + "1.2.3-rc.5+develop" + ] + }, + "available_api_versions": { + "items": { + "$ref": "#/components/schemas/AvailableApiVersion" + }, + "type": "array", + "title": "Available Api Versions", + "description": "A list of dictionaries of available API versions at other base URLs" + }, + "formats": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Formats", + "description": "List of available output formats.", + "default": [ + "json" + ] + }, + "available_endpoints": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Available Endpoints", + "description": "List of available endpoints (i.e., the string to be appended to the versioned base URL)." + }, + "entry_types_by_format": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "object", + "title": "Entry Types By Format", + "description": "Available entry endpoints as a function of output formats." + }, + "is_index": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Index", + "description": "If true, this is an index meta-database base URL (see section Index Meta-Database). If this member is not provided, the client MUST assume this is not an index meta-database base URL (i.e., the default is for `is_index` to be `false`).", + "default": false + } + }, + "type": "object", + "required": [ + "api_version", + "available_api_versions", + "available_endpoints", + "entry_types_by_format" + ], + "title": "BaseInfoAttributes", + "description": "Attributes for Base URL Info endpoint" + }, + "BaseInfoResource": { + "properties": { + "id": { + "type": "string", + "enum": [ + "/" + ], + "const": "/", + "title": "Id", + "default": "/" + }, + "type": { + "type": "string", + "enum": [ + "info" + ], + "const": "info", + "title": "Type", + "default": "info" + }, + "links": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResourceLinks" + }, + { + "type": "null" + } + ], + "description": "a links object containing links related to the resource." + }, + "meta": { + "anyOf": [ + { + "$ref": "#/components/schemas/Meta" + }, + { + "type": "null" + } + ], + "description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship." + }, + "attributes": { + "$ref": "#/components/schemas/BaseInfoAttributes" + }, + "relationships": { + "anyOf": [ + { + "$ref": "#/components/schemas/Relationships" + }, + { + "type": "null" + } + ], + "description": "[Relationships object](https://jsonapi.org/format/1.0/#document-resource-object-relationships)\ndescribing relationships between the resource and other JSON API resources." + } + }, + "type": "object", + "required": [ + "id", + "type", + "attributes" + ], + "title": "BaseInfoResource" + }, + "BaseRelationshipMeta": { + "properties": { + "description": { + "type": "string", + "title": "Description", + "description": "OPTIONAL human-readable description of the relationship." + } + }, + "additionalProperties": true, + "type": "object", + "required": [ + "description" + ], + "title": "BaseRelationshipMeta", + "description": "Specific meta field for base relationship resource" + }, + "BaseRelationshipResource": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Resource ID" + }, + "type": { + "type": "string", + "title": "Type", + "description": "Resource type" + }, + "meta": { + "anyOf": [ + { + "$ref": "#/components/schemas/BaseRelationshipMeta" + }, + { + "type": "null" + } + ], + "description": "Relationship meta field. MUST contain 'description' if supplied." + } + }, + "type": "object", + "required": [ + "id", + "type" + ], + "title": "BaseRelationshipResource", + "description": "Minimum requirements to represent a relationship resource" + }, + "DataType": { + "type": "string", + "enum": [ + "string", + "integer", + "float", + "boolean", + "timestamp", + "list", + "dictionary", + "unknown" + ], + "title": "DataType", + "description": "Optimade Data types\n\nSee the section \"Data types\" in the OPTIMADE API specification for more information." + }, + "EntryInfoProperty": { + "properties": { + "description": { + "type": "string", + "title": "Description", + "description": "A human-readable description of the entry property" + }, + "unit": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Unit", + "description": "The physical unit of the entry property.\nThis MUST be a valid representation of units according to version 2.1 of [The Unified Code for Units of Measure](https://unitsofmeasure.org/ucum.html).\nIt is RECOMMENDED that non-standard (non-SI) units are described in the description for the property." + }, + "sortable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Sortable", + "description": "Defines whether the entry property can be used for sorting with the \"sort\" parameter.\nIf the entry listing endpoint supports sorting, this key MUST be present for sortable properties with value `true`." + }, + "type": { + "anyOf": [ + { + "$ref": "#/components/schemas/DataType" + }, + { + "type": "null" + } + ], + "title": "Type", + "description": "The type of the property's value.\nThis MUST be any of the types defined in the Data types section.\nFor the purpose of compatibility with future versions of this specification, a client MUST accept values that are not `string` values specifying any of the OPTIMADE Data types, but MUST then also disregard the `type` field.\nNote, if the value is a nested type, only the outermost type should be reported.\nE.g., for the entry resource `structures`, the `species` property is defined as a list of dictionaries, hence its `type` value would be `list`." + } + }, + "type": "object", + "required": [ + "description" + ], + "title": "EntryInfoProperty" + }, + "EntryInfoResource": { + "properties": { + "formats": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Formats", + "description": "List of output formats available for this type of entry." + }, + "description": { + "type": "string", + "title": "Description", + "description": "Description of the entry." + }, + "properties": { + "patternProperties": { + "^[a-z_][a-z_0-9]+$": { + "$ref": "#/components/schemas/EntryInfoProperty" + } + }, + "type": "object", + "title": "Properties", + "description": "A dictionary describing queryable properties for this entry type, where each key is a property name." + }, + "output_fields_by_format": { + "additionalProperties": { + "items": { + "type": "string", + "pattern": "^[a-z_][a-z_0-9]+$" + }, + "type": "array" + }, + "type": "object", + "title": "Output Fields By Format", + "description": "Dictionary of available output fields for this entry type, where the keys are the values of the `formats` list and the values are the keys of the `properties` dictionary." + } + }, + "type": "object", + "required": [ + "formats", + "description", + "properties", + "output_fields_by_format" + ], + "title": "EntryInfoResource" + }, + "EntryInfoResponse": { + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/EntryInfoResource" + } + ], + "description": "OPTIMADE information for an entry endpoint." + }, + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" + }, + "errors": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Error" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "uniqueItems": true, + "title": "Errors", + "description": "A list of unique errors" + }, + "included": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Resource" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "uniqueItems": true, + "title": "Included", + "description": "A list of unique included resources" + }, + "links": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToplevelLinks" + }, + { + "type": "null" + } + ], + "description": "Links associated with the primary data or errors" + }, + "jsonapi": { + "anyOf": [ + { + "$ref": "#/components/schemas/JsonApi" + }, + { + "type": "null" + } + ], + "description": "Information about the JSON API used" } - } - } - } - }, - "components": { - "schemas": { - "Aggregate": { - "type": "string", - "enum": [ - "ok", - "test", - "staging", - "no" + }, + "type": "object", + "required": [ + "data", + "meta" ], - "title": "Aggregate", - "description": "Enumeration of aggregate values" + "title": "EntryInfoResponse" }, - "Assembly": { + "EntryRelationships": { "properties": { - "sites_in_groups": { - "items": { - "items": { - "type": "integer" + "references": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReferenceRelationship" }, - "type": "array" - }, - "type": "array", - "title": "Sites In Groups", - "description": "Index of the sites (0-based) that belong to each group for each assembly.\n\n- **Examples**:\n - `[[1], [2]]`: two groups, one with the second site, one with the third.\n - `[[1,2], [3]]`: one group with the second and third site, one with the fourth.", - "x-optimade-queryable": "optional", - "x-optimade-support": "must" + { + "type": "null" + } + ], + "description": "Object containing links to relationships with entries of the `references` type." }, - "group_probabilities": { - "items": { - "type": "number" - }, - "type": "array", - "title": "Group Probabilities", - "description": "Statistical probability of each group. It MUST have the same length as `sites_in_groups`.\nIt SHOULD sum to one.\nSee below for examples of how to specify the probability of the occurrence of a vacancy.\nThe possible reasons for the values not to sum to one are the same as already specified above for the `concentration` of each `species`.", - "x-optimade-queryable": "optional", - "x-optimade-support": "must" + "structures": { + "anyOf": [ + { + "$ref": "#/components/schemas/StructureRelationship" + }, + { + "type": "null" + } + ], + "description": "Object containing links to relationships with entries of the `structures` type." } }, "type": "object", - "required": [ - "sites_in_groups", - "group_probabilities" - ], - "title": "Assembly", - "description": "A description of groups of sites that are statistically correlated.\n\n- **Examples** (for each entry of the assemblies list):\n - `{\"sites_in_groups\": [[0], [1]], \"group_probabilities: [0.3, 0.7]}`: the first site and the second site never occur at the same time in the unit cell.\n Statistically, 30 % of the times the first site is present, while 70 % of the times the second site is present.\n - `{\"sites_in_groups\": [[1,2], [3]], \"group_probabilities: [0.3, 0.7]}`: the second and third site are either present together or not present; they form the first group of atoms for this assembly.\n The second group is formed by the fourth site. Sites of the first group (the second and the third) are never present at the same time as the fourth site.\n 30 % of times sites 1 and 2 are present (and site 3 is absent); 70 % of times site 3 is present (and sites 1 and 2 are absent)." - }, - "Attributes": { - "properties": {}, - "additionalProperties": true, - "type": "object", - "title": "Attributes", - "description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.\nThe keys for Attributes MUST NOT be:\n relationships\n links\n id\n type" + "title": "EntryRelationships", + "description": "This model wraps the JSON API Relationships to include type-specific top level keys." }, - "AvailableApiVersion": { + "EntryResource": { "properties": { - "url": { + "id": { "type": "string", - "minLength": 1, - "pattern": "^.+/v[0-1](\\.[0-9]+)*/?$", - "format": "uri", - "title": "Url", - "description": "A string specifying a versioned base URL that MUST adhere to the rules in section Base URL" + "title": "Id", + "description": "An entry's ID as defined in section Definition of Terms.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n\n- **Examples**:\n - `\"db/1234567\"`\n - `\"cod/2000000\"`\n - `\"cod/2000000@1234567\"`\n - `\"nomad/L1234567890\"`\n - `\"42\"`", + "x-optimade-queryable": "must", + "x-optimade-support": "must" }, - "version": { + "type": { "type": "string", - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", - "title": "Version", - "description": "A string containing the full version number of the API served at that versioned base URL.\nThe version number string MUST NOT be prefixed by, e.g., 'v'.\nExamples: `1.0.0`, `1.0.0-rc.2`.", - "examples": [ - "0.10.1", - "1.0.0-rc.2", - "1.2.3-rc.5+develop" - ] + "title": "Type", + "description": "The name of the type of an entry.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n - MUST be an existing entry type.\n - The entry of type `` and ID `` MUST be returned in response to a request for `//` under the versioned base URL.\n\n- **Example**: `\"structures\"`", + "x-optimade-queryable": "must", + "x-optimade-support": "must" + }, + "links": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResourceLinks" + }, + { + "type": "null" + } + ], + "description": "a links object containing links related to the resource." + }, + "meta": { + "anyOf": [ + { + "$ref": "#/components/schemas/Meta" + }, + { + "type": "null" + } + ], + "description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship." + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/EntryResourceAttributes" + } + ], + "description": "A dictionary, containing key-value pairs representing the entry's properties, except for `type` and `id`.\nDatabase-provider-specific properties need to include the database-provider-specific prefix (see section on Database-Provider-Specific Namespace Prefixes)." + }, + "relationships": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntryRelationships" + }, + { + "type": "null" + } + ], + "description": "A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships).\nThe OPTIONAL human-readable description of the relationship MAY be provided in the `description` field inside the `meta` dictionary of the JSON API resource identifier object." } }, "type": "object", "required": [ - "url", - "version" + "id", + "type", + "attributes" ], - "title": "AvailableApiVersion", - "description": "A JSON object containing information about an available API version" + "title": "EntryResource", + "description": "The base model for an entry resource." }, - "BaseInfoAttributes": { + "EntryResourceAttributes": { "properties": { - "api_version": { - "type": "string", - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", - "title": "Api Version", - "description": "Presently used full version of the OPTIMADE API.\nThe version number string MUST NOT be prefixed by, e.g., \"v\".\nExamples: `1.0.0`, `1.0.0-rc.2`.", - "examples": [ - "0.10.1", - "1.0.0-rc.2", - "1.2.3-rc.5+develop" - ] - }, - "available_api_versions": { - "items": { - "$ref": "#/components/schemas/AvailableApiVersion" - }, - "type": "array", - "title": "Available Api Versions", - "description": "A list of dictionaries of available API versions at other base URLs" - }, - "formats": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Formats", - "description": "List of available output formats.", - "default": [ - "json" - ] - }, - "available_endpoints": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Available Endpoints", - "description": "List of available endpoints (i.e., the string to be appended to the versioned base URL)." - }, - "entry_types_by_format": { - "additionalProperties": { - "items": { + "immutable_id": { + "anyOf": [ + { "type": "string" }, - "type": "array" - }, - "type": "object", - "title": "Entry Types By Format", - "description": "Available entry endpoints as a function of output formats." + { + "type": "null" + } + ], + "title": "Immutable Id", + "description": "The entry's immutable ID (e.g., an UUID). This is important for databases having preferred IDs that point to \"the latest version\" of a record, but still offer access to older variants. This ID maps to the version-specific record, in case it changes in the future.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n\n- **Examples**:\n - `\"8bd3e750-b477-41a0-9b11-3a799f21b44f\"`\n - `\"fjeiwoj,54;@=%<>#32\"` (Strings that are not URL-safe are allowed.)", + "x-optimade-queryable": "must", + "x-optimade-support": "optional" }, - "is_index": { + "last_modified": { "anyOf": [ { - "type": "boolean" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Is Index", - "description": "If true, this is an index meta-database base URL (see section Index Meta-Database). If this member is not provided, the client MUST assume this is not an index meta-database base URL (i.e., the default is for `is_index` to be `false`).", - "default": false + "title": "Last Modified", + "description": "Date and time representing when the entry was last modified.\n\n- **Type**: timestamp.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response unless the query parameter `response_fields` is present and does not include this property.\n\n- **Example**:\n - As part of JSON response format: `\"2007-04-05T14:30:20Z\"` (i.e., encoded as an [RFC 3339 Internet Date/Time Format](https://tools.ietf.org/html/rfc3339#section-5.6) string.)", + "x-optimade-queryable": "must", + "x-optimade-support": "should" } }, + "additionalProperties": true, "type": "object", "required": [ - "api_version", - "available_api_versions", - "available_endpoints", - "entry_types_by_format" + "last_modified" ], - "title": "BaseInfoAttributes", - "description": "Attributes for Base URL Info endpoint" + "title": "EntryResourceAttributes", + "description": "Contains key-value pairs representing the entry's properties." }, - "BaseInfoResource": { + "Error": { "properties": { "id": { - "type": "string", - "enum": [ - "/" - ], - "const": "/", - "title": "Id", - "default": "/" - }, - "type": { - "type": "string", - "enum": [ - "info" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ], - "const": "info", - "title": "Type", - "default": "info" + "title": "Id", + "description": "A unique identifier for this particular occurrence of the problem." }, "links": { "anyOf": [ { - "$ref": "#/components/schemas/ResourceLinks" + "$ref": "#/components/schemas/ErrorLinks" }, { "type": "null" } ], - "description": "a links object containing links related to the resource." + "description": "A links object storing about" }, - "meta": { + "status": { "anyOf": [ { - "$ref": "#/components/schemas/Meta" + "type": "string" }, { "type": "null" } ], - "description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship." - }, - "attributes": { - "$ref": "#/components/schemas/BaseInfoAttributes" + "title": "Status", + "description": "the HTTP status code applicable to this problem, expressed as a string value." }, - "relationships": { + "code": { "anyOf": [ { - "$ref": "#/components/schemas/Relationships" + "type": "string" }, { "type": "null" } ], - "description": "[Relationships object](https://jsonapi.org/format/1.0/#document-resource-object-relationships)\ndescribing relationships between the resource and other JSON API resources." - } - }, - "type": "object", - "required": [ - "id", - "type", - "attributes" - ], - "title": "BaseInfoResource" - }, - "BaseRelationshipMeta": { - "properties": { - "description": { - "type": "string", - "title": "Description", - "description": "OPTIONAL human-readable description of the relationship." - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "description" - ], - "title": "BaseRelationshipMeta", - "description": "Specific meta field for base relationship resource" - }, - "BaseRelationshipResource": { - "properties": { - "id": { - "type": "string", - "title": "Id", - "description": "Resource ID" - }, - "type": { - "type": "string", - "title": "Type", - "description": "Resource type" + "title": "Code", + "description": "an application-specific error code, expressed as a string value." }, - "meta": { + "title": { "anyOf": [ { - "$ref": "#/components/schemas/BaseRelationshipMeta" + "type": "string" }, { "type": "null" } ], - "description": "Relationship meta field. MUST contain 'description' if supplied." - } - }, - "type": "object", - "required": [ - "id", - "type" - ], - "title": "BaseRelationshipResource", - "description": "Minimum requirements to represent a relationship resource" - }, - "DataType": { - "type": "string", - "enum": [ - "string", - "integer", - "float", - "boolean", - "timestamp", - "list", - "dictionary", - "unknown" - ], - "title": "DataType", - "description": "Optimade Data types\n\nSee the section \"Data types\" in the OPTIMADE API specification for more information." - }, - "EntryInfoProperty": { - "properties": { - "description": { - "type": "string", - "title": "Description", - "description": "A human-readable description of the entry property" + "title": "Title", + "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." }, - "unit": { + "detail": { "anyOf": [ { "type": "string" @@ -1614,96 +2399,80 @@ "type": "null" } ], - "title": "Unit", - "description": "The physical unit of the entry property.\nThis MUST be a valid representation of units according to version 2.1 of [The Unified Code for Units of Measure](https://unitsofmeasure.org/ucum.html).\nIt is RECOMMENDED that non-standard (non-SI) units are described in the description for the property." + "title": "Detail", + "description": "A human-readable explanation specific to this occurrence of the problem." }, - "sortable": { + "source": { "anyOf": [ { - "type": "boolean" + "$ref": "#/components/schemas/ErrorSource" }, { "type": "null" } ], - "title": "Sortable", - "description": "Defines whether the entry property can be used for sorting with the \"sort\" parameter.\nIf the entry listing endpoint supports sorting, this key MUST be present for sortable properties with value `true`." + "description": "An object containing references to the source of the error" }, - "type": { + "meta": { "anyOf": [ { - "$ref": "#/components/schemas/DataType" + "$ref": "#/components/schemas/Meta" }, { "type": "null" } ], - "title": "Type", - "description": "The type of the property's value.\nThis MUST be any of the types defined in the Data types section.\nFor the purpose of compatibility with future versions of this specification, a client MUST accept values that are not `string` values specifying any of the OPTIMADE Data types, but MUST then also disregard the `type` field.\nNote, if the value is a nested type, only the outermost type should be reported.\nE.g., for the entry resource `structures`, the `species` property is defined as a list of dictionaries, hence its `type` value would be `list`." + "description": "a meta object containing non-standard meta-information about the error." } }, "type": "object", - "required": [ - "description" - ], - "title": "EntryInfoProperty" + "title": "Error", + "description": "An error response" }, - "EntryInfoResource": { + "ErrorLinks": { "properties": { - "formats": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Formats", - "description": "List of output formats available for this type of entry." - }, - "description": { - "type": "string", - "title": "Description", - "description": "Description of the entry." - }, - "properties": { - "patternProperties": { - "^[a-z_][a-z_0-9]+$": { - "$ref": "#/components/schemas/EntryInfoProperty" - } - }, - "type": "object", - "title": "Properties", - "description": "A dictionary describing queryable properties for this entry type, where each key is a property name." - }, - "output_fields_by_format": { - "additionalProperties": { - "items": { + "about": { + "anyOf": [ + { "type": "string", - "pattern": "^[a-z_][a-z_0-9]+$" + "minLength": 1, + "format": "uri" }, - "type": "array" - }, - "type": "object", - "title": "Output Fields By Format", - "description": "Dictionary of available output fields for this entry type, where the keys are the values of the `formats` list and the values are the keys of the `properties` dictionary." + { + "$ref": "#/components/schemas/Link" + }, + { + "type": "null" + } + ], + "title": "About", + "description": "A link that leads to further details about this particular occurrence of the problem." } }, "type": "object", - "required": [ - "formats", - "description", - "properties", - "output_fields_by_format" - ], - "title": "EntryInfoResource" + "title": "ErrorLinks", + "description": "A Links object specific to Error objects" }, - "EntryInfoResponse": { + "ErrorResponse": { "properties": { "data": { - "allOf": [ + "anyOf": [ { - "$ref": "#/components/schemas/EntryInfoResource" + "$ref": "#/components/schemas/Resource" + }, + { + "items": { + "$ref": "#/components/schemas/Resource" + }, + "type": "array" + }, + { + "type": "null" } ], - "description": "OPTIMADE information for an entry endpoint." + "uniqueItems": true, + "title": "Data", + "description": "Outputted Data" }, "meta": { "allOf": [ @@ -1711,23 +2480,16 @@ "$ref": "#/components/schemas/ResponseMeta" } ], - "description": "A meta object containing non-standard information" + "description": "A meta object containing non-standard information." }, "errors": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/Error" - }, - "type": "array" - }, - { - "type": "null" - } - ], + "items": { + "$ref": "#/components/schemas/OptimadeError" + }, + "type": "array", "uniqueItems": true, "title": "Errors", - "description": "A list of unique errors" + "description": "A list of OPTIMADE-specific JSON API error objects, where the field detail MUST be present." }, "included": { "anyOf": [ @@ -1770,41 +2532,44 @@ }, "type": "object", "required": [ - "data", - "meta" + "meta", + "errors" ], - "title": "EntryInfoResponse" + "title": "ErrorResponse", + "description": "errors MUST be present and data MUST be skipped" }, - "EntryRelationships": { + "ErrorSource": { "properties": { - "references": { + "pointer": { "anyOf": [ { - "$ref": "#/components/schemas/ReferenceRelationship" + "type": "string" }, { "type": "null" } ], - "description": "Object containing links to relationships with entries of the `references` type." + "title": "Pointer", + "description": "a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute]." }, - "structures": { + "parameter": { "anyOf": [ { - "$ref": "#/components/schemas/StructureRelationship" + "type": "string" }, { "type": "null" } ], - "description": "Object containing links to relationships with entries of the `structures` type." + "title": "Parameter", + "description": "a string indicating which URI query parameter caused the error." } }, "type": "object", - "title": "EntryRelationships", - "description": "This model wraps the JSON API Relationships to include type-specific top level keys." + "title": "ErrorSource", + "description": "an object containing references to the source of the error" }, - "EntryResource": { + "FileResource": { "properties": { "id": { "type": "string", @@ -1815,8 +2580,10 @@ }, "type": { "type": "string", + "pattern": "^files$", "title": "Type", - "description": "The name of the type of an entry.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n - MUST be an existing entry type.\n - The entry of type `` and ID `` MUST be returned in response to a request for `//` under the versioned base URL.\n\n- **Example**: `\"structures\"`", + "description": "The name of the type of an entry.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n - MUST be an existing entry type.\n - The entry of type `` and ID `` MUST be returned in response to a request for `//` under the versioned base URL.\n\n- **Examples**:\n - `\"structures\"`", + "default": "files", "x-optimade-queryable": "must", "x-optimade-support": "must" }, @@ -1843,12 +2610,7 @@ "description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship." }, "attributes": { - "allOf": [ - { - "$ref": "#/components/schemas/EntryResourceAttributes" - } - ], - "description": "A dictionary, containing key-value pairs representing the entry's properties, except for `type` and `id`.\nDatabase-provider-specific properties need to include the database-provider-specific prefix (see section on Database-Provider-Specific Namespace Prefixes)." + "$ref": "#/components/schemas/FileResourceAttributes" }, "relationships": { "anyOf": [ @@ -1868,10 +2630,10 @@ "type", "attributes" ], - "title": "EntryResource", - "description": "The base model for an entry resource." + "title": "FileResource", + "description": "Representing a structure." }, - "EntryResourceAttributes": { + "FileResourceAttributes": { "properties": { "immutable_id": { "anyOf": [ @@ -1901,42 +2663,51 @@ "description": "Date and time representing when the entry was last modified.\n\n- **Type**: timestamp.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response unless the query parameter `response_fields` is present and does not include this property.\n\n- **Example**:\n - As part of JSON response format: `\"2007-04-05T14:30:20Z\"` (i.e., encoded as an [RFC 3339 Internet Date/Time Format](https://tools.ietf.org/html/rfc3339#section-5.6) string.)", "x-optimade-queryable": "must", "x-optimade-support": "should" - } - }, - "additionalProperties": true, - "type": "object", - "required": [ - "last_modified" - ], - "title": "EntryResourceAttributes", - "description": "Contains key-value pairs representing the entry's properties." - }, - "Error": { - "properties": { - "id": { + }, + "url": { + "type": "string", + "title": "Url", + "description": "The URL to get the contents of a file.\n- **Type**: string\n- **Requirements/Conventions**:\n\n - **Support**: MUST be supported by all implementations, MUST NOT be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - **Response**: REQUIRED in the response.\n - The URL MUST point to the actual contents of a file (i.e. byte stream), not an intermediate (preview) representation.\n For example, if referring to a file on GitHub, a link should point to raw contents.\n\n- **Examples**:\n\n - :val:`\"https://example.org/files/cifs/1000000.cif\"`\n", + "x-optimade-queryable": "optional", + "x-optimade-support": "must" + }, + "url_stable_until": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Id", - "description": "A unique identifier for this particular occurrence of the problem." + "title": "Url Stable Until", + "description": "Point in time until which the URL in `url` is guaranteed to stay stable.\n- **Type**: timestamp\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - :val:`null` means that there is no stability guarantee for the URL in `url`.\n Indefinite support could be communicated by providing a date sufficiently far in the future, for example, :val:`9999-12-31`.", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional" }, - "links": { + "name": { + "type": "string", + "title": "Name", + "description": "Base name of a file.\n- **Type**: string\n- **Requirements/Conventions**:\n\n - **Support**: MUST be supported by all implementations, MUST NOT be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - File name extension is an integral part of a file name and, if available, MUST be included.\n\n- **Examples**:\n\n - :val:`\"1000000.cif\"`", + "x-optimade-queryable": "optional", + "x-optimade-support": "must" + }, + "size": { "anyOf": [ { - "$ref": "#/components/schemas/ErrorLinks" + "type": "integer" }, { "type": "null" } ], - "description": "A links object storing about" + "title": "Size", + "description": "Size of a file in bytes.\n- **Type**: integer\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - If provided, it MUST be guaranteed that either exact size of a file is given or its upper bound.\n This way if a client reserves a static buffer or truncates the download stream after this many bytes the whole file would be received.\n Such provision is included to allow the providers to serve on-the-fly compressed files.", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional" }, - "status": { + "media_type": { "anyOf": [ { "type": "string" @@ -1945,10 +2716,12 @@ "type": "null" } ], - "title": "Status", - "description": "the HTTP status code applicable to this problem, expressed as a string value." + "title": "Media Type", + "description": "Media type identifier (also known as MIME type), for a file as per `RFC 6838 Media Type Specifications and Registration Procedures `__.\n- **Type**: string\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n\n- **Examples**:\n\n - :val:`\"chemical/x-cif\"`", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional" }, - "code": { + "version": { "anyOf": [ { "type": "string" @@ -1957,22 +2730,27 @@ "type": "null" } ], - "title": "Code", - "description": "an application-specific error code, expressed as a string value." + "title": "Version", + "description": "Version information of a file (e.g. commit, revision, timestamp).\n- **Type**: string\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - If provided, it MUST be guaranteed that file contents pertaining to the same combination of :field:`id` and :field:`version` are the same", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional" }, - "title": { + "modification_timestamp": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Title", - "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." + "title": "Modification Timestamp", + "description": "Timestamp of the last modification of file contents.\n A modification is understood as an addition, change or deletion of one or more bytes, resulting in file contents different from the previous.\n- **Type**: timestamp\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - Timestamps of subsequent file modifications SHOULD be increasing (not earlier than previous timestamps).", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional" }, - "detail": { + "description": { "anyOf": [ { "type": "string" @@ -1981,80 +2759,114 @@ "type": "null" } ], - "title": "Detail", - "description": "A human-readable explanation specific to this occurrence of the problem." + "title": "Description", + "description": "Free-form description of a file.\n- **Type**: string\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n\n- **Examples**:\n\n - :val:`\"POSCAR format file\"`", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional" }, - "source": { + "checksums": { "anyOf": [ { - "$ref": "#/components/schemas/ErrorSource" + "additionalProperties": { + "type": "string" + }, + "type": "object" }, { "type": "null" } ], - "description": "An object containing references to the source of the error" + "title": "Checksums", + "description": "Dictionary providing checksums of file contents.\n* **Type**: dictionary with keys identifying checksum functions and values (strings) giving the actual checksums\n* **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - Supported dictionary keys: :property:`md5`, :property:`sha1`, :property:`sha224`, :property:`sha256`, :property:`sha384`, :property:`sha512`.\n Checksums outside this list MAY be used, but their names MUST be prefixed by database-provider-specific namespace prefix (see appendix `Database-Provider-Specific Namespace Prefixes`_).\n", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional" }, - "meta": { + "atime": { "anyOf": [ { - "$ref": "#/components/schemas/Meta" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "description": "a meta object containing non-standard meta-information about the error." - } - }, - "type": "object", - "title": "Error", - "description": "An error response" - }, - "ErrorLinks": { - "properties": { - "about": { + "title": "Atime", + "description": "Time of last access of a file as per POSIX standard.\n- **Type**: timestamp\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional" + }, + "ctime": { "anyOf": [ { "type": "string", - "minLength": 1, - "format": "uri" + "format": "date-time" }, { - "$ref": "#/components/schemas/Link" + "type": "null" + } + ], + "title": "Ctime", + "description": "Time of last status change of a file as per POSIX standard.\n- **Type**: timestamp\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional", + "x-optimade-unit": "\u00c5" + }, + "mtime": { + "anyOf": [ + { + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "About", - "description": "A link that leads to further details about this particular occurrence of the problem." + "title": "Mtime", + "description": " Time of last modification of a file as per POSIX standard.\n- **Type**: timestamp\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - It should be noted that the values of :field:`last_modified`, :field:`modification_timestamp` and :field:`mtime` do not necessary match.\n :field:`last_modified` pertains to the modification of the OPTIMADE metadata, :field:`modification_timestamp` pertains to file contents and :field:`mtime` pertains to the modification of the file (not necessary changing its contents).\n For example, appending an empty string to a file would result in the change of :field:`mtime` in some operating systems, but this would not be deemed as a modification of its contents.\n", + "x-optimade-queryable": "optional", + "x-optimade-support": "optional" } }, + "additionalProperties": true, "type": "object", - "title": "ErrorLinks", - "description": "A Links object specific to Error objects" + "required": [ + "last_modified", + "url", + "url_stable_until", + "name", + "size", + "media_type", + "modification_timestamp", + "description", + "checksums", + "atime", + "ctime", + "mtime" + ], + "title": "FileResourceAttributes", + "description": "This class contains the Field for the attributes used to represent a file, e.g. ." }, - "ErrorResponse": { + "FileResponseMany": { "properties": { "data": { "anyOf": [ - { - "$ref": "#/components/schemas/Resource" - }, { "items": { - "$ref": "#/components/schemas/Resource" + "$ref": "#/components/schemas/FileResource" }, "type": "array" }, { - "type": "null" + "items": { + "type": "object" + }, + "type": "array" } ], "uniqueItems": true, "title": "Data", - "description": "Outputted Data" + "description": "List of unique OPTIMADE files entry resource objects." }, "meta": { "allOf": [ @@ -2062,22 +2874,35 @@ "$ref": "#/components/schemas/ResponseMeta" } ], - "description": "A meta object containing non-standard information." + "description": "A meta object containing non-standard information" }, "errors": { - "items": { - "$ref": "#/components/schemas/OptimadeError" - }, - "type": "array", + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Error" + }, + "type": "array" + }, + { + "type": "null" + } + ], "uniqueItems": true, "title": "Errors", - "description": "A list of OPTIMADE-specific JSON API error objects, where the field detail MUST be present." + "description": "A list of unique errors" }, "included": { "anyOf": [ { "items": { - "$ref": "#/components/schemas/Resource" + "$ref": "#/components/schemas/EntryResource" + }, + "type": "array" + }, + { + "items": { + "type": "object" }, "type": "array" }, @@ -2087,7 +2912,7 @@ ], "uniqueItems": true, "title": "Included", - "description": "A list of unique included resources" + "description": "A list of unique included OPTIMADE entry resources." }, "links": { "anyOf": [ @@ -2114,42 +2939,103 @@ }, "type": "object", "required": [ - "meta", - "errors" + "data", + "meta" ], - "title": "ErrorResponse", - "description": "errors MUST be present and data MUST be skipped" + "title": "FileResponseMany" }, - "ErrorSource": { + "FileResponseOne": { "properties": { - "pointer": { + "data": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/FileResource" + }, + { + "type": "object" }, { "type": "null" } ], - "title": "Pointer", - "description": "a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute]." + "title": "Data", + "description": "A single files entry resource." }, - "parameter": { + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" + }, + "errors": { "anyOf": [ { - "type": "string" + "items": { + "$ref": "#/components/schemas/Error" + }, + "type": "array" }, { "type": "null" } ], - "title": "Parameter", - "description": "a string indicating which URI query parameter caused the error." + "uniqueItems": true, + "title": "Errors", + "description": "A list of unique errors" + }, + "included": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/EntryResource" + }, + "type": "array" + }, + { + "items": { + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "uniqueItems": true, + "title": "Included", + "description": "A list of unique included OPTIMADE entry resources." + }, + "links": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToplevelLinks" + }, + { + "type": "null" + } + ], + "description": "Links associated with the primary data or errors" + }, + "jsonapi": { + "anyOf": [ + { + "$ref": "#/components/schemas/JsonApi" + }, + { + "type": "null" + } + ], + "description": "Information about the JSON API used" } }, "type": "object", - "title": "ErrorSource", - "description": "an object containing references to the source of the error" + "required": [ + "data", + "meta" + ], + "title": "FileResponseOne" }, "Implementation": { "properties": { diff --git a/optimade/models/__init__.py b/optimade/models/__init__.py index e8e04800e..e587c9b4a 100644 --- a/optimade/models/__init__.py +++ b/optimade/models/__init__.py @@ -1,5 +1,6 @@ from .baseinfo import * # noqa: F403 from .entries import * # noqa: F403 +from .files import * # noqa: F403 from .index_metadb import * # noqa: F403 from .jsonapi import * # noqa: F403 from .links import * # noqa: F403 @@ -20,4 +21,5 @@ + references.__all__ # type: ignore[name-defined] # noqa: F405 + responses.__all__ # type: ignore[name-defined] # noqa: F405 + structures.__all__ # type: ignore[name-defined] # noqa: F405 + + files.__all__ # type: ignore[name-defined] # noqa: F405 ) diff --git a/optimade/models/files.py b/optimade/models/files.py new file mode 100644 index 000000000..9f57e092c --- /dev/null +++ b/optimade/models/files.py @@ -0,0 +1,225 @@ +# pylint: disable=no-self-argument,line-too-long,no-name-in-module +from datetime import datetime +from typing import Optional + +from optimade.models.entries import EntryResource, EntryResourceAttributes +from optimade.models.utils import OptimadeField, StrictField, SupportLevel + +__all__ = ( + "FileResourceAttributes", + "FileResource", +) + + +CORRELATED_FILE_FIELDS: tuple[set[str], ...] + + +class FileResourceAttributes(EntryResourceAttributes): + """This class contains the Field for the attributes used to represent a file, e.g. .""" + + url: str = OptimadeField( + ..., + description="""The URL to get the contents of a file. +- **Type**: string +- **Requirements/Conventions**: + + - **Support**: MUST be supported by all implementations, MUST NOT be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + - **Response**: REQUIRED in the response. + - The URL MUST point to the actual contents of a file (i.e. byte stream), not an intermediate (preview) representation. + For example, if referring to a file on GitHub, a link should point to raw contents. + +- **Examples**: + + - :val:`"https://example.org/files/cifs/1000000.cif"` +""", + support=SupportLevel.MUST, + queryable=SupportLevel.OPTIONAL, + ) + + url_stable_until: Optional[datetime] = OptimadeField( + ..., + description="""Point in time until which the URL in `url` is guaranteed to stay stable. +- **Type**: timestamp +- **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + - :val:`null` means that there is no stability guarantee for the URL in `url`. + Indefinite support could be communicated by providing a date sufficiently far in the future, for example, :val:`9999-12-31`.""", + support=SupportLevel.OPTIONAL, + queryable=SupportLevel.OPTIONAL, + ) + + name: str = OptimadeField( + ..., + description="""Base name of a file. +- **Type**: string +- **Requirements/Conventions**: + + - **Support**: MUST be supported by all implementations, MUST NOT be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + - File name extension is an integral part of a file name and, if available, MUST be included. + +- **Examples**: + + - :val:`"1000000.cif"`""", + support=SupportLevel.MUST, + queryable=SupportLevel.OPTIONAL, + ) + + size: Optional[int] = OptimadeField( + ..., + description="""Size of a file in bytes. +- **Type**: integer +- **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + - If provided, it MUST be guaranteed that either exact size of a file is given or its upper bound. + This way if a client reserves a static buffer or truncates the download stream after this many bytes the whole file would be received. + Such provision is included to allow the providers to serve on-the-fly compressed files.""", + support=SupportLevel.OPTIONAL, + queryable=SupportLevel.OPTIONAL, + ) + + media_type: Optional[str] = OptimadeField( + ..., + description="""Media type identifier (also known as MIME type), for a file as per `RFC 6838 Media Type Specifications and Registration Procedures `__. +- **Type**: string +- **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + +- **Examples**: + + - :val:`"chemical/x-cif"`""", + support=SupportLevel.OPTIONAL, + queryable=SupportLevel.OPTIONAL, + ) + + version: Optional[str] = OptimadeField( + None, + description="""Version information of a file (e.g. commit, revision, timestamp). +- **Type**: string +- **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + - If provided, it MUST be guaranteed that file contents pertaining to the same combination of :field:`id` and :field:`version` are the same""", + support=SupportLevel.OPTIONAL, + queryable=SupportLevel.OPTIONAL, + ) + + modification_timestamp: Optional[datetime] = OptimadeField( + ..., + description="""Timestamp of the last modification of file contents. + A modification is understood as an addition, change or deletion of one or more bytes, resulting in file contents different from the previous. +- **Type**: timestamp +- **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + - Timestamps of subsequent file modifications SHOULD be increasing (not earlier than previous timestamps).""", + support=SupportLevel.OPTIONAL, + queryable=SupportLevel.OPTIONAL, + ) + + description: Optional[str] = OptimadeField( + ..., + description="""Free-form description of a file. +- **Type**: string +- **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + +- **Examples**: + + - :val:`"POSCAR format file"`""", + support=SupportLevel.OPTIONAL, + queryable=SupportLevel.OPTIONAL, + ) + + checksums: Optional[dict[str, str]] = OptimadeField( + ..., + description="""Dictionary providing checksums of file contents. +* **Type**: dictionary with keys identifying checksum functions and values (strings) giving the actual checksums +* **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + - Supported dictionary keys: :property:`md5`, :property:`sha1`, :property:`sha224`, :property:`sha256`, :property:`sha384`, :property:`sha512`. + Checksums outside this list MAY be used, but their names MUST be prefixed by database-provider-specific namespace prefix (see appendix `Database-Provider-Specific Namespace Prefixes`_). +""", + support=SupportLevel.OPTIONAL, + queryable=SupportLevel.OPTIONAL, + ) + + atime: Optional[datetime] = OptimadeField( + ..., + description="""Time of last access of a file as per POSIX standard. +- **Type**: timestamp +- **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL.""", + support=SupportLevel.OPTIONAL, + queryable=SupportLevel.OPTIONAL, + ) + + ctime: Optional[datetime] = OptimadeField( + ..., + description="""Time of last status change of a file as per POSIX standard. +- **Type**: timestamp +- **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL.""", + unit="Å", + support=SupportLevel.OPTIONAL, + queryable=SupportLevel.OPTIONAL, + ) + + mtime: Optional[datetime] = OptimadeField( + ..., + description=""" Time of last modification of a file as per POSIX standard. +- **Type**: timestamp +- **Requirements/Conventions**: + + - **Support**: OPTIONAL support in implementations, i.e., MAY be :val:`null`. + - **Query**: Support for queries on this property is OPTIONAL. + - It should be noted that the values of :field:`last_modified`, :field:`modification_timestamp` and :field:`mtime` do not necessary match. + :field:`last_modified` pertains to the modification of the OPTIMADE metadata, :field:`modification_timestamp` pertains to file contents and :field:`mtime` pertains to the modification of the file (not necessary changing its contents). + For example, appending an empty string to a file would result in the change of :field:`mtime` in some operating systems, but this would not be deemed as a modification of its contents. +""", + queryable=SupportLevel.OPTIONAL, + support=SupportLevel.OPTIONAL, + ) + + +class FileResource(EntryResource): + """Representing a structure.""" + + type: str = StrictField( + "files", + description="""The name of the type of an entry. + +- **Type**: string. + +- **Requirements/Conventions**: + - **Support**: MUST be supported by all implementations, MUST NOT be `null`. + - **Query**: MUST be a queryable property with support for all mandatory filter features. + - **Response**: REQUIRED in the response. + - MUST be an existing entry type. + - The entry of type `` and ID `` MUST be returned in response to a request for `//` under the versioned base URL. + +- **Examples**: + - `"structures"`""", + pattern="^files$", + support=SupportLevel.MUST, + queryable=SupportLevel.MUST, + ) + + attributes: FileResourceAttributes diff --git a/optimade/models/responses.py b/optimade/models/responses.py index c7cf72855..3111a8b48 100644 --- a/optimade/models/responses.py +++ b/optimade/models/responses.py @@ -4,6 +4,7 @@ from optimade.models.baseinfo import BaseInfoResource from optimade.models.entries import EntryInfoResource, EntryResource +from optimade.models.files import FileResource from optimade.models.index_metadb import IndexInfoResource from optimade.models.jsonapi import Response from optimade.models.links import LinksResource @@ -22,6 +23,8 @@ "EntryResponseMany", "StructureResponseOne", "StructureResponseMany", + "FileResponseOne", + "FileResponseMany", "ReferenceResponseOne", "ReferenceResponseMany", ) @@ -137,6 +140,20 @@ class StructureResponseMany(EntryResponseMany): ] +class FileResponseOne(EntryResponseOne): + data: Union[FileResource, dict[str, Any], None] = StrictField( + ..., description="A single files entry resource." + ) + + +class FileResponseMany(EntryResponseMany): + data: Union[list[FileResource], list[dict[str, Any]]] = StrictField( + ..., + description="List of unique OPTIMADE files entry resource objects.", + uniqueItems=True, + ) + + class ReferenceResponseOne(EntryResponseOne): data: Annotated[ Optional[Union[ReferenceResource, dict[str, Any]]], diff --git a/optimade/server/config.py b/optimade/server/config.py index 8fdc5666c..f93039e7e 100644 --- a/optimade/server/config.py +++ b/optimade/server/config.py @@ -218,6 +218,12 @@ class ServerConfig(BaseSettings): mongo_uri: Annotated[str, Field(description="URI for the Mongo server")] = ( "localhost:27017" ) + files_collection: Annotated[ + str, + Field( + description="Mongo collection name for /files endpoint resources", + ), + ] = "files" links_collection: Annotated[ str, Field(description="Mongo collection name for /links endpoint resources") ] = "links" @@ -315,7 +321,7 @@ class ServerConfig(BaseSettings): ), ] = {} aliases: Annotated[ - dict[Literal["links", "references", "structures"], dict[str, str]], + dict[Literal["links", "references", "structures", "files"], dict[str, str]], Field( description=( "A mapping between field names in the database with their corresponding " @@ -324,7 +330,7 @@ class ServerConfig(BaseSettings): ), ] = {} length_aliases: Annotated[ - dict[Literal["links", "references", "structures"], dict[str, str]], + dict[Literal["links", "references", "structures", "files"], dict[str, str]], Field( description=( "A mapping between a list property (or otherwise) and an integer property " diff --git a/optimade/server/data/__init__.py b/optimade/server/data/__init__.py index c659845b9..954292fb6 100644 --- a/optimade/server/data/__init__.py +++ b/optimade/server/data/__init__.py @@ -8,6 +8,7 @@ "structures": "test_structures.json", "references": "test_references.json", "links": "test_links.json", + "files": "test_files.json", "providers": "providers.json", } diff --git a/optimade/server/data/test_files.json b/optimade/server/data/test_files.json new file mode 100644 index 000000000..2ec844099 --- /dev/null +++ b/optimade/server/data/test_files.json @@ -0,0 +1,11 @@ +[ + { + "url": "www.example.com/file001.pdb", + "name": "file001.pdb", + "size": 12536, + "media_type": "chemical/x-pdb", + "modification_timestamp": { + "$date": "2022-08-26T13:09:37.945Z" + } + } +] diff --git a/optimade/server/entry_collections/elastic_indexes.json b/optimade/server/entry_collections/elastic_indexes.json index a6f954582..376657f62 100644 --- a/optimade/server/entry_collections/elastic_indexes.json +++ b/optimade/server/entry_collections/elastic_indexes.json @@ -152,6 +152,60 @@ }, "lattice_vectors": { "type": "float" + }, + "immutable_id": { + "type": "keyword" + }, + "last_modified": { + "type": "date" + }, + "species": { + "type": "object" + } + } + } + }, + "files": { + "mappings": { + "properties": { + "url": { + "type": "keyword" + }, + "url_stable_until": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "size": { + "type": "integer" + }, + "media_type": { + "type": "str" + }, + "version": { + "type": "keyword" + }, + "modification_timestamp": { + "type": "date" + }, + "checksums": { + "type": "object" + }, + "id": { + "type": "keyword" + }, + "description": { + "type": "integer" + }, + "atime": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "mtime": { + "type": "date" } } } diff --git a/optimade/server/main.py b/optimade/server/main.py index 73b4a0140..36bd4d905 100644 --- a/optimade/server/main.py +++ b/optimade/server/main.py @@ -26,6 +26,7 @@ from optimade.server.logger import LOGGER from optimade.server.middleware import OPTIMADE_MIDDLEWARE from optimade.server.routers import ( + files, info, landing, links, @@ -35,6 +36,8 @@ ) from optimade.server.routers.utils import BASE_URL_PREFIXES, JSONAPIResponse +ENDPOINTS = (info, landing, links, references, structures, files) + if config_warnings: LOGGER.warn( f"Invalid config file or no config file provided, running server with default settings. Errors: " @@ -144,13 +147,13 @@ def load_entries(endpoint_name: str, endpoint_collection: EntryCollection): app.add_exception_handler(exception, handler) # Add various endpoints to unversioned URL -for endpoint in (info, links, references, structures, landing, versions): +for endpoint in ENDPOINTS + (versions,): app.include_router(endpoint.router) def add_major_version_base_url(app: FastAPI): """Add mandatory vMajor endpoints, i.e. all except versions.""" - for endpoint in (info, links, references, structures, landing): + for endpoint in ENDPOINTS: app.include_router(endpoint.router, prefix=BASE_URL_PREFIXES["major"]) @@ -162,5 +165,5 @@ def add_optional_versioned_base_urls(app: FastAPI): ``` """ for version in ("minor", "patch"): - for endpoint in (info, links, references, structures, landing): + for endpoint in ENDPOINTS: app.include_router(endpoint.router, prefix=BASE_URL_PREFIXES[version]) diff --git a/optimade/server/mappers/__init__.py b/optimade/server/mappers/__init__.py index 9fd710fd1..6472fe785 100644 --- a/optimade/server/mappers/__init__.py +++ b/optimade/server/mappers/__init__.py @@ -1,4 +1,5 @@ from .entries import * # noqa: F403 +from .files import * # noqa: F403 from .links import * # noqa: F403 from .references import * # noqa: F403 from .structures import * # noqa: F403 @@ -8,4 +9,5 @@ + links.__all__ # type: ignore[name-defined] # noqa: F405 + references.__all__ # type: ignore[name-defined] # noqa: F405 + structures.__all__ # type: ignore[name-defined] # noqa: F405 + + files.__all__ # type: ignore[name-defined] # noqa: F405 ) diff --git a/optimade/server/mappers/files.py b/optimade/server/mappers/files.py new file mode 100644 index 000000000..7d7760d8e --- /dev/null +++ b/optimade/server/mappers/files.py @@ -0,0 +1,8 @@ +from optimade.models.files import FileResource +from optimade.server.mappers.entries import BaseResourceMapper + +__all__ = ("FileMapper",) + + +class FileMapper(BaseResourceMapper): + ENTRY_RESOURCE_CLASS = FileResource diff --git a/optimade/server/routers/__init__.py b/optimade/server/routers/__init__.py index 266518dbe..a9157cf18 100644 --- a/optimade/server/routers/__init__.py +++ b/optimade/server/routers/__init__.py @@ -1,3 +1,4 @@ +from .files import files_coll from .links import links_coll from .references import references_coll from .structures import structures_coll @@ -6,4 +7,5 @@ "links": links_coll, "references": references_coll, "structures": structures_coll, + "files": files_coll, } diff --git a/optimade/server/routers/files.py b/optimade/server/routers/files.py new file mode 100644 index 000000000..ef52cec77 --- /dev/null +++ b/optimade/server/routers/files.py @@ -0,0 +1,54 @@ +from fastapi import APIRouter, Depends, Request + +from optimade.models import FileResource, FileResponseMany, FileResponseOne +from optimade.server.config import CONFIG +from optimade.server.entry_collections import create_collection +from optimade.server.mappers import FileMapper +from optimade.server.query_params import EntryListingQueryParams, SingleEntryQueryParams +from optimade.server.routers.utils import get_entries, get_single_entry +from optimade.server.schemas import ERROR_RESPONSES + +router = APIRouter(redirect_slashes=True) + +files_coll = create_collection( + name=CONFIG.files_collection, + resource_cls=FileResource, + resource_mapper=FileMapper, +) + + +@router.get( + "/files", + response_model=FileResponseMany, + response_model_exclude_unset=True, + tags=["Files"], + responses=ERROR_RESPONSES, +) +def get_files( + request: Request, params: EntryListingQueryParams = Depends() +) -> FileResponseMany: + return get_entries( + collection=files_coll, + response=FileResponseMany, + request=request, + params=params, + ) + + +@router.get( + "/files/{entry_id:path}", + response_model=FileResponseOne, + response_model_exclude_unset=True, + tags=["Files"], + responses=ERROR_RESPONSES, +) +def get_single_file( + request: Request, entry_id: str, params: SingleEntryQueryParams = Depends() +) -> FileResponseOne: + return get_single_entry( + collection=files_coll, + entry_id=entry_id, + response=FileResponseOne, + request=request, + params=params, + ) diff --git a/tests/server/entry_collections/test_entry_collections.py b/tests/server/entry_collections/test_entry_collections.py index 5387d77be..ef02ad90d 100644 --- a/tests/server/entry_collections/test_entry_collections.py +++ b/tests/server/entry_collections/test_entry_collections.py @@ -4,6 +4,7 @@ def test_get_attribute_fields(): """Test get_attribute_fields() method""" from optimade.models import ( + FileResourceAttributes, LinksResourceAttributes, ReferenceResourceAttributes, StructureResourceAttributes, @@ -14,6 +15,7 @@ def test_get_attribute_fields(): "links": LinksResourceAttributes, "references": ReferenceResourceAttributes, "structures": StructureResourceAttributes, + "files": FileResourceAttributes, } # Make sure we're hitting all collections diff --git a/tests/server/routers/test_files.py b/tests/server/routers/test_files.py new file mode 100644 index 000000000..b613e6976 --- /dev/null +++ b/tests/server/routers/test_files.py @@ -0,0 +1,217 @@ +from optimade.models import FileResponseMany # FileResponseOne,; FileResource, + +from ..utils import RegularEndpointTests + + +class TestFilesEndpoint(RegularEndpointTests): + """Tests for /structures""" + + request_str = "/files" + response_cls = FileResponseMany + + def test_files_endpoint_data(self): + """Check known properties/attributes for successful response""" + assert "meta" in self.json_response + assert self.json_response["meta"]["data_available"] == 17 + assert not self.json_response["meta"]["more_data_available"] + assert "data" in self.json_response + assert ( + len(self.json_response["data"]) + == self.json_response["meta"]["data_available"] + ) + + +# def test_get_next_responses(self, get_good_response): +# """Check pagination""" +# total_data = self.json_response["meta"]["data_available"] +# page_limit = 5 +# +# json_response = get_good_response( +# self.request_str + f"?page_limit={page_limit}" +# ) +# +# cursor = json_response["data"].copy() +# assert json_response["meta"]["more_data_available"] +# assert json_response["meta"]["data_returned"] == total_data +# more_data_available = True +# next_request = json_response["links"]["next"] +# +# while more_data_available: +# next_response = get_good_response(next_request) +# assert next_response["meta"]["data_returned"] == total_data +# next_request = next_response["links"]["next"] +# cursor.extend(next_response["data"]) +# more_data_available = next_response["meta"]["more_data_available"] +# if more_data_available: +# assert len(next_response["data"]) == page_limit +# else: +# assert len(next_response["data"]) == total_data % page_limit +# +# assert len(cursor) == total_data +# +# +# class TestSingleFileeEndpoint(RegularEndpointTests): +# """Tests for /files/""" +# +# test_id = "mpf_1" +# request_str = f"/files/{test_id}" +# response_cls = FileResponseOne +# +# def test_structures_endpoint_data(self): +# """Check known properties/attributes for successful response""" +# assert "data" in self.json_response +# assert self.json_response["data"]["id"] == self.test_id +# assert self.json_response["data"]["type"] == "structures" +# assert "attributes" in self.json_response["data"] +# assert "_exmpl_chemsys" in self.json_response["data"]["attributes"] +# +# +# def test_check_response_single_structure(check_response): +# """Tests whether check_response also handles single endpoint queries correctly.""" +# +# test_id = "mpf_1" +# expected_ids = "mpf_1" +# request = f"/files/{test_id}?response_fields=name" +# check_response(request, expected_ids=expected_ids) +# +# +# class TestMissingSingleStructureEndpoint(RegularEndpointTests): +# """Tests for /structures/ for unknown """ +# +# test_id = "mpf_random_string_that_is_not_in_test_data" +# request_str = f"/structures/{test_id}" +# response_cls = StructureResponseOne +# +# def test_structures_endpoint_data(self): +# """Check known properties/attributes for successful response""" +# assert "data" in self.json_response +# assert "meta" in self.json_response +# assert self.json_response["data"] is None +# assert self.json_response["meta"]["data_returned"] == 0 +# assert not self.json_response["meta"]["more_data_available"] +# +# +# class TestSingleStructureWithRelationships(RegularEndpointTests): +# """Tests for /structures/, where has relationships""" +# +# test_id = "mpf_1" +# request_str = f"/structures/{test_id}" +# response_cls = StructureResponseOne +# +# def test_structures_endpoint_data(self): +# """Check known properties/attributes for successful response""" +# assert "data" in self.json_response +# assert self.json_response["data"]["id"] == self.test_id +# assert self.json_response["data"]["type"] == "structures" +# assert "attributes" in self.json_response["data"] +# assert "relationships" in self.json_response["data"] +# assert self.json_response["data"]["relationships"] == { +# "references": {"data": [{"type": "references", "id": "dijkstra1968"}]} +# } +# assert "included" in self.json_response +# assert len( +# self.json_response["data"]["relationships"]["references"]["data"] +# ) == len(self.json_response["included"]) +# +# ReferenceResource(**self.json_response["included"][0]) +# +# +# class TestMultiStructureWithSharedRelationships(RegularEndpointTests): +# """Tests for /structures for entries with shared relationships""" +# +# request_str = '/structures?filter=id="mpf_1" OR id="mpf_2"' +# response_cls = StructureResponseMany +# +# def test_structures_endpoint_data(self): +# """Check known properties/attributes for successful response""" +# # mpf_1 and mpf_2 both contain the same reference relationship, so response should not duplicate it +# assert "data" in self.json_response +# assert len(self.json_response["data"]) == 2 +# assert "included" in self.json_response +# assert len(self.json_response["included"]) == 1 +# +# +# class TestMultiStructureWithRelationships(RegularEndpointTests): +# """Tests for /structures for mixed entries with and without relationships""" +# +# request_str = '/structures?filter=id="mpf_1" OR id="mpf_23"' +# response_cls = StructureResponseMany +# +# def test_structures_endpoint_data(self): +# """Check known properties/attributes for successful response""" +# # mpf_23 contains no relationships, which shouldn't break anything +# assert "data" in self.json_response +# assert len(self.json_response["data"]) == 2 +# assert "included" in self.json_response +# assert len(self.json_response["included"]) == 1 +# +# +# class TestMultiStructureWithOverlappingRelationships(RegularEndpointTests): +# """Tests for /structures with entries with overlapping relationships +# +# One entry has multiple relationships, another entry has other relationships, +# some of these relationships overlap between the entries, others don't. +# """ +# +# request_str = '/structures?filter=id="mpf_1" OR id="mpf_3"' +# response_cls = StructureResponseMany +# +# def test_structures_endpoint_data(self): +# """Check known properties/attributes for successful response""" +# assert "data" in self.json_response +# assert len(self.json_response["data"]) == 2 +# assert "included" in self.json_response +# assert len(self.json_response["included"]) == 2 +# +# +# class TestStructuresWithNullFieldsDoNotMatchNegatedFilters(RegularEndpointTests): +# """Tests that structures with e.g., `'assemblies':null` do not get +# returned for negated queries like `filter=assemblies != 1`, as mandated +# by the specification. +# +# """ +# +# request_str = "/structures?filter=assemblies != 1" +# response_cls = StructureResponseMany +# +# def test_structures_endpoint_data(self): +# """Check that no structures are returned.""" +# assert len(self.json_response["data"]) == 0 +# +# +# class TestStructuresWithNullFieldsMatchUnknownFilter(RegularEndpointTests): +# """Tests that structures with e.g., `'assemblies':null` do get +# returned for queries testing for "UNKNOWN" fields. +# +# """ +# +# request_str = "/structures?filter=assemblies IS UNKNOWN" +# response_cls = StructureResponseMany +# +# def test_structures_endpoint_data(self): +# """Check that all structures are returned.""" +# assert len(self.json_response["data"]) == 17 +# +# +# class TestStructuresWithUnknownResponseFields(RegularEndpointTests): +# """Tests that structures with e.g., `'assemblies':null` do get +# returned for queries testing for "UNKNOWN" fields. +# +# """ +# +# request_str = "/structures?filter=assemblies IS UNKNOWN&response_fields=assemblies,_other_provider_field,chemical_formula_anonymous" +# response_cls = StructureResponseMany +# +# def test_structures_endpoint_data(self): +# """Check that all structures are returned.""" +# assert len(self.json_response["data"]) == 17 +# keys = ("_other_provider_field", "assemblies", "chemical_formula_anonymous") +# for key in keys: +# assert all(key in doc["attributes"] for doc in self.json_response["data"]) +# assert all( +# doc["attributes"]["_other_provider_field"] is None +# for doc in self.json_response["data"] +# ) +# assert all( +# len(doc["attributes"]) == len(keys) for doc in self.json_response["data"] +# )