|
2 | 2 | Responsible for managing spec and routing commands to operations. |
3 | 3 | """ |
4 | 4 |
|
| 5 | +import contextlib |
| 6 | +import json |
5 | 7 | import os |
6 | 8 | import pickle |
7 | 9 | import sys |
| 10 | +from json import JSONDecodeError |
8 | 11 | from sys import version_info |
| 12 | +from typing import IO, Any, ContextManager, Dict |
9 | 13 |
|
| 14 | +import requests |
| 15 | +import yaml |
10 | 16 | from openapi3 import OpenAPI |
11 | 17 |
|
12 | 18 | from linodecli.api_request import do_request, get_all_pages |
@@ -40,11 +46,19 @@ def __init__(self, version, base_url, skip_config=False): |
40 | 46 | self.config = CLIConfig(self.base_url, skip_config=skip_config) |
41 | 47 | self.load_baked() |
42 | 48 |
|
43 | | - def bake(self, spec): |
| 49 | + def bake(self, spec_location: str): |
44 | 50 | """ |
45 | | - Generates ops and bakes them to a pickle |
| 51 | + Generates ops and bakes them to a pickle. |
| 52 | +
|
| 53 | + :param spec_location: The URL or file path of the OpenAPI spec to parse. |
46 | 54 | """ |
47 | | - spec = OpenAPI(spec) |
| 55 | + |
| 56 | + try: |
| 57 | + spec = self._load_openapi_spec(spec_location) |
| 58 | + except Exception as e: |
| 59 | + print(f"Failed to load spec: {e}") |
| 60 | + sys.exit(ExitCodes.REQUEST_FAILED) |
| 61 | + |
48 | 62 | self.spec = spec |
49 | 63 | self.ops = {} |
50 | 64 | ext = { |
@@ -206,3 +220,85 @@ def user_agent(self) -> str: |
206 | 220 | f"linode-api-docs/{self.spec_version} " |
207 | 221 | f"python/{version_info[0]}.{version_info[1]}.{version_info[2]}" |
208 | 222 | ) |
| 223 | + |
| 224 | + @staticmethod |
| 225 | + def _load_openapi_spec(spec_location: str) -> OpenAPI: |
| 226 | + """ |
| 227 | + Attempts to load the raw OpenAPI spec (YAML or JSON) at the given location. |
| 228 | +
|
| 229 | + :param spec_location: The location of the OpenAPI spec. |
| 230 | + This can be a local path or a URL. |
| 231 | +
|
| 232 | + :returns: A tuple containing the loaded OpenAPI object and the parsed spec in |
| 233 | + dict format. |
| 234 | + """ |
| 235 | + |
| 236 | + with CLI._get_spec_file_reader(spec_location) as f: |
| 237 | + parsed = CLI._parse_spec_file(f) |
| 238 | + |
| 239 | + return OpenAPI(parsed) |
| 240 | + |
| 241 | + @staticmethod |
| 242 | + @contextlib.contextmanager |
| 243 | + def _get_spec_file_reader( |
| 244 | + spec_location: str, |
| 245 | + ) -> ContextManager[IO]: |
| 246 | + """ |
| 247 | + Returns a reader for an OpenAPI spec file from the given location. |
| 248 | +
|
| 249 | + :param spec_location: The location of the OpenAPI spec. |
| 250 | + This can be a local path or a URL. |
| 251 | +
|
| 252 | + :returns: A context manager yielding the spec file's reader. |
| 253 | + """ |
| 254 | + |
| 255 | + # Case for local file |
| 256 | + local_path = os.path.expanduser(spec_location) |
| 257 | + if os.path.exists(local_path): |
| 258 | + f = open(local_path, "r", encoding="utf-8") |
| 259 | + |
| 260 | + try: |
| 261 | + yield f |
| 262 | + finally: |
| 263 | + f.close() |
| 264 | + |
| 265 | + return |
| 266 | + |
| 267 | + # Case for remote file |
| 268 | + resp = requests.get(spec_location, stream=True, timeout=120) |
| 269 | + if resp.status_code != 200: |
| 270 | + raise RuntimeError(f"Failed to GET {spec_location}") |
| 271 | + |
| 272 | + # We need to access the underlying urllib |
| 273 | + # response here so we can return a reader |
| 274 | + # usable in yaml.safe_load(...) and json.load(...) |
| 275 | + resp.raw.decode_content = True |
| 276 | + |
| 277 | + try: |
| 278 | + yield resp.raw |
| 279 | + finally: |
| 280 | + resp.close() |
| 281 | + |
| 282 | + @staticmethod |
| 283 | + def _parse_spec_file(reader: IO) -> Dict[str, Any]: |
| 284 | + """ |
| 285 | + Parses the given file reader into a dict and returns a dict. |
| 286 | +
|
| 287 | + :param reader: A reader for a YAML or JSON file. |
| 288 | +
|
| 289 | + :returns: The parsed file. |
| 290 | + """ |
| 291 | + |
| 292 | + errors = [] |
| 293 | + |
| 294 | + try: |
| 295 | + return yaml.safe_load(reader) |
| 296 | + except yaml.YAMLError as err: |
| 297 | + errors.append(str(err)) |
| 298 | + |
| 299 | + try: |
| 300 | + return json.load(reader) |
| 301 | + except JSONDecodeError as err: |
| 302 | + errors.append(str(err)) |
| 303 | + |
| 304 | + raise ValueError(f"Failed to parse spec file: {'; '.join(errors)}") |
0 commit comments