Skip to content

Commit c1ed218

Browse files
committed
add decorators for imports and fix pylint errors
1 parent 463de66 commit c1ed218

13 files changed

Lines changed: 216 additions & 32 deletions

File tree

plugins/module_utils/auth.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,54 @@
11
import json
22
import re
3+
import traceback
4+
from functools import wraps
35

46
from urllib.parse import parse_qs, quote_plus, urljoin
5-
from bs4 import BeautifulSoup
6-
from requests.models import HTTPError
77

88
from . import constants as C
99
from . import exceptions
1010

11+
try:
12+
from bs4 import BeautifulSoup
13+
HAS_BS4 = True
14+
except ImportError:
15+
HAS_BS4 = False
16+
BS4_IMPORT_ERROR = traceback.format_exc()
17+
BeautifulSoup = None
18+
19+
try:
20+
from requests.models import HTTPError
21+
HAS_REQUESTS = True
22+
except ImportError:
23+
HAS_REQUESTS = False
24+
REQUESTS_IMPORT_ERROR = traceback.format_exc()
25+
HTTPError = None
26+
1127
_GIGYA_SDK_BUILD_NUMBER = None
1228

1329

30+
def require_bs4(func):
31+
# A decorator to check for the 'beautifulsoup4' library before executing a function.
32+
@wraps(func)
33+
def wrapper(*args, **kwargs):
34+
if not HAS_BS4:
35+
raise exceptions.SapLaunchpadError(f"The 'beautifulsoup4' library is required. Error: {BS4_IMPORT_ERROR}")
36+
return func(*args, **kwargs)
37+
return wrapper
38+
39+
40+
def require_requests(func):
41+
# A decorator to check for the 'requests' library before executing a function.
42+
@wraps(func)
43+
def wrapper(*args, **kwargs):
44+
if not HAS_REQUESTS:
45+
raise exceptions.SapLaunchpadError(f"The 'requests' library is required. Error: {REQUESTS_IMPORT_ERROR}")
46+
return func(*args, **kwargs)
47+
return wrapper
48+
49+
50+
@require_requests
51+
@require_bs4
1452
def login(client, username, password):
1553
# Main authentication function.
1654
#
@@ -69,6 +107,8 @@ def login(client, username, password):
69107
client.post(endpoint, data=meta, headers=C.GIGYA_HEADERS)
70108

71109

110+
@require_requests
111+
@require_bs4
72112
def get_sso_endpoint_meta(client, url, **kwargs):
73113
# Scrapes an HTML page to find the next SSO form action URL and its input fields.
74114
method = 'POST' if kwargs.get('data') or kwargs.get('json') else 'GET'
@@ -100,6 +140,7 @@ def get_sso_endpoint_meta(client, url, **kwargs):
100140
return (endpoint, metadata)
101141

102142

143+
@require_requests
103144
def _get_gigya_login_params(client, url, data):
104145
# Follows a redirect and extracts parameters from the resulting URL's query string.
105146
gigya_idp_res = client.post(url, data=data)
@@ -109,9 +150,10 @@ def _get_gigya_login_params(client, url, data):
109150
return params
110151

111152

153+
@require_requests
112154
def _gigya_websdk_bootstrap(client, params):
113155
# Performs the initial bootstrap call to the Gigya WebSDK.
114-
page_url = f'{C.URL_ACCOUNT_SAML_PROXY}?apiKey=' + params['apiKey'],
156+
page_url = f'{C.URL_ACCOUNT_SAML_PROXY}?apiKey=' + params['apiKey']
115157
params.update({
116158
'pageURL': page_url,
117159
'sdk': 'js_latest',
@@ -124,6 +166,7 @@ def _gigya_websdk_bootstrap(client, params):
124166
headers=C.GIGYA_HEADERS)
125167

126168

169+
@require_requests
127170
def _gigya_login(client, username, password, api_key):
128171
# Performs a login using the standard Gigya accounts.login API.
129172
# This avoids a custom SAP endpoint that triggers password change notifications.
@@ -154,6 +197,7 @@ def _gigya_login(client, username, password, api_key):
154197
return login_response.get('login_token')
155198

156199

200+
@require_requests
157201
def _get_id_token(client, saml_params, login_token):
158202
# Exchanges a Gigya login token for a JWT ID token.
159203
query_params = {
@@ -166,6 +210,7 @@ def _get_id_token(client, saml_params, login_token):
166210
return token
167211

168212

213+
@require_requests
169214
def _get_uid(client, saml_params, login_token):
170215
# Retrieves the user's unique ID (UID) using the login token.
171216
query_params = {
@@ -177,6 +222,7 @@ def _get_uid(client, saml_params, login_token):
177222
return uid
178223

179224

225+
@require_requests
180226
def _get_uid_details(client, uid, id_token):
181227
# Fetches detailed account information for a given UID.
182228
url = f'{C.URL_ACCOUNT_CORE_API}/accounts/{uid}'
@@ -187,16 +233,18 @@ def _get_uid_details(client, uid, id_token):
187233
return uid_details_response
188234

189235

236+
@require_requests
190237
def _is_uid_linked_multiple_sids(uid_details):
191238
# Checks if a Universal ID (UID) is linked to more than one S-User ID.
192239
accounts = uid_details['accounts']
193240
linked = []
194-
for _, v in accounts.items():
241+
for _account_type, v in accounts.items():
195242
linked.extend(v['linkedAccounts'])
196243

197244
return len(linked) > 1
198245

199246

247+
@require_requests
200248
def _select_account(client, uid, sid, id_token):
201249
# Selects a specific S-User ID when a Universal ID is linked to multiple accounts.
202250
url = f'{C.URL_ACCOUNT_CORE_API}/accounts/{uid}/selectedAccount'
@@ -207,6 +255,7 @@ def _select_account(client, uid, sid, id_token):
207255
return client.request('PUT', url, headers=headers, json=data)
208256

209257

258+
@require_requests
210259
def _get_sdk_build_number(client, api_key):
211260
# Fetches the gigya.js file to extract and cache the SDK build number.
212261
global _GIGYA_SDK_BUILD_NUMBER
@@ -224,6 +273,7 @@ def _get_sdk_build_number(client, api_key):
224273
return build_number
225274

226275

276+
@require_requests
227277
def _cdc_api_request(client, endpoint, saml_params, query_params):
228278
# Helper to make requests to the Gigya/CDC API, handling common parameters and errors.
229279
url = '/'.join((C.URL_ACCOUNT_CDC_API, endpoint))

plugins/module_utils/client.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
1-
import requests
21
import re
3-
import urllib3
2+
import traceback
43

54
from urllib.parse import urlparse
6-
from requests.adapters import HTTPAdapter
75

86
from .constants import COMMON_HEADERS
9-
10-
11-
class _SessionAllowBasicAuthRedirects(requests.Session):
7+
from . import exceptions
8+
9+
try:
10+
import requests
11+
from requests.adapters import HTTPAdapter
12+
_RequestsSession = requests.Session
13+
HAS_REQUESTS = True
14+
except ImportError:
15+
HAS_REQUESTS = False
16+
REQUESTS_IMPORT_ERROR = traceback.format_exc()
17+
# Placeholders to prevent errors on module load
18+
requests = None
19+
HTTPAdapter = object
20+
_RequestsSession = object
21+
22+
try:
23+
import urllib3
24+
HAS_URLLIB3 = True
25+
except ImportError:
26+
HAS_URLLIB3 = False
27+
URLLIB3_IMPORT_ERROR = traceback.format_exc()
28+
# Placeholder to prevent errors on module load
29+
urllib3 = None
30+
31+
32+
class _SessionAllowBasicAuthRedirects(_RequestsSession):
1233
# By default, the `Authorization` header for Basic Auth will be removed
1334
# if the redirect is to a different host.
1435
# In our case, the DirectDownloadLink with `softwaredownloads.sap.com` domain
@@ -17,7 +38,8 @@ class _SessionAllowBasicAuthRedirects(requests.Session):
1738
# for sap.com domains.
1839
# This is only required for legacy API.
1940
def rebuild_auth(self, prepared_request, response):
20-
if 'Authorization' in prepared_request.headers:
41+
# The parent class might not be a real requests.Session if requests is not installed.
42+
if HAS_REQUESTS and 'Authorization' in prepared_request.headers:
2143
request_hostname = urlparse(prepared_request.url).hostname
2244
if not re.match(r'.*sap.com$', request_hostname):
2345
del prepared_request.headers['Authorization']
@@ -28,6 +50,9 @@ def _is_updated_urllib3():
2850
# and will be removed in v2.0.0.
2951
# Typically, the default version on RedHat 8.2 is 1.24.2,
3052
# so we need to check the version of urllib3 to see if it's updated.
53+
if not HAS_URLLIB3:
54+
return False
55+
3156
urllib3_version = urllib3.__version__.split('.')
3257
if len(urllib3_version) == 2:
3358
urllib3_version.append('0')
@@ -44,6 +69,11 @@ class ApiClient:
4469
# object-oriented interface for making API requests, replacing the
4570
# previous global session and request functions.
4671
def __init__(self):
72+
if not HAS_REQUESTS:
73+
raise exceptions.SapLaunchpadError(f"The 'requests' library is required. Error: {REQUESTS_IMPORT_ERROR}")
74+
if not HAS_URLLIB3:
75+
raise exceptions.SapLaunchpadError(f"The 'urllib3' library is required. Error: {URLLIB3_IMPORT_ERROR}")
76+
4777
self.session = _SessionAllowBasicAuthRedirects()
4878

4979
# Configure retry logic for the session.

plugins/module_utils/maintenance_planner/api.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,54 @@
11
import re
22
import time
3+
import traceback
34
from html import unescape
5+
from functools import wraps
46
from urllib.parse import urljoin
5-
from bs4 import BeautifulSoup
6-
from lxml import etree
77

88
from .. import constants as C
99
from .. import exceptions
1010
from ..auth import get_sso_endpoint_meta
1111

12+
try:
13+
from bs4 import BeautifulSoup
14+
HAS_BS4 = True
15+
except ImportError:
16+
HAS_BS4 = False
17+
BS4_IMPORT_ERROR = traceback.format_exc()
18+
19+
try:
20+
from lxml import etree
21+
HAS_LXML = True
22+
except ImportError:
23+
HAS_LXML = False
24+
LXML_IMPORT_ERROR = traceback.format_exc()
25+
1226
# Module-level cache
1327
_MP_XSRF_TOKEN = None
1428
_MP_TRANSACTIONS = None
1529
_MP_NAMESPACE = 'http://xml.sap.com/2012/01/mnp'
1630

1731

32+
def require_bs4(func):
33+
# A decorator to check for the 'beautifulsoup4' library before executing a function.
34+
@wraps(func)
35+
def wrapper(*args, **kwargs):
36+
if not HAS_BS4:
37+
raise exceptions.SapLaunchpadError(f"The 'beautifulsoup4' library is required. Error: {BS4_IMPORT_ERROR}")
38+
return func(*args, **kwargs)
39+
return wrapper
40+
41+
42+
def require_lxml(func):
43+
# A decorator to check for the 'lxml' library before executing a function.
44+
@wraps(func)
45+
def wrapper(*args, **kwargs):
46+
if not HAS_LXML:
47+
raise exceptions.SapLaunchpadError(f"The 'lxml' library is required. Error: {LXML_IMPORT_ERROR}")
48+
return func(*args, **kwargs)
49+
return wrapper
50+
51+
1852
def auth_userapps(client):
1953
# Authenticates against userapps.support.sap.com to establish a session.
2054
_clear_mp_cookies(client, 'userapps')
@@ -32,6 +66,7 @@ def auth_userapps(client):
3266
client.post(endpoint, data=meta)
3367

3468

69+
@require_bs4
3570
def get_transactions(client):
3671
# Retrieves a list of all available Maintenance Planner transactions.
3772
global _MP_TRANSACTIONS
@@ -67,6 +102,7 @@ def get_transaction_id(client, name):
67102
raise exceptions.FileNotFoundError(f"Transaction '{name}' not found by name or display ID.")
68103

69104

105+
@require_lxml
70106
def get_transaction_filename_url(client, trans_id):
71107
# Parses the files XML to get a list of (URL, Filename) tuples.
72108
xml = _get_download_files_xml(client, trans_id)
@@ -175,6 +211,7 @@ def _get_transaction(client, key, value):
175211
raise exceptions.FileNotFoundError(f"Transaction with {key}='{value}' not found.")
176212

177213

214+
@require_lxml
178215
def _build_mnp_xml(**params):
179216
# Constructs the MNP XML payload for API requests.
180217
mnp = f'{{{_MP_NAMESPACE}}}'

plugins/module_utils/maintenance_planner/main.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
from .. import auth, exceptions
44
from ..client import ApiClient
55
from . import api
6-
from requests.exceptions import HTTPError
76

7+
try:
8+
from requests.exceptions import HTTPError
9+
HAS_REQUESTS = True
10+
except ImportError:
11+
HAS_REQUESTS = False
12+
HTTPError = None
813

914
def run_files(params):
1015
# Runner for maintenance_planner_files module.
@@ -14,6 +19,11 @@ def run_files(params):
1419
msg=''
1520
)
1621

22+
if not HAS_REQUESTS:
23+
result['failed'] = True
24+
result['msg'] = "The 'requests' library is required for this module."
25+
return result
26+
1727
client = ApiClient()
1828
username = params['suser_id']
1929
password = params['suser_password']
@@ -56,6 +66,11 @@ def run_stack_xml_download(params):
5666
msg=''
5767
)
5868

69+
if not HAS_REQUESTS:
70+
result['failed'] = True
71+
result['msg'] = "The 'requests' library is required for this module."
72+
return result
73+
5974
client = ApiClient()
6075
username = params['suser_id']
6176
password = params['suser_password']

0 commit comments

Comments
 (0)