python-openstackclient/openstackclient/api/api.py
Takashi Kajinami cf6dd62dd1 Drop direct dependency on simplejson
The simplejson library is not included by the requirements but it was
directly imported. Drop the direct inclusion and use the alias in
the requests library to avoid ImportError caused by missing simplejson
in the environment.

Also fix the missing requests library in requirements.

Change-Id: I8713f45c5f2717cc53ba043aaeb479e72f641f78
2024-05-20 09:59:32 +09:00

315 lines
9.5 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Base API Library"""
from keystoneauth1 import exceptions as ks_exceptions
from keystoneauth1 import session as ks_session
from osc_lib import exceptions
import requests
from openstackclient.i18n import _
class KeystoneSession:
"""Wrapper for the Keystone Session
Restore some requests.session.Session compatibility;
keystoneauth1.session.Session.request() has the method and url
arguments swapped from the rest of the requests-using world.
"""
def __init__(self, session=None, endpoint=None, **kwargs):
"""Base object that contains some common API objects and methods
:param Session session:
The default session to be used for making the HTTP API calls.
:param string endpoint:
The URL from the Service Catalog to be used as the base for API
requests on this API.
"""
super().__init__()
# a requests.Session-style interface
self.session = session
self.endpoint = endpoint
def _request(self, method, url, session=None, **kwargs):
"""Perform call into session
All API calls are funneled through this method to provide a common
place to finalize the passed URL and other things.
:param string method:
The HTTP method name, i.e. ``GET``, ``PUT``, etc
:param string url:
The API-specific portion of the URL path
:param Session session:
HTTP client session
:param kwargs:
keyword arguments passed to requests.request().
:return: the requests.Response object
"""
if not session:
session = self.session
if not session:
session = ks_session.Session()
if self.endpoint:
if url:
url = '/'.join([self.endpoint.rstrip('/'), url.lstrip('/')])
else:
url = self.endpoint.rstrip('/')
# Why is ksc session backwards???
return session.request(url, method, **kwargs)
class BaseAPI(KeystoneSession):
"""Base API"""
def __init__(
self, session=None, service_type=None, endpoint=None, **kwargs
):
"""Base object that contains some common API objects and methods
:param Session session:
The default session to be used for making the HTTP API calls.
:param string service_type:
API name, i.e. ``identity`` or ``compute``
:param string endpoint:
The URL from the Service Catalog to be used as the base for API
requests on this API.
"""
super().__init__(session=session, endpoint=endpoint)
self.service_type = service_type
# The basic action methods all take a Session and return dict/lists
def create(self, url, session=None, method=None, **params):
"""Create a new resource
:param string url:
The API-specific portion of the URL path
:param Session session:
HTTP client session
:param string method:
HTTP method (default POST)
"""
if not method:
method = 'POST'
ret = self._request(method, url, session=session, **params)
# Should this move into _requests()?
try:
return ret.json()
except requests.JSONDecodeError:
return ret
def delete(self, url, session=None, **params):
"""Delete a resource
:param string url:
The API-specific portion of the URL path
:param Session session:
HTTP client session
"""
return self._request('DELETE', url, **params)
def list(self, path, session=None, body=None, detailed=False, **params):
"""Return a list of resources
GET ${ENDPOINT}/${PATH}?${PARAMS}
path is often the object's plural resource type
:param string path:
The API-specific portion of the URL path
:param Session session:
HTTP client session
:param body: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param bool detailed:
Adds '/details' to path for some APIs to return extended attributes
:returns:
JSON-decoded response, could be a list or a dict-wrapped-list
"""
if detailed:
path = '/'.join([path.rstrip('/'), 'details'])
if body:
ret = self._request(
'POST',
path,
json=body,
params=params,
)
else:
ret = self._request(
'GET',
path,
params=params,
)
try:
return ret.json()
except requests.JSONDecodeError:
return ret
# Layered actions built on top of the basic action methods do not
# explicitly take a Session but one may still be passed in kwargs
def find_attr(
self,
path,
value=None,
attr=None,
resource=None,
):
"""Find a resource via attribute or ID
Most APIs return a list wrapped by a dict with the resource
name as key. Some APIs (Identity) return a dict when a query
string is present and there is one return value. Take steps to
unwrap these bodies and return a single dict without any resource
wrappers.
:param string path:
The API-specific portion of the URL path
:param string value:
value to search for
:param string attr:
attribute to use for resource search
:param string resource:
plural of the object resource name; defaults to path
For example:
n = find(netclient, 'network', 'networks', 'matrix')
"""
# Default attr is 'name'
if attr is None:
attr = 'name'
# Default resource is path - in many APIs they are the same
if resource is None:
resource = path
def getlist(kw):
"""Do list call, unwrap resource dict if present"""
ret = self.list(path, **kw)
if isinstance(ret, dict) and resource in ret:
ret = ret[resource]
return ret
# Search by attribute
kwargs = {attr: value}
data = getlist(kwargs)
if isinstance(data, dict):
return data
if len(data) == 1:
return data[0]
if len(data) > 1:
msg = _("Multiple %(resource)s exist with %(attr)s='%(value)s'")
raise exceptions.CommandError(
msg % {'resource': resource, 'attr': attr, 'value': value}
)
# Search by id
kwargs = {'id': value}
data = getlist(kwargs)
if len(data) == 1:
return data[0]
msg = _("No %(resource)s with a %(attr)s or ID of '%(value)s' found")
raise exceptions.CommandError(
msg % {'resource': resource, 'attr': attr, 'value': value}
)
def find_bulk(self, path, **kwargs):
"""Bulk load and filter locally
:param string path:
The API-specific portion of the URL path
:param kwargs:
A dict of AVPs to match - logical AND
:returns: list of resource dicts
"""
items = self.list(path)
if isinstance(items, dict):
# strip off the enclosing dict
key = list(items.keys())[0]
items = items[key]
ret = []
for o in items:
try:
if all(o[attr] == kwargs[attr] for attr in kwargs.keys()):
ret.append(o)
except KeyError:
continue
return ret
def find_one(self, path, **kwargs):
"""Find a resource by name or ID
:param string path:
The API-specific portion of the URL path
:returns:
resource dict
"""
bulk_list = self.find_bulk(path, **kwargs)
num_bulk = len(bulk_list)
if num_bulk == 0:
msg = _("none found")
raise exceptions.NotFound(msg)
elif num_bulk > 1:
msg = _("many found")
raise RuntimeError(msg)
return bulk_list[0]
def find(
self,
path,
value=None,
attr=None,
):
"""Find a single resource by name or ID
:param string path:
The API-specific portion of the URL path
:param string value:
search expression
:param string attr:
name of attribute for secondary search
"""
try:
ret = self._request('GET', f"/{path}/{value}").json()
except ks_exceptions.NotFound:
kwargs = {attr: value}
try:
ret = self.find_one("/%s/detail" % (path), **kwargs)
except ks_exceptions.NotFound:
msg = _("%s not found") % value
raise exceptions.NotFound(msg)
return ret