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.
- regscale/__init__.py +1 -0
- regscale/airflow/__init__.py +9 -0
- regscale/airflow/azure/__init__.py +9 -0
- regscale/airflow/azure/cli.py +89 -0
- regscale/airflow/azure/upload_dags.py +116 -0
- regscale/airflow/click_dags.py +127 -0
- regscale/airflow/click_mixins.py +82 -0
- regscale/airflow/config.py +25 -0
- regscale/airflow/factories/__init__.py +0 -0
- regscale/airflow/factories/connections.py +58 -0
- regscale/airflow/factories/workflows.py +78 -0
- regscale/airflow/hierarchy.py +88 -0
- regscale/airflow/operators/__init__.py +0 -0
- regscale/airflow/operators/click.py +36 -0
- regscale/airflow/sensors/__init__.py +0 -0
- regscale/airflow/sensors/sql.py +107 -0
- regscale/airflow/sessions/__init__.py +0 -0
- regscale/airflow/sessions/sql/__init__.py +3 -0
- regscale/airflow/sessions/sql/queries.py +64 -0
- regscale/airflow/sessions/sql/sql_server_queries.py +248 -0
- regscale/airflow/tasks/__init__.py +0 -0
- regscale/airflow/tasks/branches.py +22 -0
- regscale/airflow/tasks/cli.py +116 -0
- regscale/airflow/tasks/click.py +73 -0
- regscale/airflow/tasks/debugging.py +9 -0
- regscale/airflow/tasks/groups.py +116 -0
- regscale/airflow/tasks/init.py +60 -0
- regscale/airflow/tasks/states.py +47 -0
- regscale/airflow/tasks/workflows.py +36 -0
- regscale/ansible/__init__.py +9 -0
- regscale/core/__init__.py +0 -0
- regscale/core/app/__init__.py +3 -0
- regscale/core/app/api.py +571 -0
- regscale/core/app/application.py +665 -0
- regscale/core/app/internal/__init__.py +136 -0
- regscale/core/app/internal/admin_actions.py +230 -0
- regscale/core/app/internal/assessments_editor.py +873 -0
- regscale/core/app/internal/catalog.py +316 -0
- regscale/core/app/internal/comparison.py +459 -0
- regscale/core/app/internal/control_editor.py +571 -0
- regscale/core/app/internal/encrypt.py +79 -0
- regscale/core/app/internal/evidence.py +1240 -0
- regscale/core/app/internal/file_uploads.py +151 -0
- regscale/core/app/internal/healthcheck.py +66 -0
- regscale/core/app/internal/login.py +305 -0
- regscale/core/app/internal/migrations.py +240 -0
- regscale/core/app/internal/model_editor.py +1701 -0
- regscale/core/app/internal/poam_editor.py +632 -0
- regscale/core/app/internal/workflow.py +105 -0
- regscale/core/app/logz.py +74 -0
- regscale/core/app/utils/XMLIR.py +258 -0
- regscale/core/app/utils/__init__.py +0 -0
- regscale/core/app/utils/api_handler.py +358 -0
- regscale/core/app/utils/app_utils.py +1110 -0
- regscale/core/app/utils/catalog_utils/__init__.py +0 -0
- regscale/core/app/utils/catalog_utils/common.py +91 -0
- regscale/core/app/utils/catalog_utils/compare_catalog.py +193 -0
- regscale/core/app/utils/catalog_utils/diagnostic_catalog.py +97 -0
- regscale/core/app/utils/catalog_utils/download_catalog.py +103 -0
- regscale/core/app/utils/catalog_utils/update_catalog.py +718 -0
- regscale/core/app/utils/catalog_utils/update_catalog_v2.py +1378 -0
- regscale/core/app/utils/catalog_utils/update_catalog_v3.py +1272 -0
- regscale/core/app/utils/catalog_utils/update_plans.py +334 -0
- regscale/core/app/utils/file_utils.py +238 -0
- regscale/core/app/utils/parser_utils.py +81 -0
- regscale/core/app/utils/pickle_file_handler.py +57 -0
- regscale/core/app/utils/regscale_utils.py +319 -0
- regscale/core/app/utils/report_utils.py +119 -0
- regscale/core/app/utils/variables.py +226 -0
- regscale/core/decorators.py +31 -0
- regscale/core/lazy_group.py +65 -0
- regscale/core/login.py +63 -0
- regscale/core/server/__init__.py +0 -0
- regscale/core/server/flask_api.py +473 -0
- regscale/core/server/helpers.py +373 -0
- regscale/core/server/rest.py +64 -0
- regscale/core/server/static/css/bootstrap.css +6030 -0
- regscale/core/server/static/css/bootstrap.min.css +6 -0
- regscale/core/server/static/css/main.css +176 -0
- regscale/core/server/static/images/regscale-cli.svg +49 -0
- regscale/core/server/static/images/regscale.svg +38 -0
- regscale/core/server/templates/base.html +74 -0
- regscale/core/server/templates/index.html +43 -0
- regscale/core/server/templates/login.html +28 -0
- regscale/core/server/templates/make_base64.html +22 -0
- regscale/core/server/templates/upload_STIG.html +109 -0
- regscale/core/server/templates/upload_STIG_result.html +26 -0
- regscale/core/server/templates/upload_ssp.html +144 -0
- regscale/core/server/templates/upload_ssp_result.html +128 -0
- regscale/core/static/__init__.py +0 -0
- regscale/core/static/regex.py +14 -0
- regscale/core/utils/__init__.py +117 -0
- regscale/core/utils/click_utils.py +13 -0
- regscale/core/utils/date.py +238 -0
- regscale/core/utils/graphql.py +254 -0
- regscale/core/utils/urls.py +23 -0
- regscale/dev/__init__.py +6 -0
- regscale/dev/analysis.py +454 -0
- regscale/dev/cli.py +235 -0
- regscale/dev/code_gen.py +492 -0
- regscale/dev/dirs.py +69 -0
- regscale/dev/docs.py +384 -0
- regscale/dev/monitoring.py +26 -0
- regscale/dev/profiling.py +216 -0
- regscale/exceptions/__init__.py +4 -0
- regscale/exceptions/license_exception.py +7 -0
- regscale/exceptions/validation_exception.py +9 -0
- regscale/integrations/__init__.py +1 -0
- regscale/integrations/commercial/__init__.py +486 -0
- regscale/integrations/commercial/ad.py +433 -0
- regscale/integrations/commercial/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/common.py +106 -0
- regscale/integrations/commercial/aqua/__init__.py +0 -0
- regscale/integrations/commercial/aqua/aqua.py +91 -0
- regscale/integrations/commercial/aws/__init__.py +6 -0
- regscale/integrations/commercial/aws/cli.py +322 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +110 -0
- regscale/integrations/commercial/aws/inventory/base.py +64 -0
- regscale/integrations/commercial/aws/inventory/resources/__init__.py +19 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +234 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +113 -0
- regscale/integrations/commercial/aws/inventory/resources/database.py +101 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +237 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +253 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +240 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +91 -0
- regscale/integrations/commercial/aws/scanner.py +823 -0
- regscale/integrations/commercial/azure/__init__.py +0 -0
- regscale/integrations/commercial/azure/common.py +32 -0
- regscale/integrations/commercial/azure/intune.py +488 -0
- regscale/integrations/commercial/azure/scanner.py +49 -0
- regscale/integrations/commercial/burp.py +78 -0
- regscale/integrations/commercial/cpe.py +144 -0
- regscale/integrations/commercial/crowdstrike.py +1117 -0
- regscale/integrations/commercial/defender.py +1511 -0
- regscale/integrations/commercial/dependabot.py +210 -0
- regscale/integrations/commercial/durosuite/__init__.py +0 -0
- regscale/integrations/commercial/durosuite/api.py +1546 -0
- regscale/integrations/commercial/durosuite/process_devices.py +101 -0
- regscale/integrations/commercial/durosuite/scanner.py +637 -0
- regscale/integrations/commercial/durosuite/variables.py +21 -0
- regscale/integrations/commercial/ecr.py +90 -0
- regscale/integrations/commercial/gcp/__init__.py +237 -0
- regscale/integrations/commercial/gcp/auth.py +96 -0
- regscale/integrations/commercial/gcp/control_tests.py +238 -0
- regscale/integrations/commercial/gcp/variables.py +18 -0
- regscale/integrations/commercial/gitlab.py +332 -0
- regscale/integrations/commercial/grype.py +165 -0
- regscale/integrations/commercial/ibm.py +90 -0
- regscale/integrations/commercial/import_all/__init__.py +0 -0
- regscale/integrations/commercial/import_all/import_all_cmd.py +467 -0
- regscale/integrations/commercial/import_all/scan_file_fingerprints.json +27 -0
- regscale/integrations/commercial/jira.py +1046 -0
- regscale/integrations/commercial/mappings/__init__.py +0 -0
- regscale/integrations/commercial/mappings/csf_controls.json +713 -0
- regscale/integrations/commercial/mappings/nist_800_53_r5_controls.json +1516 -0
- regscale/integrations/commercial/nessus/__init__.py +0 -0
- regscale/integrations/commercial/nessus/nessus_utils.py +429 -0
- regscale/integrations/commercial/nessus/scanner.py +416 -0
- regscale/integrations/commercial/nexpose.py +90 -0
- regscale/integrations/commercial/okta.py +798 -0
- regscale/integrations/commercial/opentext/__init__.py +0 -0
- regscale/integrations/commercial/opentext/click.py +99 -0
- regscale/integrations/commercial/opentext/scanner.py +143 -0
- regscale/integrations/commercial/prisma.py +91 -0
- regscale/integrations/commercial/qualys.py +1462 -0
- regscale/integrations/commercial/salesforce.py +980 -0
- regscale/integrations/commercial/sap/__init__.py +0 -0
- regscale/integrations/commercial/sap/click.py +31 -0
- regscale/integrations/commercial/sap/sysdig/__init__.py +0 -0
- regscale/integrations/commercial/sap/sysdig/click.py +57 -0
- regscale/integrations/commercial/sap/sysdig/sysdig_scanner.py +190 -0
- regscale/integrations/commercial/sap/tenable/__init__.py +0 -0
- regscale/integrations/commercial/sap/tenable/click.py +49 -0
- regscale/integrations/commercial/sap/tenable/scanner.py +196 -0
- regscale/integrations/commercial/servicenow.py +1756 -0
- regscale/integrations/commercial/sicura/__init__.py +0 -0
- regscale/integrations/commercial/sicura/api.py +855 -0
- regscale/integrations/commercial/sicura/commands.py +73 -0
- regscale/integrations/commercial/sicura/scanner.py +481 -0
- regscale/integrations/commercial/sicura/variables.py +16 -0
- regscale/integrations/commercial/snyk.py +90 -0
- regscale/integrations/commercial/sonarcloud.py +260 -0
- regscale/integrations/commercial/sqlserver.py +369 -0
- regscale/integrations/commercial/stig_mapper_integration/__init__.py +0 -0
- regscale/integrations/commercial/stig_mapper_integration/click_commands.py +38 -0
- regscale/integrations/commercial/stig_mapper_integration/mapping_engine.py +353 -0
- regscale/integrations/commercial/stigv2/__init__.py +0 -0
- regscale/integrations/commercial/stigv2/ckl_parser.py +349 -0
- regscale/integrations/commercial/stigv2/click_commands.py +95 -0
- regscale/integrations/commercial/stigv2/stig_integration.py +202 -0
- regscale/integrations/commercial/synqly/__init__.py +0 -0
- regscale/integrations/commercial/synqly/assets.py +46 -0
- regscale/integrations/commercial/synqly/ticketing.py +132 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +223 -0
- regscale/integrations/commercial/synqly_jira.py +840 -0
- regscale/integrations/commercial/tenablev2/__init__.py +0 -0
- regscale/integrations/commercial/tenablev2/authenticate.py +31 -0
- regscale/integrations/commercial/tenablev2/click.py +1584 -0
- regscale/integrations/commercial/tenablev2/scanner.py +504 -0
- regscale/integrations/commercial/tenablev2/stig_parsers.py +140 -0
- regscale/integrations/commercial/tenablev2/utils.py +78 -0
- regscale/integrations/commercial/tenablev2/variables.py +17 -0
- regscale/integrations/commercial/trivy.py +162 -0
- regscale/integrations/commercial/veracode.py +96 -0
- regscale/integrations/commercial/wizv2/WizDataMixin.py +97 -0
- regscale/integrations/commercial/wizv2/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/click.py +429 -0
- regscale/integrations/commercial/wizv2/constants.py +1001 -0
- regscale/integrations/commercial/wizv2/issue.py +361 -0
- regscale/integrations/commercial/wizv2/models.py +112 -0
- regscale/integrations/commercial/wizv2/parsers.py +339 -0
- regscale/integrations/commercial/wizv2/sbom.py +115 -0
- regscale/integrations/commercial/wizv2/scanner.py +416 -0
- regscale/integrations/commercial/wizv2/utils.py +796 -0
- regscale/integrations/commercial/wizv2/variables.py +39 -0
- regscale/integrations/commercial/wizv2/wiz_auth.py +159 -0
- regscale/integrations/commercial/xray.py +91 -0
- regscale/integrations/integration/__init__.py +2 -0
- regscale/integrations/integration/integration.py +26 -0
- regscale/integrations/integration/inventory.py +17 -0
- regscale/integrations/integration/issue.py +100 -0
- regscale/integrations/integration_override.py +149 -0
- regscale/integrations/public/__init__.py +103 -0
- regscale/integrations/public/cisa.py +641 -0
- regscale/integrations/public/criticality_updater.py +70 -0
- regscale/integrations/public/emass.py +411 -0
- regscale/integrations/public/emass_slcm_import.py +697 -0
- regscale/integrations/public/fedramp/__init__.py +0 -0
- regscale/integrations/public/fedramp/appendix_parser.py +548 -0
- regscale/integrations/public/fedramp/click.py +479 -0
- regscale/integrations/public/fedramp/components.py +714 -0
- regscale/integrations/public/fedramp/docx_parser.py +259 -0
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +1124 -0
- regscale/integrations/public/fedramp/fedramp_common.py +3181 -0
- regscale/integrations/public/fedramp/fedramp_docx.py +388 -0
- regscale/integrations/public/fedramp/fedramp_five.py +2343 -0
- regscale/integrations/public/fedramp/fedramp_traversal.py +138 -0
- regscale/integrations/public/fedramp/import_fedramp_r4_ssp.py +279 -0
- regscale/integrations/public/fedramp/import_workbook.py +495 -0
- regscale/integrations/public/fedramp/inventory_items.py +244 -0
- regscale/integrations/public/fedramp/mappings/__init__.py +0 -0
- regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +7388 -0
- regscale/integrations/public/fedramp/mappings/fedramp_r5_params.json +8636 -0
- regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +9605 -0
- regscale/integrations/public/fedramp/mappings/system_roles.py +34 -0
- regscale/integrations/public/fedramp/mappings/user.py +175 -0
- regscale/integrations/public/fedramp/mappings/values.py +141 -0
- regscale/integrations/public/fedramp/markdown_parser.py +150 -0
- regscale/integrations/public/fedramp/metadata.py +689 -0
- regscale/integrations/public/fedramp/models/__init__.py +59 -0
- regscale/integrations/public/fedramp/models/leveraged_auth_new.py +168 -0
- regscale/integrations/public/fedramp/models/poam_importer.py +522 -0
- regscale/integrations/public/fedramp/parts_mapper.py +107 -0
- regscale/integrations/public/fedramp/poam/__init__.py +0 -0
- regscale/integrations/public/fedramp/poam/scanner.py +851 -0
- regscale/integrations/public/fedramp/properties.py +201 -0
- regscale/integrations/public/fedramp/reporting.py +84 -0
- regscale/integrations/public/fedramp/resources.py +496 -0
- regscale/integrations/public/fedramp/rosetta.py +110 -0
- regscale/integrations/public/fedramp/ssp_logger.py +87 -0
- regscale/integrations/public/fedramp/system_characteristics.py +922 -0
- regscale/integrations/public/fedramp/system_control_implementations.py +582 -0
- regscale/integrations/public/fedramp/system_implementation.py +190 -0
- regscale/integrations/public/fedramp/xml_utils.py +87 -0
- regscale/integrations/public/nist_catalog.py +275 -0
- regscale/integrations/public/oscal.py +1946 -0
- regscale/integrations/public/otx.py +169 -0
- regscale/integrations/scanner_integration.py +2692 -0
- regscale/integrations/variables.py +25 -0
- regscale/models/__init__.py +7 -0
- regscale/models/app_models/__init__.py +5 -0
- regscale/models/app_models/catalog_compare.py +213 -0
- regscale/models/app_models/click.py +252 -0
- regscale/models/app_models/datetime_encoder.py +21 -0
- regscale/models/app_models/import_validater.py +321 -0
- regscale/models/app_models/mapping.py +260 -0
- regscale/models/app_models/pipeline.py +37 -0
- regscale/models/click_models.py +413 -0
- regscale/models/config.py +154 -0
- regscale/models/email_style.css +67 -0
- regscale/models/hierarchy.py +8 -0
- regscale/models/inspect_models.py +79 -0
- regscale/models/integration_models/__init__.py +0 -0
- regscale/models/integration_models/amazon_models/__init__.py +0 -0
- regscale/models/integration_models/amazon_models/inspector.py +262 -0
- regscale/models/integration_models/amazon_models/inspector_scan.py +206 -0
- regscale/models/integration_models/aqua.py +247 -0
- regscale/models/integration_models/azure_alerts.py +255 -0
- regscale/models/integration_models/base64.py +23 -0
- regscale/models/integration_models/burp.py +433 -0
- regscale/models/integration_models/burp_models.py +128 -0
- regscale/models/integration_models/cisa_kev_data.json +19333 -0
- regscale/models/integration_models/defender_data.py +93 -0
- regscale/models/integration_models/defenderimport.py +143 -0
- regscale/models/integration_models/drf.py +443 -0
- regscale/models/integration_models/ecr_models/__init__.py +0 -0
- regscale/models/integration_models/ecr_models/data.py +69 -0
- regscale/models/integration_models/ecr_models/ecr.py +239 -0
- regscale/models/integration_models/flat_file_importer.py +1079 -0
- regscale/models/integration_models/grype_import.py +247 -0
- regscale/models/integration_models/ibm.py +126 -0
- regscale/models/integration_models/implementation_results.py +85 -0
- regscale/models/integration_models/nexpose.py +140 -0
- regscale/models/integration_models/prisma.py +202 -0
- regscale/models/integration_models/qualys.py +720 -0
- regscale/models/integration_models/qualys_scanner.py +160 -0
- regscale/models/integration_models/sbom/__init__.py +0 -0
- regscale/models/integration_models/sbom/cyclone_dx.py +139 -0
- regscale/models/integration_models/send_reminders.py +620 -0
- regscale/models/integration_models/snyk.py +155 -0
- regscale/models/integration_models/synqly_models/__init__.py +0 -0
- regscale/models/integration_models/synqly_models/capabilities.json +1 -0
- regscale/models/integration_models/synqly_models/connector_types.py +22 -0
- regscale/models/integration_models/synqly_models/connectors/__init__.py +7 -0
- regscale/models/integration_models/synqly_models/connectors/assets.py +97 -0
- regscale/models/integration_models/synqly_models/connectors/ticketing.py +583 -0
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +169 -0
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +331 -0
- regscale/models/integration_models/synqly_models/param.py +72 -0
- regscale/models/integration_models/synqly_models/synqly_model.py +733 -0
- regscale/models/integration_models/synqly_models/tenants.py +39 -0
- regscale/models/integration_models/tenable_models/__init__.py +0 -0
- regscale/models/integration_models/tenable_models/integration.py +187 -0
- regscale/models/integration_models/tenable_models/models.py +513 -0
- regscale/models/integration_models/trivy_import.py +231 -0
- regscale/models/integration_models/veracode.py +217 -0
- regscale/models/integration_models/xray.py +135 -0
- regscale/models/locking.py +100 -0
- regscale/models/platform.py +110 -0
- regscale/models/regscale_models/__init__.py +67 -0
- regscale/models/regscale_models/assessment.py +570 -0
- regscale/models/regscale_models/assessment_plan.py +52 -0
- regscale/models/regscale_models/asset.py +567 -0
- regscale/models/regscale_models/asset_mapping.py +190 -0
- regscale/models/regscale_models/case.py +42 -0
- regscale/models/regscale_models/catalog.py +261 -0
- regscale/models/regscale_models/cci.py +46 -0
- regscale/models/regscale_models/change.py +167 -0
- regscale/models/regscale_models/checklist.py +372 -0
- regscale/models/regscale_models/comment.py +49 -0
- regscale/models/regscale_models/compliance_settings.py +112 -0
- regscale/models/regscale_models/component.py +412 -0
- regscale/models/regscale_models/component_mapping.py +65 -0
- regscale/models/regscale_models/control.py +38 -0
- regscale/models/regscale_models/control_implementation.py +1128 -0
- regscale/models/regscale_models/control_objective.py +261 -0
- regscale/models/regscale_models/control_parameter.py +100 -0
- regscale/models/regscale_models/control_test.py +34 -0
- regscale/models/regscale_models/control_test_plan.py +75 -0
- regscale/models/regscale_models/control_test_result.py +52 -0
- regscale/models/regscale_models/custom_field.py +245 -0
- regscale/models/regscale_models/data.py +109 -0
- regscale/models/regscale_models/data_center.py +40 -0
- regscale/models/regscale_models/deviation.py +203 -0
- regscale/models/regscale_models/email.py +97 -0
- regscale/models/regscale_models/evidence.py +47 -0
- regscale/models/regscale_models/evidence_mapping.py +40 -0
- regscale/models/regscale_models/facility.py +59 -0
- regscale/models/regscale_models/file.py +382 -0
- regscale/models/regscale_models/filetag.py +37 -0
- regscale/models/regscale_models/form_field_value.py +94 -0
- regscale/models/regscale_models/group.py +169 -0
- regscale/models/regscale_models/implementation_objective.py +335 -0
- regscale/models/regscale_models/implementation_option.py +275 -0
- regscale/models/regscale_models/implementation_role.py +33 -0
- regscale/models/regscale_models/incident.py +177 -0
- regscale/models/regscale_models/interconnection.py +43 -0
- regscale/models/regscale_models/issue.py +1176 -0
- regscale/models/regscale_models/leveraged_authorization.py +125 -0
- regscale/models/regscale_models/line_of_inquiry.py +52 -0
- regscale/models/regscale_models/link.py +205 -0
- regscale/models/regscale_models/meta_data.py +64 -0
- regscale/models/regscale_models/mixins/__init__.py +0 -0
- regscale/models/regscale_models/mixins/parent_cache.py +124 -0
- regscale/models/regscale_models/module.py +224 -0
- regscale/models/regscale_models/modules.py +191 -0
- regscale/models/regscale_models/objective.py +14 -0
- regscale/models/regscale_models/parameter.py +87 -0
- regscale/models/regscale_models/ports_protocol.py +81 -0
- regscale/models/regscale_models/privacy.py +89 -0
- regscale/models/regscale_models/profile.py +50 -0
- regscale/models/regscale_models/profile_link.py +68 -0
- regscale/models/regscale_models/profile_mapping.py +124 -0
- regscale/models/regscale_models/project.py +63 -0
- regscale/models/regscale_models/property.py +278 -0
- regscale/models/regscale_models/question.py +85 -0
- regscale/models/regscale_models/questionnaire.py +87 -0
- regscale/models/regscale_models/questionnaire_instance.py +177 -0
- regscale/models/regscale_models/rbac.py +132 -0
- regscale/models/regscale_models/reference.py +86 -0
- regscale/models/regscale_models/regscale_model.py +1643 -0
- regscale/models/regscale_models/requirement.py +29 -0
- regscale/models/regscale_models/risk.py +274 -0
- regscale/models/regscale_models/sbom.py +54 -0
- regscale/models/regscale_models/scan_history.py +436 -0
- regscale/models/regscale_models/search.py +53 -0
- regscale/models/regscale_models/security_control.py +132 -0
- regscale/models/regscale_models/security_plan.py +204 -0
- regscale/models/regscale_models/software_inventory.py +159 -0
- regscale/models/regscale_models/stake_holder.py +64 -0
- regscale/models/regscale_models/stig.py +647 -0
- regscale/models/regscale_models/supply_chain.py +152 -0
- regscale/models/regscale_models/system_role.py +188 -0
- regscale/models/regscale_models/system_role_external_assignment.py +40 -0
- regscale/models/regscale_models/tag.py +37 -0
- regscale/models/regscale_models/tag_mapping.py +19 -0
- regscale/models/regscale_models/task.py +133 -0
- regscale/models/regscale_models/threat.py +196 -0
- regscale/models/regscale_models/user.py +175 -0
- regscale/models/regscale_models/user_group.py +55 -0
- regscale/models/regscale_models/vulnerability.py +242 -0
- regscale/models/regscale_models/vulnerability_mapping.py +162 -0
- regscale/models/regscale_models/workflow.py +55 -0
- regscale/models/regscale_models/workflow_action.py +34 -0
- regscale/models/regscale_models/workflow_instance.py +269 -0
- regscale/models/regscale_models/workflow_instance_step.py +114 -0
- regscale/models/regscale_models/workflow_template.py +58 -0
- regscale/models/regscale_models/workflow_template_step.py +45 -0
- regscale/regscale.py +815 -0
- regscale/utils/__init__.py +7 -0
- regscale/utils/b64conversion.py +14 -0
- regscale/utils/click_utils.py +118 -0
- regscale/utils/decorators.py +48 -0
- regscale/utils/dict_utils.py +59 -0
- regscale/utils/files.py +79 -0
- regscale/utils/fxns.py +30 -0
- regscale/utils/graphql_client.py +113 -0
- regscale/utils/lists.py +16 -0
- regscale/utils/numbers.py +12 -0
- regscale/utils/shell.py +148 -0
- regscale/utils/string.py +121 -0
- regscale/utils/synqly_utils.py +165 -0
- regscale/utils/threading/__init__.py +8 -0
- regscale/utils/threading/threadhandler.py +131 -0
- regscale/utils/threading/threadsafe_counter.py +47 -0
- regscale/utils/threading/threadsafe_dict.py +242 -0
- regscale/utils/threading/threadsafe_list.py +83 -0
- regscale/utils/version.py +104 -0
- regscale/validation/__init__.py +0 -0
- regscale/validation/address.py +37 -0
- regscale/validation/record.py +48 -0
- regscale/visualization/__init__.py +5 -0
- regscale/visualization/click.py +34 -0
- regscale_cli-6.16.0.0.dist-info/LICENSE +21 -0
- regscale_cli-6.16.0.0.dist-info/METADATA +659 -0
- regscale_cli-6.16.0.0.dist-info/RECORD +481 -0
- regscale_cli-6.16.0.0.dist-info/WHEEL +5 -0
- regscale_cli-6.16.0.0.dist-info/entry_points.txt +6 -0
- regscale_cli-6.16.0.0.dist-info/top_level.txt +2 -0
- tests/fixtures/__init__.py +2 -0
- tests/fixtures/api.py +87 -0
- tests/fixtures/models.py +91 -0
- tests/fixtures/test_fixture.py +144 -0
- tests/mocks/__init__.py +0 -0
- tests/mocks/objects.py +3 -0
- tests/mocks/response.py +32 -0
- tests/mocks/xml.py +13 -0
- tests/regscale/__init__.py +0 -0
- tests/regscale/core/__init__.py +0 -0
- tests/regscale/core/test_api.py +232 -0
- tests/regscale/core/test_app.py +406 -0
- tests/regscale/core/test_login.py +37 -0
- tests/regscale/core/test_logz.py +66 -0
- tests/regscale/core/test_sbom_generator.py +87 -0
- tests/regscale/core/test_validation_utils.py +163 -0
- tests/regscale/core/test_version.py +78 -0
- tests/regscale/models/__init__.py +0 -0
- tests/regscale/models/test_asset.py +71 -0
- tests/regscale/models/test_config.py +26 -0
- tests/regscale/models/test_control_implementation.py +27 -0
- tests/regscale/models/test_import.py +97 -0
- tests/regscale/models/test_issue.py +36 -0
- tests/regscale/models/test_mapping.py +52 -0
- tests/regscale/models/test_platform.py +31 -0
- tests/regscale/models/test_regscale_model.py +346 -0
- tests/regscale/models/test_report.py +32 -0
- tests/regscale/models/test_tenable_integrations.py +118 -0
- tests/regscale/models/test_user_model.py +121 -0
- tests/regscale/test_about.py +19 -0
- 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 []
|