def _on_database_select(self, _):
        """Load chosen database"""
        self.structure_drop.reset()

        if (self.database[1] is None
                or getattr(self.database[1], "base_url", None) is None):
            self.query_button.tooltip = "Search - No database chosen"
            self.freeze()
        else:
            self.offset = 0
            self.number = 1
            self.structure_page_chooser.silent_reset()
            try:
                self.freeze()

                self.query_button.description = "Updating ..."
                self.query_button.icon = "cog"
                self.query_button.tooltip = "Updating filters ..."

                self._set_intslider_ranges()
                self._set_version()
            except Exception as exc:  # pylint: disable=broad-except
                LOGGER.error(
                    "Exception raised during setting IntSliderRanges: %s",
                    exc.with_traceback(),
                )
            finally:
                self.query_button.description = "Search"
                self.query_button.icon = "search"
                self.query_button.tooltip = "Search"
                self.unfreeze()
    def _set_version(self):
        """Set self.database_version from an /info query"""
        base_url = self.database[1].base_url
        if base_url not in self.__cached_versions:
            # Retrieve and cache version
            response = perform_optimade_query(
                base_url=self.database[1].base_url, endpoint="/info")
            msg, _ = handle_errors(response)
            if msg:
                raise QueryError(msg)

            if "meta" not in response:
                raise QueryError(
                    f"'meta' field not found in /info endpoint for base URL: {base_url}"
                )
            if "api_version" not in response["meta"]:
                raise QueryError(
                    f"'api_version' field not found in 'meta' for base URL: {base_url}"
                )

            version = response["meta"]["api_version"]
            if version.startswith("v"):
                version = version[1:]
            self.__cached_versions[base_url] = version
            LOGGER.debug(
                "Cached version %r for base URL: %r",
                self.__cached_versions[base_url],
                base_url,
            )

        self.database_version = self.__cached_versions[base_url]
 def _on_database_change(self, change):
     """Update database summary, since self.database has been changed"""
     LOGGER.debug("Database changed in summary. New value: %r",
                  change["new"])
     if not change["new"] or change["new"] is None:
         self.database_summary.value = ""
     else:
         self._update_database()
 def _on_provider_change(self, change: dict):
     """Update provider summary, since self.provider has been changed"""
     LOGGER.debug("Provider changed in summary. New value: %r",
                  change["new"])
     self.database_summary.value = ""
     if not change["new"] or change["new"] is None:
         self.provider_summary.value = ""
     else:
         self._update_provider()
Beispiel #5
0
 def __init__(self, *args: Tuple[Any]):
     LOGGER.error(
         "%s raised.\nError message: %s\nAbout this exception: %s",
         args[0].__class__.__name__ if args
         and isinstance(args[0], Exception) else self.__class__.__name__,
         str(args[0]) if args else "",
         args[0].__doc__
         if args and isinstance(args[0], Exception) else self.__doc__,
     )
     super().__init__(*args)
Beispiel #6
0
 def __init__(self, *args: Tuple[Any]):
     LOGGER.warning(
         "%s warned.\nWarning message: %s\nAbout this warning: %s",
         args[0].__class__.__name__ if args
         and isinstance(args[0], Exception) else self.__class__.__name__,
         str(args[0]) if args else "",
         args[0].__doc__
         if args and isinstance(args[0], Exception) else self.__doc__,
     )
     super().__init__(*args)
    def _get_file(filename: str) -> Union[str, bytes]:
        """Read and return file"""
        path = Path(filename).resolve()
        LOGGER.debug("Trying image file path: %s", str(path))
        if path.exists() and path.is_file():
            with open(path, "rb") as file_handle:
                res = file_handle.read()
            return res

        LOGGER.debug("File %s either does not exist or is not a file", str(path))
        return ""
    def _query(self, link: str = None) -> dict:
        """Query helper function"""
        # If a complete link is provided, use it straight up
        if link is not None:
            try:
                response = requests.get(link, timeout=TIMEOUT_SECONDS).json()
            except (
                    requests.exceptions.ConnectTimeout,
                    requests.exceptions.ConnectionError,
            ) as exc:
                response = {
                    "errors": {
                        "msg": "CLIENT: Connection error or timeout.",
                        "url": link,
                        "Exception": repr(exc),
                    }
                }
            except JSONDecodeError as exc:
                response = {
                    "errors": {
                        "msg": "CLIENT: Could not decode response to JSON.",
                        "url": link,
                        "Exception": repr(exc),
                    }
                }
            return response

        # Avoid structures with null positions and with assemblies.
        add_to_filter = 'NOT structure_features HAS ANY "assemblies"'
        if not self._uses_new_structure_features():
            add_to_filter += ',"unknown_positions"'

        optimade_filter = self.filters.collect_value()
        optimade_filter = ("( {} ) AND ( {} )".format(optimade_filter,
                                                      add_to_filter)
                           if optimade_filter else add_to_filter)
        LOGGER.debug("Querying with filter: %s", optimade_filter)

        # OPTIMADE queries
        queries = {
            "base_url": self.database[1].base_url,
            "filter": optimade_filter,
            "page_limit": self.page_limit,
            "page_offset": self.offset,
            "page_number": self.number,
        }
        LOGGER.debug(
            "Parameters (excluding filter) sent to query util func: %s",
            {key: value
             for key, value in queries.items() if key != "filter"},
        )

        return perform_optimade_query(**queries)
    def operator_and_integer(field: str, value: str) -> str:
        """Handle operator for values with integers and a possible operator prefixed"""
        LOGGER.debug(
            "Parsing input with operator_and_integer. <field: %r>, <value: %r>",
            field,
            value,
        )

        match_operator = re.findall(r"[<>]?=?", value)
        match_no_operator = re.findall(r"^\s*[0-9]+", value)

        LOGGER.debug(
            "Finding all operators (or none):\nmatch_operator: %r\nmatch_no_operator: %r",
            match_operator,
            match_no_operator,
        )

        if match_operator and any(match_operator):
            match_operator = [_ for _ in match_operator if _]
            if len(match_operator) != 1:
                raise ParserError(
                    "Multiple values given with operators.",
                    field,
                    value,
                    extras=("match_operator", match_operator),
                )
            number = re.findall(r"[0-9]+", value)[0]
            operator = match_operator[0].replace(r"\s*", "")
            return f"{operator}{number}"
        if match_no_operator and any(match_no_operator):
            match_no_operator = [_ for _ in match_no_operator if _]
            if len(match_no_operator) != 1:
                raise ParserError(
                    "Multiple values given, must be an integer, "
                    "either with or without an operator prefixed.",
                    field,
                    value,
                    extras=("match_no_operator", match_no_operator),
                )
            result = match_no_operator[0].replace(r"\s*", "")
            return f"={result}"
        raise ParserError(
            "Not proper input. Should be, e.g., '>=3' or '5'",
            field,
            value,
            extras=[
                ("match_operator", match_operator),
                ("match_no_operator", match_no_operator),
            ],
        )
    def ranged_int(field: str, value: Tuple[int, int]) -> str:
        """Turn IntRangeSlider widget value into OPTIMADE filter string"""
        LOGGER.debug("ranged_int: Received value %r for field %r", value,
                     field)

        low, high = value
        if low == high:
            # Exactly N of property
            res = f"={low}"
        else:
            # Range of property
            res = [f">={low}", f"<={high}"]

        LOGGER.debug("ranged_int: Concluded the response is %r", res)

        return res
    def _uses_new_structure_features(self) -> bool:
        """Check whether self.database_version is >= v1.0.0-rc.2"""
        critical_version = SemanticVersion("1.0.0-rc.2")
        version = SemanticVersion(self.database_version)

        LOGGER.debug("Semantic version: %r", version)

        if version.base_version > critical_version.base_version:
            return True

        if version.base_version == critical_version.base_version:
            if version.prerelease:
                return version.prerelease >= critical_version.prerelease

            # Version is bigger than critical version and is not a pre-release
            return True

        # Major.Minor.Patch is lower than critical version
        return False
 def _toggle_debug_logging(change: dict):
     """Set logging level depending on toggle button"""
     if change["new"]:
         # Set logging level DEBUG
         WIDGET_HANDLER.setLevel(logging.DEBUG)
         LOGGER.info("Set log output in widget to level DEBUG")
         LOGGER.debug("This should now be shown")
     else:
         # Set logging level to INFO
         WIDGET_HANDLER.setLevel(logging.INFO)
         LOGGER.info("Set log output in widget to level INFO")
         LOGGER.debug("This should now NOT be shown")
    def update_ranged_inputs(self, change: dict):
        """Update ranged inputs' min/max values"""
        ranges = change["new"]
        if not ranges or ranges is None:
            return

        for field, config in ranges.items():
            if field not in self.query_fields:
                raise ParserError(
                    field=field,
                    value="N/A",
                    extras=[
                        ("config", config),
                        ("self.query_fields.keys", self.query_fields.keys()),
                    ],
                    msg=
                    "Provided field is unknown. Can not update range for unknown field.",
                )

            widget = self.query_fields[field].input_widget
            cached_value: Tuple[int, int] = widget.value
            for attr in ("min", "max"):
                if attr in config:
                    try:
                        new_value = int(config[attr])
                    except (TypeError, ValueError) as exc:
                        raise ParserError(
                            field=field,
                            value=cached_value,
                            extras=[("attr", attr),
                                    ("config[attr]", config[attr])],
                            msg=
                            f"Could not cast config[attr] to int. Exception: {exc!s}",
                        )

                    LOGGER.debug(
                        "Setting %s for %s to %d.\nWidget immediately before: %r",
                        attr,
                        field,
                        new_value,
                        widget,
                    )

                    # Since "min" is always set first, to be able to set "min" to a valid value,
                    # "max" is first set to the new "min" value + 1 IF the new "min" value is
                    # larger than the current "max" value, otherwise there is no reason,
                    # and it may indeed lead to invalid attribute setting, if this is done.
                    # For "max", coming last, this should then be fine, as the new "min" and "max"
                    # values should never be an invalid pair.
                    if attr == "min" and new_value > cached_value[1]:
                        widget.max = new_value + 1

                    setattr(widget, attr, new_value)

                    LOGGER.debug("Updated widget %r:\n%r", attr, widget)

            widget.value = (widget.min, widget.max)

            LOGGER.debug("Final state, updated widget:\n%r", widget)
    def _update_child_dbs(
            data: List[dict]) -> Tuple[List[str], List[LinksResource]]:
        """Update child DB dropdown from response data"""
        child_dbs = []
        exclude_dbs = []

        for entry in data:
            child_db = update_old_links_resources(entry)
            if child_db is None:
                continue

            attributes = child_db.attributes

            # Skip if not a 'child' link_type database
            if attributes.link_type != LinkType.CHILD:
                LOGGER.debug(
                    "Skip %s: Links resource not a %r link_type, instead: %r",
                    attributes.name,
                    LinkType.CHILD,
                    attributes.link_type,
                )
                continue

            # Skip if there is no base_url
            if attributes.base_url is None:
                LOGGER.debug(
                    "Skip %s: Base URL found to be None for child DB: %r",
                    attributes.name,
                    child_db,
                )
                exclude_dbs.append(child_db.id)
                continue

            versioned_base_url = get_versioned_base_url(attributes.base_url)
            if versioned_base_url:
                attributes.base_url = versioned_base_url
            else:
                # Not a valid/supported child DB: skip
                LOGGER.debug(
                    "Skip %s: Could not determine versioned base URL for child DB: %r",
                    attributes.name,
                    child_db,
                )
                exclude_dbs.append(child_db.id)
                continue

            child_dbs.append((attributes.name, attributes))

        return exclude_dbs, child_dbs
    def _get_more_child_dbs(self, change):
        """Query for more child DBs according to page_offset"""
        if self.providers.value is None:
            # This may be called if a provider is suddenly removed (bad provider)
            return

        pageing: Union[int, str] = change["new"]
        LOGGER.debug(
            "Detected change in page_chooser's .page_offset, .page_number, or .page_link: %r",
            pageing,
        )
        if change["name"] == "page_offset":
            LOGGER.debug(
                "Got offset %d to retrieve more child DBs from %r",
                pageing,
                self.providers.value,
            )
            self.offset = pageing
            pageing = None
        elif change["name"] == "page_number":
            LOGGER.debug(
                "Got number %d to retrieve more child DBs from %r",
                pageing,
                self.providers.value,
            )
            self.number = pageing
            pageing = None
        else:
            LOGGER.debug(
                "Got link %r to retrieve more child DBs from %r",
                pageing,
                self.providers.value,
            )
            # It is needed to update page_offset, but we do not wish to query again
            with self.hold_trait_notifications():
                self.__perform_query = False
                self.page_chooser.update_offset()

        if not self.__perform_query:
            self.__perform_query = True
            LOGGER.debug("Will not perform query with pageing: %r", pageing)
            return

        try:
            # Freeze and disable both dropdown widgets
            # We don't want changes leading to weird things happening prior to the query ending
            self.freeze()

            # Query index meta-database
            LOGGER.debug("Querying for more child DBs using pageing: %r",
                         pageing)
            child_dbs, links, _, _ = self._query(pageing)

            data_returned = self.page_chooser.data_returned
            while True:
                # Update list of structures in dropdown widget
                exclude_child_dbs, final_child_dbs = self._update_child_dbs(
                    child_dbs)

                data_returned -= len(exclude_child_dbs)
                if exclude_child_dbs and data_returned:
                    child_dbs, links, data_returned, _ = self._query(
                        link=pageing, exclude_ids=exclude_child_dbs)
                else:
                    break
            self._set_child_dbs(final_child_dbs)

            # Update pageing
            self.page_chooser.set_pagination_data(data_returned=data_returned,
                                                  links_to_page=links)

        except QueryError as exc:
            LOGGER.debug(
                "Trying to retrieve more child DBs (new page). QueryError caught: %r",
                exc,
            )
            if exc.remove_target:
                LOGGER.debug(
                    "Remove target: %r. Will remove target at %r: %r",
                    exc.remove_target,
                    self.providers.index,
                    self.providers.value,
                )
                self.providers.options = self._remove_current_dropdown_option(
                    self.providers)
                self.reset()
            else:
                LOGGER.debug(
                    "Remove target: %r. Will NOT remove target at %r: %r",
                    exc.remove_target,
                    self.providers.index,
                    self.providers.value,
                )
                self.child_dbs.options = self.INITIAL_CHILD_DBS
                self.child_dbs.disabled = True

        else:
            self.unfreeze()
    def _set_intslider_ranges(self):
        """Update IntRangeSlider ranges according to chosen database

        Query database to retrieve ranges.
        Cache ranges in self.__cached_ranges.
        """
        defaults = {
            "nsites": {
                "min": 0,
                "max": 10000
            },
            "nelements": {
                "min": 0,
                "max": len(CHEMICAL_SYMBOLS)
            },
        }

        db_base_url = self.database[1].base_url
        if db_base_url not in self.__cached_ranges:
            self.__cached_ranges[db_base_url] = {}

        sortable_fields = check_entry_properties(
            base_url=db_base_url,
            entry_endpoint="structures",
            properties=["nsites", "nelements"],
            checks=["sort"],
        )

        for response_field in sortable_fields:
            if response_field in self.__cached_ranges[db_base_url]:
                # Use cached value(s)
                continue

            page_limit = 1

            new_range = {}
            for extremum, sort in [
                ("min", response_field),
                ("max", f"-{response_field}"),
            ]:
                query_params = {
                    "base_url": db_base_url,
                    "page_limit": page_limit,
                    "response_fields": response_field,
                    "sort": sort,
                }
                LOGGER.debug(
                    "Querying %s to get %s of %s.\nParameters: %r",
                    self.database[0],
                    extremum,
                    response_field,
                    query_params,
                )

                response = perform_optimade_query(**query_params)
                msg, _ = handle_errors(response)
                if msg:
                    raise QueryError(msg)

                if not response.get("meta", {}).get("data_available", 0):
                    new_range[extremum] = defaults[response_field][extremum]
                else:
                    new_range[extremum] = (response.get("data", [{}])[0].get(
                        "attributes", {}).get(response_field, None))

            # Cache new values
            LOGGER.debug(
                "Caching newly found range values for %s\nValue: %r",
                db_base_url,
                {response_field: new_range},
            )
            self.__cached_ranges[db_base_url].update(
                {response_field: new_range})

        if not self.__cached_ranges[db_base_url]:
            LOGGER.debug("No values found for %s, storing default values.",
                         db_base_url)
            self.__cached_ranges[db_base_url].update({
                "nsites": {
                    "min": 0,
                    "max": 10000
                },
                "nelements": {
                    "min": 0,
                    "max": len(CHEMICAL_SYMBOLS)
                },
            })

        # Set widget's new extrema
        LOGGER.debug(
            "Updating range extrema for %s\nValues: %r",
            db_base_url,
            self.__cached_ranges[db_base_url],
        )
        self.filters.update_range_filters(self.__cached_ranges[db_base_url])
    def _initialize_child_dbs(self):
        """New provider chosen; initialize child DB dropdown"""
        self.offset = 0
        self.number = 1
        try:
            # Freeze and disable list of structures in dropdown widget
            # We don't want changes leading to weird things happening prior to the query ending
            self.freeze()

            # Reset the error or status message
            if self.error_or_status_messages.value:
                self.error_or_status_messages.value = ""

            if self.provider.base_url in self.__cached_child_dbs:
                cache = self.__cached_child_dbs[self.provider.base_url]

                LOGGER.debug(
                    "Initializing child DBs for %s. Using cached info:\n%r",
                    self.provider.name,
                    cache,
                )

                self._set_child_dbs(cache["child_dbs"])
                data_returned = cache["data_returned"]
                data_available = cache["data_available"]
                links = cache["links"]
            else:
                LOGGER.debug("Initializing child DBs for %s.",
                             self.provider.name)

                # Query database and get child_dbs
                child_dbs, links, data_returned, data_available = self._query()

                while True:
                    # Update list of structures in dropdown widget
                    exclude_child_dbs, final_child_dbs = self._update_child_dbs(
                        child_dbs)

                    LOGGER.debug("Exclude child DBs: %r", exclude_child_dbs)
                    data_returned -= len(exclude_child_dbs)
                    if exclude_child_dbs and data_returned:
                        child_dbs, links, data_returned, _ = self._query(
                            exclude_ids=exclude_child_dbs)
                    else:
                        break
                self._set_child_dbs(final_child_dbs)

                # Cache initial child_dbs and related information
                self.__cached_child_dbs[self.provider.base_url] = {
                    "child_dbs": final_child_dbs,
                    "data_returned": data_returned,
                    "data_available": data_available,
                    "links": links,
                }

                LOGGER.debug(
                    "Found the following, which has now been cached:\n%r",
                    self.__cached_child_dbs[self.provider.base_url],
                )

            # Update pageing
            self.page_chooser.set_pagination_data(
                data_returned=data_returned,
                data_available=data_available,
                links_to_page=links,
                reset_cache=True,
            )

        except QueryError as exc:
            LOGGER.debug(
                "Trying to initalize child DBs. QueryError caught: %r", exc)
            if exc.remove_target:
                LOGGER.debug(
                    "Remove target: %r. Will remove target at %r: %r",
                    exc.remove_target,
                    self.providers.index,
                    self.providers.value,
                )
                self.providers.options = self._remove_current_dropdown_option(
                    self.providers)
                self.reset()
            else:
                LOGGER.debug(
                    "Remove target: %r. Will NOT remove target at %r: %r",
                    exc.remove_target,
                    self.providers.index,
                    self.providers.value,
                )
                self.child_dbs.options = self.INITIAL_CHILD_DBS
                self.child_dbs.disabled = True

        else:
            self.unfreeze()
    def _query(  # pylint: disable=too-many-locals,too-many-branches,too-many-statements
            self,
            link: str = None,
            exclude_ids: List[str] = None) -> Tuple[List[dict], dict, int,
                                                    int]:
        """Query helper function"""
        # If a complete link is provided, use it straight up
        if link is not None:
            try:
                if exclude_ids:
                    filter_value = " AND ".join(
                        [f"id!={id_}" for id_ in exclude_ids])

                    parsed_url = urllib.parse.urlparse(link)
                    queries = urllib.parse.parse_qs(parsed_url.query)
                    # Since parse_qs wraps all values in a list,
                    # this extracts the values from the list(s).
                    queries = {key: value[0] for key, value in queries.items()}

                    if "filter" in queries:
                        queries[
                            "filter"] = f"( {queries['filter']} ) AND ( {filter_value} )"
                    else:
                        queries["filter"] = filter_value

                    parsed_query = urllib.parse.urlencode(queries)

                    link = (
                        f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}"
                        f"?{parsed_query}")

                response = requests.get(link, timeout=TIMEOUT_SECONDS).json()
            except (
                    requests.exceptions.ConnectTimeout,
                    requests.exceptions.ConnectionError,
            ) as exc:
                response = {
                    "errors": {
                        "msg": "CLIENT: Connection error or timeout.",
                        "url": link,
                        "Exception": repr(exc),
                    }
                }
            except json.JSONDecodeError as exc:
                response = {
                    "errors": {
                        "msg": "CLIENT: Could not decode response to JSON.",
                        "url": link,
                        "Exception": repr(exc),
                    }
                }
        else:
            filter_ = "link_type=child OR type=child"
            if exclude_ids:
                filter_ += (" AND ( " +
                            " AND ".join([f"id!={id_}"
                                          for id_ in exclude_ids]) + " )")

            response = perform_optimade_query(
                filter=filter_,
                base_url=self.provider.base_url,
                endpoint="/links",
                page_limit=self.child_db_limit,
                page_offset=self.offset,
                page_number=self.number,
            )
        msg, http_errors = handle_errors(response)
        if msg:
            if 404 in http_errors:
                # If /links not found move on
                pass
            else:
                self.error_or_status_messages.value = msg
                raise QueryError(msg=msg, remove_target=True)

        # Check implementation API version
        msg = validate_api_version(response.get("meta",
                                                {}).get("api_version", ""),
                                   raise_on_fail=False)
        if msg:
            self.error_or_status_messages.value = (
                f"{msg}<br>The provider has been removed.")
            raise QueryError(msg=msg, remove_target=True)

        LOGGER.debug(
            "Manually remove `exclude_ids` if filters are not supported")
        child_db_data = {
            impl.get("id", "N/A"): impl
            for impl in response.get("data", [])
        }
        if exclude_ids:
            for links_id in exclude_ids:
                if links_id in list(child_db_data.keys()):
                    child_db_data.pop(links_id)
            LOGGER.debug("child_db_data after popping: %r", child_db_data)
            response["data"] = list(child_db_data.values())
            if "meta" in response:
                if "data_available" in response["meta"]:
                    old_data_available = response["meta"].get(
                        "data_available", 0)
                    if len(response["data"]) > old_data_available:
                        LOGGER.debug("raising OptimadeClientError")
                        raise OptimadeClientError(
                            f"Reported data_available ({old_data_available}) is smaller than "
                            f"curated list of responses ({len(response['data'])}).",
                        )
                response["meta"]["data_available"] = len(response["data"])
            else:
                raise OptimadeClientError(
                    "'meta' not found in response. Bad response")

        LOGGER.debug(
            "Attempt for %r (in /links): Found implementations (names+base_url only):\n%s",
            self.provider.name,
            [
                f"(id: {name}; base_url: {base_url}) " for name, base_url in [(
                    impl.get("id", "N/A"),
                    impl.get("attributes", {}).get("base_url", "N/A"),
                ) for impl in response.get("data", [])]
            ],
        )
        # Return all implementations of link_type "child"
        implementations = [
            implementation for implementation in response.get("data", [])
            if (implementation.get("attributes", {}).get("link_type", "") ==
                "child" or implementation.get("type", "") == "child")
        ]
        LOGGER.debug(
            "After curating for implementations which are of 'link_type' = 'child' or 'type' == "
            "'child' (old style):\n%s",
            [
                f"(id: {name}; base_url: {base_url}) " for name, base_url in [(
                    impl.get("id", "N/A"),
                    impl.get("attributes", {}).get("base_url", "N/A"),
                ) for impl in implementations]
            ],
        )

        # Get links, data_returned, and data_available
        links = response.get("links", {})
        data_returned = response.get("meta", {}).get("data_returned",
                                                     len(implementations))
        if data_returned > 0 and not implementations:
            # Most probably dealing with pre-v1.0.0-rc.2 implementations
            data_returned = 0
        data_available = response.get("meta", {}).get("data_available", 0)

        return implementations, links, data_returned, data_available