pyxecm 2.0.0__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyxecm might be problematic. Click here for more details.

Files changed (50) hide show
  1. pyxecm/__init__.py +2 -1
  2. pyxecm/avts.py +79 -33
  3. pyxecm/customizer/api/app.py +45 -796
  4. pyxecm/customizer/api/auth/__init__.py +1 -0
  5. pyxecm/customizer/api/{auth.py → auth/functions.py} +2 -64
  6. pyxecm/customizer/api/auth/router.py +78 -0
  7. pyxecm/customizer/api/common/__init__.py +1 -0
  8. pyxecm/customizer/api/common/functions.py +47 -0
  9. pyxecm/customizer/api/{metrics.py → common/metrics.py} +1 -1
  10. pyxecm/customizer/api/common/models.py +21 -0
  11. pyxecm/customizer/api/{payload_list.py → common/payload_list.py} +6 -1
  12. pyxecm/customizer/api/common/router.py +72 -0
  13. pyxecm/customizer/api/settings.py +25 -0
  14. pyxecm/customizer/api/terminal/__init__.py +1 -0
  15. pyxecm/customizer/api/terminal/router.py +87 -0
  16. pyxecm/customizer/api/v1_csai/__init__.py +1 -0
  17. pyxecm/customizer/api/v1_csai/router.py +87 -0
  18. pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
  19. pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
  20. pyxecm/customizer/api/v1_maintenance/models.py +12 -0
  21. pyxecm/customizer/api/v1_maintenance/router.py +76 -0
  22. pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
  23. pyxecm/customizer/api/v1_otcs/functions.py +61 -0
  24. pyxecm/customizer/api/v1_otcs/router.py +179 -0
  25. pyxecm/customizer/api/v1_payload/__init__.py +1 -0
  26. pyxecm/customizer/api/v1_payload/functions.py +179 -0
  27. pyxecm/customizer/api/v1_payload/models.py +51 -0
  28. pyxecm/customizer/api/v1_payload/router.py +499 -0
  29. pyxecm/customizer/browser_automation.py +568 -326
  30. pyxecm/customizer/customizer.py +204 -430
  31. pyxecm/customizer/guidewire.py +907 -43
  32. pyxecm/customizer/k8s.py +243 -56
  33. pyxecm/customizer/m365.py +104 -15
  34. pyxecm/customizer/payload.py +1943 -885
  35. pyxecm/customizer/pht.py +19 -2
  36. pyxecm/customizer/servicenow.py +22 -5
  37. pyxecm/customizer/settings.py +9 -6
  38. pyxecm/helper/xml.py +69 -0
  39. pyxecm/otac.py +1 -1
  40. pyxecm/otawp.py +2104 -1535
  41. pyxecm/otca.py +569 -0
  42. pyxecm/otcs.py +201 -37
  43. pyxecm/otds.py +35 -13
  44. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/METADATA +6 -29
  45. pyxecm-2.0.1.dist-info/RECORD +76 -0
  46. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
  47. pyxecm-2.0.0.dist-info/RECORD +0 -54
  48. /pyxecm/customizer/api/{models.py → auth/models.py} +0 -0
  49. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/licenses/LICENSE +0 -0
  50. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/top_level.txt +0 -0
pyxecm/customizer/m365.py CHANGED
@@ -116,9 +116,9 @@ class M365:
116
116
  # Set the data for the token request
117
117
  m365_config["tokenData"] = {
118
118
  "client_id": client_id,
119
- "scope": "https://graph.microsoft.com/.default",
120
119
  "client_secret": client_secret,
121
120
  "grant_type": "client_credentials",
121
+ "scope": "https://graph.microsoft.com/.default",
122
122
  }
123
123
 
124
124
  m365_config["meUrl"] = m365_config["graphUrl"] + "me"
@@ -157,7 +157,7 @@ class M365:
157
157
 
158
158
  return self.config()["tokenData"]
159
159
 
160
- def credentials_user(self, username: str, password: str) -> dict:
160
+ def credentials_user(self, username: str, password: str, scope: str = "Files.ReadWrite") -> dict:
161
161
  """Get user credentials.
162
162
 
163
163
  In some cases MS Graph APIs cannot be called via
@@ -173,6 +173,10 @@ class M365:
173
173
  The M365 username.
174
174
  password (str):
175
175
  The password of the M365 user.
176
+ scope (str):
177
+ The scope of the delegated permission.
178
+ It is important to provide a scope for the intended operation
179
+ like "Files.ReadWrite".
176
180
 
177
181
  Returns:
178
182
  dict:
@@ -180,13 +184,14 @@ class M365:
180
184
 
181
185
  """
182
186
 
187
+ # Use OAuth2 / ROPC (Resource Owner Password Credentials):
183
188
  credentials = {
184
189
  "client_id": self.config()["clientId"],
185
- "scope": "https://graph.microsoft.com/.default",
186
190
  "client_secret": self.config()["clientSecret"],
187
191
  "grant_type": "password",
188
192
  "username": username,
189
193
  "password": password,
194
+ "scope": scope,
190
195
  }
191
196
 
192
197
  return credentials
@@ -233,10 +238,14 @@ class M365:
233
238
  """
234
239
 
235
240
  request_header = {
236
- "Authorization": "Bearer {}".format(self._user_access_token),
237
241
  "Content-Type": content_type,
238
242
  }
239
243
 
244
+ if not self._user_access_token:
245
+ self.logger.error("No M365 user is authenticated! Cannot include Bearer token in request header!")
246
+ else:
247
+ request_header["Authorization"] = "Bearer {}".format(self._user_access_token)
248
+
240
249
  return request_header
241
250
 
242
251
  # end method definition
@@ -359,6 +368,14 @@ class M365:
359
368
  )
360
369
  time.sleep((retries + 1) * 60)
361
370
  retries += 1
371
+ elif response.status_code in [404, 429] and retries < REQUEST_MAX_RETRIES:
372
+ self.logger.warning(
373
+ "M365 Graph API is too slow or throtteling calls -> %s; retrying in %s seconds...",
374
+ response.status_code,
375
+ (retries + 1) * 60,
376
+ )
377
+ time.sleep((retries + 1) * 60)
378
+ retries += 1
362
379
  else:
363
380
  # Handle plain HTML responses to not pollute the logs
364
381
  content_type = response.headers.get("content-type", None)
@@ -679,7 +696,7 @@ class M365:
679
696
  Returns:
680
697
  str | None:
681
698
  The access token. Also stores access token in self._access_token.
682
- None in case of error.
699
+ None in case of an error.
683
700
 
684
701
  """
685
702
 
@@ -710,7 +727,7 @@ class M365:
710
727
  self.logger.warning(
711
728
  "Unable to connect to -> %s : %s",
712
729
  self.config()["authenticationUrl"],
713
- exception,
730
+ str(exception),
714
731
  )
715
732
  return None
716
733
 
@@ -718,9 +735,8 @@ class M365:
718
735
  authenticate_dict = self.parse_request_response(authenticate_response)
719
736
  if not authenticate_dict:
720
737
  return None
721
- else:
722
- access_token = authenticate_dict["access_token"]
723
- self.logger.debug("Access Token -> %s", access_token)
738
+ access_token = authenticate_dict["access_token"]
739
+ self.logger.debug("Access Token -> %s", access_token)
724
740
  else:
725
741
  self.logger.error(
726
742
  "Failed to request an M365 Access Token; error -> %s",
@@ -735,7 +751,7 @@ class M365:
735
751
 
736
752
  # end method definition
737
753
 
738
- def authenticate_user(self, username: str, password: str) -> str | None:
754
+ def authenticate_user(self, username: str, password: str, scope: str | None = None) -> str | None:
739
755
  """Authenticate at M365 Graph API with username and password.
740
756
 
741
757
  Args:
@@ -743,10 +759,14 @@ class M365:
743
759
  The name (email) of the M365 user.
744
760
  password (str):
745
761
  The password of the M365 user.
762
+ scope (str):
763
+ The scope of the delegated permission. E.g. "Files.ReadWrite".
764
+ Multiple delegated permissions should be separated by spaces.
746
765
 
747
766
  Returns:
748
767
  str | None:
749
768
  The access token for the user. Also stores access token in self._access_token.
769
+ None in case of an error.
750
770
 
751
771
  """
752
772
 
@@ -764,12 +784,13 @@ class M365:
764
784
  return None
765
785
 
766
786
  self.logger.debug(
767
- "Requesting M365 Access Token for user -> %s from -> %s",
787
+ "Requesting M365 Access Token for user -> %s from -> %s%s",
768
788
  username,
769
789
  request_url,
790
+ " with scope -> '{}'".format(scope) if scope else "",
770
791
  )
771
792
 
772
- authenticate_post_body = self.credentials_user(username, password)
793
+ authenticate_post_body = self.credentials_user(username=username, password=password, scope=scope)
773
794
  authenticate_response = None
774
795
 
775
796
  try:
@@ -784,7 +805,7 @@ class M365:
784
805
  "Unable to connect to -> %s with username -> %s: %s",
785
806
  self.config()["authenticationUrl"],
786
807
  username,
787
- exception,
808
+ str(exception),
788
809
  )
789
810
  return None
790
811
 
@@ -1354,6 +1375,74 @@ class M365:
1354
1375
 
1355
1376
  # end method definition
1356
1377
 
1378
+ def get_user_drive(self, user_id: str, me: bool = False) -> dict | None:
1379
+ """Get the mysite (OneDrive) of the user.
1380
+
1381
+ It may be required to do this before certain other operations
1382
+ are possible. These operations may require that the mydrive
1383
+ is initialized for that user. If you get errors like
1384
+ "User's mysite not found." this may be the case.
1385
+
1386
+ Args:
1387
+ user_id (str):
1388
+ The M365 GUID of the user (can also be the M365 email of the user).
1389
+ me (bool, optional):
1390
+ Should be True if the user itself is accessing the drive.
1391
+
1392
+ Returns:
1393
+ dict:
1394
+ A list of user licenses or None if request fails.
1395
+
1396
+ Example:
1397
+ {
1398
+ '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#drives/$entity',
1399
+ 'createdDateTime': '2025-04-10T23:43:26Z',
1400
+ 'description': '',
1401
+ 'id': 'b!VsxYN1IbrEqbiwMiba_M7FCkNAhL5LRFnQEZpEYbxDAxvvcvUMAhSqfgWW_4eAUP',
1402
+ 'lastModifiedDateTime': '2025-04-12T15:50:20Z',
1403
+ 'name': 'OneDrive',
1404
+ 'webUrl': 'https://ideateqa-my.sharepoint.com/personal/jbenham_qa_idea-te_eimdemo_com/Documents',
1405
+ 'driveType': 'business',
1406
+ 'createdBy': {
1407
+ 'user': {
1408
+ 'displayName': 'System Account'
1409
+ }
1410
+ },
1411
+ 'lastModifiedBy': {
1412
+ 'user': {
1413
+ 'email': 'jbenham@qa.idea-te.eimdemo.com',
1414
+ 'id': '470060cc-4d9f-439e-8a6e-8d567c5bda80',
1415
+ 'displayName': 'Jeff Benham'
1416
+ }
1417
+ },
1418
+ 'owner': {
1419
+ 'user': {...}
1420
+ },
1421
+ 'quota': {
1422
+ 'deleted': 0,
1423
+ 'remaining': 1099511518137,
1424
+ 'state': 'normal',
1425
+ 'total': 1099511627776,
1426
+ 'used': 109639
1427
+ }
1428
+ }
1429
+
1430
+ """
1431
+
1432
+ request_url = self.config()["meUrl"] if me else self.config()["usersUrl"] + "/" + user_id
1433
+ request_url += "/drive"
1434
+ request_header = self.request_header_user() if me else self.request_header()
1435
+
1436
+ return self.do_request(
1437
+ url=request_url,
1438
+ method="GET",
1439
+ headers=request_header,
1440
+ timeout=REQUEST_TIMEOUT,
1441
+ failure_message="Failed to get mySite (drive) of M365 user -> {}".format(user_id),
1442
+ )
1443
+
1444
+ # end method definition
1445
+
1357
1446
  def get_groups(
1358
1447
  self,
1359
1448
  max_number: int = 250,
@@ -4799,7 +4888,7 @@ class M365:
4799
4888
  """
4800
4889
 
4801
4890
  request_url = self.config()["sitesUrl"] + "/" + site_id + "/pages"
4802
- request_headers = self.request_header()
4891
+ request_header = self.request_header()
4803
4892
 
4804
4893
  # Page payload for a basic site page
4805
4894
  payload = {
@@ -4814,7 +4903,7 @@ class M365:
4814
4903
  response = self.do_request(
4815
4904
  url=request_url,
4816
4905
  method="POST",
4817
- headers=request_headers,
4906
+ headers=request_header,
4818
4907
  json_data=payload,
4819
4908
  timeout=REQUEST_TIMEOUT,
4820
4909
  failure_message="Failed to create SharePoint page -> '{}' in SharePoint site -> '{}'".format(