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
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
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)
def invalid_response(cls, response: HttpResponse) -> core.ApiException: request = response.request return core.ApiException( "Invalid response from {} {}".format(request.method, request.url) )
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, ), ]