Skip to main content
`Ctrl`+`K`

# Source code for silx.io.url

    # /*##########################################################################
    #
    # Copyright (c) 2016-2023 European Synchrotron Radiation Facility
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    # THE SOFTWARE.
    #
    # ###########################################################################*/
    """URL module"""

    from __future__ import annotations

    __authors__ = ["V. Valls"]
    __license__ = "MIT"
    __date__ = "29/01/2018"

    import logging
    from collections.abc import Iterable
    from typing import Union
    import urllib.parse
    from pathlib import Path

    _logger = logging.getLogger(__name__)

    SliceLike = Union[slice, int, type(Ellipsis)]

    def _slice_to_string(s: SliceLike) -> str:
        """Convert a Python slice into a string"""
        if s == Ellipsis:
            return "..."
        elif isinstance(s, slice):
            result = ""
            if s.start is None:
                result += ":"
            else:
                result += f"{s.start}:"
            if s.stop is not None:
                result += f"{s.stop}"
            if s.step is not None:
                result += f":{s.step}"
            return result
        elif isinstance(s, int):
            return str(s)
        else:
            raise TypeError("Unexpected slicing type. Found %s" % type(s))

    def slice_sequence_to_string(data_slice: Iterable[SliceLike] | SliceLike) -> str:
        """Convert a Python slice sequence or a slice into a string"""
        if isinstance(data_slice, Iterable):
            return ",".join([_slice_to_string(s) for s in data_slice])
        else:
            return _slice_to_string(data_slice)

    class DataUrl(object):
        """Non-mutable object to parse a string representing a resource data
        locator.

        It supports:

        - path to file and path inside file to the data
        - data slicing
        - fabio or silx access to the data
        - absolute and relative file access

        >>> # fabio access using absolute path
        >>> DataUrl("fabio:///data/image.edf?slice=2")
        >>> DataUrl("fabio:///C:/data/image.edf?slice=2")

        >>> # silx access using absolute path
        >>> DataUrl("silx:///data/image.h5?path=/data/dataset&slice=1,5")
        >>> DataUrl("silx:///data/image.edf?path=/scan_0/detector/data")
        >>> DataUrl("silx:///C:/data/image.edf?path=/scan_0/detector/data")

        >>> # `path=` can be omitted if there are no other query keys
        >>> DataUrl("silx:///data/image.h5?/data/dataset")
        >>> # is the same as
        >>> DataUrl("silx:///data/image.h5?path=/data/dataset")

        >>> # `::` can be used instead of `?` which can be useful with shell in
        >>> # command lines
        >>> DataUrl("silx:///data/image.h5::/data/dataset")
        >>> # is the same as
        >>> DataUrl("silx:///data/image.h5?/data/dataset")

        >>> # Relative path access
        >>> DataUrl("silx:./image.h5")
        >>> DataUrl("fabio:./image.edf")
        >>> DataUrl("silx:image.h5")
        >>> DataUrl("fabio:image.edf")

        >>> # It also supports parsing of file access for convenience
        >>> DataUrl("./foo/bar/image.edf")
        >>> DataUrl("C:/data/")

        >>> # HSDS urls are supported with a http or https schema
        >>> DataUrl("http://hsds-server.tld:8080/home/file")

        :param path: Path representing a link to a data. If specified, other
            arguments must not be provided.
        :param file_path: Link to the file containing the the data.
            None if there is no data selection.
        :param data_path: Data selection applied to the data file selected.
            None if there is no data selection.
        :param data_slice: Slicing applied of the selected
            data. None if no slicing applied.
        :param scheme: Scheme of the URL. "silx", "fabio"
            is supported. Other strings can be provided, but :meth:`is_valid` will
            be false.
        """

        def __init__(
            self,
            path: str | Path | None = None,
            file_path: str | Path | None = None,
            data_path: str | None = None,
            data_slice: tuple[SliceLike, ...] | None = None,
            scheme: str | None = None,
        ):
            self.__is_valid = False
            if path is not None:
                assert file_path is None
                assert data_path is None
                assert data_slice is None
                assert scheme is None
                self.__parse_from_path(str(path))
            else:
                if file_path is not None:
                    file_path = str(file_path)
                self.__file_path = file_path
                self.__data_path = data_path
                self.__data_slice = data_slice
                self.__scheme = scheme
                self.__path = None
                self.__check_validity()

        def __eq__(self, other):
            if not isinstance(other, DataUrl):
                return False
            if self.is_valid() != other.is_valid():
                return False
            if self.is_valid():
                if self.__scheme != other.scheme():
                    return False
                if self.__file_path != other.file_path():
                    return False
                if self.__data_path != other.data_path():
                    return False
                if self.__data_slice != other.data_slice():
                    return False
                return True
            else:
                return self.__path == other.path()

        def __ne__(self, other):
            return not (self == other)

        def __repr__(self):
            return str(self)

        def __str__(self):
            if self.is_valid() or self.__path is None:

                def quote_string(string):
                    if isinstance(string, str):
                        return "'%s'" % string
                    else:
                        return string

                template = "DataUrl(valid=%s, scheme=%s, file_path=%s, data_path=%s, data_slice=%s)"
                return template % (
                    self.__is_valid,
                    quote_string(self.__scheme),
                    quote_string(self.__file_path),
                    quote_string(self.__data_path),
                    self.__data_slice,
                )
            else:
                template = "DataUrl(valid=%s, string=%s)"
                return template % (self.__is_valid, self.__path)

        def __check_validity(self):
            """Check the validity of the attributes."""
            if self.__file_path in [None, ""]:
                self.__is_valid = False
                return

            if self.__scheme is None:
                self.__is_valid = True
            elif self.__scheme == "fabio":
                self.__is_valid = self.__data_path is None
            elif self.__scheme == "silx":
                # If there is a slice you must have a data path
                # But you can have a data path without slice
                slice_implies_data = (
                    self.__data_path is None and self.__data_slice is None
                ) or self.__data_path is not None
                self.__is_valid = slice_implies_data
            elif self.__scheme in ("http", "https"):
                # is an HSDS url for h5pyd
                self.__is_valid = True
            else:
                self.__is_valid = False

        @staticmethod
        def _parse_slice(slice_string: str) -> tuple[SliceLike, ...]:
            """Parse a slicing sequence and return an associated tuple.

            It supports a sequence of `...`, `:`, and integers separated by a coma.
            """

            def string_to_slice(string: str) -> SliceLike:
                """Convert a string to a Python slice"""
                if string == "...":
                    return Ellipsis
                elif ":" in string:
                    if string == ":":
                        return slice(None)
                    else:

                        def get_value(my_str):
                            if my_str in ("", None):
                                return None
                            else:
                                return int(my_str)

                        sss = string.split(":")
                        start = get_value(sss[0])
                        stop = get_value(sss[1] if len(sss) > 1 else None)
                        step = get_value(sss[2] if len(sss) > 2 else None)
                        return slice(start, stop, step)
                else:
                    return int(string)

            if slice_string == "":
                raise ValueError("An empty slice is not valid")

            tokens = slice_string.split(",")
            data_slice: list[SliceLike] = []
            for t in tokens:
                try:
                    data_slice.append(string_to_slice(t))
                except ValueError:
                    raise ValueError("'%s' is not a valid slicing" % t)
            return tuple(data_slice)

        def __parse_from_path(self, path: str):
            """Parse the path and initialize attributes.

            :param path: Path representing the URL.
            """
            self.__path = path
            # only replace if ? not here already. Otherwise can mess sith
            # data_slice if == ::2 for example
            if "?" not in path:
                path = path.replace("::", "?", 1)
            url = urllib.parse.urlparse(path)
            is_valid = True

            if len(url.scheme) <= 2:
                # Windows driver
                scheme = None
                pos = self.__path.index(url.path)
                file_path = self.__path[0:pos] + url.path
            else:
                scheme = url.scheme if url.scheme != "" else None
                file_path = f"{url.netloc}{url.path}"

                # Check absolute windows path
                if len(file_path) > 2 and file_path[0] == "/":
                    if file_path[1] == ":" or file_path[2] == ":":
                        file_path = file_path[1:]

            self.__scheme = scheme
            self.__file_path = file_path

            query = urllib.parse.parse_qsl(url.query, keep_blank_values=True)
            if len(query) == 1 and query[0][1] == "":
                # there is no query keys
                data_path = query[0][0]
                data_slice = None
            else:
                merged_query = {}
                for name, value in query:
                    if name in query:
                        merged_query[name].append(value)
                    else:
                        merged_query[name] = [value]

                def pop_single_value(merged_query, name):
                    if name in merged_query:
                        values = merged_query.pop(name)
                        if len(values) > 1:
                            _logger.warning(
                                "More than one query key named '%s'. The last one is used.",
                                name,
                            )
                        value = values[-1]
                    else:
                        value = None
                    return value

                data_path = pop_single_value(merged_query, "path")
                data_slice = pop_single_value(merged_query, "slice")
                if data_slice is not None:
                    try:
                        data_slice = self._parse_slice(data_slice)
                    except ValueError:
                        is_valid = False
                        data_slice = None

                for key in merged_query.keys():
                    _logger.warning("Query key %s unsupported. Key skipped.", key)

            self.__data_path = data_path
            self.__data_slice = data_slice

            if is_valid:
                self.__check_validity()
            else:
                self.__is_valid = False

        def is_valid(self) -> bool:
            """Returns true if the URL is valid. Else attributes can be None."""
            return self.__is_valid

        def path(self) -> str:
            """Returns the string representing the URL."""
            if self.__path is not None:
                return self.__path

            if self.__data_path is not None and self.__data_slice is None:
                query = self.__data_path
            else:
                queries = []
                if self.__data_path is not None:
                    queries.append("path=" + self.__data_path)
                if self.__data_slice is not None:
                    data_slice = slice_sequence_to_string(self.__data_slice)
                    queries.append("slice=" + data_slice)
                query = "&".join(queries)

            path = ""
            if self.__file_path is not None:
                path += self.__file_path

            if query != "":
                path = path + "?" + query

            if self.__scheme is not None:
                if self.is_absolute():
                    if path.startswith("/"):
                        path = self.__scheme + "://" + path
                    else:
                        path = self.__scheme + ":///" + path
                else:
                    path = self.__scheme + "://" + path

            return path

        def is_absolute(self) -> bool:
            """Returns true if the file path is an absolute path."""
            file_path = self.file_path()
            if file_path is None:
                return False
            if len(file_path) > 0:
                if file_path[0] == "/":
                    return True
            if len(file_path) > 2:
                # Windows
                if file_path[1] == ":" or file_path[2] == ":":
                    return True
            elif len(file_path) > 1:
                # Windows
                if file_path[1] == ":":
                    return True
            return False

        def file_path(self) -> str:
            """Returns the path to the file containing the data."""
            return self.__file_path

        def data_path(self) -> str | None:
            """Returns the path inside the file to the data."""
            return self.__data_path

        def data_slice(self) -> tuple[SliceLike, ...] | None:
            """Returns the slicing applied to the data.

            It is a tuple containing numbers, slice or ellipses.
            """
            return self.__data_slice

        def scheme(self) -> str | None:
            """Returns the scheme. It can be None if no scheme is specified."""
            return self.__scheme