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,873 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Module to allow user to make changes to Assessments in an Excel
|
|
4
|
+
spreadsheet for user-friendly experience"""
|
|
5
|
+
|
|
6
|
+
# standard python imports
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import pandas as pd # Type Checking
|
|
11
|
+
from regscale.core.app.api import Api
|
|
12
|
+
from regscale.core.app.application import Application
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional, Union, Any
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from openpyxl import Workbook, load_workbook
|
|
23
|
+
from openpyxl.styles import Protection, Font, NamedStyle
|
|
24
|
+
from openpyxl.worksheet.datavalidation import DataValidation
|
|
25
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
26
|
+
|
|
27
|
+
from regscale.core.app.logz import create_logger
|
|
28
|
+
from regscale.core.app.utils.app_utils import (
|
|
29
|
+
check_file_path,
|
|
30
|
+
error_and_exit,
|
|
31
|
+
reformat_str_date,
|
|
32
|
+
get_user_names,
|
|
33
|
+
get_current_datetime,
|
|
34
|
+
check_empty_nan,
|
|
35
|
+
)
|
|
36
|
+
from regscale.models.app_models.click import regscale_id, regscale_module
|
|
37
|
+
from regscale.models.regscale_models import Assessment
|
|
38
|
+
from regscale.models.regscale_models.modules import Modules
|
|
39
|
+
|
|
40
|
+
ALL_ASSESSMENTS = "all_assessments.xlsx"
|
|
41
|
+
NEW_ASSESSMENTS = "new_assessments.xlsx"
|
|
42
|
+
OLD_ASSESSMENTS = "old_assessments.xlsx"
|
|
43
|
+
DIFFERENCES_FILE = "differences.txt"
|
|
44
|
+
SELECT_PROMT = "Please select an option from the dropdown list."
|
|
45
|
+
DATE_ENTRY_PROMPT = "Please enter a valid date in the following format: mm/dd/yyyy"
|
|
46
|
+
SELECTION_ERROR = "Your entry is not one of the available options."
|
|
47
|
+
INVALID_ENTRY_ERROR = "Your entry is not a valid option."
|
|
48
|
+
INVALID_ENTRY_TITLE = "Invalid Entry"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.group(name="assessments")
|
|
52
|
+
def assessments():
|
|
53
|
+
"""
|
|
54
|
+
Performs actions on Assessments CLI Feature to create new or update assessments to RegScale.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Make Empty Spreadsheet for creating new assessments.
|
|
59
|
+
@assessments.command(name="generate_new_file")
|
|
60
|
+
@click.option(
|
|
61
|
+
"--path",
|
|
62
|
+
type=click.Path(exists=False, dir_okay=True, path_type=Path),
|
|
63
|
+
help="Provide the desired path for excel files to be generated into.",
|
|
64
|
+
default=os.path.join(os.getcwd(), "artifacts"),
|
|
65
|
+
required=True,
|
|
66
|
+
)
|
|
67
|
+
def generate_new_file(path: Path):
|
|
68
|
+
"""This function will build an Excel spreadsheet for users to be
|
|
69
|
+
able to create new assessments."""
|
|
70
|
+
new_assessment(path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def new_assessment(path: Path) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Function to build Excel spreadsheet for creation of new assessments
|
|
76
|
+
|
|
77
|
+
:param Path path: directory of file location
|
|
78
|
+
:rtype: None
|
|
79
|
+
"""
|
|
80
|
+
import pandas as pd # Optimize import performance
|
|
81
|
+
|
|
82
|
+
logger = create_logger()
|
|
83
|
+
|
|
84
|
+
check_file_path(path)
|
|
85
|
+
|
|
86
|
+
# create excel file and setting formatting
|
|
87
|
+
|
|
88
|
+
workbook = Workbook()
|
|
89
|
+
worksheet = workbook.active
|
|
90
|
+
worksheet.title = "New_Assessments"
|
|
91
|
+
|
|
92
|
+
column_headers = [
|
|
93
|
+
"Title",
|
|
94
|
+
"LeadAssessor",
|
|
95
|
+
"Facility",
|
|
96
|
+
"Organization",
|
|
97
|
+
"AssessmentType",
|
|
98
|
+
"PlannedStart",
|
|
99
|
+
"PlannedFinish",
|
|
100
|
+
"Status",
|
|
101
|
+
"ActualFinish",
|
|
102
|
+
"AssessmentResult",
|
|
103
|
+
"ParentId",
|
|
104
|
+
"ParentModule",
|
|
105
|
+
]
|
|
106
|
+
for col, val in enumerate(column_headers, start=1):
|
|
107
|
+
worksheet.cell(row=1, column=col).value = val
|
|
108
|
+
|
|
109
|
+
for col in ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"]:
|
|
110
|
+
for cell in worksheet[col]:
|
|
111
|
+
if cell.row == 1:
|
|
112
|
+
cell.font = Font(bold=True)
|
|
113
|
+
|
|
114
|
+
# create and format reference worksheets for dropdowns
|
|
115
|
+
workbook.create_sheet(title="Facilities")
|
|
116
|
+
workbook.create_sheet(title="Organizations")
|
|
117
|
+
workbook.create_sheet(title="Accounts")
|
|
118
|
+
workbook.create_sheet(title="Modules")
|
|
119
|
+
workbook.create_sheet(title="AssessmentTypes")
|
|
120
|
+
workbook.create_sheet(title="Assessment_Ids")
|
|
121
|
+
|
|
122
|
+
workbook.save(filename=path / NEW_ASSESSMENTS)
|
|
123
|
+
|
|
124
|
+
# pull in Facility, Organization, Module, and Account Usernames into Excel Spreadsheet to create drop downs
|
|
125
|
+
list_of_modules = Modules().api_names()
|
|
126
|
+
module_names = pd.DataFrame(list_of_modules, columns=["name"])
|
|
127
|
+
with pd.ExcelWriter(
|
|
128
|
+
path / NEW_ASSESSMENTS,
|
|
129
|
+
mode="a",
|
|
130
|
+
engine="openpyxl",
|
|
131
|
+
if_sheet_exists="overlay",
|
|
132
|
+
) as writer:
|
|
133
|
+
get_field_names(field_name="facilities").to_excel(
|
|
134
|
+
writer,
|
|
135
|
+
sheet_name="Facilities",
|
|
136
|
+
index=False,
|
|
137
|
+
)
|
|
138
|
+
get_field_names(field_name="organizations").to_excel(
|
|
139
|
+
writer,
|
|
140
|
+
sheet_name="Organizations",
|
|
141
|
+
index=False,
|
|
142
|
+
)
|
|
143
|
+
get_user_names().to_excel(
|
|
144
|
+
writer,
|
|
145
|
+
sheet_name="Accounts",
|
|
146
|
+
index=False,
|
|
147
|
+
)
|
|
148
|
+
module_names.to_excel(
|
|
149
|
+
writer,
|
|
150
|
+
sheet_name="Modules",
|
|
151
|
+
index=False,
|
|
152
|
+
)
|
|
153
|
+
get_assessment_types().to_excel(
|
|
154
|
+
writer,
|
|
155
|
+
sheet_name="AssessmentTypes",
|
|
156
|
+
index=False,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Creating data Validation for fields
|
|
160
|
+
workbook = load_workbook(os.path.join(path.absolute(), NEW_ASSESSMENTS))
|
|
161
|
+
worksheet = workbook.active
|
|
162
|
+
# lock worksheets containing data for dropdowns
|
|
163
|
+
for sheet in [
|
|
164
|
+
"Facilities",
|
|
165
|
+
"Accounts",
|
|
166
|
+
"Organizations",
|
|
167
|
+
"AssessmentTypes",
|
|
168
|
+
"Modules",
|
|
169
|
+
]:
|
|
170
|
+
workbook[sheet].protection.sheet = True
|
|
171
|
+
# Data structure for variable elements
|
|
172
|
+
data_validations_info = [
|
|
173
|
+
{"sheet": "Accounts", "columns": ["B"], "allow_blank": False},
|
|
174
|
+
{"sheet": "Facilities", "columns": ["C"], "allow_blank": True},
|
|
175
|
+
{"sheet": "Organizations", "columns": ["D"], "allow_blank": True},
|
|
176
|
+
{"sheet": "AssessmentTypes", "columns": ["E"], "allow_blank": False},
|
|
177
|
+
{"sheet": "Modules", "columns": ["L"], "allow_blank": True},
|
|
178
|
+
{
|
|
179
|
+
"formula1": '"Scheduled, In Progress, Complete, Cancelled"',
|
|
180
|
+
"columns": ["H"],
|
|
181
|
+
"allow_blank": True,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"formula1": '"Pass, Fail, N/A, Partial Pass"',
|
|
185
|
+
"columns": ["J"],
|
|
186
|
+
"allow_blank": True,
|
|
187
|
+
},
|
|
188
|
+
{"type": "date", "columns": ["F", "G"], "allow_blank": False},
|
|
189
|
+
{"type": "date", "columns": ["I"], "allow_blank": True},
|
|
190
|
+
]
|
|
191
|
+
# Create data validations
|
|
192
|
+
create_data_validations(
|
|
193
|
+
data_validations_info=data_validations_info,
|
|
194
|
+
workbook=workbook,
|
|
195
|
+
worksheet=worksheet,
|
|
196
|
+
)
|
|
197
|
+
workbook.save(filename=os.path.join(path.absolute(), NEW_ASSESSMENTS))
|
|
198
|
+
|
|
199
|
+
# Freezing top row and adding data style to date columns to assure validation
|
|
200
|
+
|
|
201
|
+
workbook = load_workbook(os.path.join(path.absolute(), NEW_ASSESSMENTS))
|
|
202
|
+
worksheet = workbook.active
|
|
203
|
+
freeze_range = worksheet.cell(2, 14)
|
|
204
|
+
worksheet.freeze_panes = freeze_range
|
|
205
|
+
date_style = NamedStyle(name="date_style", number_format="mm/dd/yyyy")
|
|
206
|
+
workbook.add_named_style(date_style)
|
|
207
|
+
|
|
208
|
+
for col in ["F", "G", "I"]: # Columns to edit
|
|
209
|
+
for cell in worksheet[col]:
|
|
210
|
+
if cell.row > 1:
|
|
211
|
+
cell.style = date_style
|
|
212
|
+
|
|
213
|
+
# Adjusting width of columns
|
|
214
|
+
adjust_column_widths_and_styles(worksheet)
|
|
215
|
+
|
|
216
|
+
workbook.save(filename=path / NEW_ASSESSMENTS)
|
|
217
|
+
|
|
218
|
+
logger.info(
|
|
219
|
+
"Your excel workbook has been created. Please open the new_assessments workbook and add new assessments."
|
|
220
|
+
)
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@assessments.command(name="generate")
|
|
225
|
+
@regscale_id()
|
|
226
|
+
@regscale_module()
|
|
227
|
+
@click.option(
|
|
228
|
+
"--path",
|
|
229
|
+
type=click.Path(exists=False, dir_okay=True, path_type=Path),
|
|
230
|
+
help="Provide the desired path for excel files to be generated into.",
|
|
231
|
+
default=os.path.join(os.getcwd(), "artifacts"),
|
|
232
|
+
required=True,
|
|
233
|
+
)
|
|
234
|
+
def generate(regscale_id: int, regscale_module: str, path: Path):
|
|
235
|
+
"""
|
|
236
|
+
This function will build and populate a spreadsheet of all assessments
|
|
237
|
+
with the selected RegScale Parent Id and RegScale Module for users to any necessary edits.
|
|
238
|
+
"""
|
|
239
|
+
all_assessments(parent_id=regscale_id, parent_module=regscale_module, path=path)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def all_assessments(parent_id: int, parent_module: str, path: Path) -> None:
|
|
243
|
+
"""Function takes organizer record and module and build excel worksheet of assessments
|
|
244
|
+
|
|
245
|
+
:param int parent_id: RegScale Parent Id
|
|
246
|
+
:param str parent_module: RegScale Parent Module
|
|
247
|
+
:param Path path: directory of file location
|
|
248
|
+
:rtype: None
|
|
249
|
+
"""
|
|
250
|
+
import pandas as pd # Optimize import performance
|
|
251
|
+
from regscale.core.app.application import Application
|
|
252
|
+
|
|
253
|
+
app = Application()
|
|
254
|
+
existing_assessment_data = Assessment.fetch_all_assessments_by_parent(
|
|
255
|
+
app=app,
|
|
256
|
+
parent_id=parent_id,
|
|
257
|
+
parent_module=parent_module,
|
|
258
|
+
org_and_facil=True,
|
|
259
|
+
)
|
|
260
|
+
if (
|
|
261
|
+
existing_assessment_data["assessments"]["totalCount"] > 0
|
|
262
|
+
): # Checking to see if assessment exists for selected RegScale Id and RegScale Module.
|
|
263
|
+
check_file_path(path)
|
|
264
|
+
sheet_names = ["Facilities", "Organizations", "Accounts", "AssessmentTypes"]
|
|
265
|
+
|
|
266
|
+
all_assessments_wb = path / ALL_ASSESSMENTS
|
|
267
|
+
old_assessments_wb = path / OLD_ASSESSMENTS
|
|
268
|
+
|
|
269
|
+
# Loading data from db into two workbooks.
|
|
270
|
+
workbook = Workbook()
|
|
271
|
+
worksheet = workbook.active
|
|
272
|
+
worksheet.title = f"Assessments({parent_id}_{parent_module})"
|
|
273
|
+
for worksheet in sheet_names:
|
|
274
|
+
workbook.create_sheet(title=worksheet)
|
|
275
|
+
workbook.save(filename=path / ALL_ASSESSMENTS)
|
|
276
|
+
shutil.copy(
|
|
277
|
+
all_assessments_wb,
|
|
278
|
+
old_assessments_wb,
|
|
279
|
+
)
|
|
280
|
+
assessments_data = [
|
|
281
|
+
[
|
|
282
|
+
a["id"],
|
|
283
|
+
a["title"],
|
|
284
|
+
f"{a['leadAssessor']['lastName'].strip()}, {a['leadAssessor']['firstName'].strip()} "
|
|
285
|
+
+ f"({a['leadAssessor']['userName'].strip()})",
|
|
286
|
+
a["facility"]["name"] if a["facility"] else None,
|
|
287
|
+
a["org"]["name"] if a["org"] else None,
|
|
288
|
+
a["assessmentType"],
|
|
289
|
+
reformat_str_date(a["plannedStart"]),
|
|
290
|
+
reformat_str_date(a["plannedFinish"]),
|
|
291
|
+
a["status"],
|
|
292
|
+
reformat_str_date(a["actualFinish"]) if a["actualFinish"] else None,
|
|
293
|
+
a["assessmentResult"] or None,
|
|
294
|
+
a["parentId"],
|
|
295
|
+
a["parentModule"],
|
|
296
|
+
]
|
|
297
|
+
for a in existing_assessment_data["assessments"]["items"]
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
all_ass_df = pd.DataFrame(
|
|
301
|
+
assessments_data,
|
|
302
|
+
columns=[
|
|
303
|
+
"Id",
|
|
304
|
+
"Title",
|
|
305
|
+
"LeadAssessor",
|
|
306
|
+
"Facility",
|
|
307
|
+
"Organization",
|
|
308
|
+
"AssessmentType",
|
|
309
|
+
"PlannedStart",
|
|
310
|
+
"PlannedFinish",
|
|
311
|
+
"Status",
|
|
312
|
+
"ActualFinish",
|
|
313
|
+
"AssessmentResult",
|
|
314
|
+
"ParentId",
|
|
315
|
+
"ParentModule",
|
|
316
|
+
],
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
with pd.ExcelWriter(
|
|
320
|
+
all_assessments_wb,
|
|
321
|
+
mode="a",
|
|
322
|
+
engine="openpyxl",
|
|
323
|
+
if_sheet_exists="overlay",
|
|
324
|
+
) as writer:
|
|
325
|
+
all_ass_df.to_excel(
|
|
326
|
+
writer,
|
|
327
|
+
sheet_name=f"Assessments({parent_id}_{parent_module})",
|
|
328
|
+
index=False,
|
|
329
|
+
)
|
|
330
|
+
with pd.ExcelWriter(
|
|
331
|
+
old_assessments_wb,
|
|
332
|
+
mode="a",
|
|
333
|
+
engine="openpyxl",
|
|
334
|
+
if_sheet_exists="overlay",
|
|
335
|
+
) as writer:
|
|
336
|
+
all_ass_df.to_excel(
|
|
337
|
+
writer,
|
|
338
|
+
sheet_name=f"Assessments({parent_id}_{parent_module})",
|
|
339
|
+
index=False,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Pulling in Facility Names into Excel Spreadsheet to create dropdown.
|
|
343
|
+
with pd.ExcelWriter(
|
|
344
|
+
all_assessments_wb,
|
|
345
|
+
mode="a",
|
|
346
|
+
engine="openpyxl",
|
|
347
|
+
if_sheet_exists="overlay",
|
|
348
|
+
) as writer:
|
|
349
|
+
get_field_names(field_name="facilities").to_excel(
|
|
350
|
+
writer,
|
|
351
|
+
sheet_name="Facilities",
|
|
352
|
+
index=False,
|
|
353
|
+
)
|
|
354
|
+
get_field_names(field_name="organizations").to_excel(
|
|
355
|
+
writer,
|
|
356
|
+
sheet_name="Organizations",
|
|
357
|
+
index=False,
|
|
358
|
+
)
|
|
359
|
+
get_user_names().to_excel(
|
|
360
|
+
writer,
|
|
361
|
+
sheet_name="Accounts",
|
|
362
|
+
index=False,
|
|
363
|
+
)
|
|
364
|
+
get_assessment_types().to_excel(
|
|
365
|
+
writer,
|
|
366
|
+
sheet_name="AssessmentTypes",
|
|
367
|
+
index=False,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Adding protection to OLD_ASSESSMENTS_WB file that will be used as reference.
|
|
371
|
+
workbook2 = load_workbook(old_assessments_wb)
|
|
372
|
+
worksheet2 = workbook2.active
|
|
373
|
+
worksheet2.protection.sheet = True
|
|
374
|
+
workbook2.save(filename=old_assessments_wb)
|
|
375
|
+
|
|
376
|
+
# Adding Data Validation to ALL_ASSESSMENTS_WB file to be adjusted internally.
|
|
377
|
+
workbook = load_workbook(all_assessments_wb)
|
|
378
|
+
worksheet = workbook.active
|
|
379
|
+
# lock worksheets containing data for dropdowns
|
|
380
|
+
for sheet in sheet_names:
|
|
381
|
+
workbook[sheet].protection.sheet = True
|
|
382
|
+
|
|
383
|
+
data_validations_info = [
|
|
384
|
+
{"sheet": "Accounts", "columns": ["C"], "allow_blank": False},
|
|
385
|
+
{"sheet": "Facilities", "columns": ["D"], "allow_blank": True},
|
|
386
|
+
{"sheet": "Organizations", "columns": ["E"], "allow_blank": True},
|
|
387
|
+
{"sheet": "AssessmentTypes", "columns": ["F"], "allow_blank": False},
|
|
388
|
+
{
|
|
389
|
+
"formula1": '"Scheduled, In Progress, Complete, Cancelled"',
|
|
390
|
+
"columns": ["I"],
|
|
391
|
+
"allow_blank": True,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"formula1": '"Pass, Fail, N/A, Partial Pass"',
|
|
395
|
+
"columns": ["K"],
|
|
396
|
+
"allow_blank": True,
|
|
397
|
+
},
|
|
398
|
+
{"type": "date", "columns": ["G", "H"], "allow_blank": False},
|
|
399
|
+
{"type": "date", "columns": ["J"], "allow_blank": True},
|
|
400
|
+
]
|
|
401
|
+
create_data_validations(
|
|
402
|
+
data_validations_info=data_validations_info,
|
|
403
|
+
workbook=workbook,
|
|
404
|
+
worksheet=worksheet,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Worksheet freeze top row
|
|
408
|
+
freeze_range = worksheet.cell(2, 17)
|
|
409
|
+
worksheet.freeze_panes = freeze_range
|
|
410
|
+
date_style = NamedStyle(name="date_style", number_format="mm/dd/yyyy")
|
|
411
|
+
workbook.add_named_style(date_style)
|
|
412
|
+
|
|
413
|
+
# Adding Date Style to Worksheet, formatting cells, and unlocking
|
|
414
|
+
# cells that can be edited in each assessment
|
|
415
|
+
adjust_column_widths_and_styles(
|
|
416
|
+
worksheet=worksheet,
|
|
417
|
+
editable_columns=["C", "D", "E", "F", "G", "H", "I", "J", "K"],
|
|
418
|
+
date_columns=["G", "H", "J"],
|
|
419
|
+
date_col_style=date_style,
|
|
420
|
+
)
|
|
421
|
+
workbook.save(filename=all_assessments_wb)
|
|
422
|
+
|
|
423
|
+
return app.logger.info(
|
|
424
|
+
"Your data has been loaded into your excel workbook. Please open the all_assessments workbook "
|
|
425
|
+
"and make your desired changes."
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
app.logger.info("Please check your selections for RegScale Id and RegScale Module and try again.")
|
|
429
|
+
error_and_exit(
|
|
430
|
+
"There was an error creating your workbook. No assessments exist for the given RegScale Id "
|
|
431
|
+
"and RegScale Module."
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@assessments.command(name="load")
|
|
436
|
+
@click.option(
|
|
437
|
+
"--path",
|
|
438
|
+
type=click.Path(exists=False, dir_okay=True, path_type=Path),
|
|
439
|
+
help="Provide the desired path of excel workbook locations.",
|
|
440
|
+
default=os.path.join(os.getcwd(), "artifacts"),
|
|
441
|
+
required=True,
|
|
442
|
+
)
|
|
443
|
+
def load(path: Path) -> None:
|
|
444
|
+
"""
|
|
445
|
+
This function uploads updated assessments and new assessments to
|
|
446
|
+
RegScale from the Excel files that users have edited.
|
|
447
|
+
"""
|
|
448
|
+
upload_data(path=path)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def upload_data(path: Path) -> None:
|
|
452
|
+
"""
|
|
453
|
+
Function will upload assessments to RegScale if user as made edits to any
|
|
454
|
+
of the assessment excel workbooks
|
|
455
|
+
|
|
456
|
+
:param Path path: directory of file location
|
|
457
|
+
:rtype: None
|
|
458
|
+
"""
|
|
459
|
+
import pandas as pd # Optimize import performance
|
|
460
|
+
import numpy as np # Optimize import performance
|
|
461
|
+
from regscale.core.app.application import Application
|
|
462
|
+
|
|
463
|
+
app = Application()
|
|
464
|
+
new_assessments_wb = path / NEW_ASSESSMENTS
|
|
465
|
+
all_assessments_wb = path / ALL_ASSESSMENTS
|
|
466
|
+
old_assessments_wb = path / OLD_ASSESSMENTS
|
|
467
|
+
|
|
468
|
+
# Checking if new assessments have been created and updating RegScale database.
|
|
469
|
+
if os.path.isfile(new_assessments_wb):
|
|
470
|
+
new_files = new_assessments_wb
|
|
471
|
+
new = map_workbook_to_dict(new_files)
|
|
472
|
+
new_assessments = [
|
|
473
|
+
Assessment(
|
|
474
|
+
leadAssessorId=value["LeadAssessorId"] or app.config["userId"],
|
|
475
|
+
title=value["Title"],
|
|
476
|
+
assessmentType=value["AssessmentType"],
|
|
477
|
+
plannedStart=map_pandas_timestamp(value["PlannedStart"]),
|
|
478
|
+
plannedFinish=map_pandas_timestamp(value["PlannedFinish"]),
|
|
479
|
+
status=value["Status"],
|
|
480
|
+
parentModule=check_empty_nan(value["ParentModule"]),
|
|
481
|
+
facilityId=check_empty_nan(value.get("FacilityId")),
|
|
482
|
+
orgId=check_empty_nan(value.get("OrganizationId")),
|
|
483
|
+
assessmentResult=check_assessment_result(value["AssessmentResult"]),
|
|
484
|
+
actualFinish=map_pandas_timestamp(value["ActualFinish"]),
|
|
485
|
+
parentId=check_empty_nan(value["ParentId"]),
|
|
486
|
+
lastUpdatedById=app.config["userId"],
|
|
487
|
+
dateLastUpdated=get_current_datetime(),
|
|
488
|
+
).create()
|
|
489
|
+
for value in new.values()
|
|
490
|
+
]
|
|
491
|
+
post_and_save_assessments(
|
|
492
|
+
app=app,
|
|
493
|
+
new_assessments=new_assessments,
|
|
494
|
+
workbook_path=path,
|
|
495
|
+
)
|
|
496
|
+
else:
|
|
497
|
+
app.logger.info("No new assessments detected. Checking for edited assessments")
|
|
498
|
+
|
|
499
|
+
if os.path.isfile(all_assessments_wb):
|
|
500
|
+
# Checking all_assessments file for differences before updating database
|
|
501
|
+
|
|
502
|
+
df1 = pd.read_excel(old_assessments_wb, sheet_name=0, index_col="Id")
|
|
503
|
+
|
|
504
|
+
df2 = pd.read_excel(all_assessments_wb, sheet_name=0, index_col="Id")
|
|
505
|
+
|
|
506
|
+
if df1.equals(df2):
|
|
507
|
+
error_and_exit("No differences detected.")
|
|
508
|
+
|
|
509
|
+
else:
|
|
510
|
+
app.logger.warning("Differences found!")
|
|
511
|
+
|
|
512
|
+
diff_mask = (df1 != df2) & ~(df1.isnull() & df2.isnull())
|
|
513
|
+
ne_stacked = diff_mask.stack()
|
|
514
|
+
changed = ne_stacked[ne_stacked]
|
|
515
|
+
changed.index.names = ["Id", "Column"]
|
|
516
|
+
difference_locations = np.where(diff_mask)
|
|
517
|
+
changed_from = df1.values[difference_locations]
|
|
518
|
+
changed_to = df2.values[difference_locations]
|
|
519
|
+
changes = pd.DataFrame({"From": changed_from, "To": changed_to}, index=changed.index)
|
|
520
|
+
changes.to_csv(
|
|
521
|
+
path / DIFFERENCES_FILE,
|
|
522
|
+
header=True,
|
|
523
|
+
index=True,
|
|
524
|
+
sep=" ",
|
|
525
|
+
mode="w+",
|
|
526
|
+
)
|
|
527
|
+
app.logger.info(
|
|
528
|
+
"Please check differences.txt file located in %s to see changes made.",
|
|
529
|
+
path,
|
|
530
|
+
)
|
|
531
|
+
# Loading in differences.txt file and using Id to parse xlsx file for rows to update
|
|
532
|
+
|
|
533
|
+
diff = pd.read_csv(path / DIFFERENCES_FILE, header=0, sep=" ", index_col=None)
|
|
534
|
+
ids = []
|
|
535
|
+
|
|
536
|
+
for _, row in diff.iterrows():
|
|
537
|
+
ids.append(row["Id"])
|
|
538
|
+
|
|
539
|
+
id_df = pd.DataFrame(ids, index=None, columns=["Id"])
|
|
540
|
+
id_df2 = id_df.drop_duplicates()
|
|
541
|
+
updated_files = all_assessments_wb
|
|
542
|
+
df3 = pd.read_excel(updated_files, sheet_name=0, index_col=None)
|
|
543
|
+
updated = df3[df3["Id"].isin(id_df2["Id"])]
|
|
544
|
+
updated = map_workbook_to_dict(updated_files, updated)
|
|
545
|
+
_ = [
|
|
546
|
+
Assessment(
|
|
547
|
+
leadAssessorId=value["LeadAssessorId"],
|
|
548
|
+
id=value["Id"],
|
|
549
|
+
title=value["Title"],
|
|
550
|
+
assessmentType=value["AssessmentType"],
|
|
551
|
+
plannedStart=value["PlannedStart"],
|
|
552
|
+
plannedFinish=value["PlannedFinish"],
|
|
553
|
+
status=value["Status"],
|
|
554
|
+
parentModule=value["ParentModule"],
|
|
555
|
+
facilityId=check_empty_nan(value.get("FacilityId")),
|
|
556
|
+
orgId=check_empty_nan(value.get("OrganizationId")),
|
|
557
|
+
assessmentResult=check_assessment_result(value["AssessmentResult"]),
|
|
558
|
+
actualFinish=check_empty_nan(value["ActualFinish"]),
|
|
559
|
+
parentId=value["ParentId"],
|
|
560
|
+
lastUpdatedById=app.config["userId"],
|
|
561
|
+
dateLastUpdated=get_current_datetime(),
|
|
562
|
+
).save(bulk=True)
|
|
563
|
+
for value in updated.values()
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
Assessment.bulk_save()
|
|
567
|
+
|
|
568
|
+
else:
|
|
569
|
+
app.logger.info("No Assessments exist to load to RegScale.")
|
|
570
|
+
return app.logger.info(
|
|
571
|
+
"Assessment files have been uploaded. Changes made to existing files can be seen in "
|
|
572
|
+
"differences.txt file. Thank you!"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@assessments.command(name="delete_files")
|
|
577
|
+
@click.option(
|
|
578
|
+
"--path",
|
|
579
|
+
type=click.Path(exists=False, dir_okay=True, path_type=Path),
|
|
580
|
+
help="Provide the desired path of file location.",
|
|
581
|
+
default=Path("./artifacts"),
|
|
582
|
+
required=True,
|
|
583
|
+
)
|
|
584
|
+
def delete_files(path: Path):
|
|
585
|
+
"""This command will delete files used during the Assessment editing process."""
|
|
586
|
+
delete_file(path)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def delete_file(path: Path) -> int:
|
|
590
|
+
"""
|
|
591
|
+
Deletes files used during the process
|
|
592
|
+
|
|
593
|
+
:param Path path: directory of file location
|
|
594
|
+
:return: Number of files deleted
|
|
595
|
+
:rtype: int
|
|
596
|
+
"""
|
|
597
|
+
logger = create_logger()
|
|
598
|
+
file_names = [
|
|
599
|
+
NEW_ASSESSMENTS,
|
|
600
|
+
ALL_ASSESSMENTS,
|
|
601
|
+
OLD_ASSESSMENTS,
|
|
602
|
+
DIFFERENCES_FILE,
|
|
603
|
+
]
|
|
604
|
+
deleted_files = []
|
|
605
|
+
|
|
606
|
+
for file_name in file_names:
|
|
607
|
+
if os.path.isfile(path / file_name):
|
|
608
|
+
os.remove(path / file_name)
|
|
609
|
+
deleted_files.append(file_name)
|
|
610
|
+
else:
|
|
611
|
+
logger.warning("No %s file found. Checking for other files before exiting.", file_name)
|
|
612
|
+
logger.info("%i file(s) have been deleted: %s", len(deleted_files), ", ".join(deleted_files))
|
|
613
|
+
return len(deleted_files)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def get_maximum_rows(*, sheet_object: Any) -> int:
|
|
617
|
+
"""
|
|
618
|
+
This function finds the last row containing data in a spreadsheet
|
|
619
|
+
|
|
620
|
+
:param Any sheet_object: excel worksheet to be referenced
|
|
621
|
+
:return: integer representing last row with data in spreadsheet
|
|
622
|
+
:rtype: int
|
|
623
|
+
"""
|
|
624
|
+
return sum(any(col.value is not None for col in row) for max_row, row in enumerate(sheet_object, 1))
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def get_field_names(field_name: str) -> "pd.DataFrame":
|
|
628
|
+
"""
|
|
629
|
+
This function uses GraphQL to retrieve all names of a given parent table in database
|
|
630
|
+
|
|
631
|
+
:param str field_name: name of parent table to retrieve names from
|
|
632
|
+
:return: pandas dataframe with facility names
|
|
633
|
+
:rtype: pd.DataFrame
|
|
634
|
+
"""
|
|
635
|
+
import pandas as pd # Optimize import performance
|
|
636
|
+
from regscale.core.app.api import Api
|
|
637
|
+
|
|
638
|
+
api = Api()
|
|
639
|
+
|
|
640
|
+
body = """
|
|
641
|
+
query {
|
|
642
|
+
field_name(skip: 0, take: 50, order: {name: ASC}, ) {
|
|
643
|
+
items {
|
|
644
|
+
name
|
|
645
|
+
id
|
|
646
|
+
}
|
|
647
|
+
totalCount
|
|
648
|
+
pageInfo {
|
|
649
|
+
hasNextPage
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
""".replace(
|
|
654
|
+
"field_name", field_name
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
field_items = api.graph(query=body)
|
|
658
|
+
names = field_items[str(field_name)]["items"]
|
|
659
|
+
field_names = [[i["name"], i["id"]] for i in names]
|
|
660
|
+
all_names = pd.DataFrame(field_names, index=None, columns=["name", "id"])
|
|
661
|
+
|
|
662
|
+
return all_names
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def get_assessment_types() -> "pd.DataFrame":
|
|
666
|
+
"""
|
|
667
|
+
This function uses GraphQL to retrieve all assessment types in database
|
|
668
|
+
|
|
669
|
+
:return: pandas dataframe with assessment types
|
|
670
|
+
:rtype: pd.DataFrame
|
|
671
|
+
"""
|
|
672
|
+
import pandas as pd # Optimize import performance
|
|
673
|
+
from regscale.core.app.api import Api
|
|
674
|
+
|
|
675
|
+
api = Api()
|
|
676
|
+
|
|
677
|
+
body = """
|
|
678
|
+
query{
|
|
679
|
+
assessments (skip: 0, take: 50, order: {assessmentType: ASC}, ) {
|
|
680
|
+
items {
|
|
681
|
+
assessmentType
|
|
682
|
+
}
|
|
683
|
+
totalCount
|
|
684
|
+
pageInfo {
|
|
685
|
+
hasNextPage
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} """
|
|
689
|
+
|
|
690
|
+
assessments_raw = api.graph(query=body)
|
|
691
|
+
assessment_types = assessments_raw["assessments"]["items"]
|
|
692
|
+
field_names = [i["assessmentType"] for i in assessment_types]
|
|
693
|
+
all_assessment_types = pd.DataFrame(field_names, index=None, columns=["assessmentType"])
|
|
694
|
+
return all_assessment_types.drop_duplicates()
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def check_assessment_result(value: Any) -> Union[str, float]:
|
|
698
|
+
"""
|
|
699
|
+
This function takes a given value for an assessment and
|
|
700
|
+
checks if value is empty or NaN based on value type.
|
|
701
|
+
|
|
702
|
+
:param Any value: A string or float object
|
|
703
|
+
:return: A string value, float value. or ""
|
|
704
|
+
:rtype: Union[str, float]
|
|
705
|
+
"""
|
|
706
|
+
# this function has to be checked separate to account for API
|
|
707
|
+
# only accepting empty string unlike other class params
|
|
708
|
+
if isinstance(value, str) and value.strip() == "":
|
|
709
|
+
return ""
|
|
710
|
+
if isinstance(value, float) and math.isnan(value):
|
|
711
|
+
return ""
|
|
712
|
+
return value
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def adjust_column_widths_and_styles(
|
|
716
|
+
worksheet: Worksheet,
|
|
717
|
+
editable_columns: Optional[list[str]] = None,
|
|
718
|
+
date_columns: Optional[list[str]] = None,
|
|
719
|
+
date_col_style: Optional[NamedStyle] = None,
|
|
720
|
+
) -> None:
|
|
721
|
+
"""
|
|
722
|
+
Function to adjust column widths based on length of data in column, and apply
|
|
723
|
+
styles to specific columns and rows
|
|
724
|
+
|
|
725
|
+
:param Worksheet worksheet: Worksheet to adjust column widths for
|
|
726
|
+
:param Optional[list[str]] editable_columns: List of rows to unlock for editing
|
|
727
|
+
:param Optional[list[str]] date_columns: List of columns to add date style to
|
|
728
|
+
:param Optional[NamedStyle] date_col_style: NamedStyle object to apply to date columns, defaults to None
|
|
729
|
+
:rtype: None
|
|
730
|
+
"""
|
|
731
|
+
editable_columns = editable_columns or []
|
|
732
|
+
date_columns = date_columns or []
|
|
733
|
+
for col in worksheet.columns:
|
|
734
|
+
max_length = 0
|
|
735
|
+
column_letter = col[0].column_letter
|
|
736
|
+
|
|
737
|
+
for cell in col:
|
|
738
|
+
# Determine max length for column width
|
|
739
|
+
cell_length = len(str(cell.value))
|
|
740
|
+
max_length = max(max_length, cell_length)
|
|
741
|
+
|
|
742
|
+
# Set cell protection for specific columns
|
|
743
|
+
if column_letter in editable_columns and cell.row > 1:
|
|
744
|
+
cell.protection = Protection(locked=False)
|
|
745
|
+
|
|
746
|
+
# Apply date style for specific columns and rows
|
|
747
|
+
if column_letter in date_columns and cell.row > 1 and date_col_style:
|
|
748
|
+
cell.style = date_col_style
|
|
749
|
+
|
|
750
|
+
# Set adjusted column width
|
|
751
|
+
adjusted_width = (max_length + 2) * 1.2
|
|
752
|
+
worksheet.column_dimensions[column_letter].width = adjusted_width
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def create_data_validations(data_validations_info: list[dict], workbook: Workbook, worksheet: Worksheet) -> None:
|
|
756
|
+
"""
|
|
757
|
+
Function to create data validations for excel worksheet
|
|
758
|
+
|
|
759
|
+
:param list[dict] data_validations_info: List containing dictionaries with
|
|
760
|
+
information for data validations
|
|
761
|
+
:param Workbook workbook: Workbook object to add data validations to
|
|
762
|
+
:param Worksheet worksheet: The worksheet object to add data validations to
|
|
763
|
+
:rtype: None
|
|
764
|
+
"""
|
|
765
|
+
for _, dv_info in enumerate(data_validations_info, start=1):
|
|
766
|
+
formula1 = dv_info.get("formula1")
|
|
767
|
+
if sheet_name := dv_info.get("sheet"):
|
|
768
|
+
formula1 = f"={sheet_name}!$A$2:$A${str(get_maximum_rows(sheet_object=workbook[sheet_name]))}"
|
|
769
|
+
|
|
770
|
+
data_validation = DataValidation(
|
|
771
|
+
type=dv_info.get("type", "list"),
|
|
772
|
+
formula1=formula1,
|
|
773
|
+
allow_blank=dv_info.get("allow_blank", True),
|
|
774
|
+
showDropDown=False,
|
|
775
|
+
error=(SELECTION_ERROR if dv_info.get("type", "list") == "list" else INVALID_ENTRY_ERROR),
|
|
776
|
+
errorTitle=INVALID_ENTRY_TITLE,
|
|
777
|
+
prompt=(SELECT_PROMT if dv_info.get("type", "list") == "list" else DATE_ENTRY_PROMPT),
|
|
778
|
+
showErrorMessage=True if dv_info.get("type", "date") else None,
|
|
779
|
+
showInputMessage=True if dv_info.get("type", "date") else None,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
worksheet.add_data_validation(data_validation)
|
|
783
|
+
for column in dv_info["columns"]:
|
|
784
|
+
data_validation.add(f"{column}2:{column}1048576")
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def post_and_save_assessments(app: "Application", new_assessments: list[Assessment], workbook_path: Path) -> None:
|
|
788
|
+
"""
|
|
789
|
+
Function to post new assessments to RegScale and save assessment ids to excel workbook
|
|
790
|
+
|
|
791
|
+
:param Application app: RegScale CLI Application object
|
|
792
|
+
:param list[Assessment] new_assessments: List of new assessments to post to RegScale
|
|
793
|
+
:param Path workbook_path: Path to workbook to save assessment ids to
|
|
794
|
+
:rtype: None
|
|
795
|
+
"""
|
|
796
|
+
import pandas as pd # Optimize import performance
|
|
797
|
+
|
|
798
|
+
new_assessments_df = pd.DataFrame([assessment.id for assessment in new_assessments], columns=["id_number"])
|
|
799
|
+
for file_name in [NEW_ASSESSMENTS, ALL_ASSESSMENTS]:
|
|
800
|
+
with pd.ExcelWriter(
|
|
801
|
+
workbook_path / file_name,
|
|
802
|
+
mode="a",
|
|
803
|
+
engine="openpyxl",
|
|
804
|
+
if_sheet_exists="overlay",
|
|
805
|
+
) as writer:
|
|
806
|
+
new_assessments_df.to_excel(
|
|
807
|
+
writer,
|
|
808
|
+
sheet_name="Assessment_Ids",
|
|
809
|
+
index=False,
|
|
810
|
+
)
|
|
811
|
+
app.logger.info(
|
|
812
|
+
"%i total assessment(s) were added to RegScale.",
|
|
813
|
+
len(new_assessments),
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def map_pandas_timestamp(date_time: "pd.Timestamp") -> Optional[str]:
|
|
818
|
+
"""
|
|
819
|
+
Function to map pandas timestamp to string
|
|
820
|
+
|
|
821
|
+
:param pd.Timestamp date_time: Timestamp to map to string
|
|
822
|
+
:return: String representation of pandas timestamp
|
|
823
|
+
:rtype: Optional[str]
|
|
824
|
+
"""
|
|
825
|
+
import pandas as pd # Optimize import performance
|
|
826
|
+
|
|
827
|
+
if isinstance(date_time, float):
|
|
828
|
+
return None
|
|
829
|
+
elif date_time is not None and not pd.isna(date_time) and not isinstance(date_time, str):
|
|
830
|
+
return date_time.strftime("%Y-%m-%d %H:%M:%S")
|
|
831
|
+
else:
|
|
832
|
+
return date_time or None
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def map_workbook_to_dict(file_path: str, workbook_data: Optional["pd.DataFrame"] = None) -> dict:
|
|
836
|
+
"""
|
|
837
|
+
Function to map workbook to dictionary
|
|
838
|
+
|
|
839
|
+
:param str file_path: Path to workbook file
|
|
840
|
+
:param Optional[pd.DataFrame] workbook_data: Dataframe to map to dictionary
|
|
841
|
+
:return: Dictionary representation of workbook
|
|
842
|
+
:rtype: dict
|
|
843
|
+
"""
|
|
844
|
+
import pandas as pd # Optimize import performance
|
|
845
|
+
|
|
846
|
+
if workbook_data is not None:
|
|
847
|
+
wb_data = workbook_data
|
|
848
|
+
else:
|
|
849
|
+
wb_data = pd.read_excel(file_path)
|
|
850
|
+
wb_data["Facility"] = wb_data["Facility"].astype(str).fillna("None") # Handle missing facilities
|
|
851
|
+
wb_data["Organization"] = wb_data["Organization"].astype(str).fillna("None") # Handle missing organizations
|
|
852
|
+
|
|
853
|
+
# Reading and preparing the 'Facilities' sheet
|
|
854
|
+
facilities = pd.read_excel(file_path, sheet_name="Facilities")
|
|
855
|
+
facilities = facilities.rename(columns={"name": "Facility", "id": "FacilityId"})
|
|
856
|
+
facilities["Facility"] = facilities["Facility"].astype(str) # Ensure consistent data type
|
|
857
|
+
|
|
858
|
+
# Reading and preparing the 'Organizations' sheet
|
|
859
|
+
organizations = pd.read_excel(file_path, sheet_name="Organizations")
|
|
860
|
+
organizations = organizations.rename(columns={"name": "Organization", "id": "OrganizationId"})
|
|
861
|
+
organizations["Organization"] = (
|
|
862
|
+
organizations["Organization"].astype(str).fillna("None")
|
|
863
|
+
) # Handle missing organizations
|
|
864
|
+
|
|
865
|
+
# Reading and preparing the 'Accounts' sheet
|
|
866
|
+
accounts = pd.read_excel(file_path, sheet_name="Accounts")
|
|
867
|
+
accounts = accounts.rename(columns={"User": "LeadAssessor", "UserId": "LeadAssessorId"})
|
|
868
|
+
|
|
869
|
+
# Merging dataframes
|
|
870
|
+
wb_data = wb_data.merge(accounts, how="left", on="LeadAssessor", validate="many_to_many")
|
|
871
|
+
wb_data = wb_data.merge(facilities, how="left", on="Facility", validate="many_to_many")
|
|
872
|
+
wb_data = wb_data.merge(organizations, how="left", on="Organization", validate="many_to_many")
|
|
873
|
+
return wb_data.T.to_dict()
|