Coverage for src/ensembl/utils/rloader.py: 100%
46 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-06 14:10 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-06 14:10 +0000
1# See the NOTICE file distributed with this work for additional information
2# regarding copyright ownership.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Allow to seamlessly load / read the content of a remote file as if it was located locally."""
17from __future__ import annotations
19__all__ = ["RemoteFileLoader"]
21import configparser
22import json
23import logging
24from io import StringIO
25from typing import Optional, Any
27import dotenv
28import requests
29import requests.exceptions
30import yaml
33logger = logging.getLogger(__name__)
36class RemoteFileLoader:
37 """Loads remote files, allowing specific format parsing options.
39 Args:
40 parser: Parser to use for this object. Default: `None` (no format-specific parsing done).
42 Attributes:
43 available_formats: File formats with ad-hoc parsers available.
44 parser: Parser selected for this object.
46 """
48 available_formats: set[str] = {"yaml", "ini", "env", "json"}
49 parser: Optional[str] = None
51 def __init__(self, parser: Optional[str] = None) -> None:
52 if parser in self.available_formats:
53 self.parser = parser
55 def __parse(self, content: str) -> Any:
56 if self.parser == "yaml":
57 return yaml.load(content, yaml.SafeLoader)
58 if self.parser == "ini":
59 config = configparser.ConfigParser()
60 try:
61 config.read_string(content)
62 except configparser.MissingSectionHeaderError:
63 content = "[DEFAULT]\n" + content
64 config.read_string(content)
65 return config
66 if self.parser == "env":
67 return dotenv.dotenv_values(stream=StringIO(content))
68 if self.parser == "json":
69 return json.loads(content)
70 # Only return content, no parsing
71 return content
73 def r_open(self, url: str) -> Any:
74 """Returns the parsed remote file from the given URL.
76 Args:
77 url: URL of the remote file to fetch.
79 Raises:
80 requests.exception.HTTPError: If loading or requesting the given URL returned an error.
81 requests.exception.Timeout: If a timeout was raised whilst requesting the given URL.
83 """
84 try:
85 r = requests.get(url, timeout=120)
86 if r.status_code == 200:
87 return self.__parse(r.text)
88 raise requests.exceptions.HTTPError(response=r)
89 except requests.exceptions.HTTPError as ex:
90 logger.exception(f"Error with request to {url}: {ex}")
91 raise ex
92 except requests.exceptions.Timeout as ex:
93 logger.exception(f"Request timed out {url}: {ex}")
94 raise ex