Mise à jour de Monitor.py et autres scripts
This commit is contained in:
45
myenv/lib/python3.11/site-packages/ovh/__init__.py
Normal file
45
myenv/lib/python3.11/site-packages/ovh/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Copyright (c) 2013-2024, OVH SAS.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of OVH SAS nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
|
||||
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# flake8: noqa
|
||||
from .client import Client
|
||||
from .consumer_key import API_READ_ONLY, API_READ_WRITE, API_READ_WRITE_SAFE, ConsumerKeyRequest
|
||||
from .exceptions import (
|
||||
APIError,
|
||||
BadParametersError,
|
||||
Forbidden,
|
||||
HTTPError,
|
||||
InvalidCredential,
|
||||
InvalidKey,
|
||||
InvalidRegion,
|
||||
InvalidResponse,
|
||||
NetworkError,
|
||||
NotCredential,
|
||||
NotGrantedCall,
|
||||
ReadOnlyError,
|
||||
ResourceConflictError,
|
||||
ResourceNotFoundError,
|
||||
)
|
||||
618
myenv/lib/python3.11/site-packages/ovh/client.py
Normal file
618
myenv/lib/python3.11/site-packages/ovh/client.py
Normal file
@@ -0,0 +1,618 @@
|
||||
# Copyright (c) 2013-2024, OVH SAS.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of OVH SAS nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
|
||||
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
This module provides a simple python wrapper over the OVH REST API.
|
||||
It handles requesting credential, signing queries...
|
||||
|
||||
- To get your API keys: https://eu.api.ovh.com/createApp/
|
||||
- To get started with API:
|
||||
https://help.ovhcloud.com/csm/en-gb-api-getting-started-ovhcloud-api?id=kb_article_view&sysparm_article=KB0042784
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import keyword
|
||||
import time
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from requests import Session
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from . import config
|
||||
from .consumer_key import ConsumerKeyRequest
|
||||
from .exceptions import (
|
||||
APIError,
|
||||
BadParametersError,
|
||||
Forbidden,
|
||||
HTTPError,
|
||||
InvalidConfiguration,
|
||||
InvalidCredential,
|
||||
InvalidKey,
|
||||
InvalidRegion,
|
||||
InvalidResponse,
|
||||
NetworkError,
|
||||
NotCredential,
|
||||
NotGrantedCall,
|
||||
ResourceConflictError,
|
||||
ResourceExpiredError,
|
||||
ResourceNotFoundError,
|
||||
)
|
||||
from .oauth2 import OAuth2
|
||||
|
||||
# Mapping between OVH API region names and corresponding endpoints
|
||||
ENDPOINTS = {
|
||||
"ovh-eu": "https://eu.api.ovh.com/1.0",
|
||||
"ovh-us": "https://api.us.ovhcloud.com/1.0",
|
||||
"ovh-ca": "https://ca.api.ovh.com/1.0",
|
||||
"kimsufi-eu": "https://eu.api.kimsufi.com/1.0",
|
||||
"kimsufi-ca": "https://ca.api.kimsufi.com/1.0",
|
||||
"soyoustart-eu": "https://eu.api.soyoustart.com/1.0",
|
||||
"soyoustart-ca": "https://ca.api.soyoustart.com/1.0",
|
||||
}
|
||||
|
||||
# Default timeout for each request. 180 seconds connect, 180 seconds read.
|
||||
TIMEOUT = 180
|
||||
|
||||
# OAuth2 token provider URLs
|
||||
OAUTH2_TOKEN_URLS = {
|
||||
"ovh-eu": "https://www.ovh.com/auth/oauth2/token",
|
||||
"ovh-ca": "https://ca.ovh.com/auth/oauth2/token",
|
||||
"ovh-us": "https://us.ovhcloud.com/auth/oauth2/token",
|
||||
}
|
||||
|
||||
|
||||
class Client:
|
||||
"""
|
||||
Low level OVH Client. It abstracts all the authentication and request
|
||||
signing logic along with some nice tools helping with key generation.
|
||||
|
||||
All low level request logic including signing and error handling takes place
|
||||
in :py:func:`Client.call` function. Convenient wrappers
|
||||
:py:func:`Client.get` :py:func:`Client.post`, :py:func:`Client.put`,
|
||||
:py:func:`Client.delete` should be used instead. :py:func:`Client.post`,
|
||||
:py:func:`Client.put` both accept arbitrary list of keyword arguments
|
||||
mapped to ``data`` param of :py:func:`Client.call`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from ovh import Client, APIError
|
||||
|
||||
REGION = 'ovh-eu'
|
||||
APP_KEY="<application key>"
|
||||
APP_SECRET="<application secret key>"
|
||||
CONSUMER_KEY="<consumer key>"
|
||||
|
||||
client = Client(REGION, APP_KEY, APP_SECRET, CONSUMER_KEY)
|
||||
|
||||
try:
|
||||
print(client.get('/me'))
|
||||
except APIError as e:
|
||||
print("Ooops, failed to get my info:", e.msg)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
endpoint=None,
|
||||
application_key=None,
|
||||
application_secret=None,
|
||||
consumer_key=None,
|
||||
timeout=TIMEOUT,
|
||||
config_file=None,
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
):
|
||||
"""
|
||||
Creates a new Client. No credential check is done at this point.
|
||||
|
||||
When using OAuth2 authentication, ``client_id`` and ``client_secret``
|
||||
will be used to initiate a Client Credential OAuth2 flow.
|
||||
|
||||
When using the OVHcloud authentication method, the ``application_key``
|
||||
identifies your application while ``application_secret`` authenticates
|
||||
it. On the other hand, the ``consumer_key`` uniquely identifies your
|
||||
application's end user without requiring his personal password.
|
||||
|
||||
If any of ``endpoint``, ``application_key``, ``application_secret``,
|
||||
``consumer_key``, ``client_id`` or ``client_secret`` is not provided,
|
||||
this client will attempt to locate from them from environment,
|
||||
``~/.ovh.cfg`` or ``/etc/ovh.cfg``.
|
||||
|
||||
See :py:mod:`ovh.config` for more information on supported
|
||||
configuration mechanisms.
|
||||
|
||||
``timeout`` can either be a float or a tuple. If it is a float it
|
||||
sets the same timeout for both connection and read. If it is a tuple
|
||||
connection and read timeout will be set independently. To use the
|
||||
latter approach you need at least requests v2.4.0. Default value is
|
||||
180 seconds for connection and 180 seconds for read.
|
||||
|
||||
:param str endpoint: API endpoint to use. Valid values in ``ENDPOINTS``
|
||||
:param str application_key: Application key as provided by OVHcloud
|
||||
:param str application_secret: Application secret key as provided by OVHcloud
|
||||
:param str consumer_key: uniquely identifies
|
||||
:param str client_id: OAuth2 client ID
|
||||
:param str client_secret: OAuth2 client secret
|
||||
:param tuple timeout: Connection and read timeout for each request
|
||||
:param float timeout: Same timeout for both connection and read
|
||||
:raises InvalidRegion: if ``endpoint`` can't be found in ``ENDPOINTS``.
|
||||
"""
|
||||
|
||||
configuration = config.ConfigurationManager()
|
||||
|
||||
# Load a custom config file if requested
|
||||
if config_file is not None:
|
||||
configuration.read(config_file)
|
||||
|
||||
# load endpoint
|
||||
if endpoint is None:
|
||||
endpoint = configuration.get("default", "endpoint")
|
||||
|
||||
try:
|
||||
self._endpoint = ENDPOINTS[endpoint]
|
||||
except KeyError:
|
||||
raise InvalidRegion("Unknown endpoint %s. Valid endpoints: %s", endpoint, ENDPOINTS.keys())
|
||||
|
||||
# load keys
|
||||
if application_key is None:
|
||||
application_key = configuration.get(endpoint, "application_key")
|
||||
self._application_key = application_key
|
||||
|
||||
if application_secret is None:
|
||||
application_secret = configuration.get(endpoint, "application_secret")
|
||||
self._application_secret = application_secret
|
||||
|
||||
if consumer_key is None:
|
||||
consumer_key = configuration.get(endpoint, "consumer_key")
|
||||
self._consumer_key = consumer_key
|
||||
|
||||
# load OAuth2 data
|
||||
if client_id is None:
|
||||
client_id = configuration.get(endpoint, "client_id")
|
||||
self._client_id = client_id
|
||||
|
||||
if client_secret is None:
|
||||
client_secret = configuration.get(endpoint, "client_secret")
|
||||
self._client_secret = client_secret
|
||||
|
||||
# configuration validation
|
||||
if bool(self._client_id) is not bool(self._client_secret):
|
||||
raise InvalidConfiguration("Invalid OAuth2 config, both client_id and client_secret must be given")
|
||||
|
||||
if bool(self._application_key) is not bool(self._application_secret):
|
||||
raise InvalidConfiguration(
|
||||
"Invalid authentication config, both application_key and application_secret must be given"
|
||||
)
|
||||
|
||||
if self._client_id is not None and self._application_key is not None:
|
||||
raise InvalidConfiguration(
|
||||
"Can't use both application_key/application_secret and OAuth2 client_id/client_secret"
|
||||
)
|
||||
if self._client_id is None and self._application_key is None:
|
||||
raise InvalidConfiguration(
|
||||
"Missing authentication information, you need to provide at least an application_key/application_secret"
|
||||
" or a client_id/client_secret"
|
||||
)
|
||||
if self._client_id and endpoint not in OAUTH2_TOKEN_URLS:
|
||||
raise InvalidConfiguration(
|
||||
"OAuth2 authentication is not compatible with endpoint "
|
||||
+ endpoint
|
||||
+ " (it can only be used with ovh-eu, ovh-ca and ovh-us)"
|
||||
)
|
||||
|
||||
# when in OAuth2 mode, instantiate the oauthlib client
|
||||
if self._client_id:
|
||||
self._oauth2 = OAuth2(
|
||||
client_id=self._client_id,
|
||||
client_secret=self._client_secret,
|
||||
token_url=OAUTH2_TOKEN_URLS[endpoint],
|
||||
)
|
||||
else:
|
||||
self._oauth2 = None
|
||||
|
||||
# lazy load time delta
|
||||
self._time_delta = None
|
||||
|
||||
# use a requests session to reuse HTTPS connections between requests
|
||||
self._session = Session()
|
||||
|
||||
# Override default timeout
|
||||
self._timeout = timeout
|
||||
|
||||
# high level API
|
||||
|
||||
@property
|
||||
def time_delta(self):
|
||||
"""
|
||||
Request signatures are valid only for a short amount of time to mitigate
|
||||
risk of attack replay scenarii which requires to use a common time
|
||||
reference. This function queries endpoint's time and computes the delta.
|
||||
This entrypoint does not require authentication.
|
||||
|
||||
This method is *lazy*. It will only load it once even though it is used
|
||||
for each request.
|
||||
|
||||
.. note:: You should not need to use this property directly
|
||||
|
||||
:returns: time distance between local and server time in seconds.
|
||||
:rtype: int
|
||||
"""
|
||||
if self._time_delta is None:
|
||||
server_time = self.get("/auth/time", _need_auth=False)
|
||||
self._time_delta = server_time - int(time.time())
|
||||
return self._time_delta
|
||||
|
||||
def new_consumer_key_request(self):
|
||||
"""
|
||||
Create a new consumer key request. This is the recommended way to create
|
||||
a new consumer key request.
|
||||
|
||||
Full example:
|
||||
|
||||
>>> import ovh
|
||||
>>> client = ovh.Client("ovh-eu")
|
||||
>>> ck = client.new_consumer_key_request()
|
||||
>>> ck.add_rules(ovh.API_READ_ONLY, "/me")
|
||||
>>> ck.add_recursive_rules(ovh.API_READ_WRITE, "/sms")
|
||||
>>> ck.request()
|
||||
{
|
||||
'state': 'pendingValidation',
|
||||
'consumerKey': 'TnpZAd5pYNqxk4RhlPiSRfJ4WrkmII2i',
|
||||
'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=now2OOAVO4Wp6t7bemyN9DMWIobhGjFNZSHmixtVJM4S7mzjkN2L5VBfG96Iy1i0'
|
||||
}
|
||||
""" # noqa:E501
|
||||
return ConsumerKeyRequest(self)
|
||||
|
||||
def request_consumerkey(self, access_rules, redirect_url=None, allowedIPs=None):
|
||||
"""
|
||||
Create a new "consumer key" identifying this application's end user. API
|
||||
will return a ``consumerKey`` and a ``validationUrl``. The end user must
|
||||
visit the ``validationUrl``, authenticate and validate the requested
|
||||
``access_rules`` to link his account to the ``consumerKey``. Once this
|
||||
is done, he may optionally be redirected to ``redirect_url`` and the
|
||||
application can start using the ``consumerKey``. If adding an ``allowedIPs``
|
||||
parameter, the generated credentials will only be usable from these IPs.
|
||||
|
||||
The new ``consumerKey`` is automatically loaded into
|
||||
``self._consumer_key`` and is ready to used as soon as validated.
|
||||
|
||||
As signing requires a valid ``consumerKey``, the method does not require
|
||||
authentication, only a valid ``applicationKey``
|
||||
|
||||
``access_rules`` is a list of the form:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Grant full, unrestricted API access
|
||||
access_rules = [
|
||||
{'method': 'GET', 'path': '/*'},
|
||||
{'method': 'POST', 'path': '/*'},
|
||||
{'method': 'PUT', 'path': '/*'},
|
||||
{'method': 'DELETE', 'path': '/*'}
|
||||
]
|
||||
|
||||
To request a new consumer key, you may use a code like:
|
||||
|
||||
.. code:: python
|
||||
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
# Request RO, /me API access
|
||||
access_rules = [
|
||||
{'method': 'GET', 'path': '/me'},
|
||||
]
|
||||
|
||||
# Request token
|
||||
validation = client.request_consumerkey(access_rules, redirect_url="https://optional-redirect-url.example.org", allowedIPs=["127.0.0.1/32"])
|
||||
|
||||
print("Please visit", validation['validationUrl'], "to authenticate")
|
||||
input("and press Enter to continue...")
|
||||
|
||||
# Print nice welcome message
|
||||
print("Welcome", client.get('/me')['firstname'])
|
||||
|
||||
|
||||
:param list access_rules: Mapping specifying requested privileges.
|
||||
:param str redirect_url: Where to redirect end user upon validation (optional).
|
||||
:param list allowedIPs: CIDRs that will be allowed to use these credentials (optional).
|
||||
:raises APIError: When ``self.call`` fails.
|
||||
:returns: dict with ``consumerKey`` and ``validationUrl`` keys
|
||||
:rtype: dict
|
||||
""" # noqa:E501
|
||||
res = self.post(
|
||||
"/auth/credential",
|
||||
_need_auth=False,
|
||||
accessRules=access_rules,
|
||||
redirection=redirect_url,
|
||||
allowedIPs=allowedIPs,
|
||||
)
|
||||
self._consumer_key = res["consumerKey"]
|
||||
return res
|
||||
|
||||
# API shortcuts
|
||||
|
||||
def _canonicalize_kwargs(self, kwargs):
|
||||
"""
|
||||
If an API needs an argument colliding with a Python reserved keyword, it
|
||||
can be prefixed with an underscore. For example, ``from`` argument of
|
||||
``POST /email/domain/{domain}/redirection`` may be replaced by ``_from``
|
||||
|
||||
:param dict kwargs: input kwargs
|
||||
:return dict: filtered kawrgs
|
||||
"""
|
||||
arguments = {}
|
||||
|
||||
for k, v in kwargs.items():
|
||||
if k[0] == "_" and k[1:] in keyword.kwlist:
|
||||
k = k[1:]
|
||||
arguments[k] = v
|
||||
|
||||
return arguments
|
||||
|
||||
def _prepare_query_string(self, kwargs):
|
||||
"""
|
||||
Boolean needs to be send as lowercase 'false' or 'true' in querystring.
|
||||
This function prepares arguments for querystring and encodes them.
|
||||
|
||||
:param dict kwargs: input kwargs
|
||||
:return string: prepared querystring
|
||||
"""
|
||||
arguments = {}
|
||||
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, bool):
|
||||
v = str(v).lower()
|
||||
elif v is None:
|
||||
v = "null"
|
||||
arguments[k] = v
|
||||
|
||||
return urlencode(arguments)
|
||||
|
||||
def get(self, _target, _need_auth=True, **kwargs):
|
||||
"""
|
||||
'GET' :py:func:`Client.call` wrapper.
|
||||
|
||||
Query string parameters can be set either directly in ``_target`` or as
|
||||
keyword arguments. If an argument collides with a Python reserved
|
||||
keyword, prefix it with a '_'. For instance, ``from`` becomes ``_from``.
|
||||
|
||||
:param string _target: API method to call
|
||||
:param string _need_auth: If True, send authentication headers. This is
|
||||
the default
|
||||
"""
|
||||
if kwargs:
|
||||
kwargs = self._canonicalize_kwargs(kwargs)
|
||||
query_string = self._prepare_query_string(kwargs)
|
||||
if query_string != "":
|
||||
if "?" in _target:
|
||||
_target = "%s&%s" % (_target, query_string)
|
||||
else:
|
||||
_target = "%s?%s" % (_target, query_string)
|
||||
|
||||
return self.call("GET", _target, None, _need_auth)
|
||||
|
||||
def put(self, _target, _need_auth=True, **kwargs):
|
||||
"""
|
||||
'PUT' :py:func:`Client.call` wrapper
|
||||
|
||||
Body parameters can be set either directly in ``_target`` or as keyword
|
||||
arguments. If an argument collides with a Python reserved keyword,
|
||||
prefix it with a '_'. For instance, ``from`` becomes ``_from``.
|
||||
|
||||
:param string _target: API method to call
|
||||
:param string _need_auth: If True, send authentication headers. This is
|
||||
the default
|
||||
"""
|
||||
kwargs = self._canonicalize_kwargs(kwargs)
|
||||
if not kwargs:
|
||||
kwargs = None
|
||||
return self.call("PUT", _target, kwargs, _need_auth)
|
||||
|
||||
def post(self, _target, _need_auth=True, **kwargs):
|
||||
"""
|
||||
'POST' :py:func:`Client.call` wrapper
|
||||
|
||||
Body parameters can be set either directly in ``_target`` or as keyword
|
||||
arguments. If an argument collides with a Python reserved keyword,
|
||||
prefix it with a '_'. For instance, ``from`` becomes ``_from``.
|
||||
|
||||
:param string _target: API method to call
|
||||
:param string _need_auth: If True, send authentication headers. This is
|
||||
the default
|
||||
"""
|
||||
kwargs = self._canonicalize_kwargs(kwargs)
|
||||
if not kwargs:
|
||||
kwargs = None
|
||||
return self.call("POST", _target, kwargs, _need_auth)
|
||||
|
||||
def delete(self, _target, _need_auth=True, **kwargs):
|
||||
"""
|
||||
'DELETE' :py:func:`Client.call` wrapper
|
||||
|
||||
Query string parameters can be set either directly in ``_target`` or as
|
||||
keyword arguments. If an argument collides with a Python reserved
|
||||
keyword, prefix it with a '_'. For instance, ``from`` becomes ``_from``.
|
||||
|
||||
:param string _target: API method to call
|
||||
:param string _need_auth: If True, send authentication headers. This is
|
||||
the default
|
||||
"""
|
||||
if kwargs:
|
||||
kwargs = self._canonicalize_kwargs(kwargs)
|
||||
query_string = self._prepare_query_string(kwargs)
|
||||
if query_string != "":
|
||||
if "?" in _target:
|
||||
_target = "%s&%s" % (_target, query_string)
|
||||
else:
|
||||
_target = "%s?%s" % (_target, query_string)
|
||||
|
||||
return self.call("DELETE", _target, None, _need_auth)
|
||||
|
||||
# low level helpers
|
||||
|
||||
def call(self, method, path, data=None, need_auth=True):
|
||||
"""
|
||||
Low level call helper. If ``consumer_key`` is not ``None``, inject
|
||||
authentication headers and sign the request.
|
||||
|
||||
Request signature is a sha1 hash on following fields, joined by '+'
|
||||
- application_secret
|
||||
- consumer_key
|
||||
- METHOD
|
||||
- full request url
|
||||
- body
|
||||
- server current time (takes time delta into account)
|
||||
|
||||
:param str method: HTTP verb. Usually one of GET, POST, PUT, DELETE
|
||||
:param str path: api entrypoint to call, relative to endpoint base path
|
||||
:param data: any json serializable data to send as request's body
|
||||
:param boolean need_auth: if False, bypass signature
|
||||
:raises HTTPError: when underlying request failed for network reason
|
||||
:raises InvalidResponse: when API response could not be decoded
|
||||
"""
|
||||
# attempt request
|
||||
try:
|
||||
result = self.raw_call(method=method, path=path, data=data, need_auth=need_auth)
|
||||
except RequestException as error:
|
||||
raise HTTPError("Low HTTP request failed error", error)
|
||||
|
||||
status = result.status_code
|
||||
|
||||
# attempt to decode and return the response
|
||||
try:
|
||||
if status != 204:
|
||||
json_result = result.json()
|
||||
else:
|
||||
json_result = None
|
||||
except ValueError as error:
|
||||
raise InvalidResponse("Failed to decode API response", error)
|
||||
|
||||
# error check
|
||||
if status >= 100 and status < 300:
|
||||
return json_result
|
||||
elif status == 403 and json_result.get("errorCode") == "NOT_GRANTED_CALL":
|
||||
raise NotGrantedCall(json_result.get("message"), response=result)
|
||||
elif status == 403 and json_result.get("errorCode") == "NOT_CREDENTIAL":
|
||||
raise NotCredential(json_result.get("message"), response=result)
|
||||
elif status == 403 and json_result.get("errorCode") == "INVALID_KEY":
|
||||
raise InvalidKey(json_result.get("message"), response=result)
|
||||
elif status == 403 and json_result.get("errorCode") == "INVALID_CREDENTIAL":
|
||||
raise InvalidCredential(json_result.get("message"), response=result)
|
||||
elif status == 403 and json_result.get("errorCode") == "FORBIDDEN":
|
||||
raise Forbidden(json_result.get("message"), response=result)
|
||||
elif status == 404:
|
||||
raise ResourceNotFoundError(json_result.get("message"), response=result)
|
||||
elif status == 400:
|
||||
raise BadParametersError(json_result.get("message"), response=result)
|
||||
elif status == 409:
|
||||
raise ResourceConflictError(json_result.get("message"), response=result)
|
||||
elif status == 460:
|
||||
raise ResourceExpiredError(json_result.get("message"), response=result)
|
||||
elif status == 0:
|
||||
raise NetworkError()
|
||||
else:
|
||||
raise APIError(json_result.get("message"), response=result)
|
||||
|
||||
def _get_target(self, path):
|
||||
"""
|
||||
_get_target returns the URL to target given an endpoint and a path.
|
||||
If the path starts with `/v1` or `/v2`, then remove the trailing `/1.0` from the endpoint.
|
||||
|
||||
:param str path: path to use prefix from
|
||||
:returns: target with one of /1.0 and /v1|2 path segment
|
||||
:rtype: str
|
||||
"""
|
||||
endpoint = self._endpoint
|
||||
if endpoint.endswith("/1.0") and path.startswith(("/v1", "/v2")):
|
||||
endpoint = endpoint[:-4]
|
||||
return endpoint + path
|
||||
|
||||
def raw_call(self, method, path, data=None, need_auth=True, headers=None):
|
||||
"""
|
||||
Lowest level call helper. If ``consumer_key`` is not ``None``, inject
|
||||
authentication headers and sign the request.
|
||||
Will return ``requests.Response`` object or let any
|
||||
``requests`` exception pass through.
|
||||
|
||||
Request signature is a sha1 hash on following fields, joined by '+'
|
||||
- application_secret
|
||||
- consumer_key
|
||||
- METHOD
|
||||
- full request url
|
||||
- body
|
||||
- server current time (takes time delta into account)
|
||||
|
||||
:param str method: HTTP verb. Usually one of GET, POST, PUT, DELETE
|
||||
:param str path: api entrypoint to call, relative to endpoint base path
|
||||
:param data: any json serializable data to send as request's body
|
||||
:param boolean need_auth: if False, bypass signature
|
||||
:param dict headers: A dict containing the headers that should be sent to
|
||||
the OVH API. ``raw_call`` will override the
|
||||
OVH API authentication headers, as well as
|
||||
the Content-Type header.
|
||||
"""
|
||||
body = ""
|
||||
target = self._get_target(path)
|
||||
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
# include payload
|
||||
if data is not None:
|
||||
headers["Content-type"] = "application/json"
|
||||
body = json.dumps(data, separators=(",", ":")) # Separators to prevent adding useless spaces
|
||||
|
||||
# sign request. Never sign 'time' or will recurse infinitely
|
||||
if need_auth:
|
||||
if self._oauth2:
|
||||
return self._oauth2.session.request(method, target, headers=headers, data=body, timeout=self._timeout)
|
||||
|
||||
if not self._application_secret:
|
||||
raise InvalidKey("Invalid ApplicationSecret '%s'" % self._application_secret)
|
||||
|
||||
if not self._consumer_key:
|
||||
raise InvalidKey("Invalid ConsumerKey '%s'" % self._consumer_key)
|
||||
|
||||
now = str(int(time.time()) + self.time_delta)
|
||||
signature = hashlib.sha1()
|
||||
signature.update(
|
||||
"+".join([self._application_secret, self._consumer_key, method.upper(), target, body, now]).encode(
|
||||
"utf-8"
|
||||
)
|
||||
)
|
||||
|
||||
headers["X-Ovh-Consumer"] = self._consumer_key
|
||||
headers["X-Ovh-Timestamp"] = now
|
||||
headers["X-Ovh-Signature"] = "$1$" + signature.hexdigest()
|
||||
|
||||
headers["X-Ovh-Application"] = self._application_key
|
||||
return self._session.request(method, target, headers=headers, data=body, timeout=self._timeout)
|
||||
123
myenv/lib/python3.11/site-packages/ovh/config.py
Normal file
123
myenv/lib/python3.11/site-packages/ovh/config.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# Copyright (c) 2013-2024, OVH SAS.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of OVH SAS nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
|
||||
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
The straightforward way to use OVH's API keys is to embed them directly in the
|
||||
application code. While this is very convenient, it lacks of elegance and
|
||||
flexibility.
|
||||
|
||||
Alternatively it is suggested to use configuration files or environment
|
||||
variables so that the same code may run seamlessly in multiple environments.
|
||||
Production and development for instance.
|
||||
|
||||
This wrapper will first look for direct instantiation parameters then
|
||||
``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and
|
||||
``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not
|
||||
provided, it will look for a configuration file of the form:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[default]
|
||||
; general configuration: default endpoint
|
||||
endpoint=ovh-eu
|
||||
|
||||
[ovh-eu]
|
||||
; configuration specific to 'ovh-eu' endpoint
|
||||
application_key=my_app_key
|
||||
application_secret=my_application_secret
|
||||
consumer_key=my_consumer_key
|
||||
client_id=my_client_id
|
||||
client_secret=my_client_secret
|
||||
|
||||
The client will successively attempt to locate this configuration file in
|
||||
|
||||
1. Current working directory: ``./ovh.conf``
|
||||
2. Current user's home directory ``~/.ovh.conf``
|
||||
3. System wide configuration ``/etc/ovh.conf``
|
||||
|
||||
This lookup mechanism makes it easy to overload credentials for a specific
|
||||
project or user.
|
||||
"""
|
||||
|
||||
from configparser import NoOptionError, NoSectionError, RawConfigParser
|
||||
import os
|
||||
|
||||
__all__ = ["config"]
|
||||
|
||||
#: Locations where to look for configuration file by *increasing* priority
|
||||
CONFIG_PATH = [
|
||||
"/etc/ovh.conf",
|
||||
os.path.expanduser("~/.ovh.conf"),
|
||||
os.path.realpath("./ovh.conf"),
|
||||
]
|
||||
|
||||
|
||||
class ConfigurationManager:
|
||||
"""
|
||||
Application wide configuration manager
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Create a config parser and load config from environment.
|
||||
"""
|
||||
# create config parser
|
||||
self.config = RawConfigParser()
|
||||
self.config.read(CONFIG_PATH)
|
||||
|
||||
def get(self, section, name):
|
||||
"""
|
||||
Load parameter ``name`` from configuration, respecting priority order.
|
||||
Most of the time, ``section`` will correspond to the current api
|
||||
``endpoint``. ``default`` section only contains ``endpoint`` and general
|
||||
configuration.
|
||||
|
||||
:param str section: configuration section or region name. Ignored when
|
||||
looking in environment
|
||||
:param str name: configuration parameter to lookup
|
||||
"""
|
||||
# 1/ try env
|
||||
try:
|
||||
return os.environ["OVH_" + name.upper()]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# 2/ try from specified section/endpoint
|
||||
try:
|
||||
return self.config.get(section, name)
|
||||
except (NoSectionError, NoOptionError):
|
||||
pass
|
||||
|
||||
# not found, sorry
|
||||
return None
|
||||
|
||||
def read(self, config_file):
|
||||
# Read an other config file
|
||||
self.config.read(config_file)
|
||||
|
||||
|
||||
#: System wide instance :py:class:`ConfigurationManager` instance
|
||||
config = ConfigurationManager()
|
||||
111
myenv/lib/python3.11/site-packages/ovh/consumer_key.py
Normal file
111
myenv/lib/python3.11/site-packages/ovh/consumer_key.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# Copyright (c) 2013-2024, OVH SAS.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of OVH SAS nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
|
||||
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""
|
||||
This module provides a consumer key creation helper. Consumer keys are linked
|
||||
with permissions defining which endpoint they are allowed to call. Just like
|
||||
a physical key can unlock some doors but not others.
|
||||
|
||||
OVH API consumer keys authorization is pattern based. This makes it extremely
|
||||
powerful and flexible as it may apply on only a very specific subset of the API
|
||||
but it's also trickier to get right on simple scenarios.
|
||||
|
||||
Hence this module
|
||||
"""
|
||||
|
||||
# Common authorization patterns
|
||||
API_READ_ONLY = ["GET"]
|
||||
API_READ_WRITE = ["GET", "POST", "PUT", "DELETE"]
|
||||
API_READ_WRITE_SAFE = ["GET", "POST", "PUT"]
|
||||
|
||||
|
||||
class ConsumerKeyRequest(object):
|
||||
"""
|
||||
ConsumerKey request. The generated consumer key will be linked to the
|
||||
client's ``application_key``. When performing the request, the
|
||||
``consumer_key`` will automatically be registered in the client.
|
||||
|
||||
It is recommended to save the generated key as soon as it validated to avoid
|
||||
requesting a new one on each API access.
|
||||
"""
|
||||
|
||||
def __init__(self, client):
|
||||
"""
|
||||
Create a new consumer key helper on API ``client``. The keys will be
|
||||
tied to the ``application_key`` defined in the client.
|
||||
"""
|
||||
self._client = client
|
||||
self._access_rules = []
|
||||
|
||||
def request(self, redirect_url=None, allowedIPs=None):
|
||||
"""
|
||||
Create the consumer key with the configures autorizations. The user will
|
||||
need to validate it before it can be used with the API
|
||||
|
||||
>>> ck.request()
|
||||
{
|
||||
'state': 'pendingValidation',
|
||||
'consumerKey': 'TnpZAd5pYNqxk4RhlPiSRfJ4WrkmII2i',
|
||||
'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=now2OOAVO4Wp6t7bemyN9DMWIobhGjFNZSHmixtVJM4S7mzjkN2L5VBfG96Iy1i0'
|
||||
}
|
||||
""" # noqa: E501
|
||||
return self._client.request_consumerkey(self._access_rules, redirect_url, allowedIPs)
|
||||
|
||||
def add_rule(self, method, path):
|
||||
"""
|
||||
Add a new rule to the request. Will grant the ``(method, path)`` tuple.
|
||||
Path can be any API route pattern like ``/sms/*`` or ``/me``. For example,
|
||||
to grant RO access on personal data:
|
||||
|
||||
>>> ck.add_rule("GET", "/me")
|
||||
"""
|
||||
self._access_rules.append({"method": method.upper(), "path": path})
|
||||
|
||||
def add_rules(self, methods, path):
|
||||
"""
|
||||
Add rules for ``path`` pattern, for each methods in ``methods``. This is
|
||||
a convenient helper over ``add_rule``. For example, this could be used
|
||||
to grant all access on the API at once:
|
||||
|
||||
>>> ck.add_rules(["GET", "POST", "PUT", "DELETE"], "/*")
|
||||
"""
|
||||
for method in methods:
|
||||
self.add_rule(method, path)
|
||||
|
||||
def add_recursive_rules(self, methods, path):
|
||||
"""
|
||||
Use this method to grant access on a full API tree. This is the
|
||||
recommended way to grant access in the API. It will take care of granted
|
||||
the root call *AND* sub-calls for you. Which is commonly forgotten...
|
||||
For example, to grant a full access on ``/sms``:
|
||||
|
||||
>>> ck.add_recursive_rules(["GET", "POST", "PUT", "DELETE"], "/sms")
|
||||
"""
|
||||
path = path.rstrip("*/ ")
|
||||
if path:
|
||||
self.add_rules(methods, path)
|
||||
self.add_rules(methods, path + "/*")
|
||||
111
myenv/lib/python3.11/site-packages/ovh/exceptions.py
Normal file
111
myenv/lib/python3.11/site-packages/ovh/exceptions.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# Copyright (c) 2013-2024, OVH SAS.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of OVH SAS nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
|
||||
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
All exceptions used in OVH SDK derives from `APIError`
|
||||
"""
|
||||
|
||||
|
||||
class APIError(Exception):
|
||||
"""Base OVH API exception, all specific exceptions inherits from it."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.response = kwargs.pop("response", None)
|
||||
if self.response is not None:
|
||||
self.query_id = self.response.headers.get("X-OVH-QUERYID")
|
||||
else:
|
||||
self.query_id = None
|
||||
super(APIError, self).__init__(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
if self.query_id: # pragma: no cover
|
||||
return "{} \nOVH-Query-ID: {}".format(super(APIError, self).__str__(), self.query_id)
|
||||
else: # pragma: no cover
|
||||
return super(APIError, self).__str__()
|
||||
|
||||
|
||||
class HTTPError(APIError):
|
||||
"""Raised when the request fails at a low level (DNS, network, ...)"""
|
||||
|
||||
|
||||
class InvalidKey(APIError):
|
||||
"""Raised when trying to sign request with invalid key"""
|
||||
|
||||
|
||||
class InvalidCredential(APIError):
|
||||
"""Raised when trying to sign request with invalid consumer key"""
|
||||
|
||||
|
||||
class InvalidConfiguration(APIError):
|
||||
"""Raised when trying to load an invalid configuration into a client"""
|
||||
|
||||
|
||||
class InvalidResponse(APIError):
|
||||
"""Raised when api response is not valid json"""
|
||||
|
||||
|
||||
class InvalidRegion(APIError):
|
||||
"""Raised when region is not in `REGIONS`."""
|
||||
|
||||
|
||||
class ReadOnlyError(APIError):
|
||||
"""Raised when attempting to modify readonly data."""
|
||||
|
||||
|
||||
class ResourceNotFoundError(APIError):
|
||||
"""Raised when requested resource does not exist."""
|
||||
|
||||
|
||||
class BadParametersError(APIError):
|
||||
"""Raised when request contains bad parameters."""
|
||||
|
||||
|
||||
class ResourceConflictError(APIError):
|
||||
"""Raised when trying to create an already existing resource."""
|
||||
|
||||
|
||||
class NetworkError(APIError):
|
||||
"""Raised when there is an error from network layer."""
|
||||
|
||||
|
||||
class NotGrantedCall(APIError):
|
||||
"""Raised when there is an error from network layer."""
|
||||
|
||||
|
||||
class NotCredential(APIError):
|
||||
"""Raised when there is an error from network layer."""
|
||||
|
||||
|
||||
class Forbidden(APIError):
|
||||
"""Raised when there is an error from network layer."""
|
||||
|
||||
|
||||
class ResourceExpiredError(APIError):
|
||||
"""Raised when requested resource expired."""
|
||||
|
||||
|
||||
class OAuth2FailureError(APIError):
|
||||
"""Raised when the OAuth2 workflow fails"""
|
||||
125
myenv/lib/python3.11/site-packages/ovh/oauth2.py
Normal file
125
myenv/lib/python3.11/site-packages/ovh/oauth2.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# Copyright (c) 2013-2024, OVH SAS.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of OVH SAS nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
|
||||
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Thanks to https://github.com/requests/requests-oauthlib/issues/260 for the base used in this file.
|
||||
"""
|
||||
|
||||
from oauthlib.oauth2 import BackendApplicationClient, MissingTokenError, OAuth2Error, TokenExpiredError
|
||||
from requests_oauthlib import OAuth2Session
|
||||
|
||||
from .exceptions import OAuth2FailureError
|
||||
|
||||
|
||||
class RefreshOAuth2Session(OAuth2Session):
|
||||
_error = None
|
||||
|
||||
def __init__(self, token_url, **kwargs):
|
||||
self.token_url = token_url
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# This hijacks the hook mechanism to save details about the last token creation failure.
|
||||
# For now, there is no easy other way to access to these details;
|
||||
# see https://github.com/requests/requests-oauthlib/pull/441
|
||||
self.register_compliance_hook("access_token_response", self.save_error)
|
||||
self.register_compliance_hook("refresh_token_response", self.save_error)
|
||||
|
||||
# See __init__, used as compliance hooks
|
||||
def save_error(self, resp):
|
||||
if 200 <= resp.status_code <= 299:
|
||||
self._error = "Received invalid body: " + resp.text
|
||||
if resp.status_code >= 400:
|
||||
self._error = "Token creation failed with status_code={}, body={}".format(resp.status_code, resp.text)
|
||||
return resp
|
||||
|
||||
# Wraps OAuth2Session.fetch_token to enrich returned exception messages, wrapped in an unique class
|
||||
def fetch_token(self, *args, **kwargs):
|
||||
try:
|
||||
return super().fetch_token(*args, **kwargs)
|
||||
except MissingTokenError as e:
|
||||
desc = "OAuth2 failure: " + e.description
|
||||
if self._error:
|
||||
desc += " " + self._error
|
||||
|
||||
raise OAuth2FailureError(desc) from e
|
||||
except OAuth2Error as e:
|
||||
raise OAuth2FailureError("OAuth2 failure: " + str(e)) from e
|
||||
|
||||
# Wraps OAuth2Session.request to handle TokenExpiredError by fetching a new token and retrying
|
||||
def request(self, *args, **kwargs):
|
||||
try:
|
||||
return super().request(*args, **kwargs)
|
||||
except TokenExpiredError:
|
||||
self.token = self.fetch_token(token_url=self.token_url, **self.auto_refresh_kwargs)
|
||||
self.token_updater(self.token)
|
||||
return super().request(*args, **kwargs)
|
||||
|
||||
|
||||
class OAuth2:
|
||||
_session = None
|
||||
_token = None
|
||||
|
||||
def __init__(self, client_id, client_secret, token_url):
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.token_url = token_url
|
||||
|
||||
def token_updater(self, token):
|
||||
self._token = token
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
if self._session is None:
|
||||
self._session = RefreshOAuth2Session(
|
||||
token_url=self.token_url,
|
||||
client=BackendApplicationClient(
|
||||
client_id=self.client_id,
|
||||
scope=["all"],
|
||||
),
|
||||
token=self.token,
|
||||
token_updater=self.token_updater,
|
||||
auto_refresh_kwargs={
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
},
|
||||
)
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
if self._token is None:
|
||||
self._token = RefreshOAuth2Session(
|
||||
token_url=self.token_url,
|
||||
client=BackendApplicationClient(
|
||||
client_id=self.client_id,
|
||||
scope=["all"],
|
||||
),
|
||||
).fetch_token(
|
||||
token_url=self.token_url,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
)
|
||||
return self._token
|
||||
Reference in New Issue
Block a user