regscale-cli 6.16.0.0__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 regscale-cli might be problematic. Click here for more details.

Files changed (481) hide show
  1. regscale/__init__.py +1 -0
  2. regscale/airflow/__init__.py +9 -0
  3. regscale/airflow/azure/__init__.py +9 -0
  4. regscale/airflow/azure/cli.py +89 -0
  5. regscale/airflow/azure/upload_dags.py +116 -0
  6. regscale/airflow/click_dags.py +127 -0
  7. regscale/airflow/click_mixins.py +82 -0
  8. regscale/airflow/config.py +25 -0
  9. regscale/airflow/factories/__init__.py +0 -0
  10. regscale/airflow/factories/connections.py +58 -0
  11. regscale/airflow/factories/workflows.py +78 -0
  12. regscale/airflow/hierarchy.py +88 -0
  13. regscale/airflow/operators/__init__.py +0 -0
  14. regscale/airflow/operators/click.py +36 -0
  15. regscale/airflow/sensors/__init__.py +0 -0
  16. regscale/airflow/sensors/sql.py +107 -0
  17. regscale/airflow/sessions/__init__.py +0 -0
  18. regscale/airflow/sessions/sql/__init__.py +3 -0
  19. regscale/airflow/sessions/sql/queries.py +64 -0
  20. regscale/airflow/sessions/sql/sql_server_queries.py +248 -0
  21. regscale/airflow/tasks/__init__.py +0 -0
  22. regscale/airflow/tasks/branches.py +22 -0
  23. regscale/airflow/tasks/cli.py +116 -0
  24. regscale/airflow/tasks/click.py +73 -0
  25. regscale/airflow/tasks/debugging.py +9 -0
  26. regscale/airflow/tasks/groups.py +116 -0
  27. regscale/airflow/tasks/init.py +60 -0
  28. regscale/airflow/tasks/states.py +47 -0
  29. regscale/airflow/tasks/workflows.py +36 -0
  30. regscale/ansible/__init__.py +9 -0
  31. regscale/core/__init__.py +0 -0
  32. regscale/core/app/__init__.py +3 -0
  33. regscale/core/app/api.py +571 -0
  34. regscale/core/app/application.py +665 -0
  35. regscale/core/app/internal/__init__.py +136 -0
  36. regscale/core/app/internal/admin_actions.py +230 -0
  37. regscale/core/app/internal/assessments_editor.py +873 -0
  38. regscale/core/app/internal/catalog.py +316 -0
  39. regscale/core/app/internal/comparison.py +459 -0
  40. regscale/core/app/internal/control_editor.py +571 -0
  41. regscale/core/app/internal/encrypt.py +79 -0
  42. regscale/core/app/internal/evidence.py +1240 -0
  43. regscale/core/app/internal/file_uploads.py +151 -0
  44. regscale/core/app/internal/healthcheck.py +66 -0
  45. regscale/core/app/internal/login.py +305 -0
  46. regscale/core/app/internal/migrations.py +240 -0
  47. regscale/core/app/internal/model_editor.py +1701 -0
  48. regscale/core/app/internal/poam_editor.py +632 -0
  49. regscale/core/app/internal/workflow.py +105 -0
  50. regscale/core/app/logz.py +74 -0
  51. regscale/core/app/utils/XMLIR.py +258 -0
  52. regscale/core/app/utils/__init__.py +0 -0
  53. regscale/core/app/utils/api_handler.py +358 -0
  54. regscale/core/app/utils/app_utils.py +1110 -0
  55. regscale/core/app/utils/catalog_utils/__init__.py +0 -0
  56. regscale/core/app/utils/catalog_utils/common.py +91 -0
  57. regscale/core/app/utils/catalog_utils/compare_catalog.py +193 -0
  58. regscale/core/app/utils/catalog_utils/diagnostic_catalog.py +97 -0
  59. regscale/core/app/utils/catalog_utils/download_catalog.py +103 -0
  60. regscale/core/app/utils/catalog_utils/update_catalog.py +718 -0
  61. regscale/core/app/utils/catalog_utils/update_catalog_v2.py +1378 -0
  62. regscale/core/app/utils/catalog_utils/update_catalog_v3.py +1272 -0
  63. regscale/core/app/utils/catalog_utils/update_plans.py +334 -0
  64. regscale/core/app/utils/file_utils.py +238 -0
  65. regscale/core/app/utils/parser_utils.py +81 -0
  66. regscale/core/app/utils/pickle_file_handler.py +57 -0
  67. regscale/core/app/utils/regscale_utils.py +319 -0
  68. regscale/core/app/utils/report_utils.py +119 -0
  69. regscale/core/app/utils/variables.py +226 -0
  70. regscale/core/decorators.py +31 -0
  71. regscale/core/lazy_group.py +65 -0
  72. regscale/core/login.py +63 -0
  73. regscale/core/server/__init__.py +0 -0
  74. regscale/core/server/flask_api.py +473 -0
  75. regscale/core/server/helpers.py +373 -0
  76. regscale/core/server/rest.py +64 -0
  77. regscale/core/server/static/css/bootstrap.css +6030 -0
  78. regscale/core/server/static/css/bootstrap.min.css +6 -0
  79. regscale/core/server/static/css/main.css +176 -0
  80. regscale/core/server/static/images/regscale-cli.svg +49 -0
  81. regscale/core/server/static/images/regscale.svg +38 -0
  82. regscale/core/server/templates/base.html +74 -0
  83. regscale/core/server/templates/index.html +43 -0
  84. regscale/core/server/templates/login.html +28 -0
  85. regscale/core/server/templates/make_base64.html +22 -0
  86. regscale/core/server/templates/upload_STIG.html +109 -0
  87. regscale/core/server/templates/upload_STIG_result.html +26 -0
  88. regscale/core/server/templates/upload_ssp.html +144 -0
  89. regscale/core/server/templates/upload_ssp_result.html +128 -0
  90. regscale/core/static/__init__.py +0 -0
  91. regscale/core/static/regex.py +14 -0
  92. regscale/core/utils/__init__.py +117 -0
  93. regscale/core/utils/click_utils.py +13 -0
  94. regscale/core/utils/date.py +238 -0
  95. regscale/core/utils/graphql.py +254 -0
  96. regscale/core/utils/urls.py +23 -0
  97. regscale/dev/__init__.py +6 -0
  98. regscale/dev/analysis.py +454 -0
  99. regscale/dev/cli.py +235 -0
  100. regscale/dev/code_gen.py +492 -0
  101. regscale/dev/dirs.py +69 -0
  102. regscale/dev/docs.py +384 -0
  103. regscale/dev/monitoring.py +26 -0
  104. regscale/dev/profiling.py +216 -0
  105. regscale/exceptions/__init__.py +4 -0
  106. regscale/exceptions/license_exception.py +7 -0
  107. regscale/exceptions/validation_exception.py +9 -0
  108. regscale/integrations/__init__.py +1 -0
  109. regscale/integrations/commercial/__init__.py +486 -0
  110. regscale/integrations/commercial/ad.py +433 -0
  111. regscale/integrations/commercial/amazon/__init__.py +0 -0
  112. regscale/integrations/commercial/amazon/common.py +106 -0
  113. regscale/integrations/commercial/aqua/__init__.py +0 -0
  114. regscale/integrations/commercial/aqua/aqua.py +91 -0
  115. regscale/integrations/commercial/aws/__init__.py +6 -0
  116. regscale/integrations/commercial/aws/cli.py +322 -0
  117. regscale/integrations/commercial/aws/inventory/__init__.py +110 -0
  118. regscale/integrations/commercial/aws/inventory/base.py +64 -0
  119. regscale/integrations/commercial/aws/inventory/resources/__init__.py +19 -0
  120. regscale/integrations/commercial/aws/inventory/resources/compute.py +234 -0
  121. regscale/integrations/commercial/aws/inventory/resources/containers.py +113 -0
  122. regscale/integrations/commercial/aws/inventory/resources/database.py +101 -0
  123. regscale/integrations/commercial/aws/inventory/resources/integration.py +237 -0
  124. regscale/integrations/commercial/aws/inventory/resources/networking.py +253 -0
  125. regscale/integrations/commercial/aws/inventory/resources/security.py +240 -0
  126. regscale/integrations/commercial/aws/inventory/resources/storage.py +91 -0
  127. regscale/integrations/commercial/aws/scanner.py +823 -0
  128. regscale/integrations/commercial/azure/__init__.py +0 -0
  129. regscale/integrations/commercial/azure/common.py +32 -0
  130. regscale/integrations/commercial/azure/intune.py +488 -0
  131. regscale/integrations/commercial/azure/scanner.py +49 -0
  132. regscale/integrations/commercial/burp.py +78 -0
  133. regscale/integrations/commercial/cpe.py +144 -0
  134. regscale/integrations/commercial/crowdstrike.py +1117 -0
  135. regscale/integrations/commercial/defender.py +1511 -0
  136. regscale/integrations/commercial/dependabot.py +210 -0
  137. regscale/integrations/commercial/durosuite/__init__.py +0 -0
  138. regscale/integrations/commercial/durosuite/api.py +1546 -0
  139. regscale/integrations/commercial/durosuite/process_devices.py +101 -0
  140. regscale/integrations/commercial/durosuite/scanner.py +637 -0
  141. regscale/integrations/commercial/durosuite/variables.py +21 -0
  142. regscale/integrations/commercial/ecr.py +90 -0
  143. regscale/integrations/commercial/gcp/__init__.py +237 -0
  144. regscale/integrations/commercial/gcp/auth.py +96 -0
  145. regscale/integrations/commercial/gcp/control_tests.py +238 -0
  146. regscale/integrations/commercial/gcp/variables.py +18 -0
  147. regscale/integrations/commercial/gitlab.py +332 -0
  148. regscale/integrations/commercial/grype.py +165 -0
  149. regscale/integrations/commercial/ibm.py +90 -0
  150. regscale/integrations/commercial/import_all/__init__.py +0 -0
  151. regscale/integrations/commercial/import_all/import_all_cmd.py +467 -0
  152. regscale/integrations/commercial/import_all/scan_file_fingerprints.json +27 -0
  153. regscale/integrations/commercial/jira.py +1046 -0
  154. regscale/integrations/commercial/mappings/__init__.py +0 -0
  155. regscale/integrations/commercial/mappings/csf_controls.json +713 -0
  156. regscale/integrations/commercial/mappings/nist_800_53_r5_controls.json +1516 -0
  157. regscale/integrations/commercial/nessus/__init__.py +0 -0
  158. regscale/integrations/commercial/nessus/nessus_utils.py +429 -0
  159. regscale/integrations/commercial/nessus/scanner.py +416 -0
  160. regscale/integrations/commercial/nexpose.py +90 -0
  161. regscale/integrations/commercial/okta.py +798 -0
  162. regscale/integrations/commercial/opentext/__init__.py +0 -0
  163. regscale/integrations/commercial/opentext/click.py +99 -0
  164. regscale/integrations/commercial/opentext/scanner.py +143 -0
  165. regscale/integrations/commercial/prisma.py +91 -0
  166. regscale/integrations/commercial/qualys.py +1462 -0
  167. regscale/integrations/commercial/salesforce.py +980 -0
  168. regscale/integrations/commercial/sap/__init__.py +0 -0
  169. regscale/integrations/commercial/sap/click.py +31 -0
  170. regscale/integrations/commercial/sap/sysdig/__init__.py +0 -0
  171. regscale/integrations/commercial/sap/sysdig/click.py +57 -0
  172. regscale/integrations/commercial/sap/sysdig/sysdig_scanner.py +190 -0
  173. regscale/integrations/commercial/sap/tenable/__init__.py +0 -0
  174. regscale/integrations/commercial/sap/tenable/click.py +49 -0
  175. regscale/integrations/commercial/sap/tenable/scanner.py +196 -0
  176. regscale/integrations/commercial/servicenow.py +1756 -0
  177. regscale/integrations/commercial/sicura/__init__.py +0 -0
  178. regscale/integrations/commercial/sicura/api.py +855 -0
  179. regscale/integrations/commercial/sicura/commands.py +73 -0
  180. regscale/integrations/commercial/sicura/scanner.py +481 -0
  181. regscale/integrations/commercial/sicura/variables.py +16 -0
  182. regscale/integrations/commercial/snyk.py +90 -0
  183. regscale/integrations/commercial/sonarcloud.py +260 -0
  184. regscale/integrations/commercial/sqlserver.py +369 -0
  185. regscale/integrations/commercial/stig_mapper_integration/__init__.py +0 -0
  186. regscale/integrations/commercial/stig_mapper_integration/click_commands.py +38 -0
  187. regscale/integrations/commercial/stig_mapper_integration/mapping_engine.py +353 -0
  188. regscale/integrations/commercial/stigv2/__init__.py +0 -0
  189. regscale/integrations/commercial/stigv2/ckl_parser.py +349 -0
  190. regscale/integrations/commercial/stigv2/click_commands.py +95 -0
  191. regscale/integrations/commercial/stigv2/stig_integration.py +202 -0
  192. regscale/integrations/commercial/synqly/__init__.py +0 -0
  193. regscale/integrations/commercial/synqly/assets.py +46 -0
  194. regscale/integrations/commercial/synqly/ticketing.py +132 -0
  195. regscale/integrations/commercial/synqly/vulnerabilities.py +223 -0
  196. regscale/integrations/commercial/synqly_jira.py +840 -0
  197. regscale/integrations/commercial/tenablev2/__init__.py +0 -0
  198. regscale/integrations/commercial/tenablev2/authenticate.py +31 -0
  199. regscale/integrations/commercial/tenablev2/click.py +1584 -0
  200. regscale/integrations/commercial/tenablev2/scanner.py +504 -0
  201. regscale/integrations/commercial/tenablev2/stig_parsers.py +140 -0
  202. regscale/integrations/commercial/tenablev2/utils.py +78 -0
  203. regscale/integrations/commercial/tenablev2/variables.py +17 -0
  204. regscale/integrations/commercial/trivy.py +162 -0
  205. regscale/integrations/commercial/veracode.py +96 -0
  206. regscale/integrations/commercial/wizv2/WizDataMixin.py +97 -0
  207. regscale/integrations/commercial/wizv2/__init__.py +0 -0
  208. regscale/integrations/commercial/wizv2/click.py +429 -0
  209. regscale/integrations/commercial/wizv2/constants.py +1001 -0
  210. regscale/integrations/commercial/wizv2/issue.py +361 -0
  211. regscale/integrations/commercial/wizv2/models.py +112 -0
  212. regscale/integrations/commercial/wizv2/parsers.py +339 -0
  213. regscale/integrations/commercial/wizv2/sbom.py +115 -0
  214. regscale/integrations/commercial/wizv2/scanner.py +416 -0
  215. regscale/integrations/commercial/wizv2/utils.py +796 -0
  216. regscale/integrations/commercial/wizv2/variables.py +39 -0
  217. regscale/integrations/commercial/wizv2/wiz_auth.py +159 -0
  218. regscale/integrations/commercial/xray.py +91 -0
  219. regscale/integrations/integration/__init__.py +2 -0
  220. regscale/integrations/integration/integration.py +26 -0
  221. regscale/integrations/integration/inventory.py +17 -0
  222. regscale/integrations/integration/issue.py +100 -0
  223. regscale/integrations/integration_override.py +149 -0
  224. regscale/integrations/public/__init__.py +103 -0
  225. regscale/integrations/public/cisa.py +641 -0
  226. regscale/integrations/public/criticality_updater.py +70 -0
  227. regscale/integrations/public/emass.py +411 -0
  228. regscale/integrations/public/emass_slcm_import.py +697 -0
  229. regscale/integrations/public/fedramp/__init__.py +0 -0
  230. regscale/integrations/public/fedramp/appendix_parser.py +548 -0
  231. regscale/integrations/public/fedramp/click.py +479 -0
  232. regscale/integrations/public/fedramp/components.py +714 -0
  233. regscale/integrations/public/fedramp/docx_parser.py +259 -0
  234. regscale/integrations/public/fedramp/fedramp_cis_crm.py +1124 -0
  235. regscale/integrations/public/fedramp/fedramp_common.py +3181 -0
  236. regscale/integrations/public/fedramp/fedramp_docx.py +388 -0
  237. regscale/integrations/public/fedramp/fedramp_five.py +2343 -0
  238. regscale/integrations/public/fedramp/fedramp_traversal.py +138 -0
  239. regscale/integrations/public/fedramp/import_fedramp_r4_ssp.py +279 -0
  240. regscale/integrations/public/fedramp/import_workbook.py +495 -0
  241. regscale/integrations/public/fedramp/inventory_items.py +244 -0
  242. regscale/integrations/public/fedramp/mappings/__init__.py +0 -0
  243. regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +7388 -0
  244. regscale/integrations/public/fedramp/mappings/fedramp_r5_params.json +8636 -0
  245. regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +9605 -0
  246. regscale/integrations/public/fedramp/mappings/system_roles.py +34 -0
  247. regscale/integrations/public/fedramp/mappings/user.py +175 -0
  248. regscale/integrations/public/fedramp/mappings/values.py +141 -0
  249. regscale/integrations/public/fedramp/markdown_parser.py +150 -0
  250. regscale/integrations/public/fedramp/metadata.py +689 -0
  251. regscale/integrations/public/fedramp/models/__init__.py +59 -0
  252. regscale/integrations/public/fedramp/models/leveraged_auth_new.py +168 -0
  253. regscale/integrations/public/fedramp/models/poam_importer.py +522 -0
  254. regscale/integrations/public/fedramp/parts_mapper.py +107 -0
  255. regscale/integrations/public/fedramp/poam/__init__.py +0 -0
  256. regscale/integrations/public/fedramp/poam/scanner.py +851 -0
  257. regscale/integrations/public/fedramp/properties.py +201 -0
  258. regscale/integrations/public/fedramp/reporting.py +84 -0
  259. regscale/integrations/public/fedramp/resources.py +496 -0
  260. regscale/integrations/public/fedramp/rosetta.py +110 -0
  261. regscale/integrations/public/fedramp/ssp_logger.py +87 -0
  262. regscale/integrations/public/fedramp/system_characteristics.py +922 -0
  263. regscale/integrations/public/fedramp/system_control_implementations.py +582 -0
  264. regscale/integrations/public/fedramp/system_implementation.py +190 -0
  265. regscale/integrations/public/fedramp/xml_utils.py +87 -0
  266. regscale/integrations/public/nist_catalog.py +275 -0
  267. regscale/integrations/public/oscal.py +1946 -0
  268. regscale/integrations/public/otx.py +169 -0
  269. regscale/integrations/scanner_integration.py +2692 -0
  270. regscale/integrations/variables.py +25 -0
  271. regscale/models/__init__.py +7 -0
  272. regscale/models/app_models/__init__.py +5 -0
  273. regscale/models/app_models/catalog_compare.py +213 -0
  274. regscale/models/app_models/click.py +252 -0
  275. regscale/models/app_models/datetime_encoder.py +21 -0
  276. regscale/models/app_models/import_validater.py +321 -0
  277. regscale/models/app_models/mapping.py +260 -0
  278. regscale/models/app_models/pipeline.py +37 -0
  279. regscale/models/click_models.py +413 -0
  280. regscale/models/config.py +154 -0
  281. regscale/models/email_style.css +67 -0
  282. regscale/models/hierarchy.py +8 -0
  283. regscale/models/inspect_models.py +79 -0
  284. regscale/models/integration_models/__init__.py +0 -0
  285. regscale/models/integration_models/amazon_models/__init__.py +0 -0
  286. regscale/models/integration_models/amazon_models/inspector.py +262 -0
  287. regscale/models/integration_models/amazon_models/inspector_scan.py +206 -0
  288. regscale/models/integration_models/aqua.py +247 -0
  289. regscale/models/integration_models/azure_alerts.py +255 -0
  290. regscale/models/integration_models/base64.py +23 -0
  291. regscale/models/integration_models/burp.py +433 -0
  292. regscale/models/integration_models/burp_models.py +128 -0
  293. regscale/models/integration_models/cisa_kev_data.json +19333 -0
  294. regscale/models/integration_models/defender_data.py +93 -0
  295. regscale/models/integration_models/defenderimport.py +143 -0
  296. regscale/models/integration_models/drf.py +443 -0
  297. regscale/models/integration_models/ecr_models/__init__.py +0 -0
  298. regscale/models/integration_models/ecr_models/data.py +69 -0
  299. regscale/models/integration_models/ecr_models/ecr.py +239 -0
  300. regscale/models/integration_models/flat_file_importer.py +1079 -0
  301. regscale/models/integration_models/grype_import.py +247 -0
  302. regscale/models/integration_models/ibm.py +126 -0
  303. regscale/models/integration_models/implementation_results.py +85 -0
  304. regscale/models/integration_models/nexpose.py +140 -0
  305. regscale/models/integration_models/prisma.py +202 -0
  306. regscale/models/integration_models/qualys.py +720 -0
  307. regscale/models/integration_models/qualys_scanner.py +160 -0
  308. regscale/models/integration_models/sbom/__init__.py +0 -0
  309. regscale/models/integration_models/sbom/cyclone_dx.py +139 -0
  310. regscale/models/integration_models/send_reminders.py +620 -0
  311. regscale/models/integration_models/snyk.py +155 -0
  312. regscale/models/integration_models/synqly_models/__init__.py +0 -0
  313. regscale/models/integration_models/synqly_models/capabilities.json +1 -0
  314. regscale/models/integration_models/synqly_models/connector_types.py +22 -0
  315. regscale/models/integration_models/synqly_models/connectors/__init__.py +7 -0
  316. regscale/models/integration_models/synqly_models/connectors/assets.py +97 -0
  317. regscale/models/integration_models/synqly_models/connectors/ticketing.py +583 -0
  318. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +169 -0
  319. regscale/models/integration_models/synqly_models/ocsf_mapper.py +331 -0
  320. regscale/models/integration_models/synqly_models/param.py +72 -0
  321. regscale/models/integration_models/synqly_models/synqly_model.py +733 -0
  322. regscale/models/integration_models/synqly_models/tenants.py +39 -0
  323. regscale/models/integration_models/tenable_models/__init__.py +0 -0
  324. regscale/models/integration_models/tenable_models/integration.py +187 -0
  325. regscale/models/integration_models/tenable_models/models.py +513 -0
  326. regscale/models/integration_models/trivy_import.py +231 -0
  327. regscale/models/integration_models/veracode.py +217 -0
  328. regscale/models/integration_models/xray.py +135 -0
  329. regscale/models/locking.py +100 -0
  330. regscale/models/platform.py +110 -0
  331. regscale/models/regscale_models/__init__.py +67 -0
  332. regscale/models/regscale_models/assessment.py +570 -0
  333. regscale/models/regscale_models/assessment_plan.py +52 -0
  334. regscale/models/regscale_models/asset.py +567 -0
  335. regscale/models/regscale_models/asset_mapping.py +190 -0
  336. regscale/models/regscale_models/case.py +42 -0
  337. regscale/models/regscale_models/catalog.py +261 -0
  338. regscale/models/regscale_models/cci.py +46 -0
  339. regscale/models/regscale_models/change.py +167 -0
  340. regscale/models/regscale_models/checklist.py +372 -0
  341. regscale/models/regscale_models/comment.py +49 -0
  342. regscale/models/regscale_models/compliance_settings.py +112 -0
  343. regscale/models/regscale_models/component.py +412 -0
  344. regscale/models/regscale_models/component_mapping.py +65 -0
  345. regscale/models/regscale_models/control.py +38 -0
  346. regscale/models/regscale_models/control_implementation.py +1128 -0
  347. regscale/models/regscale_models/control_objective.py +261 -0
  348. regscale/models/regscale_models/control_parameter.py +100 -0
  349. regscale/models/regscale_models/control_test.py +34 -0
  350. regscale/models/regscale_models/control_test_plan.py +75 -0
  351. regscale/models/regscale_models/control_test_result.py +52 -0
  352. regscale/models/regscale_models/custom_field.py +245 -0
  353. regscale/models/regscale_models/data.py +109 -0
  354. regscale/models/regscale_models/data_center.py +40 -0
  355. regscale/models/regscale_models/deviation.py +203 -0
  356. regscale/models/regscale_models/email.py +97 -0
  357. regscale/models/regscale_models/evidence.py +47 -0
  358. regscale/models/regscale_models/evidence_mapping.py +40 -0
  359. regscale/models/regscale_models/facility.py +59 -0
  360. regscale/models/regscale_models/file.py +382 -0
  361. regscale/models/regscale_models/filetag.py +37 -0
  362. regscale/models/regscale_models/form_field_value.py +94 -0
  363. regscale/models/regscale_models/group.py +169 -0
  364. regscale/models/regscale_models/implementation_objective.py +335 -0
  365. regscale/models/regscale_models/implementation_option.py +275 -0
  366. regscale/models/regscale_models/implementation_role.py +33 -0
  367. regscale/models/regscale_models/incident.py +177 -0
  368. regscale/models/regscale_models/interconnection.py +43 -0
  369. regscale/models/regscale_models/issue.py +1176 -0
  370. regscale/models/regscale_models/leveraged_authorization.py +125 -0
  371. regscale/models/regscale_models/line_of_inquiry.py +52 -0
  372. regscale/models/regscale_models/link.py +205 -0
  373. regscale/models/regscale_models/meta_data.py +64 -0
  374. regscale/models/regscale_models/mixins/__init__.py +0 -0
  375. regscale/models/regscale_models/mixins/parent_cache.py +124 -0
  376. regscale/models/regscale_models/module.py +224 -0
  377. regscale/models/regscale_models/modules.py +191 -0
  378. regscale/models/regscale_models/objective.py +14 -0
  379. regscale/models/regscale_models/parameter.py +87 -0
  380. regscale/models/regscale_models/ports_protocol.py +81 -0
  381. regscale/models/regscale_models/privacy.py +89 -0
  382. regscale/models/regscale_models/profile.py +50 -0
  383. regscale/models/regscale_models/profile_link.py +68 -0
  384. regscale/models/regscale_models/profile_mapping.py +124 -0
  385. regscale/models/regscale_models/project.py +63 -0
  386. regscale/models/regscale_models/property.py +278 -0
  387. regscale/models/regscale_models/question.py +85 -0
  388. regscale/models/regscale_models/questionnaire.py +87 -0
  389. regscale/models/regscale_models/questionnaire_instance.py +177 -0
  390. regscale/models/regscale_models/rbac.py +132 -0
  391. regscale/models/regscale_models/reference.py +86 -0
  392. regscale/models/regscale_models/regscale_model.py +1643 -0
  393. regscale/models/regscale_models/requirement.py +29 -0
  394. regscale/models/regscale_models/risk.py +274 -0
  395. regscale/models/regscale_models/sbom.py +54 -0
  396. regscale/models/regscale_models/scan_history.py +436 -0
  397. regscale/models/regscale_models/search.py +53 -0
  398. regscale/models/regscale_models/security_control.py +132 -0
  399. regscale/models/regscale_models/security_plan.py +204 -0
  400. regscale/models/regscale_models/software_inventory.py +159 -0
  401. regscale/models/regscale_models/stake_holder.py +64 -0
  402. regscale/models/regscale_models/stig.py +647 -0
  403. regscale/models/regscale_models/supply_chain.py +152 -0
  404. regscale/models/regscale_models/system_role.py +188 -0
  405. regscale/models/regscale_models/system_role_external_assignment.py +40 -0
  406. regscale/models/regscale_models/tag.py +37 -0
  407. regscale/models/regscale_models/tag_mapping.py +19 -0
  408. regscale/models/regscale_models/task.py +133 -0
  409. regscale/models/regscale_models/threat.py +196 -0
  410. regscale/models/regscale_models/user.py +175 -0
  411. regscale/models/regscale_models/user_group.py +55 -0
  412. regscale/models/regscale_models/vulnerability.py +242 -0
  413. regscale/models/regscale_models/vulnerability_mapping.py +162 -0
  414. regscale/models/regscale_models/workflow.py +55 -0
  415. regscale/models/regscale_models/workflow_action.py +34 -0
  416. regscale/models/regscale_models/workflow_instance.py +269 -0
  417. regscale/models/regscale_models/workflow_instance_step.py +114 -0
  418. regscale/models/regscale_models/workflow_template.py +58 -0
  419. regscale/models/regscale_models/workflow_template_step.py +45 -0
  420. regscale/regscale.py +815 -0
  421. regscale/utils/__init__.py +7 -0
  422. regscale/utils/b64conversion.py +14 -0
  423. regscale/utils/click_utils.py +118 -0
  424. regscale/utils/decorators.py +48 -0
  425. regscale/utils/dict_utils.py +59 -0
  426. regscale/utils/files.py +79 -0
  427. regscale/utils/fxns.py +30 -0
  428. regscale/utils/graphql_client.py +113 -0
  429. regscale/utils/lists.py +16 -0
  430. regscale/utils/numbers.py +12 -0
  431. regscale/utils/shell.py +148 -0
  432. regscale/utils/string.py +121 -0
  433. regscale/utils/synqly_utils.py +165 -0
  434. regscale/utils/threading/__init__.py +8 -0
  435. regscale/utils/threading/threadhandler.py +131 -0
  436. regscale/utils/threading/threadsafe_counter.py +47 -0
  437. regscale/utils/threading/threadsafe_dict.py +242 -0
  438. regscale/utils/threading/threadsafe_list.py +83 -0
  439. regscale/utils/version.py +104 -0
  440. regscale/validation/__init__.py +0 -0
  441. regscale/validation/address.py +37 -0
  442. regscale/validation/record.py +48 -0
  443. regscale/visualization/__init__.py +5 -0
  444. regscale/visualization/click.py +34 -0
  445. regscale_cli-6.16.0.0.dist-info/LICENSE +21 -0
  446. regscale_cli-6.16.0.0.dist-info/METADATA +659 -0
  447. regscale_cli-6.16.0.0.dist-info/RECORD +481 -0
  448. regscale_cli-6.16.0.0.dist-info/WHEEL +5 -0
  449. regscale_cli-6.16.0.0.dist-info/entry_points.txt +6 -0
  450. regscale_cli-6.16.0.0.dist-info/top_level.txt +2 -0
  451. tests/fixtures/__init__.py +2 -0
  452. tests/fixtures/api.py +87 -0
  453. tests/fixtures/models.py +91 -0
  454. tests/fixtures/test_fixture.py +144 -0
  455. tests/mocks/__init__.py +0 -0
  456. tests/mocks/objects.py +3 -0
  457. tests/mocks/response.py +32 -0
  458. tests/mocks/xml.py +13 -0
  459. tests/regscale/__init__.py +0 -0
  460. tests/regscale/core/__init__.py +0 -0
  461. tests/regscale/core/test_api.py +232 -0
  462. tests/regscale/core/test_app.py +406 -0
  463. tests/regscale/core/test_login.py +37 -0
  464. tests/regscale/core/test_logz.py +66 -0
  465. tests/regscale/core/test_sbom_generator.py +87 -0
  466. tests/regscale/core/test_validation_utils.py +163 -0
  467. tests/regscale/core/test_version.py +78 -0
  468. tests/regscale/models/__init__.py +0 -0
  469. tests/regscale/models/test_asset.py +71 -0
  470. tests/regscale/models/test_config.py +26 -0
  471. tests/regscale/models/test_control_implementation.py +27 -0
  472. tests/regscale/models/test_import.py +97 -0
  473. tests/regscale/models/test_issue.py +36 -0
  474. tests/regscale/models/test_mapping.py +52 -0
  475. tests/regscale/models/test_platform.py +31 -0
  476. tests/regscale/models/test_regscale_model.py +346 -0
  477. tests/regscale/models/test_report.py +32 -0
  478. tests/regscale/models/test_tenable_integrations.py +118 -0
  479. tests/regscale/models/test_user_model.py +121 -0
  480. tests/regscale/test_about.py +19 -0
  481. tests/regscale/test_authorization.py +65 -0
@@ -0,0 +1,1643 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Base Regscale Model"""
4
+ import copy
5
+ import hashlib
6
+ import json
7
+ import logging
8
+ import os
9
+ import threading
10
+ import warnings
11
+ from abc import ABC
12
+ from threading import RLock
13
+ from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, TypeVar, Union, cast, get_type_hints
14
+
15
+ from cacheout import Cache
16
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
17
+ from requests import Response
18
+ from rich.progress import Progress, TaskID
19
+ from yaml import dump
20
+
21
+ from regscale.core.app.application import Application
22
+ from regscale.core.app.utils.api_handler import APIHandler, APIInsertionError, APIResponseError, APIUpdateError
23
+ from regscale.core.app.utils.app_utils import create_progress_object
24
+ from regscale.models.regscale_models.search import Search
25
+ from regscale.utils.threading import ThreadSafeList
26
+ from regscale.utils.threading.threadsafe_dict import ThreadSafeDict
27
+
28
+ # Suppress specific Pydantic warnings
29
+ warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")
30
+
31
+ T = TypeVar("T", bound="RegScaleModel")
32
+
33
+ logger = logging.getLogger("regscale")
34
+
35
+
36
+ class RegScaleModel(BaseModel, ABC):
37
+ """Mixin class for RegScale Models to add functionality to interact with RegScale API"""
38
+
39
+ model_config = ConfigDict(populate_by_name=True, use_enum_values=True, arbitrary_types_allowed=True)
40
+
41
+ _x_api_version: ClassVar[str] = "1"
42
+ _module_slug: ClassVar[str] = "model_slug"
43
+ _module_string: ClassVar[str] = ""
44
+ _module_slug_id_url: ClassVar[str] = "/api/{model_slug}/{id}"
45
+ _module_slug_url: ClassVar[str] = "/api/{model_slug}"
46
+ _module_id: ClassVar[int] = 0
47
+ _api_handler: ClassVar[APIHandler] = None
48
+ _parent_id_field: ClassVar[str] = "parentId"
49
+ _unique_fields: ClassVar[List[List[str]]] = []
50
+ _get_objects_for_list: ClassVar[bool] = False
51
+ _get_objects_for_list_id: ClassVar[str] = "id"
52
+ _exclude_graphql_fields: ClassVar[List[str]] = ["extra_data", "tenantsId"]
53
+ _original_data: Optional[Dict[str, Any]] = None
54
+
55
+ # Caching
56
+ _object_cache: ClassVar[Cache] = Cache(maxsize=100000)
57
+ _parent_cache: ClassVar[Cache] = Cache(maxsize=50000)
58
+ _lock_registry: ClassVar[ThreadSafeDict] = ThreadSafeDict()
59
+ _global_lock: ClassVar[threading.Lock] = threading.Lock() # Class-level lock
60
+
61
+ _pending_updates: ClassVar[Dict[str, Set[int]]] = {}
62
+ _pending_creations: ClassVar[Dict[str, Set[str]]] = {}
63
+
64
+ id: int = 0
65
+ extra_data: Dict[str, Any] = Field(default={}, exclude=True)
66
+ createdById: Optional[str] = None
67
+ lastUpdatedById: Optional[str] = None
68
+
69
+ def __init__(self: T, *args, **data) -> None:
70
+ """
71
+ Initialize the RegScaleModel.
72
+
73
+ :param T self: The instance being initialized
74
+ :param *args: Variable length argument list
75
+ :param **data: Arbitrary keyword arguments
76
+ :return: None
77
+ :rtype: None
78
+ """
79
+ try:
80
+ super().__init__(*args, **data)
81
+ # Capture initial state after initialization
82
+ self._original_data = self.dict(exclude_unset=True)
83
+ except Exception as e:
84
+ logger.error(f"Error creating {self.__class__.__name__}: {e} {data}", exc_info=True)
85
+
86
+ @classmethod
87
+ def _get_api_handler(cls) -> APIHandler:
88
+ """
89
+ Get or initialize the API handler.
90
+
91
+ :return: The API handler instance
92
+ :rtype: APIHandler
93
+ """
94
+ if cls._api_handler is None:
95
+ cls._api_handler = APIHandler()
96
+ return cls._api_handler
97
+
98
+ def get_object_id(self) -> int:
99
+ """
100
+ Get the object ID.
101
+
102
+ :return: The object ID
103
+ :rtype: int
104
+ """
105
+ if not hasattr(self, "id"):
106
+ return 0
107
+ logger.debug(f"Getting object ID for {self.__class__.__name__} {self.id}")
108
+ return self.id
109
+
110
+ @classmethod
111
+ def _get_lock(cls, cache_key: str) -> RLock:
112
+ """
113
+ Get or create a lock associated with a cache key.
114
+
115
+ :param str cache_key: The cache key
116
+ :return: A reentrant lock
117
+ :rtype: RLock
118
+ """
119
+ lock = cls._lock_registry.get(cache_key)
120
+ if lock is None:
121
+ with cls._global_lock: # Use a class-level lock to ensure thread safety
122
+ lock = cls._lock_registry.get(cache_key)
123
+ if lock is None:
124
+ lock = RLock()
125
+ cls._lock_registry[cache_key] = lock
126
+ return lock
127
+
128
+ @classmethod
129
+ def _get_cache_key(cls, obj: T, defaults: Optional[Dict[str, Any]] = None) -> str:
130
+ """
131
+ Generate a cache key based on the object's unique fields using SHA256 hash.
132
+
133
+ :param T obj: The object to generate a key for
134
+ :param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply to the object, defaults to None
135
+ :return: A string representing the cache key
136
+ :rtype: str
137
+ """
138
+ defaults = defaults or {}
139
+ # Iterate over each set of unique fields
140
+ for fields in cls.get_unique_fields():
141
+ unique_fields = []
142
+ # Iterate over each field in the current set of unique fields
143
+ for field in fields:
144
+ value = getattr(obj, field, defaults.get(field))
145
+ if value is not None:
146
+ # If the value is longer than 15 characters, hash it using SHA256
147
+ if len(str(value)) > 15:
148
+ # Hash long values
149
+ hash_object = hashlib.sha256(str(value).encode())
150
+ value = hash_object.hexdigest()
151
+ # Append the field and its value to the unique_fields list
152
+ unique_fields.append(f"{field}:{value}")
153
+
154
+ # If all fields in the current set have values, use them to generate the cache key
155
+ if len(unique_fields) == len(fields):
156
+ unique_string = ":".join(unique_fields)
157
+ cache_key = f"{cls.__name__}:{unique_string}"
158
+ return cache_key
159
+
160
+ # Fallback if no complete set of unique fields is found, use object ID
161
+ return f"{cls.__name__}:{obj.get_object_id()}"
162
+
163
+ @classmethod
164
+ def get_cached_object(cls, cache_key: str) -> Optional[T]:
165
+ """
166
+ Get an object from the cache based on its cache key.
167
+
168
+ :param str cache_key: The cache key of the object
169
+ :return: The cached object if found, None otherwise
170
+ :rtype: Optional[T]
171
+ """
172
+ with cls._get_lock(cache_key):
173
+ return cls._object_cache.get(cache_key)
174
+
175
+ @classmethod
176
+ def cache_object(cls, obj: T) -> None:
177
+ """
178
+ Cache an object and update the parent cache if applicable.
179
+
180
+ :param T obj: The object to cache
181
+ :return: None
182
+ :rtype: None
183
+ """
184
+ try:
185
+ if not obj:
186
+ return
187
+ cache_key = cls._get_cache_key(obj)
188
+ cls._object_cache.set(cache_key, obj)
189
+
190
+ # Update parent cache
191
+ cls._update_parent_cache(obj)
192
+ except Exception as e:
193
+ logger.error(f"Error caching object: {e}", exc_info=True)
194
+
195
+ @classmethod
196
+ def get_tenant_id(cls) -> Optional[int]:
197
+ """
198
+ Get the tenant ID from the token in init.yaml
199
+
200
+ :return: Tenant ID
201
+ :rtype: Optional[int]
202
+ """
203
+ from regscale.models.regscale_models.user import User
204
+
205
+ user_id = cls.get_user_id()
206
+ return User.get_tenant_id_for_user_id(user_id) if user_id else None
207
+
208
+ @classmethod
209
+ def get_user_id(cls) -> Optional[str]:
210
+ """
211
+ Get the user ID from parsing the token from REGSCALE_TOKEN envar or the token in init.yaml
212
+ If it isn't found, fall back to the userId from the init.yaml
213
+
214
+ :return: User ID, if available
215
+ :rtype: str
216
+ """
217
+ from regscale.core.app.internal.login import parse_user_id_from_jwt
218
+
219
+ app = Application()
220
+ token = os.environ.get("REGSCALE_TOKEN") or app.config.get("token")
221
+ return parse_user_id_from_jwt(app, token) or app.config.get("userId")
222
+
223
+ @classmethod
224
+ def _update_parent_cache(cls, obj: T) -> None:
225
+ """
226
+ Update the parent cache with the new or updated object.
227
+
228
+ :param T obj: The object to add or update in the parent cache
229
+ :return: None
230
+ :rtype: None
231
+ """
232
+ parent_id = getattr(obj, cls._parent_id_field, None)
233
+ parent_module = getattr(obj, "parentModule", getattr(obj, "parent_module", ""))
234
+ if parent_id and parent_module:
235
+ cache_key = f"{parent_id}:{cls.__name__}"
236
+ with cls._get_lock(cache_key):
237
+ parent_objects = cls._parent_cache.get(cache_key, [])
238
+ # Remove the old version of the object if it exists
239
+ parent_objects = [o for o in parent_objects if o.id != obj.id]
240
+ # Add the new or updated object
241
+ parent_objects.append(obj)
242
+ cls._parent_cache.set(cache_key, parent_objects)
243
+ logger.debug(f"Updated parent cache for {cls.__name__} with parent ID {parent_id}")
244
+
245
+ @classmethod
246
+ def cache_list_objects(cls, cache_key: str, objects: List[T]) -> None:
247
+ """
248
+ Cache a list of objects.
249
+
250
+ :param str cache_key: The cache key
251
+ :param List[T] objects: The objects to cache
252
+ :return: None
253
+ :rtype: None
254
+ """
255
+ with cls._get_lock(cache_key):
256
+ for obj in objects:
257
+ cls.cache_object(obj)
258
+ cls._parent_cache.set(cache_key, objects)
259
+
260
+ @classmethod
261
+ def clear_cache(cls) -> None:
262
+ """
263
+ Clear the object cache.
264
+
265
+ :return: None
266
+ :rtype: None
267
+ """
268
+ cls._object_cache.clear()
269
+
270
+ @classmethod
271
+ def delete_object_cache(cls, obj: T) -> None:
272
+ """
273
+ Delete an object from the cache.
274
+
275
+ :param T obj: The object to delete from the cache
276
+ :return: None
277
+ :rtype: None
278
+ """
279
+ cache_key = cls._get_cache_key(obj)
280
+ with cls._get_lock(cache_key):
281
+ cls._object_cache.delete(cache_key)
282
+
283
+ parent_id = getattr(obj, cls._parent_id_field, None)
284
+ parent_module = getattr(obj, "parentModule", getattr(obj, "parent_module", ""))
285
+
286
+ # update parent cache
287
+ if parent_id and parent_module:
288
+ parent_cache_key = f"{parent_id}:{obj.__class__.__name__}"
289
+ with obj._get_lock(parent_cache_key):
290
+ parent_objects = [o for o in obj._parent_cache.get(parent_cache_key, []) if o.id != obj.id]
291
+ obj._parent_cache.set(parent_cache_key, parent_objects)
292
+
293
+ def has_changed(self, comp_object: Optional[T] = None) -> bool:
294
+ """
295
+ Check if current data differs from the original data or the provided comparison object.
296
+
297
+ :param Optional[T] comp_object: The object to compare against, defaults to None
298
+ :return: True if the data has changed, False otherwise
299
+ :rtype: bool
300
+ """
301
+ if comp_object is None:
302
+ comp_object = self._original_data
303
+
304
+ if not comp_object:
305
+ return True
306
+
307
+ current_data = self.dict(exclude_unset=True)
308
+ for key, value in current_data.items():
309
+ if key not in ["id", "dateCreated"] and value != comp_object.get(key):
310
+ return True
311
+ return False
312
+
313
+ def show_changes(self, comp_object: Optional[T] = None) -> Dict[str, Any]:
314
+ """
315
+ Display the changes between the original data and the current data.
316
+
317
+ :param Optional[T] comp_object: The object to compare, defaults to None
318
+ :return: A dictionary of changes
319
+ :rtype: Dict[str, Any]
320
+ """
321
+ if comp_object:
322
+ original_data = comp_object.dict(exclude_unset=True)
323
+ else:
324
+ original_data = self._original_data
325
+
326
+ if getattr(self, "id", 0) == 0:
327
+ return original_data
328
+ if not original_data:
329
+ return {}
330
+ current_data = self.dict(exclude_unset=True)
331
+ changes = {
332
+ key: {"from": original_data.get(key), "to": current_data.get(key)}
333
+ for key in current_data
334
+ if current_data.get(key) != original_data.get(key) # and key != "id"
335
+ }
336
+ return changes
337
+
338
+ def diff(self, other: Any) -> Dict[str, Tuple[Any, Any]]:
339
+ """
340
+ Find the differences between two objects
341
+
342
+ :param Any other: The other object to compare
343
+ :return: A dictionary of differences
344
+ :rtype: Dict[str, Tuple[Any, Any]]
345
+ """
346
+ differences = {}
347
+ for attr in vars(self):
348
+ if getattr(self, attr) != getattr(other, attr):
349
+ differences[attr] = (getattr(self, attr), getattr(other, attr))
350
+ return differences
351
+
352
+ def dict(self, exclude_unset: bool = False, **kwargs: Optional[Dict[str, Any]]) -> Dict[str, Any]:
353
+ """
354
+ Override the default dict method to exclude hidden fields
355
+
356
+ :param bool exclude_unset: Whether to exclude unset fields, defaults to False
357
+ :return: Dictionary representation of the object
358
+ :rtype: Dict[str, Any]
359
+ """
360
+ hidden_fields = set(
361
+ attribute_name
362
+ for attribute_name, model_field in self.model_fields.items()
363
+ if model_field.from_field("hidden") == "hidden"
364
+ )
365
+ unset_fields = set(
366
+ attribute_name
367
+ for attribute_name, model_field in self.model_fields.items()
368
+ if getattr(self, attribute_name, None) is None
369
+ )
370
+ excluded_fields = hidden_fields.union(unset_fields)
371
+ kwargs.setdefault("exclude", excluded_fields)
372
+ return super().model_dump(**kwargs)
373
+
374
+ @classmethod
375
+ def get_module_id(cls) -> int:
376
+ """
377
+ Get the module ID for the model.
378
+
379
+ :return: Module ID #
380
+ :rtype: int
381
+ """
382
+ return cls._module_id
383
+
384
+ @classmethod
385
+ def get_module_slug(cls) -> str:
386
+ """
387
+ Get the module slug for the model.
388
+
389
+ :return: Module slug
390
+ :rtype: str
391
+ """
392
+ return cls._module_slug
393
+
394
+ @classmethod
395
+ def get_module_string(cls) -> str:
396
+ """
397
+ Get the module name for the model.
398
+
399
+ :return: Module name
400
+ :rtype: str
401
+ """
402
+ return cls._module_string or cls.get_module_slug()
403
+
404
+ @classmethod
405
+ def get_unique_fields(cls) -> List[List[str]]:
406
+ """
407
+ Get the unique fields for the model.
408
+
409
+ Maintains backward compatibility with old format (List[str]) while supporting
410
+ new format (List[List[str]]).
411
+
412
+ :return: Unique fields as a list of lists
413
+ :rtype: List[List[str]]
414
+ :raises DeprecationWarning: If using old format (List[str])
415
+ """
416
+ if not cls._unique_fields:
417
+ return []
418
+
419
+ # Check if the first element is a string (old format) or a list (new format)
420
+ if cls._unique_fields and isinstance(cls._unique_fields[0], str):
421
+ import warnings
422
+
423
+ warnings.warn(
424
+ f"Single list of unique fields is deprecated for {cls.__name__}. "
425
+ "Use list of lists format instead: [[field1, field2], [field3]]",
426
+ DeprecationWarning,
427
+ stacklevel=2,
428
+ )
429
+ # Convert old format to new format by wrapping in a list
430
+ return [cls._unique_fields] # type: ignore
431
+
432
+ return cls._check_override()
433
+
434
+ @classmethod
435
+ def _check_override(cls) -> List[List[str]]:
436
+ """
437
+ Check if the unique fields have been overridden in the configuration.
438
+
439
+ :raises ValueError: If the primary fields are invalid
440
+ :return: A list of unique fields
441
+ :rtype: List[List[str]]
442
+ """
443
+ sample_format = {"uniqueOverride": {"asset": ["ipAddress"]}}
444
+ config = Application().config
445
+
446
+ try:
447
+ primary = config.get("uniqueOverride", {}).get(cls.__name__.lower())
448
+ if primary:
449
+ if not isinstance(primary, list):
450
+ raise ValueError(
451
+ f"Invalid config format in uniqueOverride.{cls.__name__.lower()}, the configuration must be in a format like so:\n{dump(sample_format, default_flow_style=False)}"
452
+ )
453
+ if primary != cls._unique_fields:
454
+ if all(attr in cls.model_fields for attr in primary):
455
+ if primary not in cls._unique_fields:
456
+ cls._unique_fields.insert(1, primary)
457
+ else:
458
+ # Move primary to index 1 if it exists
459
+ cls._unique_fields.insert(1, cls._unique_fields.pop(cls._unique_fields.index(primary)))
460
+ else:
461
+ raise ValueError(
462
+ f"One or more invalid attribute(s) detected: {primary}, falling back on default unique fields for type: {cls.__name__.lower()}"
463
+ )
464
+ except ValueError as e:
465
+ logger.warning(e)
466
+ return cls._unique_fields
467
+
468
+ @classmethod
469
+ def _get_endpoints(cls) -> ConfigDict:
470
+ """
471
+ Get the endpoints for the API.
472
+
473
+ :return: A dictionary of endpoints
474
+ :rtype: ConfigDict
475
+ """
476
+ endpoints = ConfigDict( # type: ignore
477
+ get=cls._module_slug_id_url, # type: ignore
478
+ insert="/api/{model_slug}/", # type: ignore
479
+ update=cls._module_slug_id_url, # type: ignore
480
+ delete=cls._module_slug_id_url, # type: ignore
481
+ list="/api/{model_slug}/getList", # type: ignore
482
+ get_all_by_parent="/api/{model_slug}/getAllByParent/{intParentID}/{strModule}", # type: ignore
483
+ )
484
+ endpoints.update(cls._get_additional_endpoints())
485
+ return endpoints
486
+
487
+ def __hash__(self) -> hash:
488
+ """
489
+ Enable object to be hashable
490
+
491
+ :return: Hashed Vulnerability
492
+ :rtype: hash
493
+ """
494
+ return hash(tuple(tuple(getattr(self, field) for field in sublist) for sublist in self.get_unique_fields()))
495
+
496
+ def __eq__(self, other: object) -> bool:
497
+ """
498
+ Enable object to be equal
499
+
500
+ :param object other: Object to compare to
501
+ :return: Whether the objects are equal
502
+ :rtype: bool
503
+ """
504
+ if not isinstance(other, type(self)):
505
+ return NotImplemented
506
+ return any(
507
+ all(getattr(self, field) == getattr(other, field) for field in sublist)
508
+ for sublist in self.get_unique_fields()
509
+ )
510
+
511
+ def __repr__(self) -> str:
512
+ """
513
+ Override the default repr method to return a string representation of the object.
514
+
515
+ :return: String representation of the object
516
+ :rtype: str
517
+ """
518
+ return f"<{self.__str__()}>"
519
+
520
+ def __str__(self) -> str:
521
+ """
522
+ Override the default str method to return a string representation of the object.
523
+
524
+ :return: String representation of the object
525
+ :rtype: str
526
+ """
527
+ fields = (
528
+ "\n "
529
+ + "\n ".join(
530
+ f"{name}={value!r},"
531
+ for name, value in self.dict().items()
532
+ # if value is not None
533
+ )
534
+ + "\n"
535
+ )
536
+ return f"{self.__class__.__name__}({fields})"
537
+
538
+ def find_by_unique(self, parent_id_field: Optional[str] = None) -> Optional[T]:
539
+ """
540
+ Find a unique instance of the object. First tries the defined unique fields,
541
+ then falls back to alternative matching strategies if no match is found.
542
+
543
+ :param Optional[str] parent_id_field: The parent ID field, defaults to None
544
+ :raises NotImplementedError: If the method is not implemented
545
+ :raises ValueError: If parent ID is not found
546
+ :return: The instance or None if not found
547
+ :rtype: Optional[T]
548
+ """
549
+ if not self.get_unique_fields():
550
+ raise NotImplementedError(f"_unique_fields not defined for {self.__class__.__name__}")
551
+
552
+ parent_id = getattr(self, parent_id_field or self._parent_id_field, None)
553
+ if parent_id is None:
554
+ raise ValueError(f"Parent ID not found for {self.__class__.__name__}")
555
+
556
+ parent_module = getattr(self, "parentModule", getattr(self, "parent_module", ""))
557
+ cache_key = self._get_cache_key(self)
558
+
559
+ with self._get_lock(cache_key):
560
+ # Check cache first
561
+ if cached_object := self.get_cached_object(cache_key):
562
+ return cached_object
563
+
564
+ # Get all instances from parent
565
+ instances: List[T] = self.get_all_by_parent(parent_id=parent_id, parent_module=parent_module)
566
+
567
+ # Try to find matching instance using unique fields
568
+ for keys in self._unique_fields:
569
+ matching_instance = next(
570
+ (
571
+ instance
572
+ for instance in instances
573
+ if all(
574
+ getattr(instance, field) not in [None, ""]
575
+ and getattr(self, field) not in [None, ""]
576
+ and str(getattr(instance, field)).lower() == str(getattr(self, field)).lower()
577
+ for field in keys
578
+ )
579
+ ),
580
+ None,
581
+ )
582
+ if matching_instance:
583
+ return matching_instance
584
+
585
+ return None
586
+
587
+ def get_or_create_with_status(self: T, bulk: bool = False) -> Tuple[bool, T]:
588
+ """
589
+ Get or create an instance, returning both creation status and instance.
590
+
591
+ :param bool bulk: Whether to perform a bulk create operation, defaults to False
592
+ :return: Tuple of (was_created, instance)
593
+ :rtype: Tuple[bool, T]
594
+ """
595
+ cache_key = self._get_cache_key(self)
596
+ with self._get_lock(cache_key):
597
+ if cached_object := self.get_cached_object(cache_key):
598
+ return False, cached_object
599
+
600
+ instance = self.find_by_unique()
601
+
602
+ if instance:
603
+ self.cache_object(instance)
604
+ return False, instance
605
+ else:
606
+ created_instance = self.create(bulk=bulk)
607
+ self.cache_object(created_instance)
608
+ return True, created_instance
609
+
610
+ def get_or_create(self: T, bulk: bool = False) -> T:
611
+ """
612
+ Get or create an instance.
613
+
614
+ :param bool bulk: Whether to perform a bulk create operation, defaults to False
615
+ :return: The instance
616
+ :rtype: T
617
+ """
618
+ _, instance = self.get_or_create_with_status(bulk=bulk)
619
+ return instance
620
+
621
+ def create_or_update(
622
+ self: T,
623
+ bulk_create: bool = False,
624
+ bulk_update: bool = False,
625
+ defaults: Optional[Dict[str, Any]] = None,
626
+ ) -> T:
627
+ """
628
+ Create or update an instance.
629
+
630
+ :param bool bulk_create: Whether to perform a bulk create, defaults to False
631
+ :param bool bulk_update: Whether to perform a bulk update, defaults to False
632
+ :param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply to the instance if it is created, defaults to {}
633
+ :return: The instance
634
+ :rtype: T
635
+ """
636
+ _, instance = self.create_or_update_with_status(
637
+ bulk_create=bulk_create, bulk_update=bulk_update, defaults=defaults
638
+ )
639
+ return instance
640
+
641
+ def create_or_update_with_status(
642
+ self: T,
643
+ bulk_create: bool = False,
644
+ bulk_update: bool = False,
645
+ defaults: Optional[Dict[str, Any]] = None,
646
+ ) -> Tuple[bool, T]:
647
+ """
648
+ Create or update an instance. Use cache methods to retrieve and store instances based on unique fields.
649
+
650
+ :param bool bulk_create: Whether to perform a bulk create, defaults to False
651
+ :param bool bulk_update: Whether to perform a bulk update, defaults to False
652
+ :param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply to the instance if it is created, defaults to {}
653
+ :return: The instance of the class
654
+ :rtype: Tuple[bool, T]
655
+ """
656
+ logger.debug(f"Starting create_or_update for {self.__class__.__name__}: #{getattr(self, 'id', '')}")
657
+
658
+ cache_key = self._get_cache_key(self)
659
+
660
+ with self._get_lock(cache_key):
661
+ # Check if the object is already in the cache
662
+ cached_object = self.get_cached_object(cache_key)
663
+
664
+ # If not in cache, try to find it in the database
665
+ instance = cached_object or self.find_by_unique()
666
+
667
+ if instance:
668
+ # An existing instance was found (either in cache or database)
669
+ logger.debug(f"Found {'cached' if cached_object else 'existing'} instance of {self.__class__.__name__}")
670
+ # Update the current object's ID with the found instance's ID
671
+ self.id = instance.id
672
+ # If the object has a 'dateCreated' attribute, update it
673
+ if hasattr(self, "dateCreated"):
674
+ self.dateCreated = instance.dateCreated # noqa
675
+
676
+ # Update the _original_data attribute with the instance data
677
+ self._original_data = instance.dict(exclude_unset=True)
678
+
679
+ # Check if the current object has any changes compared to the found instance
680
+ if self.has_changed():
681
+ logger.debug(f"Instance of {self.__class__.__name__} has changed, updating")
682
+ # Save the changes, potentially in bulk
683
+ updated_instance = self.save(bulk=bulk_update)
684
+ # Update the cache with the new instance
685
+ self.cache_object(updated_instance)
686
+ # Return the updated instance, optionally with a flag indicating it wasn't newly created
687
+ return False, updated_instance
688
+
689
+ # If no changes, return the existing instance
690
+ return False, instance
691
+
692
+ # No existing instance was found, so create a new one
693
+ # apply defaults if they are provided
694
+ if defaults:
695
+ for key, value in defaults.items():
696
+ # Handle callable values by executing them
697
+ if callable(value):
698
+ value = value()
699
+ setattr(self, key, value)
700
+ logger.debug(f"No existing instance found for {self.__class__.__name__}, creating new")
701
+ created_instance = self.create(bulk=bulk_create)
702
+ # Cache the newly created instance
703
+ self.cache_object(created_instance)
704
+ # Return the created instance, optionally with a flag indicating it was newly created
705
+ return True, created_instance
706
+
707
+ @classmethod
708
+ def _handle_list_response(
709
+ cls,
710
+ response: Response,
711
+ suppress_error: bool = False,
712
+ override_values: Optional[Dict] = None,
713
+ parent_id: Optional[int] = None,
714
+ parent_module: Optional[str] = None,
715
+ ) -> List[T]:
716
+ """
717
+ Handles the response for a list of items from an API call.
718
+
719
+ This method processes the response object to extract a list of items. If the response is successful and contains
720
+ a list of items (either directly or within a 'items' key for JSON responses), it returns a list of class
721
+ instances created from the items. If the response is unsuccessful or does not contain any items, it logs an
722
+ error and returns an empty list.
723
+
724
+ :param Response response: The response object from the API call
725
+ :param bool suppress_error: Whether to suppress error logging, defaults to False
726
+ :param Optional[Dict] override_values: Dictionary of values to override in the response items, defaults to None
727
+ :param Optional[int] parent_id: The ID of the parent object, if applicable, defaults to None
728
+ :param Optional[str] parent_module: The module of the parent object, if applicable, defaults to None
729
+ :return: A list of class instances created from the response items
730
+ :rtype: List[T]
731
+ """
732
+ logger.debug(f"Handling list response with status_code {response.status_code if response else ''}")
733
+
734
+ if cls._is_response_invalid(response):
735
+ logger.debug("No response or status code 204, 404, or 400")
736
+ return []
737
+
738
+ if response.ok and response.status_code != 400:
739
+ items = cls._extract_items(response)
740
+ cls._apply_override_values(items, override_values)
741
+ return cls._create_objects_from_items(items, parent_id=parent_id, parent_module=parent_module)
742
+
743
+ cls._log_response_error(response, suppress_error)
744
+ return []
745
+
746
+ @staticmethod
747
+ def _is_response_invalid(response: Response) -> bool:
748
+ """
749
+ Check if the response is invalid.
750
+
751
+ :param Response response: The response object to check
752
+ :return: True if the response is invalid, False otherwise
753
+ :rtype: bool
754
+ """
755
+ # regscale is sending ok with 400 status code for some reason
756
+ return not response or response.status_code in [204, 404]
757
+
758
+ @staticmethod
759
+ def _extract_items(response: Response) -> List[Dict]:
760
+ """
761
+ Extract items from the response.
762
+
763
+ :param Response response: The response object to extract items from
764
+ :return: A list of items extracted from the response
765
+ :rtype: List[Dict]
766
+ """
767
+ from requests.exceptions import JSONDecodeError
768
+
769
+ try:
770
+ json_response = response.json()
771
+ except JSONDecodeError:
772
+ return []
773
+ if isinstance(json_response, dict) and "items" in json_response:
774
+ return json_response.get("items", [])
775
+ return json_response
776
+
777
+ @staticmethod
778
+ def _apply_override_values(items: List[Dict], override_values: Optional[Dict]) -> None:
779
+ """
780
+ Apply override values to the items.
781
+
782
+ :param List[Dict] items: List of items to apply override values to
783
+ :param Optional[Dict] override_values: Dictionary of values to override in the items, defaults to None
784
+ :rtype: None
785
+ """
786
+ if override_values:
787
+ for item in items:
788
+ for key, value in override_values.items():
789
+ item[key] = value
790
+
791
+ @classmethod
792
+ def cast_list_object(
793
+ cls,
794
+ item: Dict,
795
+ parent_id: Optional[int] = None,
796
+ parent_module: Optional[str] = None,
797
+ ) -> T:
798
+ """
799
+ Cast list of items to class instances.
800
+
801
+ :param Dict item: Item to cast to a class instance
802
+ :param Optional[int] parent_id: The ID of the parent object, if applicable, defaults to None
803
+ :param Optional[str] parent_module: The module of the parent object, if applicable, defaults to None
804
+ :return: Class instance created from the item
805
+ :rtype: T
806
+ """
807
+ if parent_id is not None and "parentId" in cls.model_fields and "parentId" not in item:
808
+ item["parentId"] = parent_id
809
+ if parent_module is not None and "parentModule" in cls.model_fields and "parentModule" not in item:
810
+ item["parentModule"] = parent_module
811
+ return cls._cast_object(item)
812
+
813
+ @classmethod
814
+ def _cast_object(cls, item: Dict) -> T:
815
+ """
816
+ Cast an item to a class instance.
817
+
818
+ :param Dict item: Item to cast to a class instance
819
+ :return: Class instance created from the item
820
+ :rtype: T
821
+ :raises ValidationError: If the item fails validation when creating the class instance
822
+ :raises TypeError: If there's a type mismatch when creating the class instance
823
+ """
824
+ try:
825
+ obj: T = cls(**item)
826
+ except ValidationError as e:
827
+ logger.error(f"Failed to cast item to {cls.__name__}: {e}", exc_info=True)
828
+ raise e
829
+ except TypeError as e:
830
+ logger.error(f"Failed to cast item to {cls.__name__}: {e}", exc_info=True)
831
+ raise
832
+ return obj
833
+
834
+ @classmethod
835
+ def _create_objects_from_items(
836
+ cls,
837
+ items: List[Dict],
838
+ parent_id: Optional[int] = None,
839
+ parent_module: Optional[str] = None,
840
+ ) -> List[T]:
841
+ """
842
+ Create objects from items using threading to improve performance.
843
+
844
+ :param List[Dict] items: List of items to create objects from
845
+ :param Optional[int] parent_id: The ID of the parent object, if applicable, defaults to None
846
+ :param Optional[str] parent_module: The module of the parent object, if applicable, defaults to None
847
+ :return: List of class instances created from the items
848
+ :rtype: List[T]
849
+ """
850
+ from concurrent.futures import ThreadPoolExecutor
851
+
852
+ def fetch_object(item):
853
+ return cls.get_object(object_id=item.get(cls._get_objects_for_list_id))
854
+
855
+ if cls._get_objects_for_list:
856
+ with ThreadPoolExecutor(max_workers=3) as executor:
857
+ objects: List[T] = list(executor.map(fetch_object, items))
858
+ return [item for item in objects if item]
859
+ return [cls.cast_list_object(item, parent_id=parent_id, parent_module=parent_module) for item in items if item]
860
+
861
+ @classmethod
862
+ def _log_response_error(cls, response: Response, suppress_error: bool) -> None:
863
+ """
864
+ Log an error message for the response.
865
+
866
+ :param Response response: The response object to log an error for
867
+ :param bool suppress_error: Whether to suppress error logging
868
+ :rtype: None
869
+ """
870
+ if not suppress_error:
871
+ logger.error(f"Error in response: {response.status_code}, {response.text}")
872
+
873
+ @classmethod
874
+ def _handle_response(cls, response: Response) -> Optional[T]:
875
+ """
876
+ Handles the response for a single item from an API call.
877
+
878
+ This method processes the response object to extract a single item. If the response is successful and contains
879
+ an item, it returns an instance of the class created from the item. If the response is unsuccessful or does not
880
+ contain an item, it logs an error and returns None.
881
+
882
+ :param Response response: The response object from the API call
883
+ :return: An instance of the class created from the response item, or None if unsuccessful
884
+ :rtype: Optional[T]
885
+ """
886
+ if not response or response.status_code in [204, 404]:
887
+ return None
888
+ if response.ok:
889
+ return cast(T, cls(**response.json()))
890
+ else:
891
+ logger.error(f"Failed to get {cls.get_module_slug()} for {cls.__name__}")
892
+ return None
893
+
894
+ @classmethod
895
+ def _handle_graph_response(cls, response: Dict[Any, Any], child: Optional[Any] = None) -> List[T]:
896
+ """
897
+ Handle graph response
898
+
899
+ :param Dict[Any, Any] response: Response from API
900
+ :param Optional[Any] child: Child object, defaults to None
901
+ :return: List of RegScale model objects
902
+ :rtype: List[T]
903
+ """
904
+ items = []
905
+ for k, v in response.items():
906
+ if hasattr(v, "items"):
907
+ for o in v["items"]:
908
+ if child:
909
+ items.append(cast(T, cls(**o[child])))
910
+ else:
911
+ items.append(cast(T, cls(**o)))
912
+ return items
913
+
914
+ @classmethod
915
+ def get_field_names(cls) -> List[str]:
916
+ """
917
+ Get the field names for the Asset model.
918
+
919
+ :return: List of field names
920
+ :rtype: List[str]
921
+ """
922
+ return [x for x in get_type_hints(cls).keys() if not x.startswith("_")]
923
+
924
+ @classmethod
925
+ def build_graphql_fields(cls) -> str:
926
+ """
927
+ Dynamically builds a GraphQL query for a given Pydantic model class.
928
+
929
+ :return: A string representing the GraphQL query
930
+ :rtype: str
931
+ """
932
+ return "\n".join(x for x in cls.get_field_names() if x not in cls._exclude_graphql_fields and x != "extra_data")
933
+
934
+ @classmethod
935
+ def get_by_parent(cls, parent_id: int, parent_module: str) -> List[T]:
936
+ """
937
+ Get a list of objects by parent.
938
+
939
+ DEPRECATED: This method will be removed in future versions. Use 'get_all_by_parent' instead.
940
+
941
+ :param int parent_id: The ID of the parent
942
+ :param str parent_module: The module of the parent
943
+ :return: A list of objects
944
+ :rtype: List[T]
945
+ """
946
+ warnings.warn(
947
+ "The method 'get_by_parent' is deprecated and will be removed in future versions. "
948
+ "Use 'get_all_by_parent' instead.",
949
+ DeprecationWarning,
950
+ stacklevel=2,
951
+ )
952
+ return cls.get_all_by_parent(parent_id, parent_module)
953
+
954
+ @classmethod
955
+ def get_all_by_parent(
956
+ cls,
957
+ parent_id: int,
958
+ parent_module: Optional[str] = None,
959
+ search: Optional[Search] = None,
960
+ ) -> List[T]:
961
+ """
962
+ Get a list of objects by parent, optimized for speed.
963
+
964
+ :param int parent_id: The ID of the parent
965
+ :param Optional[str] parent_module: The module of the parent, defaults to None
966
+ :param Optional[Search] search: The search object, defaults to None
967
+ :return: A list of objects
968
+ :rtype: List[T]
969
+ """
970
+ cache_key = f"{parent_id}:{cls.__name__}"
971
+
972
+ with cls._get_lock(cache_key):
973
+ cached_objects = cls._parent_cache.get(cache_key)
974
+ # Check for None and empty list
975
+ if cached_objects is not None and len(cached_objects) > 0:
976
+ return cached_objects
977
+
978
+ if "get_all_by_search" in cls._get_endpoints() and parent_id and parent_module and not search:
979
+ logger.debug("Using get_all_by_search")
980
+ search = Search(parentID=parent_id, module=parent_module)
981
+ if search:
982
+ objects: List[T] = cls._handle_looping_response(search)
983
+ else:
984
+ try:
985
+ endpoint = cls.get_endpoint("get_all_by_parent").format(
986
+ intParentID=parent_id, strModule=parent_module
987
+ )
988
+ objects: List[T] = cls._handle_list_response(
989
+ cls._get_api_handler().get(endpoint=endpoint), parent_id=parent_id, parent_module=parent_module
990
+ )
991
+ except ValueError as e:
992
+ logger.error(f"Failed to get endpoint: {e}", exc_info=True)
993
+ objects = []
994
+
995
+ cls.cache_list_objects(cache_key=cache_key, objects=objects)
996
+
997
+ return objects
998
+
999
+ @classmethod
1000
+ def _handle_looping_response(cls, search: Search, page: int = 1, page_size: int = 500) -> List[T]:
1001
+ """
1002
+ Handles the response for a list of items from an API call.
1003
+
1004
+ :param Search search: The search object
1005
+ :param int page: The starting page, defaults to 1
1006
+ :param int page_size: The number of items per page, defaults to 500
1007
+ :return: A list of objects
1008
+ :rtype: List[T]
1009
+ """
1010
+ items: List[T] = []
1011
+ this_search = copy.deepcopy(search)
1012
+ this_search.page = page
1013
+ this_search.pageSize = page_size
1014
+
1015
+ while True:
1016
+ data: List[T] = cls._handle_list_response(
1017
+ cls._get_api_handler().post(
1018
+ endpoint=cls.get_endpoint("get_all_by_search"),
1019
+ data=this_search.model_dump(),
1020
+ )
1021
+ )
1022
+ try:
1023
+ if not any(data):
1024
+ break
1025
+ except AttributeError:
1026
+ break
1027
+
1028
+ items.extend(data)
1029
+ this_search.page += 1
1030
+
1031
+ return items
1032
+
1033
+ @staticmethod
1034
+ def _get_additional_endpoints() -> Union[ConfigDict, dict]:
1035
+ """
1036
+ Get additional endpoints for the API.
1037
+
1038
+ :return: A dictionary of additional endpoints
1039
+ :rtype: Union[ConfigDict, dict]
1040
+ """
1041
+ return ConfigDict()
1042
+
1043
+ @classmethod
1044
+ def get_endpoint(cls, endpoint_type: str) -> str:
1045
+ """
1046
+ Get the endpoint for a specific type.
1047
+
1048
+ :param str endpoint_type: The type of endpoint
1049
+ :raises ValueError: If the endpoint type is not found
1050
+ :return: The endpoint
1051
+ :rtype: str
1052
+ """
1053
+ endpoint = cls._get_endpoints().get(endpoint_type, "na") # noqa
1054
+ if not endpoint or endpoint == "na":
1055
+ logger.error(f"{cls.__name__} does not have endpoint {endpoint_type}")
1056
+ raise ValueError(f"Endpoint {endpoint_type} not found")
1057
+ endpoint = str(endpoint).replace("{model_slug}", cls.get_module_slug())
1058
+ return endpoint
1059
+
1060
+ @classmethod
1061
+ def _get_pending_updates(cls) -> Set[int]:
1062
+ """
1063
+ Get the set of pending updates for the class.
1064
+
1065
+ :return: Set of pending update IDs
1066
+ :rtype: Set[int]
1067
+ """
1068
+ class_name = cls.__name__
1069
+ if class_name not in cls._pending_updates:
1070
+ cls._pending_updates[class_name] = set()
1071
+ return cls._pending_updates[class_name]
1072
+
1073
+ @classmethod
1074
+ def _get_pending_creations(cls) -> Set[str]:
1075
+ """
1076
+ Get the set of pending creations for the class.
1077
+
1078
+ :return: Set of pending creation cache keys
1079
+ :rtype: Set[str]
1080
+ """
1081
+ class_name = cls.__name__
1082
+ if class_name not in cls._pending_creations:
1083
+ cls._pending_creations[class_name] = set()
1084
+ return cls._pending_creations[class_name]
1085
+
1086
+ def save(self: T, bulk: bool = False) -> T:
1087
+ """
1088
+ Save the current object, either immediately or in bulk.
1089
+
1090
+ :param bool bulk: Whether to perform a bulk save operation, defaults to False
1091
+ :return: The saved object
1092
+ :rtype: T
1093
+ """
1094
+ if self.has_changed():
1095
+ if bulk:
1096
+ logger.debug(f"Adding {self.__class__.__name__} {self.id} to pending updates")
1097
+ self._get_pending_updates().add(self._get_cache_key(self))
1098
+ self.cache_object(self) # Update the cache with the current state
1099
+ return self
1100
+ else:
1101
+ return self._perform_save()
1102
+ else:
1103
+ logger.debug(f"No changes detected for {self.__class__.__name__} {self.id}")
1104
+ return self
1105
+
1106
+ def create(self: T, bulk: bool = False) -> T:
1107
+ """
1108
+ Create a new object, either immediately or in bulk.
1109
+
1110
+ :param bool bulk: Whether to perform a bulk create operation, defaults to False
1111
+ :return: The created object
1112
+ :rtype: T
1113
+ """
1114
+ if bulk:
1115
+ logger.debug(f"Adding new {self.__class__.__name__} to pending creations")
1116
+ cache_key = self._get_cache_key(self)
1117
+ with self._get_lock(cache_key):
1118
+ self._get_pending_creations().add(cache_key)
1119
+ self.cache_object(self)
1120
+ return self
1121
+ else:
1122
+ with self._get_lock(self._get_cache_key(self)):
1123
+ created_object = self._perform_create()
1124
+ self.cache_object(created_object)
1125
+ return created_object
1126
+
1127
+ @classmethod
1128
+ def bulk_save(cls, progress_context: Optional[Progress] = None) -> Dict[str, List[T]]:
1129
+ """
1130
+ Perform bulk save operations for both updates and creations.
1131
+
1132
+ :param Optional[Progress] progress_context: Optional progress context for tracking
1133
+ :return: Dictionary containing lists of updated and created objects
1134
+ :rtype: Dict[str, List[T]]
1135
+ """
1136
+ result = {"updated": [], "created": []}
1137
+
1138
+ # Handle updates
1139
+ pending_updates = cls._get_pending_updates()
1140
+ if pending_updates:
1141
+ logger.info(f"Performing bulk update for {len(pending_updates)} {cls.__name__} objects")
1142
+ objects_to_update = [cls.get_cached_object(cache_key=cache_key) for cache_key in pending_updates]
1143
+ if objects_to_update:
1144
+ result["updated"] = cls.batch_update(items=objects_to_update, progress_context=progress_context)
1145
+ pending_updates.clear()
1146
+
1147
+ # Handle creations
1148
+ pending_creations = cls._get_pending_creations()
1149
+ if pending_creations:
1150
+ logger.info(f"Performing bulk creation for {len(pending_creations)} {cls.__name__} objects")
1151
+ objects_to_create = [cls.get_cached_object(cache_key=cache_key) for cache_key in pending_creations]
1152
+ if objects_to_create:
1153
+ result["created"] = cls.batch_create(items=objects_to_create, progress_context=progress_context)
1154
+ pending_creations.clear()
1155
+
1156
+ return result
1157
+
1158
+ @classmethod
1159
+ def _get_headers(cls) -> Optional[Dict[str, str]]:
1160
+ """
1161
+ Get the headers for the API request.
1162
+
1163
+ :return: Dictionary of headers if api version is not 1, otherwise None
1164
+ :rtype: Optional[Dict[str, str]]
1165
+ """
1166
+ if cls._x_api_version != "1":
1167
+ return {"x-api-version": cls._x_api_version}
1168
+ return None
1169
+
1170
+ def _perform_create(self: T) -> T:
1171
+ """
1172
+ Perform the actual create operation.
1173
+
1174
+ :raises APIInsertionError: If the insert fails
1175
+ :return: The created object
1176
+ :rtype: T
1177
+ """
1178
+ endpoint = self.get_endpoint("insert")
1179
+ response = self._get_api_handler().post(endpoint=endpoint, data=self.dict(), headers=self._get_headers())
1180
+ if response and response.ok:
1181
+ obj = self.__class__(**response.json())
1182
+ self.cache_object(obj)
1183
+ return obj
1184
+ else:
1185
+ logger.error(
1186
+ f"Failed to create {self.__class__.__name__}\n Endpoint: {endpoint}\n Payload: "
1187
+ f"{json.dumps(self.dict(), indent=2)}",
1188
+ exc_info=True,
1189
+ )
1190
+ if response and not response.ok:
1191
+ logger.error(f"Response Error: Code #{response.status_code}: {response.reason}\n{response.text}")
1192
+ if response is None:
1193
+ error_msg = "Response was None"
1194
+ logger.error(error_msg)
1195
+ raise APIInsertionError(error_msg)
1196
+ error_msg = f"Response Code: {response.status_code}:{response.reason} - {response.text}"
1197
+ logger.error(error_msg)
1198
+ raise APIInsertionError(error_msg)
1199
+
1200
+ def _perform_save(self: T) -> T:
1201
+ """
1202
+ Perform the actual save operation.
1203
+
1204
+ :raises APIUpdateError: If the update fails
1205
+ :return: The updated object
1206
+ :rtype: T
1207
+ """
1208
+ logger.debug(f"Updating {self.__class__.__name__} {self.id}")
1209
+ endpoint = self.get_endpoint("update").format(id=self.id)
1210
+ response = self._get_api_handler().put(endpoint=endpoint, data=self.dict(), headers=self._get_headers())
1211
+ if hasattr(response, "ok") and response.ok:
1212
+ obj = self.__class__(**response.json())
1213
+ self.cache_object(obj)
1214
+ return obj
1215
+ else:
1216
+ logger.error(
1217
+ f"Failed to update {self.__class__.__name__}\n Endpoint: {endpoint}\n Payload: "
1218
+ f"{json.dumps(self.dict(), indent=2)}"
1219
+ )
1220
+ if response is not None:
1221
+ raise APIUpdateError(f"Response Code: {response.status_code} - {response.text}")
1222
+ else:
1223
+ raise APIUpdateError("Response was None")
1224
+
1225
+ @classmethod
1226
+ def batch_create(
1227
+ cls,
1228
+ items: Union[List[T], ThreadSafeList[T]],
1229
+ progress_context: Optional[Progress] = None,
1230
+ remove_progress: Optional[bool] = False,
1231
+ ) -> List[T]:
1232
+ """
1233
+ Use bulk_create method to create assets.
1234
+
1235
+ :param List[T] items: List of Asset Objects
1236
+ :param Optional[Progress] progress_context: Optional progress context for tracking
1237
+ :param Optional[bool] remove_progress: Whether to remove the progress bar after completion, defaults to False
1238
+ :return: List of cls items from RegScale
1239
+ :rtype: List[T]
1240
+ """
1241
+ batch_size = 100
1242
+ results = []
1243
+ total_items = len(items)
1244
+
1245
+ def process_batch(progress: Optional[Progress] = None, remove_progress_bar: Optional[bool] = False):
1246
+ """
1247
+ Process the batch of items
1248
+
1249
+ :param Optional[Progress] progress: Optional progress context for tracking
1250
+ :param Optional[bool] remove_progress_bar: Whether to remove the progress bar after completion, defaults to False
1251
+ """
1252
+ nonlocal results
1253
+ create_job = None
1254
+ if progress:
1255
+ create_job = progress.add_task(
1256
+ f"[#f68d1f]Creating {total_items} RegScale {cls.__name__}s...",
1257
+ total=total_items,
1258
+ )
1259
+ for i in range(0, total_items, batch_size):
1260
+ batch = items[i : i + batch_size]
1261
+ batch_results = cls._handle_list_response(
1262
+ cls._get_api_handler().post(
1263
+ endpoint=cls.get_endpoint("batch_create"),
1264
+ data=[item.model_dump() for item in batch if item],
1265
+ )
1266
+ )
1267
+ results.extend(batch_results)
1268
+ if progress and create_job is not None:
1269
+ progress_increment = min(batch_size, total_items - i)
1270
+ progress.advance(create_job, progress_increment)
1271
+ for created_item in batch_results:
1272
+ cls.cache_object(created_item)
1273
+ cls._check_and_remove_progress_object(progress, remove_progress_bar, create_job)
1274
+
1275
+ if progress_context:
1276
+ process_batch(progress=progress_context, remove_progress_bar=remove_progress)
1277
+ else:
1278
+ with create_progress_object() as create_progress:
1279
+ process_batch(progress=create_progress, remove_progress_bar=remove_progress)
1280
+ return results
1281
+
1282
+ @classmethod
1283
+ def batch_update(
1284
+ cls,
1285
+ items: Union[List[T], ThreadSafeList[T]],
1286
+ progress_context: Optional[Progress] = None,
1287
+ remove_progress: Optional[bool] = False,
1288
+ ) -> List[T]:
1289
+ """
1290
+ Use bulk_update method to update assets.
1291
+
1292
+ :param List[T] items: List of cls Objects
1293
+ :param Optional[Progress] progress_context: Optional progress context for tracking
1294
+ :param Optional[bool] remove_progress: Whether to remove the progress bar after completion, defaults to False
1295
+ :return: List of cls items from RegScale
1296
+ :rtype: List[T]
1297
+ """
1298
+ batch_size = 100
1299
+ results: List[T] = []
1300
+ total_items = len(items)
1301
+
1302
+ def process_batch(progress: Optional[Progress] = None, remove_progress_bar: Optional[bool] = False):
1303
+ """
1304
+ Process the batch of items
1305
+
1306
+ :param Optional[Progress] progress: Optional progress context for tracking
1307
+ :param Optional[bool] remove_progress_bar: Whether to remove the progress bar after completion, defaults to False
1308
+ """
1309
+ nonlocal results
1310
+ update_job = None
1311
+ if progress:
1312
+ update_job = progress.add_task(
1313
+ f"[#f68d1f]Updating {total_items} RegScale {cls.__name__}s...",
1314
+ total=total_items,
1315
+ )
1316
+ for i in range(0, total_items, batch_size):
1317
+ batch = items[i : i + batch_size]
1318
+ batch_results = cls._handle_list_response(
1319
+ cls._get_api_handler().put(
1320
+ endpoint=cls.get_endpoint("batch_update"),
1321
+ data=[item.model_dump() for item in batch if item],
1322
+ )
1323
+ )
1324
+ results.extend(batch_results)
1325
+ if progress and update_job is not None:
1326
+ progress_increment = min(batch_size, total_items - i)
1327
+ progress.advance(update_job, progress_increment)
1328
+ for item in batch_results:
1329
+ cls.cache_object(item)
1330
+ cls._check_and_remove_progress_object(progress_context, remove_progress_bar, update_job)
1331
+
1332
+ if progress_context:
1333
+ process_batch(progress_context)
1334
+ else:
1335
+ with create_progress_object() as create_progress:
1336
+ process_batch(create_progress)
1337
+
1338
+ return results
1339
+
1340
+ @staticmethod
1341
+ def _check_and_remove_progress_object(
1342
+ progress_context: Optional[Progress] = None,
1343
+ remove_progress: Optional[bool] = False,
1344
+ progress_task: Optional[TaskID] = None,
1345
+ ) -> None:
1346
+ """
1347
+ Check if the progress object exists and remove it.
1348
+
1349
+ :param Optional[Progress] progress_context: Optional progress context for tracking
1350
+ :param Optional[bool] remove_progress: Whether to remove the progress bar after completion, defaults to False
1351
+ :param Optional[TaskID] progress_task: Optional progress task ID to remove, defaults to None
1352
+ :rtype: None
1353
+ """
1354
+ if progress_context and remove_progress and progress_task is not None:
1355
+ progress_context.remove_task(progress_task)
1356
+
1357
+ @classmethod
1358
+ def get_object(cls, object_id: Union[str, int]) -> Optional[T]:
1359
+ """
1360
+ Get a RegScale object by ID.
1361
+
1362
+ :param Union[str, int] object_id: The ID of the object
1363
+ :return: The object or None if not found
1364
+ :rtype: Optional[T]
1365
+ """
1366
+ response = cls._get_api_handler().get(endpoint=cls.get_endpoint("get").format(id=object_id))
1367
+ if response and response.ok:
1368
+ if response.json() and isinstance(response.json(), list):
1369
+ return cast(T, cls(**response.json()[0]))
1370
+ else:
1371
+ return cast(T, cls(**response.json()))
1372
+ else:
1373
+ logger.debug(f"Failing response: {response.status_code}: {response.reason} {response.text}")
1374
+ logger.warning(f"{cls.__name__}: No matching record found for ID: {cls.__name__} {object_id}")
1375
+ return None
1376
+
1377
+ @classmethod
1378
+ def get_list(cls) -> List[T]:
1379
+ """
1380
+ Get a list of objects.
1381
+
1382
+ :return: A list of objects
1383
+ :rtype: List[T]
1384
+ """
1385
+ response = cls._get_api_handler().get(endpoint=cls.get_endpoint("list"))
1386
+ if response.ok:
1387
+ return cast(List[T], [cls.get_object(object_id=sp["id"]) for sp in response.json()])
1388
+ else:
1389
+ logger.error(f"Failed to get list of {cls.__name__} {response}")
1390
+ return []
1391
+
1392
+ def delete(self) -> bool:
1393
+ """
1394
+ Delete an object in RegScale.
1395
+
1396
+ :return: True if successful, False otherwise
1397
+ :rtype: bool
1398
+ """
1399
+ # Clear the cache for this object
1400
+ self.delete_object_cache(self)
1401
+
1402
+ response = self._get_api_handler().delete(
1403
+ endpoint=self.get_endpoint("delete").format(id=self.id), headers=self._get_headers()
1404
+ )
1405
+ if response.ok:
1406
+ return True
1407
+ elif response.ok is False and response.status_code == 404:
1408
+ logger.debug(f"Failed to delete {self.__class__.__name__} {self.dict()}, {response.status_code}")
1409
+ return False
1410
+ else:
1411
+ logger.error(f"Failed to delete {self.__class__.__name__} {self.dict()}")
1412
+ return False
1413
+
1414
+ @classmethod
1415
+ def from_dict(cls, obj: Dict[str, Any], copy_object: bool = False) -> T: # type: ignore
1416
+ """
1417
+ Create RegScale Model from dictionary
1418
+
1419
+ :param Dict[str, Any] obj: dictionary
1420
+ :param bool copy_object: Whether to copy the object without an id, defaults to False
1421
+ :return: Instance of RegScale Model
1422
+ :rtype: T
1423
+ """
1424
+ copy_obj = copy.copy(obj)
1425
+ if "id" in copy_obj and copy_object:
1426
+ del copy_obj["id"]
1427
+ return cast(T, cls(**copy_obj))
1428
+
1429
+ @classmethod
1430
+ def parse_response(cls, response: Response, suppress_error: bool = False) -> Optional[T]:
1431
+ """
1432
+ Parse a response.
1433
+
1434
+ :param Response response: The response
1435
+ :param bool suppress_error: Whether to suppress the error, defaults to False
1436
+ :return: An object or None
1437
+ :rtype: Optional[T]
1438
+ """
1439
+ if response and response.ok:
1440
+ logger.info(json.dumps(response.json(), indent=4))
1441
+ return cast(T, cls(**response.json()))
1442
+ else:
1443
+ cls.log_response_error(response=response, suppress_error=suppress_error)
1444
+ return None
1445
+
1446
+ @classmethod
1447
+ def log_response_error(cls, response: Response, suppress_error: bool = False) -> None:
1448
+ """
1449
+ Log an error message.
1450
+
1451
+ :param Response response: The response
1452
+ :param bool suppress_error: Whether to suppress the error, defaults to False
1453
+ :raises APIResponseError: If the response is None
1454
+ :rtype: None
1455
+ """
1456
+ if response is not None:
1457
+ message = f"{cls.__name__}: - StatusCode: {response.status_code} Reason: {response.reason}"
1458
+ if response.text:
1459
+ message += f" - {response.text}"
1460
+ if suppress_error:
1461
+ logger.error(message)
1462
+ else:
1463
+ raise APIResponseError(message)
1464
+ else:
1465
+ if suppress_error:
1466
+ logger.error(f"{cls.__name__}: Response was None")
1467
+ else:
1468
+ raise APIResponseError(f"{cls.__name__}: Response was None")
1469
+
1470
+ # pylint: disable=W0613
1471
+ @classmethod
1472
+ def get_sort_position_dict(cls) -> dict:
1473
+ """
1474
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1475
+ by all models that can be instantiated by that module.
1476
+ The purpose is to provide a sort-order for populating the columns
1477
+ in the generated spreadsheet.
1478
+
1479
+ Any field name that returns a sort position of -1 will be supressed in the generated Excel
1480
+ workbook.
1481
+ :return: dict The sort position in the list of properties
1482
+ :rtype: dict
1483
+ """
1484
+ return {}
1485
+
1486
+ @classmethod
1487
+ def get_enum_values(cls, field_name: str) -> list:
1488
+ """
1489
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1490
+ by all models that can be instantiated by that module.
1491
+ The purpose is to provide a list of enumerated values that can be used for the specified
1492
+ property on the model. This is to be used for building a drop-down of values that can be
1493
+ used to set the property.
1494
+
1495
+ :param str field_name: The property name to provide enum values for
1496
+ :return: list of strings
1497
+ :rtype: list
1498
+ """
1499
+ return []
1500
+
1501
+ @classmethod
1502
+ def get_lookup_field(cls, field_name: str) -> str:
1503
+ """
1504
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1505
+ by all models that can be instantiated by that module.
1506
+ The purpose is to provide a query that can be used to pull a list of records and IDs for
1507
+ building a drop-down of lookup values that can be used to populate the appropriate
1508
+ foreign-key value into the specified property.
1509
+
1510
+ :param str field_name: The property name to provide lookup value query for
1511
+ :return: str The GraphQL query for building the list of lookup values and IDs
1512
+ :rtype: str
1513
+ """
1514
+ return ""
1515
+
1516
+ @classmethod
1517
+ def is_date_field(cls, field_name: str) -> bool:
1518
+ """
1519
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1520
+ by all models that can be instantiated by that module.
1521
+ The purpose is to provide a flag that the field specified should be treated/formatted as
1522
+ a date field in the generated spreadsheet.
1523
+
1524
+ :param str field_name: The property name to specify whether should be
1525
+ treated as a date field
1526
+ :return: bool
1527
+ :rtype: bool
1528
+ """
1529
+ return False
1530
+
1531
+ @classmethod
1532
+ def get_export_query(cls, app: Application, parent_id: int, parent_module: str) -> list:
1533
+ """
1534
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1535
+ by all models that can be instantiated by that module.
1536
+ The purpose is to provide a graphQL query for retrieving all data to
1537
+ be edited in an Excel workbook.
1538
+
1539
+ :param Application app: RegScale Application object
1540
+ :param int parent_id: RegScale ID of parent
1541
+ :param str parent_module: Module of parent
1542
+ :return: list GraphQL response from RegScale
1543
+ :rtype: list
1544
+ """
1545
+ return []
1546
+
1547
+ @classmethod
1548
+ def use_query(cls) -> bool:
1549
+ """
1550
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1551
+ by all models that can be instantiated by that module.
1552
+ The purpose is to determine whether the model instantiated will use a graphQL query
1553
+ to produce the data for the Excel workbook export. If a query isn't used, then the
1554
+ get_all_by_parent method will be used.
1555
+
1556
+ :return: bool
1557
+ :rtype: bool
1558
+ """
1559
+ return False
1560
+
1561
+ @classmethod
1562
+ def get_extra_fields(cls) -> list:
1563
+ """
1564
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1565
+ by all models that can be instantiated by that module.
1566
+ The purpose is to provide a list of extra fields to include in the workbook.
1567
+ These are fields that are pulled in as part of the graphQL query, but are not members of
1568
+ the model definition.
1569
+
1570
+ :return: list of extra field names
1571
+ :rtype: list
1572
+ """
1573
+ return []
1574
+
1575
+ @classmethod
1576
+ def get_include_fields(cls) -> list:
1577
+ """
1578
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1579
+ by all models that can be instantiated by that module.
1580
+ The purpose of this method is to provide a list of fields to be
1581
+ included in the Excel workbook despite not being included in the graphQL query.
1582
+
1583
+ :return: list of field names
1584
+ :rtype: list
1585
+ """
1586
+ return []
1587
+
1588
+ @classmethod
1589
+ def is_required_field(cls, field_name: str) -> bool:
1590
+ """
1591
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1592
+ by all models that can be instantiated by that module.
1593
+ The purpose of this method is to provide a list of fields that are required when
1594
+ creating a new record of the class type. This is to indicate when fields are defined
1595
+ as Optional in the class definition, but are required when creating a new record.
1596
+
1597
+ :param str field_name: field name to check
1598
+ :return: bool indicating if the field is required
1599
+ :rtype: bool
1600
+ """
1601
+ return False
1602
+
1603
+ @classmethod
1604
+ def is_new_excel_record_allowed(cls) -> bool:
1605
+ """
1606
+ This method is for use with the genericized bulk loader, and is intended to be overridden
1607
+ by all models that can be instantiated by that module.
1608
+ The purpose of this method is to provide a boolean indicator of whether new records are
1609
+ allowed when editing an excel spreadsheet export of the model.
1610
+
1611
+ :return: bool indicating if the field is required
1612
+ :rtype: bool
1613
+ """
1614
+ return True
1615
+
1616
+ @classmethod
1617
+ def create_new_connecting_model(cls, instance: Any) -> Any:
1618
+ """
1619
+ This method is used to create a required supporting model for connecting the
1620
+ current object to another in the database.
1621
+
1622
+ :param Any instance: The instance to create a new connecting model for when loading new records.
1623
+ :return Any:
1624
+ :rtype Any:
1625
+ """
1626
+ return None
1627
+
1628
+ @classmethod
1629
+ def get_bool_enums(cls, field_name: str) -> list:
1630
+ """
1631
+ This method is used to provide a list of boolean values that can be used to populate a
1632
+ drop-down list in the Excel workbook.
1633
+
1634
+ :param str field_name: The field name to provide boolean values for
1635
+ :return: list of boolean values
1636
+ :rtype: list
1637
+ """
1638
+ try:
1639
+ if cls.__annotations__[field_name] in [Optional[bool], bool]:
1640
+ return ["TRUE", "FALSE"]
1641
+ except (AttributeError, KeyError):
1642
+ return []
1643
+ return []