Пример #1
0
    def _read_configurations(cls) -> Dict[str, core.HttpConfiguration]:
        """Discover and loads the HTTP configuration files.

        Returns:
            A dictionary mapping each loaded configuration ID to its corresponding
            :class:`HttpConfiguration`.

        Raises:
            ApiException: if multiple configurations with the same ID are simultaneously
                present.
            ApiException: if an OS or permission error prevents reading the directory
                that contains HTTP configurations.
        """
        configurations = {}  # type: Dict[str, core.HttpConfiguration]
        path = cls._http_configurations_directory()
        if not path.exists():
            return configurations
        try:
            json_files = path.glob("*.json")
        except PermissionError as e:
            raise core.ApiException(
                "Not authorized to access HTTP configurations directory: " +
                str(e))
        except OSError as e:
            raise core.ApiException(
                "Error while accessing HTTP configurations directory: " +
                str(e))

        for json_file in json_files:
            try:
                config_file = cls._read_configuration_file(json_file)
                if config_file is None or config_file.id is None:
                    continue
                if config_file.id in configurations:
                    raise core.ApiException(
                        "Duplicate configuration IDs detected.")
                if not config_file.uri:
                    continue

                cert_path = None  # type: Optional[pathlib.Path]
                if config_file.cert_path:
                    cert_path = typing.cast(
                        pathlib.Path,
                        PathConstants.application_data_directory /
                        "Certificates" / config_file.cert_path,
                    )
                    if not cert_path.exists():
                        cert_path = None
                configurations[config_file.id] = core.HttpConfiguration(
                    config_file.uri, config_file.api_key, cert_path=cert_path)
            except PermissionError:
                pass
            except OSError:
                # The individual file is inaccessible or badly formatted, so just skip it
                pass
            except json.JSONDecodeError:
                pass
            except ValueError:
                pass
        return configurations
    def __init__(
        self,
        path: str,
        data_type: tbase.DataType,
        value: _Any,
        timestamp: Optional[datetime.datetime] = None,
        count: Optional[int] = None,
        min: Optional[Union[int, float]] = None,
        max: Optional[Union[int, float]] = None,
        mean: Optional[float] = None,
    ) -> None:
        """Initialize an instance.

        Args:
            path: The path of the tag associated with the value.
            data_type: The data type of the value.
            value: The value.
            timestamp: The timestamp associated with the value.
            count: The number of times the tag has been written, or None if the tag is
                not collecting aggregates.
            min: The minimum value of the tag, or None if the tag is not collecting
                aggregates or the data type of the tag does not track a minimum value.
            max: The maximum value of the tag, or None if the tag is not collecting
                aggregates or the data type of the tag does not track a maximum value.
            mean: The mean value of the tag, or None if the tag is not collecting
                aggregates or the data type of the tag does not track a mean value.

        Raises:
            ApiException: if min, max, or mean is None when it shouldn't be or non-None
                when it should be

        :meta private:
        """
        if data_type in _NUMERIC_TYPES:
            if count is not None:
                if mean is None or min is None or max is None:
                    # TODO: Error information
                    raise core.ApiException()
            elif min is not None or max is not None or mean is not None:
                # TODO: Error information: only valid if count is set
                raise core.ApiException()
        else:
            if (min is not None or max is not None
                    or (mean is not None and not math.isnan(mean))):
                # TODO: Error information: not supported for non-numerics
                raise core.ApiException()

        self._path = path
        self._data_type = data_type
        self._value = value
        self._timestamp = timestamp
        self._count = count
        self._min = min
        self._max = max
        self._mean = mean
    async def read_async(
        self,
        *,
        include_timestamp: bool = False,
        include_aggregates: bool = False
    ) -> Optional[tbase.TagWithAggregates[_Any]]:
        """Read the current value of the tag.

        Args:
            include_timestamp: True to include the timestamp associated with the value
                in the result.
            include_aggregates: True to include the tag's aggregate values in the result
                if the tag is set to :attr:`TagData.collect_aggregates`.

        Returns:
            The value, and the timestamp and/or aggregate values if requested, or None
            if the tag exists but doesn't have a value.

        Raises:
            ReferenceError: if the underlying reader has been closed.
            ApiException: if the API call fails.
        """
        ret = await self._reader.read_async(
            self._path,
            include_timestamp=include_timestamp,
            include_aggregates=include_aggregates,
        )
        if ret is None:
            return None
        if ret.data_type != self.data_type:
            raise core.ApiException("Tag data type does not match")

        return ret
Пример #4
0
    def get_configuration(
            cls,
            id: Optional[str] = None,
            enable_fallbacks: Optional[bool] = True) -> core.HttpConfiguration:
        """Get the requested or default configuration.

        Args:
            id: The ID of the configuration to find.
            enable_fallbacks: Whether or not to fallback to other known configurations,
                if ``id`` is unavailable.

        Returns:
            The configuration.

        Raises:
            ValueError: if ``id`` is None and ``enable_fallbacks`` is False.
            ApiException: if the specified (or default) configuration is not found.
        """
        if id is not None:
            id = id.upper()
        else:
            if not enable_fallbacks:
                raise ValueError(
                    "id cannot be None if enable_fallbacks is False")
            fallback_configuration = cls._fallback()
            if fallback_configuration is None:
                raise core.ApiException(
                    "No SystemLink configurations available")
            return fallback_configuration

        if cls._configs is None:
            cls._configs = cls._read_configurations()
        config = cls._configs.get(id)
        if config is not None:
            return config
        if enable_fallbacks:
            fallback_configuration = cls._fallback()
            if fallback_configuration is None:
                raise core.ApiException(
                    "Configuration with ID {!r} was not found, and no ".format(
                        id) + "fallback configurations were available.")
            return fallback_configuration
        raise core.ApiException(
            "Configuration with ID {!r} was not found.".format(id))
    async def test__server_selection_expired__async_api_function_called__selection_recreated_and_operation_retried(
        self,
    ):
        path = "tag1"
        token1 = uuid.uuid4()
        token2 = uuid.uuid4()
        tags = [{"type": "DATE_TIME", "path": path}]
        tokens = [token1, token2]
        tag_delete_results = [core.ApiException("404", http_status_code=404), None]

        def mock_request(method, uri, params=None, data=None):
            if uri == "/nitag/v2/selections":
                data = dict(data)
                data.update({"id": tokens.pop(0)})
                return data, MockResponse(method, uri)
            elif uri.endswith("/tags"):
                if method == "DELETE":
                    result = tag_delete_results.pop(0)
                    if isinstance(result, Exception):
                        raise result
                    else:
                        return result, MockResponse(method, uri)
                else:
                    return tags, MockResponse(method, uri)
            elif uri.startswith("/nitag/v2/selections/"):
                assert method == "DELETE"
            else:
                assert False

        self._client.all_requests.configure_mock(side_effect=mock_request)

        uut = await HttpTagSelection.open_async(self._client, [path])
        await uut.delete_tags_from_server_async()

        assert self._client.all_requests.call_args_list == [
            mock.call(
                "POST",
                "/nitag/v2/selections",
                params=None,
                data={"searchPaths": AnyOrderList([path])},
            ),
            mock.call("GET", "/nitag/v2/selections/{id}/tags", params={"id": token1}),
            mock.call(
                "DELETE", "/nitag/v2/selections/{id}/tags", params={"id": token1}
            ),
            mock.call(
                "POST",
                "/nitag/v2/selections",
                params=None,
                data={"searchPaths": AnyOrderList([path])},
            ),
            mock.call(
                "DELETE", "/nitag/v2/selections/{id}/tags", params={"id": token2}
            ),
        ]
def _handle_response(response: HttpResponse, method: str, uri: str) -> Any:
    try:
        data = response.json() if len(response.text) > 0 else None
        non_json_error = None
    except json.decoder.JSONDecodeError as ex:
        # For error statuses (e.g. 403), if the body isn't JSON, raise an ApiException
        # with the body text as a message
        data = None
        non_json_error = response.text

        # But if this was supposed to be a non-error, raise the decode error
        if 200 <= response.status_code < 300:
            # TODO: This works around Bug 369006 where SystemLink Cloud returns non-JSON
            # text for some APIs (but only those for which no response is necessary)
            if response.status_code == 200 and response.text in ("Success", "OK"):
                data = None
            elif response.status_code == 201 and response.text.startswith("Created"):
                data = None
            else:
                raise json.decoder.JSONDecodeError(
                    "Error from <{} {}>: {}\n\nResponse text:\n  {}".format(
                        method, uri, ex.args[0], response.text.replace("\n", "\n  ")
                    ),
                    ex.doc,
                    ex.pos,
                ) from None

    if not 200 <= response.status_code < 300:
        msg = "Server responded with <{} {}> when calling {} ({})".format(
            response.status_code,
            getattr(response, "reason_phrase", None) or getattr(response, "reason"),
            method,
            uri,
        )
        if non_json_error:
            msg += ":\n\n" + non_json_error

        if data:
            err_dict = typing.cast(Dict[str, Any], data).get("error", {})
            err_obj = core.ApiError.from_json_dict(err_dict) if err_dict else None
        else:
            err_obj = None

        raise core.ApiException(
            msg, error=err_obj, http_status_code=response.status_code
        )

    return data
    async def read_async(
            self,
            path: str,
            *,
            include_timestamp: bool = False,
            include_aggregates: bool = False
    ) -> Optional[tbase.TagWithAggregates]:
        """Asynchronously retrieve the current value of the tag with the given ``path`` from the server.

        Optionally retrieves the aggregate values as well. The tag must exist.

        Args:
            path: The path of the tag to read.
            include_timestamp: True to include the timestamp associated with the value
                in the result.
            include_aggregates: True to include the tag's aggregate values in the result
                if the tag is set to :attr:`TagData.collect_aggregates`.

        Returns:
            The value, and the timestamp and/or aggregate values if requested, or None
            if the tag exists but doesn't have a value.

        Raises:
            ValueError: if ``path`` is empty or invalid.
            ValueError: if ``path`` is None.
            ReferenceError: if the reader has been closed.
            ApiException: if the API call fails.
        """
        data = await self._read_async(path, include_timestamp,
                                      include_aggregates)
        if data is None or data.value is None:
            return None

        value = self._deserialize_value(data.value, data.data_type)
        if value is None:
            # TODO: Error information
            raise core.ApiException()

        return tbase.TagWithAggregates(
            data.path,
            data.data_type,
            value,
            data.timestamp,
            data.count,
            self._deserialize_value(data.min, data.data_type),
            self._deserialize_value(data.max, data.data_type),
            data.mean,
        )
    async def test__error_on_tag_query__open_async__selection_deleted(self):
        paths = ["path"]
        token = uuid.uuid4()
        self._client.all_requests.configure_mock(
            side_effect=self._get_mock_request(token, core.ApiException("Oops"))
        )

        with pytest.raises(core.ApiException):
            await HttpTagSelection.open_async(self._client, paths)

        assert self._client.all_requests.call_args_list == [
            mock.call(
                "POST",
                "/nitag/v2/selections",
                params=None,
                data={"searchPaths": AnyOrderList(paths)},
            ),
            mock.call("GET", "/nitag/v2/selections/{id}/tags", params={"id": token}),
            mock.call("DELETE", "/nitag/v2/selections/{id}", params={"id": token}),
        ]
    def __init__(self, first_page: List[tbase.TagData], total_count: int,
                 skip: int) -> None:
        """Initialize an instance with the first page of query results.

        Args:
            first_page: The first page of results, or None if there are no results.
            total_count: The total number of results in the query.
            skip: The skip used for the first page of results.
        """
        self._total_count = total_count
        self._current_page = None  # type: Optional[List[tbase.TagData]]
        if first_page:
            if skip >= total_count:
                raise core.ApiException(
                    "skip is >= totalCount, but the tag list isn't empty")
            self._current_page = first_page
        else:
            pass  # leave it as None, even if passed in as []
        self._skip = skip
        self._current_skip = skip
Пример #10
0
    async def update_async(
        self, updates: Union[Sequence[tbase.TagData], Sequence[tbase.TagDataUpdate]]
    ) -> None:
        """Asynchronously update the metadata of one or more tags on the server, creating tags that don't exist.

        If ``updates`` contains :class:`TagData` objects, existing metadata will be
        replaced. If ``updates`` contains :class:`TagDataUpdate` objects instead, tags
        that already exist will have their existing keywords, properties, and settings
        merged with those specified in the corresponding :class:`TagDataUpdate`.

        Args:
            updates: The tags to update (if :class:`TagData` objects are given), or the
                tag metadata updates to send (if :class:`TagDataUpdate` objects are
                given).

        Returns:
            A task representing the asynchronous operation.

        Raises:
            ValueError: if ``updates`` is None or empty.
            ValueError: if ``updates`` contains any invalid tags.
            ValueError: if ``updates`` contains both ``TagData`` objects and
                ``TagDataUpdate`` objects.
            ApiException: if the API call fails.
        """
        tag_models, merge = self._prepare_update(updates)
        partial_success, _ = await self._api.as_async.post(
            "/update-tags", data={"tags": tag_models, "merge": merge}
        )

        if partial_success is not None:
            err_dict = partial_success.get("error")
            if err_dict is None and "code" in partial_success:
                # SystemLink Cloud has a bug in which it returns the error directly
                # instead of under the "error" key
                err_dict = partial_success
            err_obj = core.ApiError.from_json_dict(err_dict) if err_dict else None
            if err_dict is None:
                assert False, partial_success
            raise core.ApiException(error=err_obj)
    def test__update_fails__update_timer_elapsed__error_ignored(self):
        token = "test subscription"
        timer = mock.Mock(ManualResetTimer, wraps=ManualResetTimer.null_timer)
        type(timer).elapsed = events.events._EventSlot("elapsed")
        self._client.all_requests.configure_mock(
            side_effect=self._get_mock_request(token, {})
        )
        updates = []

        def on_tag_changed(
            tag: tbase.TagData, reader: Optional[tbase.TagValueReader]
        ) -> None:
            updates.append((tag, reader))

        uut = HttpTagSubscription.create(
            self._client, [], timer, ManualResetTimer.null_timer
        )
        uut.tag_changed += on_tag_changed

        assert timer.start.call_count == 1
        assert self._client.all_requests.call_count == 2
        assert self._client.all_requests.call_args[0][1].endswith("/values/current")

        self._client.all_requests.configure_mock(side_effect=core.ApiException("oops"))

        timer.elapsed()
        assert timer.start.call_count == 2
        assert self._client.all_requests.call_count == 3
        assert self._client.all_requests.call_args[0][1].endswith("/values/current")

        self._client.all_requests.configure_mock(
            side_effect=self._get_mock_request(None, {})
        )

        timer.elapsed()
        assert timer.start.call_count == 3
        assert self._client.all_requests.call_count == 4
        assert self._client.all_requests.call_args[0][1].endswith("/values/current")

        assert 0 == len(updates)
Пример #12
0
 def invalid_response(cls, response: HttpResponse) -> core.ApiException:
     request = response.request
     return core.ApiException(
         "Invalid response from {} {}".format(request.method, request.url)
     )
Пример #13
0
    async def open_async(
        self,
        path: str,
        data_type: Optional[tbase.DataType] = None,
        *,
        create: Optional[bool] = None
    ) -> tbase.TagData:
        """Asynchronously query the server for the metadata of a tag, optionally
        creating it if it doesn't already exist.

        The call fails if the tag already exists as a different data type than specified
        or if it doesn't exist and ``create`` is False.

        Args:
            path: The path of the tag to open.
            data_type: The expected data type of the tag.
            create: True to create the tag if it doesn't already exist, False to fail if
                it doesn't exist.

        Returns:
            A task representing the asynchronous operation. On success, contains
            information about the tag.

        Raises:
            ValueError: if ``path`` is None or empty.
            ValueError: if ``data_type`` is invalid.
            ValueError: if ``create`` is True, but ``data_type`` is None.
            ApiException: if the API call fails.
        """
        if create is None:
            create = data_type is not None
        elif create is True:
            if data_type is None:
                raise ValueError("Cannot create if data_type is not specified")

        if data_type == tbase.DataType.UNKNOWN:
            raise ValueError("Must specify a valid data type")

        tag = None  # type: Optional[Dict[str, Any]]
        try:
            tag, _ = await self._api.as_async.get(
                "/tags/{path}", params={"path": tbase.TagPathUtilities.validate(path)}
            )
        except core.ApiException as ex:
            error_name = None if ex.error is None else ex.error.name
            if create and (error_name or "").startswith("Tag.NoSuchTag"):
                pass  # continue on and create the tag
            else:
                raise

        if tag is not None:
            if data_type is not None and tag["type"] != data_type.api_name:
                raise core.ApiException("Tag exists with a conflicting data type")

            return tbase.TagData.from_json_dict(tag)
        else:
            if data_type is None:
                raise ValueError("data_type cannot be None when create is True")

            # Tag didn't already exist, so try to create it.
            await self._api.as_async.post(
                "/tags", data={"type": data_type.api_name, "path": path}
            )
            return tbase.TagData(path, data_type)
    def test__heartbeat_query_errors__subscription_recreated(self):
        token1 = "test subscription"
        token2 = "second test subscription"
        paths = ["tag1", "tag2", "tag3"]
        timer = mock.MagicMock(ManualResetTimer, wraps=ManualResetTimer.null_timer)
        type(timer).elapsed = events.events._EventSlot("elapsed")
        self._client.all_requests.configure_mock(
            side_effect=self._get_mock_request(token1, {})
        )

        uut = HttpTagSubscription.create(
            self._client, paths, ManualResetTimer.null_timer, timer
        )

        assert uut
        assert timer.start.call_count == 1
        assert self._client.all_requests.call_count == 2
        assert self._client.all_requests.call_args_list == [
            mock.call(
                "POST",
                "/nitag/v2/subscriptions",
                params=None,
                data={"tags": paths, "updatesOnly": True},
            ),
            mock.call(
                "GET",
                "/nitag/v2/subscriptions/{id}/values/current",
                params={"id": token1},
            ),
        ]

        self._client.all_requests.configure_mock(
            side_effect=self._get_mock_request(
                token2, {}, core.ApiException("Unknown subscription")
            )
        )

        timer.elapsed()

        assert timer.start.call_count == 2
        assert self._client.all_requests.call_count == 5
        assert self._client.all_requests.call_args_list[-3:] == [
            mock.call(
                "PUT",
                "/nitag/v2/subscriptions/{id}/heartbeat",
                params={"id": token1},
                data=None,
            ),
            mock.call(
                "POST",
                "/nitag/v2/subscriptions",
                params=None,
                data={"tags": paths, "updatesOnly": True},
            ),
            mock.call(
                "GET",
                "/nitag/v2/subscriptions/{id}/values/current",
                params={"id": token2},
            ),
        ]

        self._client.all_requests.configure_mock(
            side_effect=self._get_mock_request(None, None)
        )

        timer.elapsed()

        assert timer.start.call_count == 3
        assert self._client.all_requests.call_count == 6
        assert self._client.all_requests.call_args_list[-1:] == [
            mock.call(
                "PUT",
                "/nitag/v2/subscriptions/{id}/heartbeat",
                params={"id": token2},
                data=None,
            ),
        ]