centralcli 8.2.2__tar.gz → 8.2.4__tar.gz

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.
Files changed (175) hide show
  1. {centralcli-8.2.2 → centralcli-8.2.4}/PKG-INFO +1 -1
  2. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cache.py +54 -8
  3. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cleaner.py +0 -7
  4. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clibatch.py +7 -3
  5. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clicommon.py +97 -1
  6. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishow.py +13 -6
  7. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/models.py +2 -2
  8. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/render.py +58 -26
  9. {centralcli-8.2.2 → centralcli-8.2.4}/pyproject.toml +1 -1
  10. centralcli-8.2.2/centralcli/cnxcli/__init__.py +0 -25
  11. centralcli-8.2.2/centralcli/cnxcli/api/__init__.py +0 -12
  12. centralcli-8.2.2/centralcli/cnxcli/api/cnxapi.py +0 -488
  13. centralcli-8.2.2/centralcli/cnxcli/cli.py +0 -71
  14. centralcli-8.2.2/centralcli/cnxcli/client.py +0 -706
  15. centralcli-8.2.2/centralcli/cnxcli/clioptions.py +0 -66
  16. centralcli-8.2.2/centralcli/cnxcli/clitree/__init__.py +0 -0
  17. centralcli-8.2.2/centralcli/cnxcli/clitree/assign/__init__.py +0 -27
  18. centralcli-8.2.2/centralcli/cnxcli/clitree/assign/assign.py +0 -74
  19. centralcli-8.2.2/centralcli/cnxcli/clitree/common.py +0 -96
  20. centralcli-8.2.2/centralcli/cnxcli/clitree/show/__init__.py +0 -28
  21. centralcli-8.2.2/centralcli/cnxcli/clitree/show/auth.py +0 -48
  22. centralcli-8.2.2/centralcli/cnxcli/clitree/show/show.py +0 -181
  23. centralcli-8.2.2/centralcli/cnxcli/clitree/show/switch.py +0 -64
  24. centralcli-8.2.2/centralcli/cnxcli/clitree/update/__init__.py +0 -27
  25. centralcli-8.2.2/centralcli/cnxcli/clitree/update/auth.py +0 -121
  26. centralcli-8.2.2/centralcli/cnxcli/clitree/update/update.py +0 -343
  27. centralcli-8.2.2/centralcli/cnxcli/cnx_cleaner.py +0 -85
  28. centralcli-8.2.2/centralcli/cnxcli/models/__init__.py +0 -0
  29. centralcli-8.2.2/centralcli/cnxcli/models/auth_server_global.py +0 -719
  30. centralcli-8.2.2/centralcli/cnxcli/models/local_management.py +0 -5415
  31. centralcli-8.2.2/centralcli/cnxcli/models/system_info.py +0 -396
  32. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/__init__.py +0 -0
  33. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/base.py +0 -260
  34. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/base_utils.py +0 -188
  35. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/__init__.py +0 -0
  36. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/audit_logs.py +0 -182
  37. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/base.py +0 -689
  38. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/base_utils.py +0 -250
  39. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/configuration.py +0 -1389
  40. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/constants.py +0 -42
  41. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/device_inventory.py +0 -252
  42. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/firmware_management.py +0 -277
  43. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/licensing.py +0 -488
  44. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/monitoring.py +0 -313
  45. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/msp.py +0 -891
  46. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/rapids.py +0 -513
  47. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/refresh_api_token.py +0 -67
  48. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/topology.py +0 -145
  49. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/url_utils.py +0 -244
  50. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/user_management.py +0 -427
  51. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/visualrf.py +0 -351
  52. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/workflows/__init__.py +0 -0
  53. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/workflows/workflows_utils.py +0 -154
  54. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/configuration/__init__.py +0 -0
  55. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/__init__.py +0 -6
  56. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/generic_op_error.py +0 -35
  57. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/login_error.py +0 -30
  58. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/parameter_error.py +0 -11
  59. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/pycentral_error.py +0 -31
  60. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/response_error.py +0 -32
  61. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/unsupported_capability_error.py +0 -12
  62. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/verification_error.py +0 -31
  63. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/__init__.py +0 -3
  64. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/devices.py +0 -556
  65. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/glp_utils.py +0 -80
  66. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/service_manager.py +0 -44
  67. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/subscription.py +0 -196
  68. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/user_management.py +0 -142
  69. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/monitoring/__init__.py +0 -0
  70. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/__init__.py +0 -3
  71. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/ntp.py +0 -19
  72. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/profiles.py +0 -234
  73. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/system_info.py +0 -291
  74. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/vlan.py +0 -278
  75. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/wlan.py +0 -20
  76. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/__init__.py +0 -6
  77. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/device.py +0 -92
  78. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/scope_maps.py +0 -207
  79. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/scope_utils.py +0 -338
  80. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/scopes.py +0 -1053
  81. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/site.py +0 -396
  82. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/site_collection.py +0 -435
  83. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/__init__.py +0 -0
  84. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/assign_vlan_profile_site.py +0 -131
  85. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/assign_wlan_profile_site.py +0 -115
  86. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/__init__.py +0 -0
  87. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/account_credentials.json +0 -14
  88. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/cleanup_site_profile_workflow.py +0 -84
  89. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/profile_config.json +0 -65
  90. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/site_profile_workflow.py +0 -103
  91. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/sites_config.json +0 -38
  92. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/__init__.py +0 -0
  93. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/account_credentials.json +0 -16
  94. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/glp_onboarding.py +0 -219
  95. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/reset/old_account_credentials.json +0 -16
  96. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/reset/region_name_mapping.json +0 -14
  97. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/workflow_vars.json +0 -33
  98. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/exception_test/account_credentials.json +0 -14
  99. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/exception_test/cleanup_exception.py +0 -42
  100. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/exception_test/exception_test.py +0 -63
  101. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/exception_test/profile_config.json +0 -65
  102. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp/__init__.py +0 -0
  103. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp/test_glp.py +0 -354
  104. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp/test_glp_vars.json +0 -115
  105. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp-access-token-example.py +0 -19
  106. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp-client-creds-example.py +0 -20
  107. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/profiles/central_token.json +0 -7
  108. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/profiles/profile_config.json +0 -21
  109. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/profiles/system_info_profile.py +0 -83
  110. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/profiles/system_info_profile_cleanup.py +0 -83
  111. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scope-management-example.py +0 -171
  112. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scope-management-vars.json +0 -24
  113. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scopes/__init__.py +0 -0
  114. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scopes/test_scopes.py +0 -706
  115. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scopes/test_scopes_vars.json +0 -185
  116. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/url_utils.py +0 -83
  117. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/utils/__init__.py +0 -1
  118. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/utils/common_utils.py +0 -41
  119. centralcli-8.2.2/centralcli/cnxcli/pycentralv2/utils/url_utils.py +0 -107
  120. {centralcli-8.2.2 → centralcli-8.2.4}/LICENSE +0 -0
  121. {centralcli-8.2.2 → centralcli-8.2.4}/README.md +0 -0
  122. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/__init__.py +0 -0
  123. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/boilerplate/README.md +0 -0
  124. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/boilerplate/_cnx_allcalls.py +0 -0
  125. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/boilerplate/allcalls.py +0 -0
  126. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/caas.py +0 -0
  127. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/central.py +0 -0
  128. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cli.py +0 -0
  129. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliadd.py +0 -0
  130. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliassign.py +0 -0
  131. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clicaas.py +0 -0
  132. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clicancel.py +0 -0
  133. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clicheck.py +0 -0
  134. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliclone.py +0 -0
  135. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clidel.py +0 -0
  136. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clidelfirmware.py +0 -0
  137. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliexport.py +0 -0
  138. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clikick.py +0 -0
  139. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clioptions.py +0 -0
  140. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clirefresh.py +0 -0
  141. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clirename.py +0 -0
  142. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliset.py +0 -0
  143. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clisetfirmware.py +0 -0
  144. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowaudit.py +0 -0
  145. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowbandwidth.py +0 -0
  146. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowbranch.py +0 -0
  147. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowcloudauth.py +0 -0
  148. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowfirmware.py +0 -0
  149. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowmpsk.py +0 -0
  150. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowospf.py +0 -0
  151. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowoverlay.py +0 -0
  152. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowtshoot.py +0 -0
  153. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowwids.py +0 -0
  154. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clitest.py +0 -0
  155. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clitshoot.py +0 -0
  156. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliunassign.py +0 -0
  157. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliupdate.py +0 -0
  158. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliupgrade.py +0 -0
  159. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/config.py +0 -0
  160. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/constants.py +0 -0
  161. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/exceptions.py +0 -0
  162. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/logger.py +0 -0
  163. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/objects.py +0 -0
  164. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/response.py +0 -0
  165. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/setup.py +0 -0
  166. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/static/favicon.ico +0 -0
  167. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/strings.py +0 -0
  168. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/typedefs.py +0 -0
  169. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/utils.py +0 -0
  170. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/vendored/csvlexer/__init__.py +0 -0
  171. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/vendored/csvlexer/csv.py +0 -0
  172. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/vscodeargs.py +0 -0
  173. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/wh2snow.py +0 -0
  174. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/wh_proxy.py +0 -0
  175. {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/wh_proxy_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: centralcli
3
- Version: 8.2.2
3
+ Version: 8.2.4
4
4
  Summary: A CLI for interacting with Aruba Central (Cloud Management Platform). Facilitates bulk imports, exports, reporting. A handy tool if you have devices managed by Aruba Central.
5
5
  License: MIT
6
6
  Keywords: cli,Aruba,Aruba Networks,Aruba Central,HPE,API,RESTFUL,REST
@@ -2537,14 +2537,16 @@ class Cache:
2537
2537
  return await self.update_db(DB, data=list(updated_devs_by_serial.values()), truncate=True)
2538
2538
 
2539
2539
 
2540
- async def update_db(self, db: Table, data: List[Dict[str, Any]] | Dict[str, Any] = None, doc_ids: List[int] | int = None, truncate: bool = True) -> bool:
2540
+ async def update_db(self, db: Table, data: List[Dict[str, Any]] | Dict[str, Any] = None, *, doc_ids: List[int] | int = None, dev_types: constants.GenericDeviceTypes | List[constants.GenericDeviceTypes] = None, truncate: bool = True,) -> bool:
2541
2541
  """Update Local Cache DB
2542
2542
 
2543
2543
  Args:
2544
2544
  db (Table): TinyDB Table object to be updated.
2545
2545
  data (List[Dict[str, Any]] | Dict[str, Any], optional): Data to be added to database. Defaults to None.
2546
- truncate (bool, optional): Existing DB data will be discarded, and all data in DB will be replaced with provided. Defaults to True.
2547
2546
  doc_ids (List[int] | int, optional): doc_ids to be deleted from the DB. Defaults to None.
2547
+ dev_types (Literal["ap", "gw", "cx", "sw", "switch"] | List["ap", "gw" ...], optional): List of dev_types the data represents as current for those types.
2548
+ This will result in any devices of the specified types that do not exist in the provided data being removed from cache. Defaults to None.
2549
+ truncate (bool, optional): Existing DB data will be discarded, and all data in DB will be replaced with provided. Defaults to True.
2548
2550
 
2549
2551
  Returns:
2550
2552
  bool: _description_
@@ -2589,6 +2591,43 @@ class Cache:
2589
2591
  else:
2590
2592
  return await self.update_db(self.DevDB, doc_ids=data)
2591
2593
 
2594
+ async def prep_filtered_devs_for_cache(self, raw_models: List[models.Device], dev_type: constants.GenericDeviceTypes | List[constants.GenericDeviceTypes] = None, site: str = None, group: str = None) -> List[dict]:
2595
+ new_by_serial = {d.serial: d.model_dump() for d in raw_models}
2596
+ filters = {
2597
+ "dev_type": dev_type,
2598
+ "site": site,
2599
+ "group": group
2600
+ }
2601
+ filter_msg = ", ".join([f"{k}: {v if k != 'dev_type' else utils.unlistify(v)}" for k, v in filters.items() if v])
2602
+
2603
+ if dev_type:
2604
+ switch_types = ["cx", "sw"] if "switch" in dev_type else []
2605
+ cache_type = [*[t for t in dev_type if t != "switch"], *switch_types]
2606
+ else:
2607
+ cache_type = []
2608
+
2609
+ def include_device(dev: dict) -> bool:
2610
+ criteria = []
2611
+ if cache_type:
2612
+ criteria += [dev["type"] in cache_type]
2613
+ if site:
2614
+ criteria += [dev["site"] == site]
2615
+ if group:
2616
+ criteria += [dev["group"] == group]
2617
+
2618
+ if all(criteria):
2619
+ if dev["serial"] not in new_by_serial:
2620
+ return False
2621
+
2622
+ return True
2623
+
2624
+ cache_devices = {cd["serial"]: cd for cd in self.devices if include_device(cd)}
2625
+
2626
+ update_data = {**cache_devices, **new_by_serial}
2627
+ log.info(f"Data prepared for device cache update. Filters: {filter_msg}. Add/update {len(new_by_serial)} devices. Devices in cache: Now: {len(self.devices)}, After Update: {len(update_data)}.")
2628
+
2629
+ return list(update_data.values())
2630
+
2592
2631
  async def refresh_dev_db(
2593
2632
  self,
2594
2633
  dev_type: constants.GenericDeviceTypes | List[constants.GenericDeviceTypes] = None, # TODO make consistent throughout using device_type in many places
@@ -2657,7 +2696,9 @@ class Cache:
2657
2696
  offset=offset,
2658
2697
  limit=limit,
2659
2698
  )
2660
- if isinstance(resp, CombinedResponse) and resp.ok:
2699
+ if isinstance(resp, CombinedResponse) and resp.ok: # Can be Response | List[Response] if get_all_devices aborted due to failures
2700
+ # Any filters not in list below do not result in a cache update
2701
+ filtered_resonse = True if any([label, serial, mac, model, stack_id, swarm_id, cluster_id, public_ip_address, status]) else False
2661
2702
  raw_data = await self.format_raw_devices_for_cache(resp)
2662
2703
  with console.status(f"preparing {len(resp)} records for cache update"):
2663
2704
  _start_time = time.perf_counter()
@@ -2665,13 +2706,18 @@ class Cache:
2665
2706
  raw_models = [*raw_models_by_type.aps, *raw_models_by_type.switches, *raw_models_by_type.gateways]
2666
2707
  log.debug(f"prepared {len(resp)} records for dev cache update in {round(time.perf_counter() - _start_time, 2)}")
2667
2708
 
2668
- filtered_resonse = True if any([dev_type, group, site, label, serial, mac, model, stack_id, swarm_id, cluster_id, public_ip_address, status]) else False
2709
+ if dev_type:
2710
+ update_data = await self.prep_filtered_devs_for_cache(raw_models=raw_models, dev_type=dev_type, site=site, group=group)
2711
+ else:
2712
+ update_data = [dev.model_dump() for dev in raw_models]
2713
+
2669
2714
  if resp.all_ok and not filtered_resonse:
2670
- self.updated.append(self.central.get_all_devices)
2671
- self.responses.dev = resp
2672
- _ = await self.update_db(self.DevDB, data=[dev.model_dump() for dev in raw_models], truncate=True)
2715
+ if not dev_type:
2716
+ self.updated.append(self.central.get_all_devices)
2717
+ self.responses.dev = resp
2718
+ _ = await self.update_db(self.DevDB, data=update_data, truncate=True)
2673
2719
  else: # Response is filtered or incomplete due to partial failure merge with existing cache data (update)
2674
- _ = await self._add_update_devices([dev.model_dump() for dev in raw_models])
2720
+ _ = await self._add_update_devices(update_data)
2675
2721
 
2676
2722
  return resp
2677
2723
 
@@ -2069,14 +2069,7 @@ def get_swarm_firmware_details(data: List[Dict[str, Any]]) -> List[Dict[str, Any
2069
2069
  return simple_kv_formatter(data, key_order=key_order)
2070
2070
 
2071
2071
  def show_radios(data: List[Dict[str, str | int]]) -> List[Dict[str, str | int]]:
2072
- def by_name_blocks(name: str, exist_names=[]) -> str:
2073
- if name not in exist_names:
2074
- exist_names += [name]
2075
- return name
2076
- return ''
2077
-
2078
2072
  key_order = ["name", "macaddr", "radio_name", "status", "channel", "radio_type", "spatial_stream", "mode", "tx_power", "utilization",] # "band", "index"]
2079
- data = [{k: v if k != "name" else by_name_blocks(v) for k, v in inner.items()} for inner in data]
2080
2073
  data = simple_kv_formatter(data, key_order=key_order)
2081
2074
 
2082
2075
  return data
@@ -378,7 +378,7 @@ def batch_add_sites(import_file: Path = None, data: dict = None, yes: bool = Fal
378
378
  address_fields = {"site_name": "bright_green", "name": "bright_green", "address": "bright_cyan", "city": "turquoise4", "state": "dark_olive_green3", "country": "magenta", "zipcode": "blue", "zip": "blue"}
379
379
  confirm_msg = utils.summarize_list(
380
380
  [
381
- "|".join([f'[{address_fields[k]}]{v}[/]' for k, v in site.items() if v and k in address_fields]) for site in verified_sites.model_dump()
381
+ "|".join([f'[{address_fields[k]}]{v}[/]' for k, v in site.model_dump().items() if v and k in address_fields]) for site in list(verified_sites)
382
382
  ],
383
383
  max=7
384
384
  )
@@ -974,8 +974,12 @@ def add(
974
974
  caption, tablefmt = None, "action"
975
975
  if what == "sites":
976
976
  resp = batch_add_sites(import_file, yes=yes)
977
+ if resp.ok:
978
+ try:
979
+ resp.output = cleaner.sites(resp.output)
980
+ except Exception as e:
981
+ log.error(f"Error cleaning output of batch site addition {repr(e)}", caption=True, log=True)
977
982
  tablefmt = "rich"
978
- # TODO should re-order columns so output is more consistent with show sites (i.e. site_name is not guaranteed to be first col in output of these calls)
979
983
  elif what == "groups":
980
984
  resp = batch_add_groups(import_file, yes=yes)
981
985
  elif what == "devices":
@@ -1004,7 +1008,7 @@ def add(
1004
1008
  "Use [cyan]cencli show cloud-auth upload mpsk[/] to see the status of the import."
1005
1009
  )
1006
1010
 
1007
- cli.display_results(resp, tablefmt=tablefmt, title=f"Batch Add {what}", caption=caption)
1011
+ cli.display_results(resp, tablefmt=tablefmt, title=f"Batch Add {what.value}", caption=caption)
1008
1012
 
1009
1013
 
1010
1014
  # TODO archive and unarchive have the same block this is used by batch delete
@@ -521,6 +521,7 @@ class CLICommon:
521
521
  reverse: bool = False,
522
522
  stash: bool = True,
523
523
  output_by_key: str | List[str] = "name",
524
+ group_by: str = None,
524
525
  set_width_cols: dict = None,
525
526
  full_cols: Union[List[str], str] = [],
526
527
  fold_cols: Union[List[str], str] = [],
@@ -555,6 +556,7 @@ class CLICommon:
555
556
  "account": None if config.account in ["central_info", "default"] else config.account,
556
557
  "config": config,
557
558
  "output_by_key": output_by_key,
559
+ "group_by": group_by,
558
560
  "set_width_cols": set_width_cols,
559
561
  "full_cols": full_cols,
560
562
  "fold_cols": fold_cols,
@@ -592,6 +594,7 @@ class CLICommon:
592
594
  reverse: bool = False,
593
595
  stash: bool = True,
594
596
  output_by_key: str | List[str] = "name",
597
+ group_by: str = None,
595
598
  exit_on_fail: bool = False, # TODO make default True so failed calls return a failed return code to the shell. Need to validate everywhere it needs to be set to False
596
599
  cache_update_pending: bool = False,
597
600
  set_width_cols: dict = None,
@@ -625,6 +628,8 @@ class CLICommon:
625
628
  show last. Default: True
626
629
  output_by_key: For json or yaml output, if any of the provided keys are foound in the List of dicts
627
630
  the List will be converted to a Dict[value of provided key, original_inner_dict]. Defaults to name.
631
+ group_by: When provided output will be grouped by this key. For outputs where multiple entries relate to a common device, and multiple devices exist in the output.
632
+ i.e. interfaces for a device when the output contains multiple devices. Results in special formatting. Defaults to None
628
633
  exit_on_fail: (bool, optional): If provided resp indicates a failure exit after display. Defaults to False
629
634
  cache_update_pending: (bool, optional): If a cache update is to be performed if resp is success.
630
635
  Results in a warning before exit if failure. Defaults to False
@@ -753,6 +758,7 @@ class CLICommon:
753
758
  reverse=reverse,
754
759
  stash=stash,
755
760
  output_by_key=output_by_key,
761
+ group_by=group_by,
756
762
  set_width_cols=set_width_cols,
757
763
  full_cols=full_cols,
758
764
  fold_cols=fold_cols,
@@ -1509,7 +1515,7 @@ class CLICommon:
1509
1515
  cache_devs += [this_dev]
1510
1516
 
1511
1517
  not_found_devs: List[str] = [s for s, c in zip(serials_in, cache_devs) if c is None]
1512
- cache_found_devs: List[CacheDevice | CacheInvDevice] = [d for d in cache_devs if d]
1518
+ cache_found_devs: List[CacheDevice | CacheInvDevice] = [d for d in cache_devs if d is not None]
1513
1519
  cache_mon_devs: List[CacheDevice] = [d for d in cache_found_devs if d.db.name == "devices"]
1514
1520
  cache_inv_devs: List[CacheInvDevice] = [d for d in cache_found_devs if d.db.name == "inventory"]
1515
1521
 
@@ -1616,6 +1622,96 @@ class CLICommon:
1616
1622
  if inv_doc_ids:
1617
1623
  self.central.request(self.cache.update_inv_db, inv_doc_ids, remove=True)
1618
1624
 
1625
+ # Header rows used by CAS
1626
+ #DEVICE NAME,SERIAL,MAC,GROUP,SITE,LABELS,LICENSE,ZONE,SWARM MODE,RF PROFILE,INSTALLATION TYPE,RADIO 0 MODE,RADIO 1 MODE,RADIO 2 MODE,DUAL 5GHZ MODE,SPLIT 5GHZ MODE,FLEX DUAL BAND,ANTENNA WIDTH,ALTITUDE,IP ADDRESS,SUBNET MASK,DEFAULT GATEWAY,DNS SERVER,DOMAIN NAME,TIMEZONE,AP1X USERNAME,AP1X PASSWORD
1627
+
1628
+ # def batch_update_aps(self, data: List[dict]) -> List[Response]:
1629
+ # data: List[CacheDevice] = [self.cache.get_dev_identifier(ap, dev_type="ap") for ap in data]
1630
+
1631
+ # disable_radios = None if not disable_radios else [r.value for r in disable_radios]
1632
+ # enable_radios = None if not enable_radios else [r.value for r in enable_radios]
1633
+ # flex_dual_exclude = None if not flex_dual_exclude else flex_dual_exclude.value
1634
+ # antenna_width = None if not antenna_width else antenna_width.value
1635
+
1636
+ # radio_24_disable = None if not enable_radios or "2.4" not in enable_radios else False
1637
+ # radio_5_disable = None if not enable_radios or "5" not in enable_radios else False
1638
+ # radio_6_disable = None if not enable_radios or "6" not in enable_radios else False
1639
+ # if disable_radios:
1640
+ # for radio, var in zip(["2.4", "5", "6"], [radio_24_disable, radio_5_disable, radio_6_disable]):
1641
+ # if radio in disable_radios and var is not None:
1642
+ # cli.exit(f"Invalid combination you tried to enable and disable the {radio}Ghz radio")
1643
+ # # var = None if radio not in disable_radios else True # doesn't work
1644
+ # radio_24_disable = None if "2.4" not in disable_radios else True
1645
+ # radio_5_disable = None if "5" not in disable_radios else True
1646
+ # radio_6_disable = None if "6" not in disable_radios else True
1647
+
1648
+
1649
+ # kwargs = {
1650
+ # "hostname": hostname,
1651
+ # "ip": ip,
1652
+ # "mask": mask,
1653
+ # "gateway": gateway,
1654
+ # "dns": dns,
1655
+ # "domain": domain,
1656
+ # "radio_24_disable": radio_24_disable,
1657
+ # "radio_5_disable": radio_5_disable,
1658
+ # "radio_6_disable": radio_6_disable,
1659
+ # "uplink_vlan": tagged_uplink_vlan,
1660
+ # "flex_dual_exclude": flex_dual_exclude,
1661
+ # "dynamic_ant_mode": antenna_width,
1662
+ # }
1663
+ # if ip and not all([mask, gateway, dns]):
1664
+ # cli.exit("[cyan]mask[/], [cyan]gateway[/], and [cyan]--dns[/] are required when [cyan]--ip[/] is provided.")
1665
+ # if len(data) > 1 and hostname or ip:
1666
+ # cli.exit("Setting hostname/ip on multiple APs doesn't make sesnse")
1667
+
1668
+ # print(f"[bright_green]Updating[/]: {utils.summarize_list([ap.summary_text for ap in data], color=None, pad=10).lstrip()}")
1669
+ # print("\n[green italic]With the following per-ap-settings[/]:")
1670
+ # _ = [print(f" {k}: {v}") for k, v in kwargs.items() if v is not None]
1671
+ # skip_flex = [ap for ap in data if ap.model not in flex_dual_models]
1672
+ # skip_width = [ap for ap in data if ap.model not in ["679"]]
1673
+
1674
+ # warnings = []
1675
+ # if flex_dual_exclude is not None and skip_flex:
1676
+ # warnings += [f"[yellow]:information:[/] Flexible dual radio [red]will be ignored[/] for {len(skip_flex)} AP, as the setting doesn't apply to those models."]
1677
+ # if antenna_width is not None and skip_width:
1678
+ # warnings += [f"[yellow]:information:[/] Dynamic antenna width [red]will be ignored[/] for {len(skip_width)} AP, as the setting doesn't apply to those models."]
1679
+ # if warnings:
1680
+ # warn_text = '\n'.join(warnings)
1681
+ # print(f"\n{warn_text}")
1682
+
1683
+ # # determine if any effective changes after skips for settings on invalid AP models
1684
+ # changes = 2
1685
+ # if not list(filter(None, list(kwargs.values())[0:-2])):
1686
+ # if not flex_dual_exclude or (flex_dual_exclude and not [ap for ap in data if ap not in skip_flex]):
1687
+ # changes -= 1
1688
+ # if not antenna_width or (antenna_width and not [ap for ap in data if ap not in skip_width]):
1689
+ # changes -= 1
1690
+ # if not changes:
1691
+ # cli.exit("No valid updates provided for the selected AP models... Nothing to do.")
1692
+
1693
+ # self.confirm(yes) # exits here if they abort
1694
+ # batch_resp = cli.central.batch_request(
1695
+ # [
1696
+ # BatchRequest(
1697
+ # cli.central.update_per_ap_settings,
1698
+ # ap.serial,
1699
+ # hostname=hostname,
1700
+ # ip=ip,
1701
+ # mask=mask,
1702
+ # gateway=gateway,
1703
+ # dns=dns,
1704
+ # domain=domain,
1705
+ # radio_24_disable=radio_24_disable,
1706
+ # radio_5_disable=radio_5_disable,
1707
+ # radio_6_disable=radio_6_disable,
1708
+ # uplink_vlan=tagged_uplink_vlan,
1709
+ # flex_dual_exclude=None if ap.model not in flex_dual_models else flex_dual_exclude,
1710
+ # dynamic_ant_mode=None if ap.model != "679" else antenna_width,
1711
+ # ) for ap in data
1712
+ # ]
1713
+ # )
1714
+
1619
1715
  def help_default(self, default_txt: str) -> str:
1620
1716
  """Helper function that returns properly escaped default text, including rich color markup, for use in CLI help.
1621
1717
 
@@ -962,7 +962,11 @@ def parse_interface_responses(dev_type: GenericDeviceTypes, responses: List[Resp
962
962
  _passed = responses if not _failed else [r for r in responses if r.ok]
963
963
 
964
964
  if _failed:
965
- log.warning(f"Incomplete output!! {len(_failed)} calls failed. Devices: {utils.color([r.url.path.split("/")[-2:][0] for r in _failed])}. [cyan]cencli show logs --cencli[/] for details.", caption=True)
965
+ try:
966
+ log.warning(f"Incomplete output!! {len(_failed)} calls failed. Devices: {utils.color([r.url.path.split('/')[-2:][0] for r in _failed])}. [cyan]cencli show logs --cencli[/] for details.", caption=True)
967
+ except Exception:
968
+ log.warning("Incomplete output, failures occured, see log")
969
+
966
970
 
967
971
  # output = [i for r in _passed for i in utils.listify(r.output)]
968
972
  output = [r.output for r in _passed]
@@ -1132,10 +1136,10 @@ def interfaces(
1132
1136
 
1133
1137
  caption = []
1134
1138
  if dev_type == "switch":
1135
- if "sw" in [d.type for d in devs]:
1139
+ if "sw" in [d.type for d in devs] and resp.ok:
1136
1140
  dev_type = dev_type if len(batch_resp) > 1 else "sw" # So single device cleaner gets specific dev_type
1137
1141
  caption = [render.rich_capture(":information: Native VLAN for trunk ports not shown for aos-sw as not provided by the API", emoji=True)]
1138
- if "cx" in [d.type for d in devs]:
1142
+ if "cx" in [d.type for d in devs] and resp.ok:
1139
1143
  caption = [render.rich_capture(":information: L3 interfaces for CX switches will show as Access/VLAN 1 as the L3 details are not provided by the API", emoji=True)]
1140
1144
 
1141
1145
  if resp:
@@ -1161,6 +1165,7 @@ def interfaces(
1161
1165
  sort_by=sort_by,
1162
1166
  reverse=reverse,
1163
1167
  output_by_key=None,
1168
+ group_by=None if len(batch_resp) <= 1 else "device",
1164
1169
  cleaner=cleaner.show_interfaces if len(batch_resp) == 1 else None, # Multi device listing is ran through cleaner already
1165
1170
  verbosity=verbose,
1166
1171
  dev_type=dev_type,
@@ -3201,22 +3206,24 @@ def radios(
3201
3206
  resp = sorted(passed, key=lambda ap: ap.rl)[0]
3202
3207
  resp.output = [{"name": ap["name"], **rdict} for ap in combined for rdict in ap["radios"]]
3203
3208
  if failed:
3204
- cli.display_results(failed)
3209
+ cli.display_results(failed, tablefmt="action")
3205
3210
  else:
3206
3211
  resp = cli.central.request(cli.cache.refresh_dev_db, dev_type="ap", **{**params, **default_params})
3207
3212
  if resp.ok:
3208
3213
  resp.output = [{"name": ap["name"], **rdict} for ap in resp.output for rdict in ap["radios"]]
3209
3214
 
3210
3215
  if resp.ok:
3216
+ # We sort before sending data to renderer to keep groupings by AP name
3211
3217
  if sort_by and resp.output and sort_by in resp.output[0].keys():
3212
3218
  resp.output = list(sorted(resp.output, key=lambda ap: (ap["name"], ap[sort_by])))
3213
3219
  else:
3214
3220
  resp.output = list(sorted(resp.output, key=lambda ap: (ap["name"], ap["radio_name"])))
3221
+
3222
+ caption = _build_radio_caption(resp.output)
3215
3223
  if status:
3216
3224
  resp.output = list(filter(lambda radio: radio["status"] == status, resp.output))
3217
- caption = _build_radio_caption(resp.output)
3218
3225
 
3219
- cli.display_results(resp, tablefmt=tablefmt, title="Radio Details", reverse=reverse, outfile=outfile, pager=pager, caption=caption, cleaner=cleaner.show_radios)
3226
+ cli.display_results(resp, tablefmt=tablefmt, title="Radio Details", reverse=reverse, outfile=outfile, pager=pager, caption=caption, group_by="name", cleaner=cleaner.show_radios)
3220
3227
 
3221
3228
 
3222
3229
  @app.command()
@@ -233,7 +233,7 @@ class ImportSites(RootModel):
233
233
 
234
234
  @staticmethod
235
235
  def _convert_site_key(_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
236
- def auto_usa(data: Dict[str, str | int | float]) -> str:
236
+ def auto_usa(data: Dict[str, str | int | float]) -> str | None:
237
237
  _country = data.get("country", "")
238
238
  if _country.isdigit(): # Data from large customer had country as '1' for some sites
239
239
  _country = ""
@@ -243,7 +243,7 @@ class ImportSites(RootModel):
243
243
  if _country.upper() in ["USA", "US"]:
244
244
  return "United States"
245
245
 
246
- return _country
246
+ return _country or None
247
247
 
248
248
  _data = [
249
249
  {
@@ -170,12 +170,12 @@ def do_pretty(key: str, value: str) -> str:
170
170
  value = "" if value is None else value # testing error on cop
171
171
  return value if key != "status" else f'[b {color}]{value.title()}[/b {color}]'
172
172
 
173
- def _do_subtables(data: List[dict], tablefmt: str = "rich") -> List[dict]:
173
+ def _do_subtables(data: List[dict], *, tablefmt: str = "rich") -> List[dict]:
174
174
  """Parse data and format any values that are dict, list, tuple
175
175
 
176
176
  Args:
177
177
  data (list): The data
178
- tablefmt (str, optional): table format. Defaults to "rich".
178
+ tablefmt (str, optional): table format. Defaults to "rich"
179
179
 
180
180
  Returns:
181
181
  List[dict]: Original dict with any inner (dict/list/tuples)
@@ -250,11 +250,35 @@ def tabulate_output(outdata: List[dict]) -> tuple:
250
250
 
251
251
  return raw_data, table_data
252
252
 
253
+ def build_rich_table_rows(data: List[Dict[str, Text | str]], table: Table, group_by: str) -> Table:
254
+ if not group_by:
255
+ [table.add_row(*list(in_dict.values())) for in_dict in data]
256
+ return table
257
+
258
+ if not isinstance(data, list) or group_by not in data[0]:
259
+ log.error(f"Error in render.do_group_by_table invalid type {type(data)} or {group_by} not found in header.")
260
+ [table.add_row(*list(in_dict.values())) for in_dict in data]
261
+ return table
262
+
263
+ field_idx = list(data[0].keys()).index(group_by)
264
+ this = "_start_"
265
+ for in_dict in data:
266
+ if in_dict[group_by] == this:
267
+ table.add_row(*[v if idx != field_idx else "" for idx, v in enumerate(in_dict.values())]) # only show value for first entry in group
268
+ else:
269
+ this = in_dict[group_by] # first entry in group
270
+ table.add_section()
271
+ table.add_row(*list(in_dict.values()))
272
+
273
+ return table
274
+
275
+
253
276
  def rich_output(
254
277
  outdata: List[dict],
255
278
  title: str = None,
256
279
  caption: str = None,
257
280
  account: str = None,
281
+ group_by: str = None,
258
282
  set_width_cols: dict = None,
259
283
  full_cols: Union[List[str], str] = [],
260
284
  fold_cols: Union[List[str], str] = [],
@@ -266,6 +290,7 @@ def rich_output(
266
290
  title (str, optional): Table Title. Defaults to None.
267
291
  caption (str, optional): Table Caption. Defaults to None.
268
292
  account (str, optional): The account (displayed in caption if not the default). Defaults to None.
293
+ group_by (str, optional): Group output by the value of the provided field. Results in special formatting. Defaults to None.
269
294
  set_width_cols (dict, optional): cols that need to be rendered with a specific width. Defaults to None.
270
295
  full_cols (Union[List[str], str], optional): cols that should not be truncated. Defaults to [].
271
296
  fold_cols (Union[List[str], str], optional): cols that can be folded (wrapped). Defaults to [].
@@ -324,7 +349,7 @@ def rich_output(
324
349
  formatted = _do_subtables(outdata)
325
350
  log.debug(f"render.rich_output.do_subtables took {time.perf_counter() - _start:.2f} to process {len(outdata)} records")
326
351
 
327
- [table.add_row(*list(in_dict.values())) for in_dict in formatted]
352
+ table = build_rich_table_rows(formatted, table=table, group_by=group_by)
328
353
 
329
354
  if title:
330
355
  table.title = f'[italic cornflower_blue]{constants.what_to_pretty(title)}'
@@ -352,6 +377,32 @@ def rich_output(
352
377
 
353
378
  return outdata, outdata
354
379
 
380
+
381
+ def format_data_by_key(data: List[Dict[str, Any]], output_by_key: str) -> Dict[str, Any]:
382
+ # -- modify keys potentially formatted with \n for narrower rich output to format appropriate for json/yaml
383
+ data = utils.listify(data)
384
+ if isinstance(data[0], dict) and all([isinstance(k, str) for k in list(data[0].keys())]):
385
+ data = [{k.replace(" ", "_").replace("\n", "_"): v for k, v in d.items()} for d in data]
386
+
387
+ # -- convert List[dict] --> Dict[dev_name: dict] for yaml/json outputs unless output_dict_by_key is specified, then use the provided key(s) rather than name
388
+ if output_by_key and data and isinstance(data[0], dict):
389
+ if len(output_by_key) == 1 and "+" in output_by_key[0]:
390
+ found_keys = [k for k in output_by_key[0].split("+") if k in data[0]]
391
+ if len(found_keys) == len(output_by_key[0].split("+")):
392
+ data: Dict[dict] = {
393
+ f"{'-'.join([item[key] for key in found_keys])}": {k: v for k, v in item.items()} for item in data
394
+ }
395
+ else:
396
+ _output_key = [k for k in output_by_key if k in data[0]]
397
+ if _output_key:
398
+ _output_key = _output_key[0]
399
+ data: Dict[dict] = {
400
+ item[_output_key]: {k: v for k, v in item.items() if k != _output_key}
401
+ for item in data
402
+ }
403
+ return data
404
+
405
+
355
406
  def output(
356
407
  outdata: List[str] | List[Dict[str, Any]] | Dict[str, Any],
357
408
  tablefmt: TableFormat = "rich", # "action" and "raw" are not sent through formatter, handled in clicommon.display_output
@@ -360,6 +411,7 @@ def output(
360
411
  account: str = None,
361
412
  config: Config = None,
362
413
  output_by_key: str | List[str] = "name",
414
+ group_by: str = None,
363
415
  set_width_cols: dict = None,
364
416
  full_cols: Union[List[str], str] = [],
365
417
  fold_cols: Union[List[str], str] = [],
@@ -368,6 +420,7 @@ def output(
368
420
  raw_data = outdata
369
421
  _lexer = table_data = None
370
422
 
423
+ # sanitize output for demos
371
424
  if config and config.sanitize and raw_data and all(isinstance(x, dict) for x in raw_data):
372
425
  outdata = [{k: d[k] if k not in REDACT else "--redacted--" for k in d} for d in raw_data]
373
426
 
@@ -376,27 +429,7 @@ def output(
376
429
  tablefmt = "simple"
377
430
 
378
431
  if tablefmt in ['json', 'yaml', 'yml']:
379
- # -- modify keys potentially formatted with \n for narrower rich output to format appropriate for json/yaml
380
- outdata = utils.listify(outdata)
381
- if isinstance(outdata[0], dict) and all([isinstance(k, str) for k in list(outdata[0].keys())]):
382
- outdata = [{k.replace(" ", "_").replace("\n", "_"): v for k, v in data.items()} for data in outdata]
383
-
384
- # -- convert List[dict] --> Dict[dev_name: dict] for yaml/json outputs unless output_dict_by_key is specified, then use the provided key(s) rather than name
385
- if output_by_key and outdata and isinstance(outdata[0], dict):
386
- if len(output_by_key) == 1 and "+" in output_by_key[0]:
387
- found_keys = [k for k in output_by_key[0].split("+") if k in outdata[0]]
388
- if len(found_keys) == len(output_by_key[0].split("+")):
389
- outdata: Dict[dict] = {
390
- f"{'-'.join([item[key] for key in found_keys])}": {k: v for k, v in item.items()} for item in outdata
391
- }
392
- else:
393
- _output_key = [k for k in output_by_key if k in outdata[0]]
394
- if _output_key:
395
- _output_key = _output_key[0]
396
- outdata: Dict[dict] = {
397
- item[_output_key]: {k: v for k, v in item.items() if k != _output_key}
398
- for item in outdata
399
- }
432
+ outdata = format_data_by_key(outdata, output_by_key)
400
433
 
401
434
  if tablefmt == "json":
402
435
  outdata = utils.unlistify(outdata)
@@ -445,8 +478,7 @@ def output(
445
478
  table_data = rich_capture(table_data)
446
479
 
447
480
  elif tablefmt == "rich":
448
- raw_data, table_data = rich_output(outdata, title=title, caption=caption, account=account, set_width_cols=set_width_cols, full_cols=full_cols, fold_cols=fold_cols)
449
- ...
481
+ raw_data, table_data = rich_output(outdata, title=title, caption=caption, account=account, set_width_cols=set_width_cols, full_cols=full_cols, fold_cols=fold_cols, group_by=group_by)
450
482
 
451
483
  elif tablefmt == "tabulate":
452
484
  raw_data, table_data = tabulate_output(outdata)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "centralcli"
3
- version = "8.2.2"
3
+ version = "8.2.4"
4
4
  description = "A CLI for interacting with Aruba Central (Cloud Management Platform). Facilitates bulk imports, exports, reporting. A handy tool if you have devices managed by Aruba Central."
5
5
  license = "MIT"
6
6
  authors = [{"name" = "Wade Wells (Pack3tL0ss)","email" = "cencli@consolepi.com"}]
@@ -1,25 +0,0 @@
1
- from centralcli import config, log, cnx_client, utils, cleaner
2
- from centralcli.typedefs import Method, StrOrURL
3
- from centralcli.constants import lib_to_api, STRIP_KEYS
4
- from centralcli.exceptions import CentralCliException
5
- from centralcli.response import RateLimit, BatchRequest, LoggedRequests, Spinner, Response
6
- from .pycentralv2.base import ArubaCentralNewBase
7
-
8
- __all__ = [
9
- config,
10
- log,
11
- cnx_client,
12
- Method,
13
- StrOrURL,
14
- lib_to_api,
15
- STRIP_KEYS,
16
- cleaner,
17
- utils,
18
- CentralCliException,
19
- RateLimit,
20
- BatchRequest,
21
- LoggedRequests,
22
- Spinner,
23
- Response,
24
- ArubaCentralNewBase
25
- ]
@@ -1,12 +0,0 @@
1
- # from ...response import BatchRequest, Response, LoggedRequests, Spinner
2
- # from ... import utils
3
- # from ..pycentralv2.base import ArubaCentralNewBase
4
-
5
- # __all__ = [
6
- # BatchRequest,
7
- # Response,
8
- # LoggedRequests,
9
- # Spinner,
10
- # ArubaCentralNewBase,
11
- # utils
12
- # ]