regscale-cli 6.21.2.0__py3-none-any.whl → 6.28.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/api.py +5 -2
- regscale/core/app/application.py +36 -6
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/evidence.py +727 -204
- regscale/core/app/internal/login.py +4 -2
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +86 -12
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/core/login.py +21 -4
- regscale/core/utils/async_graphql_client.py +363 -0
- regscale/core/utils/date.py +77 -1
- regscale/dev/cli.py +26 -0
- regscale/dev/code_gen.py +109 -24
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +30 -2
- regscale/integrations/commercial/aws/audit_manager_compliance.py +3908 -0
- regscale/integrations/commercial/aws/cli.py +3107 -54
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/{amazon → aws}/common.py +71 -19
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/control_compliance_analyzer.py +439 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +338 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/analytics.py +390 -0
- regscale/integrations/commercial/aws/inventory/resources/applications.py +234 -0
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +328 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +481 -31
- regscale/integrations/commercial/aws/inventory/resources/developer_tools.py +253 -0
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/machine_learning.py +358 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +390 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +288 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +1072 -205
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/jira.py +489 -153
- regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
- regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
- regscale/integrations/commercial/qualys/__init__.py +167 -68
- regscale/integrations/commercial/qualys/scanner.py +305 -39
- regscale/integrations/commercial/sarif/sairf_importer.py +432 -0
- regscale/integrations/commercial/sarif/sarif_converter.py +67 -0
- regscale/integrations/commercial/sicura/api.py +79 -42
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +83 -44
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +133 -16
- regscale/integrations/commercial/synqly/edr.py +2 -8
- regscale/integrations/commercial/synqly/query_builder.py +536 -0
- regscale/integrations/commercial/synqly/ticketing.py +27 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +165 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +146 -5
- regscale/integrations/commercial/tenablev2/scanner.py +1 -3
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +191 -76
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +1592 -0
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +7 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +92 -89
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +66 -9
- regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
- regscale/integrations/commercial/wizv2/issue.py +776 -28
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +243 -0
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +1031 -441
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +1036 -151
- regscale/integrations/control_matcher.py +432 -0
- regscale/integrations/due_date_handler.py +333 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +14 -0
- regscale/integrations/public/cci_importer.py +834 -0
- regscale/integrations/public/csam/__init__.py +0 -0
- regscale/integrations/public/csam/csam.py +938 -0
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +77 -6
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +675 -289
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/poam/scanner.py +75 -7
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +1961 -430
- regscale/models/integration_models/CCI_List.xml +1 -0
- regscale/models/integration_models/aqua.py +2 -2
- regscale/models/integration_models/cisa_kev_data.json +805 -11
- regscale/models/integration_models/flat_file_importer/__init__.py +5 -8
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +87 -18
- regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +124 -25
- regscale/models/integration_models/synqly_models/synqly_model.py +89 -16
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +4 -2
- regscale/models/regscale_models/__init__.py +7 -0
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/catalog.py +1 -1
- regscale/models/regscale_models/compliance_settings.py +251 -1
- regscale/models/regscale_models/component.py +1 -0
- regscale/models/regscale_models/control_implementation.py +236 -41
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/form_field_value.py +5 -3
- regscale/models/regscale_models/inheritance.py +44 -0
- regscale/models/regscale_models/issue.py +301 -102
- regscale/models/regscale_models/milestone.py +33 -14
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +310 -73
- regscale/models/regscale_models/security_plan.py +4 -2
- regscale/models/regscale_models/vulnerability.py +3 -3
- regscale/regscale.py +25 -4
- regscale/templates/__init__.py +0 -0
- regscale/utils/threading/threadhandler.py +20 -15
- regscale/validation/record.py +23 -1
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/METADATA +17 -33
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/RECORD +310 -111
- tests/core/__init__.py +0 -0
- tests/core/utils/__init__.py +0 -0
- tests/core/utils/test_async_graphql_client.py +472 -0
- tests/fixtures/test_fixture.py +13 -8
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_analytics_collector.py +260 -0
- tests/regscale/integrations/commercial/aws/test_aws_applications_collector.py +242 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_developer_tools_collector.py +203 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_machine_learning_collector.py +237 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_control_compliance_analyzer.py +375 -0
- tests/regscale/integrations/commercial/aws/test_datetime_parsing.py +223 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3742 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +2204 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +349 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1365 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1218 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
- tests/regscale/integrations/public/__init__.py +0 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_gen_asset_list.py +150 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/public/test_alienvault.py +220 -0
- tests/regscale/integrations/public/test_cci.py +1053 -0
- tests/regscale/integrations/public/test_cisa.py +1021 -0
- tests/regscale/integrations/public/test_emass.py +518 -0
- tests/regscale/integrations/public/test_fedramp.py +1152 -0
- tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
- tests/regscale/integrations/public/test_file_uploads.py +506 -0
- tests/regscale/integrations/public/test_oscal.py +453 -0
- tests/regscale/integrations/test_compliance_status_mapping.py +406 -0
- tests/regscale/integrations/test_control_matcher.py +1421 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- tests/regscale/models/test_form_field_value_integration.py +304 -0
- tests/regscale/models/test_issue.py +378 -1
- tests/regscale/models/test_module_integration.py +582 -0
- tests/regscale/models/test_tenable_integrations.py +811 -105
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3057
- regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
- regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
- regscale/integrations/public/fedramp/parts_mapper.py +0 -107
- /regscale/integrations/commercial/{amazon → sarif}/__init__.py +0 -0
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/top_level.txt +0 -0
regscale/core/login.py
CHANGED
|
@@ -6,6 +6,7 @@ from os import getenv
|
|
|
6
6
|
from typing import Optional, Tuple
|
|
7
7
|
from urllib.parse import urljoin
|
|
8
8
|
|
|
9
|
+
from requests.exceptions import HTTPError
|
|
9
10
|
from regscale.core.app.api import Api
|
|
10
11
|
from regscale.core.app.utils.app_utils import error_and_exit
|
|
11
12
|
|
|
@@ -14,10 +15,11 @@ logger = logging.getLogger("regscale")
|
|
|
14
15
|
|
|
15
16
|
def get_regscale_token(
|
|
16
17
|
api: Api,
|
|
17
|
-
username: str = getenv("REGSCALE_USER"),
|
|
18
|
-
password: str = getenv("REGSCALE_PASSWORD"),
|
|
19
|
-
domain: str = getenv("REGSCALE_DOMAIN"),
|
|
18
|
+
username: Optional[str] = getenv("REGSCALE_USER"),
|
|
19
|
+
password: Optional[str] = getenv("REGSCALE_PASSWORD"),
|
|
20
|
+
domain: Optional[str] = getenv("REGSCALE_DOMAIN"),
|
|
20
21
|
mfa_token: Optional[str] = "",
|
|
22
|
+
app_id: Optional[int] = 1,
|
|
21
23
|
) -> Tuple[str, str]:
|
|
22
24
|
"""
|
|
23
25
|
Authenticate with RegScale and return a token
|
|
@@ -27,6 +29,7 @@ def get_regscale_token(
|
|
|
27
29
|
:param str password: a string defaulting to the envar REGSCALE_PASSWORD
|
|
28
30
|
:param str domain: a string representing the RegScale domain, checks environment REGSCALE_DOMAIN
|
|
29
31
|
:param Optional[str] mfa_token: MFA token to login with
|
|
32
|
+
:param Optional[int] app_id: The app ID to login with
|
|
30
33
|
:raises EnvironmentError: if domain is not passed or retrieved
|
|
31
34
|
:return: a tuple of user_id and auth_token
|
|
32
35
|
:rtype: Tuple[str, str]
|
|
@@ -47,7 +50,19 @@ def get_regscale_token(
|
|
|
47
50
|
logger.info("Logging into: %s", domain)
|
|
48
51
|
# suggest structuring the login paths so that they all exist in one place
|
|
49
52
|
url = urljoin(domain, "/api/authentication/login")
|
|
50
|
-
|
|
53
|
+
try:
|
|
54
|
+
# Try to authenticate with the new API version
|
|
55
|
+
auth["appId"] = app_id
|
|
56
|
+
response = api.post(url=url, json=auth, headers={"X-Api-Version": "2.0"})
|
|
57
|
+
if response is None:
|
|
58
|
+
raise HTTPError("No response received from api.post(). Possible connection issue or internal error.")
|
|
59
|
+
response.raise_for_status()
|
|
60
|
+
app_id_compatible = True
|
|
61
|
+
except HTTPError:
|
|
62
|
+
# Fallback to the old API version
|
|
63
|
+
del auth["appId"]
|
|
64
|
+
response = api.post(url=url, json=auth, headers={})
|
|
65
|
+
app_id_compatible = False
|
|
51
66
|
error_msg = "Unable to authenticate with RegScale. Please check your credentials."
|
|
52
67
|
if response is None:
|
|
53
68
|
logger.error("No response received from api.post(). Possible connection issue or internal error.")
|
|
@@ -63,4 +78,6 @@ def get_regscale_token(
|
|
|
63
78
|
error_and_exit(f"{error_msg}\n{response.status_code}: {response.text}")
|
|
64
79
|
if isinstance(response_dict, str):
|
|
65
80
|
response_dict = json.loads(response_dict)
|
|
81
|
+
if app_id_compatible:
|
|
82
|
+
return response_dict["accessToken"]["id"], response_dict["accessToken"]["authToken"]
|
|
66
83
|
return response_dict["id"], response_dict["auth_token"]
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Generic async GraphQL client for concurrent query processing in RegScale CLI."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import anyio
|
|
10
|
+
from gql import Client, gql
|
|
11
|
+
from gql.transport.aiohttp import AIOHTTPTransport
|
|
12
|
+
from gql.transport.aiohttp import log as aiohttp_logger
|
|
13
|
+
|
|
14
|
+
from regscale.core.app.application import Application
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("regscale")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GraphQLQueryError(Exception):
|
|
20
|
+
"""Exception raised when a GraphQL query fails."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GraphQLAuthenticationError(GraphQLQueryError):
|
|
26
|
+
"""Exception raised when GraphQL authentication fails."""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncRegScaleGraphQLClient:
|
|
32
|
+
"""
|
|
33
|
+
Generic async GraphQL client optimized for concurrent RegScale API queries.
|
|
34
|
+
|
|
35
|
+
This client can execute multiple GraphQL queries concurrently, significantly
|
|
36
|
+
improving performance when fetching paginated data from RegScale.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
endpoint: str,
|
|
42
|
+
headers: Optional[Dict[str, str]] = None,
|
|
43
|
+
timeout: float = 30.0,
|
|
44
|
+
max_concurrent: int = 5,
|
|
45
|
+
token_refresh_callback: Optional[Callable[[], str]] = None,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the async GraphQL client.
|
|
49
|
+
|
|
50
|
+
:param str endpoint: GraphQL endpoint URL
|
|
51
|
+
:param Optional[Dict[str, str]] headers: HTTP headers for requests
|
|
52
|
+
:param float timeout: Request timeout in seconds
|
|
53
|
+
:param int max_concurrent: Maximum concurrent requests
|
|
54
|
+
:param Optional[Callable[[], str]] token_refresh_callback: Callback to refresh auth token
|
|
55
|
+
"""
|
|
56
|
+
self.app = Application()
|
|
57
|
+
self.ssl_verify = self.app.config.get("sslVerify", True)
|
|
58
|
+
self.endpoint = endpoint
|
|
59
|
+
self.headers = headers or {"Authorization": self.app.config.get("token")}
|
|
60
|
+
self.timeout = timeout
|
|
61
|
+
self.max_concurrent = max_concurrent
|
|
62
|
+
self.token_refresh_callback = token_refresh_callback
|
|
63
|
+
self._semaphore = anyio.Semaphore(max_concurrent)
|
|
64
|
+
|
|
65
|
+
# Set logging level for aiohttp transport
|
|
66
|
+
aiohttp_logger.setLevel(logging.CRITICAL)
|
|
67
|
+
|
|
68
|
+
def _create_client(self) -> Client:
|
|
69
|
+
"""
|
|
70
|
+
Create a new GQL client with transport.
|
|
71
|
+
|
|
72
|
+
Each concurrent request needs its own transport and client to avoid
|
|
73
|
+
"Transport is already connected" errors.
|
|
74
|
+
|
|
75
|
+
:return: New GQL Client instance
|
|
76
|
+
:rtype: Client
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
# Create the transport with authentication headers
|
|
80
|
+
# Note: AIOHTTPTransport uses ssl parameter, not verify_ssl
|
|
81
|
+
import ssl as ssl_module
|
|
82
|
+
|
|
83
|
+
ssl_context = None
|
|
84
|
+
if not self.ssl_verify:
|
|
85
|
+
# SECURITY WARNING: SSL verification is intentionally disabled
|
|
86
|
+
# This is required for environments with self-signed certificates or corporate proxies
|
|
87
|
+
# where SSL verification cannot be performed. This should only be used when:
|
|
88
|
+
# 1. Working with internal/trusted networks with self-signed certificates
|
|
89
|
+
# 2. Behind corporate proxies that intercept SSL/TLS traffic
|
|
90
|
+
# 3. Development/testing environments
|
|
91
|
+
# DO NOT use in production with untrusted networks
|
|
92
|
+
logger.warning(
|
|
93
|
+
"SSL certificate verification is disabled. "
|
|
94
|
+
"This is insecure and should only be used in controlled environments "
|
|
95
|
+
"with self-signed certificates or corporate proxies."
|
|
96
|
+
)
|
|
97
|
+
# Use default context but disable verification for compatibility
|
|
98
|
+
# The default context still uses secure TLS 1.2+ protocols
|
|
99
|
+
ssl_context = ssl_module.create_default_context() # NOSONAR - Uses TLS 1.2+ by default in Python 3.9+
|
|
100
|
+
ssl_context.check_hostname = False # NOSONAR - Intentionally disabled when sslVerify=false is configured
|
|
101
|
+
ssl_context.verify_mode = ssl_module.CERT_NONE # NOSONAR - Required for self-signed certs/corporate proxies
|
|
102
|
+
|
|
103
|
+
transport = AIOHTTPTransport(
|
|
104
|
+
url=self.endpoint,
|
|
105
|
+
headers=self.headers,
|
|
106
|
+
timeout=int(self.timeout),
|
|
107
|
+
ssl=ssl_context if not self.ssl_verify else True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Create and return a new GQL client
|
|
111
|
+
return Client(
|
|
112
|
+
transport=transport,
|
|
113
|
+
fetch_schema_from_transport=False, # Skip schema introspection for performance
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _is_auth_error(self, error_msg: str) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Check if an error message indicates an authentication issue.
|
|
119
|
+
|
|
120
|
+
:param str error_msg: Error message to check
|
|
121
|
+
:return: True if the error is authentication-related
|
|
122
|
+
:rtype: bool
|
|
123
|
+
"""
|
|
124
|
+
auth_indicators = ["AUTH_NOT_AUTHENTICATED", "UNAUTHENTICATED", "401", "403"]
|
|
125
|
+
return any(indicator in error_msg for indicator in auth_indicators)
|
|
126
|
+
|
|
127
|
+
async def _execute_single_attempt(
|
|
128
|
+
self, query: str, variables: Optional[Dict[str, Any]], progress_callback: Optional[Callable], task_name: str
|
|
129
|
+
) -> Dict[str, Any]:
|
|
130
|
+
"""
|
|
131
|
+
Execute a single query attempt.
|
|
132
|
+
|
|
133
|
+
:param str query: GraphQL query string
|
|
134
|
+
:param Optional[Dict[str, Any]] variables: Query variables
|
|
135
|
+
:param Optional[callable] progress_callback: Callback for progress updates
|
|
136
|
+
:param str task_name: Name for progress tracking
|
|
137
|
+
:return: Query response data
|
|
138
|
+
:rtype: Dict[str, Any]
|
|
139
|
+
"""
|
|
140
|
+
if progress_callback:
|
|
141
|
+
progress_callback(task_name, "requesting")
|
|
142
|
+
|
|
143
|
+
# Parse the query string to a GraphQL document
|
|
144
|
+
doc = gql(query)
|
|
145
|
+
|
|
146
|
+
# Create a new client for this request to avoid connection conflicts
|
|
147
|
+
client = self._create_client()
|
|
148
|
+
|
|
149
|
+
# Execute the query using the GQL client
|
|
150
|
+
async with client as session:
|
|
151
|
+
result = await session.execute(doc, variable_values=variables or {})
|
|
152
|
+
|
|
153
|
+
if progress_callback:
|
|
154
|
+
progress_callback(task_name, "completed")
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
async def execute_query(
|
|
159
|
+
self,
|
|
160
|
+
query: str,
|
|
161
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
162
|
+
progress_callback: Optional[Callable] = None,
|
|
163
|
+
task_name: str = "GraphQL Query",
|
|
164
|
+
) -> Dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
Execute a single GraphQL query asynchronously.
|
|
167
|
+
|
|
168
|
+
:param str query: GraphQL query string
|
|
169
|
+
:param Optional[Dict[str, Any]] variables: Query variables
|
|
170
|
+
:param Optional[callable] progress_callback: Callback for progress updates
|
|
171
|
+
:param str task_name: Name for progress tracking
|
|
172
|
+
:return: Query response data
|
|
173
|
+
:rtype: Dict[str, Any]
|
|
174
|
+
"""
|
|
175
|
+
async with self._semaphore: # Limit concurrent requests
|
|
176
|
+
if progress_callback:
|
|
177
|
+
progress_callback(task_name, "starting")
|
|
178
|
+
|
|
179
|
+
logger.debug("Async GraphQL request to %s", self.endpoint)
|
|
180
|
+
|
|
181
|
+
# Try up to 2 times (initial + 1 retry with token refresh)
|
|
182
|
+
max_attempts = 2
|
|
183
|
+
for attempt in range(max_attempts):
|
|
184
|
+
try:
|
|
185
|
+
result = await self._execute_single_attempt(query, variables, progress_callback, task_name)
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
error_msg = str(e)
|
|
190
|
+
|
|
191
|
+
# Check for authentication errors and retry if possible
|
|
192
|
+
if self._is_auth_error(error_msg):
|
|
193
|
+
if attempt < max_attempts - 1 and self.token_refresh_callback:
|
|
194
|
+
logger.warning("Authentication error, refreshing token and retrying")
|
|
195
|
+
new_token = self.token_refresh_callback()
|
|
196
|
+
self.headers["Authorization"] = f"Bearer {new_token}"
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Log and re-raise other errors
|
|
200
|
+
error_msg = f"Error in {task_name}: {error_msg}"
|
|
201
|
+
logger.error(error_msg)
|
|
202
|
+
if progress_callback:
|
|
203
|
+
progress_callback(task_name, "failed")
|
|
204
|
+
raise GraphQLQueryError(error_msg) from e
|
|
205
|
+
|
|
206
|
+
# Should never reach here, but just in case
|
|
207
|
+
raise GraphQLQueryError(f"Failed to execute query after {max_attempts} attempts")
|
|
208
|
+
|
|
209
|
+
async def execute_paginated_query_concurrent(
|
|
210
|
+
self,
|
|
211
|
+
query_builder: Callable[[int, int], str],
|
|
212
|
+
topic_key: str,
|
|
213
|
+
total_count: int,
|
|
214
|
+
page_size: int = 50,
|
|
215
|
+
starting_skip: int = 0,
|
|
216
|
+
progress_callback: Optional[Callable] = None,
|
|
217
|
+
task_name: str = "Paginated Query",
|
|
218
|
+
) -> List[Dict[str, Any]]:
|
|
219
|
+
"""
|
|
220
|
+
Execute a paginated GraphQL query with concurrent page fetching.
|
|
221
|
+
|
|
222
|
+
:param Callable[[int, int], str] query_builder: Function that builds query with skip and take
|
|
223
|
+
:param str topic_key: Key to extract nodes from response
|
|
224
|
+
:param int total_count: Total number of items expected
|
|
225
|
+
:param int page_size: Items per page (default: 50)
|
|
226
|
+
:param int starting_skip: Starting skip value (default: 0)
|
|
227
|
+
:param Optional[callable] progress_callback: Callback for progress updates
|
|
228
|
+
:param str task_name: Name for progress tracking
|
|
229
|
+
:return: All nodes from all pages
|
|
230
|
+
:rtype: List[Dict[str, Any]]
|
|
231
|
+
"""
|
|
232
|
+
# Calculate number of pages needed
|
|
233
|
+
num_pages = (total_count + page_size - 1) // page_size
|
|
234
|
+
|
|
235
|
+
logger.debug("Fetching %d pages concurrently for %s", num_pages, task_name)
|
|
236
|
+
|
|
237
|
+
# Create tasks for all pages
|
|
238
|
+
tasks = []
|
|
239
|
+
for page_num in range(num_pages):
|
|
240
|
+
skip = starting_skip + (page_num * page_size)
|
|
241
|
+
query = query_builder(skip, page_size)
|
|
242
|
+
|
|
243
|
+
page_task_name = f"{task_name} (Page {page_num + 1}/{num_pages})"
|
|
244
|
+
tasks.append(
|
|
245
|
+
self._fetch_single_page(
|
|
246
|
+
query=query,
|
|
247
|
+
variables={},
|
|
248
|
+
topic_key=topic_key,
|
|
249
|
+
page_num=page_num + 1,
|
|
250
|
+
progress_callback=progress_callback,
|
|
251
|
+
task_name=page_task_name,
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Execute all page fetches concurrently
|
|
256
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
257
|
+
|
|
258
|
+
# Collect all nodes from successful pages
|
|
259
|
+
all_nodes: List[Dict[str, Any]] = []
|
|
260
|
+
for i, result in enumerate(results):
|
|
261
|
+
if isinstance(result, Exception):
|
|
262
|
+
logger.error("Error fetching page %d: %s", i + 1, str(result))
|
|
263
|
+
elif isinstance(result, list):
|
|
264
|
+
all_nodes.extend(result)
|
|
265
|
+
|
|
266
|
+
return all_nodes
|
|
267
|
+
|
|
268
|
+
async def _fetch_single_page(
|
|
269
|
+
self,
|
|
270
|
+
query: str,
|
|
271
|
+
variables: Dict[str, Any],
|
|
272
|
+
topic_key: str,
|
|
273
|
+
page_num: int,
|
|
274
|
+
progress_callback: Optional[Callable] = None,
|
|
275
|
+
task_name: str = "Page",
|
|
276
|
+
) -> List[Dict[str, Any]]:
|
|
277
|
+
"""
|
|
278
|
+
Fetch a single page of results.
|
|
279
|
+
|
|
280
|
+
:param str query: GraphQL query string
|
|
281
|
+
:param Dict[str, Any] variables: Query variables
|
|
282
|
+
:param str topic_key: Key to extract nodes from response
|
|
283
|
+
:param int page_num: Page number for logging
|
|
284
|
+
:param Optional[callable] progress_callback: Callback for progress updates
|
|
285
|
+
:param str task_name: Name for progress tracking
|
|
286
|
+
:return: Nodes from this page
|
|
287
|
+
:rtype: List[Dict[str, Any]]
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
data = await self.execute_query(
|
|
291
|
+
query=query, variables=variables, progress_callback=progress_callback, task_name=task_name
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
topic_data = data.get(topic_key, {})
|
|
295
|
+
nodes = topic_data.get("items", [])
|
|
296
|
+
|
|
297
|
+
# Handle case where nodes is explicitly None
|
|
298
|
+
if nodes is None:
|
|
299
|
+
nodes = []
|
|
300
|
+
|
|
301
|
+
if progress_callback:
|
|
302
|
+
progress_callback(task_name, f"fetched_{len(nodes)}_items")
|
|
303
|
+
|
|
304
|
+
return nodes
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error("Error fetching page %d: %s", page_num, str(e))
|
|
308
|
+
raise
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def run_async_paginated_query(
|
|
312
|
+
endpoint: str,
|
|
313
|
+
headers: Dict[str, str],
|
|
314
|
+
query_builder: Callable[[int, int], str],
|
|
315
|
+
topic_key: str,
|
|
316
|
+
total_count: int,
|
|
317
|
+
page_size: int = 50,
|
|
318
|
+
starting_skip: int = 0,
|
|
319
|
+
max_concurrent: int = 5,
|
|
320
|
+
timeout: int = 60,
|
|
321
|
+
progress_callback: Optional[Callable] = None,
|
|
322
|
+
task_name: str = "Paginated Query",
|
|
323
|
+
token_refresh_callback: Optional[Callable[[], str]] = None,
|
|
324
|
+
) -> List[Dict[str, Any]]:
|
|
325
|
+
"""
|
|
326
|
+
Convenience function to run async paginated query from synchronous code.
|
|
327
|
+
|
|
328
|
+
:param str endpoint: GraphQL endpoint URL
|
|
329
|
+
:param Dict[str, str] headers: HTTP headers
|
|
330
|
+
:param Callable[[int, int], str] query_builder: Function to build query with skip and take
|
|
331
|
+
:param str topic_key: Key to extract data from response
|
|
332
|
+
:param int total_count: Total number of items to fetch
|
|
333
|
+
:param int page_size: Items per page
|
|
334
|
+
:param int starting_skip: Starting skip value
|
|
335
|
+
:param int max_concurrent: Maximum concurrent requests
|
|
336
|
+
:param int timeout: Request timeout in seconds
|
|
337
|
+
:param Optional[callable] progress_callback: Progress callback
|
|
338
|
+
:param str task_name: Task name for progress tracking
|
|
339
|
+
:param Optional[Callable[[], str]] token_refresh_callback: Callback to refresh auth token
|
|
340
|
+
:return: Query results
|
|
341
|
+
:rtype: List[Dict[str, Any]]
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
async def _run():
|
|
345
|
+
client = AsyncRegScaleGraphQLClient(
|
|
346
|
+
endpoint=endpoint,
|
|
347
|
+
headers=headers,
|
|
348
|
+
max_concurrent=max_concurrent,
|
|
349
|
+
timeout=timeout,
|
|
350
|
+
token_refresh_callback=token_refresh_callback,
|
|
351
|
+
)
|
|
352
|
+
return await client.execute_paginated_query_concurrent(
|
|
353
|
+
query_builder=query_builder,
|
|
354
|
+
topic_key=topic_key,
|
|
355
|
+
total_count=total_count,
|
|
356
|
+
page_size=page_size,
|
|
357
|
+
starting_skip=starting_skip,
|
|
358
|
+
progress_callback=progress_callback,
|
|
359
|
+
task_name=task_name,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Use anyio.run for better compatibility
|
|
363
|
+
return anyio.run(_run)
|
regscale/core/utils/date.py
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""Utility functions for handling date and datetime conversions"""
|
|
4
4
|
|
|
5
|
+
import calendar
|
|
5
6
|
import datetime
|
|
6
7
|
import logging
|
|
8
|
+
import re
|
|
7
9
|
from typing import Any, List, Optional, Union
|
|
8
10
|
|
|
9
11
|
|
|
@@ -39,7 +41,8 @@ def date_str(date_object: Union[str, datetime.datetime, datetime.date, None], da
|
|
|
39
41
|
return date_object.strftime(date_format)
|
|
40
42
|
|
|
41
43
|
return date_object.isoformat()
|
|
42
|
-
except
|
|
44
|
+
except (AttributeError, TypeError, ValueError) as e:
|
|
45
|
+
logger.debug(f"Error converting date object to string: {e}")
|
|
43
46
|
return ""
|
|
44
47
|
|
|
45
48
|
|
|
@@ -82,6 +85,7 @@ def date_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None])
|
|
|
82
85
|
def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None]) -> Optional[datetime.datetime]:
|
|
83
86
|
"""
|
|
84
87
|
Convert a string, datetime, date, integer, or timestamp string to a datetime object.
|
|
88
|
+
If the day of the month is invalid (e.g., November 31), adjusts to the last valid day of that month.
|
|
85
89
|
|
|
86
90
|
:param Union[str, datetime.datetime, datetime.date, int, None] date_str: The value to convert.
|
|
87
91
|
:return: The datetime object.
|
|
@@ -93,6 +97,10 @@ def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, Non
|
|
|
93
97
|
try:
|
|
94
98
|
return parse(date_str)
|
|
95
99
|
except ParserError as e:
|
|
100
|
+
# Try to fix invalid day of month (e.g., 2023/11/31 -> 2023/11/30)
|
|
101
|
+
if fixed_date := _fix_invalid_day_of_month(date_str):
|
|
102
|
+
return fixed_date
|
|
103
|
+
|
|
96
104
|
if date_str and str(date_str).lower() not in ["n/a", "none"]:
|
|
97
105
|
logger.warning(f"Warning could not parse date string: {date_str}\n{e}")
|
|
98
106
|
return None
|
|
@@ -105,6 +113,74 @@ def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, Non
|
|
|
105
113
|
return None
|
|
106
114
|
|
|
107
115
|
|
|
116
|
+
def _parse_date_components(match_groups: tuple) -> tuple[int, int, int]:
|
|
117
|
+
"""
|
|
118
|
+
Parse year, month, day from regex match groups.
|
|
119
|
+
|
|
120
|
+
:param tuple match_groups: Tuple of matched groups from regex
|
|
121
|
+
:return: Tuple of (year, month, day)
|
|
122
|
+
:rtype: tuple[int, int, int]
|
|
123
|
+
"""
|
|
124
|
+
if len(match_groups[0]) == 4: # First group is year (YYYY/MM/DD)
|
|
125
|
+
return int(match_groups[0]), int(match_groups[1]), int(match_groups[2])
|
|
126
|
+
# Last group is year (MM/DD/YYYY)
|
|
127
|
+
return int(match_groups[2]), int(match_groups[0]), int(match_groups[1])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _adjust_invalid_day(year: int, month: int, day: int) -> Optional[datetime.datetime]:
|
|
131
|
+
"""
|
|
132
|
+
Adjust invalid day of month to last valid day.
|
|
133
|
+
|
|
134
|
+
:param int year: Year
|
|
135
|
+
:param int month: Month
|
|
136
|
+
:param int day: Day
|
|
137
|
+
:return: Adjusted datetime or None if invalid
|
|
138
|
+
:rtype: Optional[datetime.datetime]
|
|
139
|
+
"""
|
|
140
|
+
last_valid_day = calendar.monthrange(year, month)[1]
|
|
141
|
+
if day <= last_valid_day:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
logger.warning(f"Invalid day {day} for month {month}/{year}. Adjusting to last valid day: {last_valid_day}")
|
|
145
|
+
try:
|
|
146
|
+
return datetime.datetime(year, month, last_valid_day)
|
|
147
|
+
except ValueError:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _fix_invalid_day_of_month(date_str: str) -> Optional[datetime.datetime]:
|
|
152
|
+
"""
|
|
153
|
+
Attempt to fix an invalid day of month in a date string.
|
|
154
|
+
For example, 2023/11/31 would become 2023/11/30.
|
|
155
|
+
|
|
156
|
+
:param str date_str: The date string to fix
|
|
157
|
+
:return: A datetime object with a valid day, or None if it can't be fixed
|
|
158
|
+
:rtype: Optional[datetime.datetime]
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
patterns = [
|
|
162
|
+
r"(\d{4})[/-](\d{1,2})[/-](\d{1,2})", # YYYY/MM/DD or YYYY-MM-DD
|
|
163
|
+
r"(\d{1,2})[/-](\d{1,2})[/-](\d{4})", # MM/DD/YYYY or MM-DD-YYYY
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
for pattern in patterns:
|
|
167
|
+
if not (match := re.search(pattern, date_str)):
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
year, month, day = _parse_date_components(match.groups())
|
|
171
|
+
|
|
172
|
+
if not (1 <= month <= 12):
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
if result := _adjust_invalid_day(year, month, day):
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
except (ValueError, TypeError, AttributeError, IndexError) as e:
|
|
180
|
+
logger.debug(f"Could not fix invalid day of month for: {date_str} - {e}")
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
108
184
|
def time_str(time_obj: Union[str, datetime.datetime, datetime.time]) -> str:
|
|
109
185
|
"""
|
|
110
186
|
Convert a datetime/time object to a string.
|
regscale/dev/cli.py
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import sys
|
|
6
7
|
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
7
10
|
import click
|
|
8
11
|
from rich.console import Console
|
|
9
12
|
|
|
@@ -231,5 +234,28 @@ def update_docs(readme_token: str, confluence_user: str, confluence_token: str,
|
|
|
231
234
|
update_confluence(api, confluence_url, root, file)
|
|
232
235
|
|
|
233
236
|
|
|
237
|
+
@cli.command()
|
|
238
|
+
@click.option("--version", "-v", type=click.STRING, help="The version to upgrade the CLI to use.")
|
|
239
|
+
@click.option("--current", "-c", is_flag=True, help="Get the current version of the CLI.")
|
|
240
|
+
def version(version: str, current: bool) -> None:
|
|
241
|
+
"""Manage the version of the regscale-cli package."""
|
|
242
|
+
from regscale.dev.version import (
|
|
243
|
+
get_current_version,
|
|
244
|
+
update_fallback_version_in_version_py,
|
|
245
|
+
update_version_in_pyproject_toml,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if current:
|
|
249
|
+
print(get_current_version())
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
if not version:
|
|
253
|
+
print("❌ Please provide a version to upgrade to using the --version flag.")
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
update_version_in_pyproject_toml(version)
|
|
257
|
+
update_fallback_version_in_version_py(version)
|
|
258
|
+
|
|
259
|
+
|
|
234
260
|
if __name__ == "__main__":
|
|
235
261
|
cli()
|