""".. versionadded:: 0.36.0Miscellaneous public classes"""from__future__importannotationsfromabcimportABC,abstractmethodfromcollections.abcimportIteratorfromdataclassesimportdataclassfromdatetimeimportdatetimefromfnmatchimportfnmatchcaseimportos.pathfrompathlibimportPathfromtypingimportIO,TypeVar,castfromdandischema.modelsimportDigestType
[docs]@dataclassclassDigest:"""A computed digest for a file or directory"""#: The digest algorithm usedalgorithm:DigestType#: The digest itselfvalue:str
[docs]@classmethoddefdandi_etag(cls,value:str)->Digest:""" Construct a `Digest` with the given value and a ``algorithm`` of ``DigestType.dandi_etag`` """returncls(algorithm=DigestType.dandi_etag,value=value)
[docs]@classmethoddefdandi_zarr(cls,value:str)->Digest:""" Construct a `Digest` with the given value and a ``algorithm`` of ``DigestType.dandi_zarr_checksum`` """returncls(algorithm=DigestType.dandi_zarr_checksum,value=value)
[docs]defasdict(self)->dict[DigestType,str]:""" Convert the instance to a single-item `dict` mapping the digest algorithm to the digest value """return{self.algorithm:self.value}
#: Placeholder digest used in some situations where a digest is required but#: not actually relevant and would be too expensive to calculateDUMMY_DANDI_ETAG=Digest(algorithm=DigestType.dandi_etag,value=32*"d"+"-1")DUMMY_DANDI_ZARR_CHECKSUM=Digest(algorithm=DigestType.dandi_zarr_checksum,value=32*"d"+"-1--1",)P=TypeVar("P",bound="BasePath")
[docs]@dataclass# type: ignore[misc] # <https://github.com/python/mypy/issues/5374>classBasePath(ABC):""" An abstract base class for path-like objects that can be traversed with the ``/`` operator *à la* `pathlib.Path` (though, unlike `pathlib.Path` instances, "dividing" by another non-string path is not allowed). All paths are treated as forward-slash-separated relative paths under an empty-name "root" path. """#: The path components of the objectparts:tuple[str,...]def__str__(self)->str:return"/".join(self.parts)@propertydefname(self)->str:""" The basename of the path object. When the object represents the root of a path hierarchy, this is the empty string. """ifself.is_root():return""else:assertself.partsreturnself.parts[-1]@abstractmethoddef_get_subpath(self:P,name:str)->P:""" Return the path immediately under the instance with the given name. A name of ``"."`` should cause ``self`` to be returned, and a name of ``".."`` should cause ``self.parent`` to be returned. An empty name or a name containing a forward slash should result in a `ValueError`. """...def__truediv__(self:P,path:str)->P:p=selfforqinself._split_path(path):p=p._get_subpath(q)returnp
[docs]defjoinpath(self:P,*paths:str)->P:""" Combine the path with each name or relative path in ``paths`` using the ``/`` operator """p=selfforqinpaths:p/=qreturnp
@staticmethoddef_split_path(path:str)->tuple[str,...]:"""Split a path into its path components"""ifpath.startswith("/"):raiseValueError(f"Absolute paths not allowed: {path!r}")returntuple(qforqinpath.split("/")ifq)
[docs]defis_root(self)->bool:""" Returns true if this path object represents the root of its hierarchy """returnself.parts==()
@propertydefroot_path(self:P)->P:"""The root of the path object's hierarchy"""p=selfwhilenotp.is_root():p=p.parentreturnp@property@abstractmethoddefparent(self:P)->P:""" The parent path of the object. The parent of the root of a path hierarchy is itself. """...@propertydefparents(self:P)->tuple[P,...]:""" A tuple of the path's ancestors, starting at the parent and going up to (and including) the root of the hierarchy """ps:list[P]=[]p=selfwhilenotp.is_root():q=p.parentps.append(q)p=qreturntuple(ps)
[docs]defwith_name(self:P,name:str)->P:"""Equivalent to ``p.parent / name``"""returnself.parent/name
@propertydefsuffix(self)->str:"""The final file extension of the basename, if any"""i=self.name.rfind(".")if0<i<len(self.name)-1:returnself.name[i:]else:return""@propertydefsuffixes(self)->list[str]:"""A list of the basename's file extensions"""ifself.name.endswith("."):return[]name=self.name.lstrip(".")return["."+suffixforsuffixinname.split(".")[1:]]@propertydefstem(self)->str:"""The basename without its final file extension, if any"""i=self.name.rfind(".")if0<i<len(self.name)-1:returnself.name[:i]else:returnself.name
[docs]defwith_stem(self:P,stem:str)->P:"""Returns a new path with the stem changed"""returnself.with_name(stem+self.suffix)
[docs]defwith_suffix(self:P,suffix:str)->P:"""Returns a new path with the final file extension changed"""if"/"insuffixor(suffixandnotsuffix.startswith("."))orsuffix==".":raiseValueError(f"Invalid suffix: {suffix!r}")ifnotself.name:raiseValueError("Path has an empty name")ifnotself.suffix:name=self.name+suffixelse:name=self.name[:-len(self.suffix)]+suffixreturnself.with_name(name)
[docs]defmatch(self,pattern:str)->bool:"""Tests whether the path matches the given glob pattern"""patparts=self._split_path(pattern)ifnotpatparts:raiseValueError("Empty pattern")iflen(patparts)>len(self.parts):returnFalseforpart,patinzip(reversed(self.parts),reversed(patparts)):ifnotfnmatchcase(part,pat):returnFalsereturnTrue
[docs]@abstractmethoddefexists(self)->bool:"""True iff the resource at the given path exists"""...
[docs]@abstractmethoddefis_file(self)->bool:"""True if the resource at the given path exists and is a file"""...
[docs]@abstractmethoddefis_dir(self)->bool:"""True if the resource at the given path exists and is a directory"""...
[docs]@abstractmethoddefiterdir(self:P)->Iterator[P]:""" Returns a generator of the paths under the instance, which must be a directory """...
@property@abstractmethoddefsize(self)->int:"""The size of the resource at the path"""...
[docs]classReadable(ABC):""" .. versionadded:: 0.50.0 An abstract base class representing a local or remote resource that can be opened & read like a file """
[docs]@abstractmethoddefopen(self)->IO[bytes]:""" Returns a readable binary filehandle for accessing the resource's bytes """...
[docs]@abstractmethoddefget_size(self)->int:"""Returns the size in bytes of the resource"""...
[docs]@abstractmethoddefget_mtime(self)->datetime|None:""" Returns the time at which the resource's contents were last modified, if it can be determined """...
[docs]@abstractmethoddefget_filename(self)->str:""" Returns the base name of the resource, suitable for use as a file name """...
[docs]classLocalReadableFile(Readable):""" A concrete implementation of `Readable` for local files. Instances of this class are obtained by calling `LocalFileAsset.as_readable()` or `DandisetMetadataFile.as_readable()`. """def__init__(self,filepath:str|Path)->None:#: The path to a local file to readself.filepath=Path(filepath)def__fspath__(self)->str:returnstr(self.filepath)def__str__(self)->str:returnstr(self.filepath)
[docs]@dataclassclassRemoteReadableAsset(Readable):""" A concrete implementation of `Readable` for DANDI blob assets on a remote server. The fsspec_ library must be installed with the ``http`` extra (e.g., ``pip install "fsspec[http]"``) in order for `.open()` to be usable. Instances of this class are obtained by calling `BaseRemoteBlobAsset.as_readable()`. .. _fsspec: http://github.com/fsspec/filesystem_spec """#: The URL that data is read fromurl:str#: :meta private:size:int#: :meta private:mtime:datetime|None#: :meta private:name:str
[docs]defopen(self)->IO[bytes]:# Optional dependency:importfsspec# We need to call open() on the return value of fsspec.open() because# otherwise the filehandle will only be opened when used to enter a# context manager.returncast(IO[bytes],fsspec.open(self.url,mode="rb").open())