Skip to content

Commit 76e92a4

Browse files
committed
feat(rocrate): ✨ add check_availability() with AvailabilityStatus on entities
Replace ROCrateEntity.is_available() with check_availability() returning AvailabilityStatus, resolve @id via is_external_reference(), and report non-natively-checkable remote schemes as UNCHECKABLE. is_available() becomes a boolean wrapper.
1 parent 469bbe5 commit 76e92a4

1 file changed

Lines changed: 57 additions & 40 deletions

File tree

rocrate_validator/rocrate.py

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from rocrate_validator.errors import ROCrateInvalidURIError
3131
from rocrate_validator.utils.uri import validate_rocrate_uri
3232
from rocrate_validator.utils.http import HttpRequester
33-
from rocrate_validator.utils.uri import URI
33+
from rocrate_validator.utils.uri import URI, AvailabilityStatus, is_external_reference
3434

3535
# set up logging
3636
logger = logging.getLogger(__name__)
@@ -140,8 +140,17 @@ def id_as_path(self) -> Path:
140140
@classmethod
141141
def get_id_as_uri(cls, entity_id: str, ro_crate: ROCrate) -> URI:
142142
assert entity_id, "Entity ID cannot be None"
143-
if entity_id.startswith("http"):
143+
# Per RO-Crate 1.1 § 4.2.2, an `@id` is either a relative URI path or
144+
# an external URI/IRI (RFC 3986/3987). External references are used
145+
# as-is (without resolving them against the crate URI) so the entity
146+
# is classified as remote/web-based; this covers both authority-based
147+
# forms (``http://``, ``scp://``) and scheme-only ones (``urn:``,
148+
# ``doi:``, ``arcp:``).
149+
if is_external_reference(entity_id):
144150
return URI(entity_id)
151+
# Otherwise the `@id` is a relative path: if the RO-Crate itself is
152+
# remote, resolve it against the crate URI so the entity is still
153+
# classified as remote/web-based.
145154
if ro_crate.uri.is_remote_resource():
146155
if entity_id.startswith("./"):
147156
return URI(f"{ro_crate.uri}/{entity_id[2:]}")
@@ -208,58 +217,66 @@ def raw_data(self) -> object:
208217
def is_local(self) -> bool:
209218
return not self.is_remote()
210219

211-
def is_available(self) -> bool:
220+
def check_availability(self) -> AvailabilityStatus:
221+
"""
222+
Return a fine-grained availability status for this entity.
223+
224+
This is the primary check; :meth:`is_available` is the boolean
225+
shortcut built on top of it. The status distinguishes definitely
226+
unavailable resources, auth-protected ones, and remote URIs whose
227+
scheme the validator cannot natively check (scp://, s3://, ...).
228+
"""
212229
try:
213-
# check if the entity points to an external file
214-
if self.id.startswith("http"):
230+
entity_uri = self.id_as_uri
231+
# Remote entities with a scheme we can natively reach are checked
232+
# by inspecting the remote response status.
233+
if entity_uri.is_natively_checkable():
215234
logger.debug("Checking the availability of a remote entity")
216-
return self.ro_crate.get_external_file_size(self.id) > 0
217-
218-
# check if the entity is part of the local RO-Crate
235+
return entity_uri.check_availability()
236+
237+
# Remote entities with a non-natively-checkable scheme cannot be
238+
# verified (scp://, sftp://, s3://, ...): report UNCHECKABLE so
239+
# callers can warn without invalidating the validation.
240+
if entity_uri.is_remote_resource():
241+
logger.debug(
242+
"Cannot natively verify availability for entity '%s' (scheme '%s')",
243+
self.id,
244+
entity_uri.scheme,
245+
)
246+
return AvailabilityStatus.UNCHECKABLE
247+
248+
# Local entity: locate it inside the (local or remote) RO-Crate.
219249
if self.ro_crate.uri.is_local_resource():
220-
# check if the file exists in the local file system
221250
if isinstance(self.ro_crate, ROCrateLocalFolder):
222-
logger.debug(
223-
"Checking the availability of a local entity in a local folder"
224-
)
225-
return self.ro_crate.has_file(
226-
self.id_as_path
227-
) or self.ro_crate.has_directory(self.id_as_path)
228-
# check if the file exists in the local zip file
251+
found = self.ro_crate.has_file(self.id_as_path) or self.ro_crate.has_directory(self.id_as_path)
252+
return AvailabilityStatus.AVAILABLE if found else AvailabilityStatus.UNAVAILABLE
229253
if isinstance(self.ro_crate, ROCrateLocalZip):
230-
logger.debug(
231-
"Checking the availability of a local entity in a local zip file"
232-
)
233-
# Skip the check for the root of a ZIP archive
234254
if self.id == "./":
235-
logger.debug(
236-
"Skipping the check for the presence of the Data Entity '%s' within the RO-Crate "
237-
"as it is the root of a ZIP archive",
238-
self.id,
239-
)
240-
return True
241-
return self.ro_crate.has_directory(
255+
return AvailabilityStatus.AVAILABLE
256+
found = self.ro_crate.has_directory(unquote(str(self.id))) or self.ro_crate.has_file(
242257
unquote(str(self.id))
243-
) or self.ro_crate.has_file(unquote(str(self.id)))
258+
)
259+
return AvailabilityStatus.AVAILABLE if found else AvailabilityStatus.UNAVAILABLE
244260

245-
# check if the entity is part of the remote RO-Crate
246-
logger.debug(
247-
"Checking the availability of a remote entity in a remote RO-Crate"
248-
)
249261
if self.ro_crate.uri.is_remote_resource():
250262
if self.id == "./":
251-
return self.ro_crate.get_file_size(Path(self.id_as_uri())) > 0
252-
return self.ro_crate.has_directory(
253-
unquote(str(self.id))
254-
) or self.ro_crate.has_file(unquote(str(self.id)))
263+
found = self.ro_crate.get_file_size(Path(self.id_as_uri())) > 0
264+
else:
265+
found = self.ro_crate.has_directory(unquote(str(self.id))) or self.ro_crate.has_file(
266+
unquote(str(self.id))
267+
)
268+
return AvailabilityStatus.AVAILABLE if found else AvailabilityStatus.UNAVAILABLE
255269
except Exception as e:
256270
if logger.isEnabledFor(logging.DEBUG):
257271
logger.exception(e)
258-
return False
272+
return AvailabilityStatus.UNAVAILABLE
259273

260-
raise ROCrateInvalidURIError(
261-
uri=self.id, message="Could not determine the availability of the entity"
262-
)
274+
# Fallthrough: the crate URI is neither a recognized local nor a
275+
# remote resource — the entity location cannot be determined.
276+
raise ROCrateInvalidURIError(uri=self.id, message="Could not determine the availability of the entity")
277+
278+
def is_available(self) -> bool:
279+
return self.check_availability() == AvailabilityStatus.AVAILABLE
263280

264281
def get_size(self) -> int:
265282
try:

0 commit comments

Comments
 (0)