regscale-cli 6.23.0.0__py3-none-any.whl → 6.24.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (44) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +2 -0
  3. regscale/integrations/commercial/__init__.py +1 -0
  4. regscale/integrations/commercial/sarif/sarif_converter.py +1 -1
  5. regscale/integrations/commercial/wizv2/click.py +109 -2
  6. regscale/integrations/commercial/wizv2/compliance_report.py +1485 -0
  7. regscale/integrations/commercial/wizv2/constants.py +72 -2
  8. regscale/integrations/commercial/wizv2/data_fetcher.py +61 -0
  9. regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
  10. regscale/integrations/commercial/wizv2/issue.py +775 -27
  11. regscale/integrations/commercial/wizv2/policy_compliance.py +599 -181
  12. regscale/integrations/commercial/wizv2/reports.py +243 -0
  13. regscale/integrations/commercial/wizv2/scanner.py +668 -245
  14. regscale/integrations/compliance_integration.py +304 -51
  15. regscale/integrations/due_date_handler.py +210 -0
  16. regscale/integrations/public/cci_importer.py +444 -0
  17. regscale/integrations/scanner_integration.py +718 -153
  18. regscale/models/integration_models/CCI_List.xml +1 -0
  19. regscale/models/integration_models/cisa_kev_data.json +61 -3
  20. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  21. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +3 -3
  22. regscale/models/regscale_models/form_field_value.py +1 -1
  23. regscale/models/regscale_models/milestone.py +1 -0
  24. regscale/models/regscale_models/regscale_model.py +225 -60
  25. regscale/models/regscale_models/security_plan.py +3 -2
  26. regscale/regscale.py +7 -0
  27. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/METADATA +9 -9
  28. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/RECORD +44 -27
  29. tests/fixtures/test_fixture.py +13 -8
  30. tests/regscale/integrations/public/__init__.py +0 -0
  31. tests/regscale/integrations/public/test_alienvault.py +220 -0
  32. tests/regscale/integrations/public/test_cci.py +458 -0
  33. tests/regscale/integrations/public/test_cisa.py +1021 -0
  34. tests/regscale/integrations/public/test_emass.py +518 -0
  35. tests/regscale/integrations/public/test_fedramp.py +851 -0
  36. tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
  37. tests/regscale/integrations/public/test_file_uploads.py +506 -0
  38. tests/regscale/integrations/public/test_oscal.py +453 -0
  39. tests/regscale/models/test_form_field_value_integration.py +304 -0
  40. tests/regscale/models/test_module_integration.py +582 -0
  41. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/LICENSE +0 -0
  42. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/WHEEL +0 -0
  43. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/entry_points.txt +0 -0
  44. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/top_level.txt +0 -0
@@ -80,10 +80,10 @@ class Vulnerabilities(SynqlyModel):
80
80
  if kwargs.get("all_scans"):
81
81
  vuln_filter.append("finding.last_seen_time[gte]915148800") # Friday, January 1, 1999 12:00:00 AM UTC
82
82
  elif scan_date := kwargs.get("scan_date"):
83
- from regscale.core.utils.date import date_obj
83
+ from regscale.core.utils.date import datetime_obj
84
84
 
85
- if scan_date := date_obj(scan_date):
86
- vuln_filter.append(f"finding.last_seen_time[gte]{scan_date.isoformat()}")
85
+ if scan_date := datetime_obj(scan_date):
86
+ vuln_filter.append(f"finding.last_seen_time[gte]{int(scan_date.timestamp())}")
87
87
  else:
88
88
  vuln_filter.append(f"finding.last_seen_time[gte]{get_last_pull_epoch(regscale_ssp_id)}")
89
89
  else:
@@ -148,7 +148,7 @@ class FormFieldValue(RegScaleModel):
148
148
  ]
149
149
  if data:
150
150
  FormFieldValue.save_custom_data(
151
- record_id=form_field_value.get["record_id"],
151
+ record_id=form_field_value.get("record_id"),
152
152
  module_name=form_field_value.get("record_module", "securityplans"),
153
153
  data=data,
154
154
  )
@@ -16,6 +16,7 @@ class Milestone(RegScaleModel):
16
16
  _module_slug = "milestones"
17
17
  _module_string = "milestones"
18
18
  _unique_fields = ["title", "parentModule"]
19
+ _parent_id_field = "parentID"
19
20
 
20
21
  title: str
21
22
  id: int = 0
@@ -502,31 +502,93 @@ class RegScaleModel(BaseModel, ABC):
502
502
  :return: A list of unique fields
503
503
  :rtype: List[List[str]]
504
504
  """
505
- sample_format = {"uniqueOverride": {"asset": ["ipAddress"]}}
506
505
  config = Application().config
507
506
 
507
+ # First, ensure _unique_fields is in new format (List[List[str]])
508
+ cls._ensure_unique_fields_format()
509
+
508
510
  try:
509
511
  primary = config.get("uniqueOverride", {}).get(cls.__name__.lower())
510
512
  if primary:
511
- if not isinstance(primary, list):
512
- raise ValueError(
513
- f"Invalid config format in uniqueOverride.{cls.__name__.lower()}, the configuration must be in a format like so:\n{dump(sample_format, default_flow_style=False)}"
514
- )
515
- if primary != cls._unique_fields:
516
- if all(attr in cls.model_fields for attr in primary):
517
- if primary not in cls._unique_fields:
518
- cls._unique_fields.insert(1, primary)
519
- else:
520
- # Move primary to index 1 if it exists
521
- cls._unique_fields.insert(1, cls._unique_fields.pop(cls._unique_fields.index(primary)))
522
- else:
523
- raise ValueError(
524
- f"One or more invalid attribute(s) detected: {primary}, falling back on default unique fields for type: {cls.__name__.lower()}"
525
- )
513
+ cls._process_primary_override(primary)
526
514
  except ValueError as e:
527
515
  logger.warning(e)
528
516
  return cls._unique_fields
529
517
 
518
+ @classmethod
519
+ def _ensure_unique_fields_format(cls) -> None:
520
+ """
521
+ Ensure _unique_fields is in the new format (List[List[str]]).
522
+
523
+ :rtype: None
524
+ """
525
+ # Check if it's still in old format (List[str])
526
+ if cls._unique_fields and isinstance(cls._unique_fields[0], str):
527
+ # Convert old format to new format
528
+ cls._unique_fields = [cls._unique_fields] # type: ignore
529
+
530
+ @classmethod
531
+ def _process_primary_override(cls, primary: List[str]) -> None:
532
+ """
533
+ Process the primary override configuration.
534
+
535
+ :param List[str] primary: The primary override fields
536
+ :raises ValueError: If the primary fields are invalid
537
+ :rtype: None
538
+ """
539
+ cls._validate_primary_format(primary)
540
+
541
+ # Now cls._unique_fields is guaranteed to be List[List[str]]
542
+ # Check if primary is different from any existing unique field set
543
+ if primary not in cls._unique_fields:
544
+ cls._handle_new_primary_fields(primary)
545
+
546
+ @classmethod
547
+ def _validate_primary_format(cls, primary: List[str]) -> None:
548
+ """
549
+ Validate the format of the primary override configuration.
550
+
551
+ :param List[str] primary: The primary override fields
552
+ :raises ValueError: If the primary format is invalid
553
+ :rtype: None
554
+ """
555
+ if not isinstance(primary, list):
556
+ sample_format = {"uniqueOverride": {"asset": ["ipAddress"]}}
557
+ raise ValueError(
558
+ f"Invalid config format in uniqueOverride.{cls.__name__.lower()}, the configuration must be in a format like so:\n{dump(sample_format, default_flow_style=False)}"
559
+ )
560
+
561
+ @classmethod
562
+ def _handle_new_primary_fields(cls, primary: List[str]) -> None:
563
+ """
564
+ Handle new primary fields that are not in existing unique fields.
565
+
566
+ :param List[str] primary: The primary override fields
567
+ :raises ValueError: If any attributes are invalid
568
+ :rtype: None
569
+ """
570
+ if all(attr in cls.model_fields for attr in primary):
571
+ cls._insert_primary_fields(primary)
572
+ else:
573
+ raise ValueError(
574
+ f"One or more invalid attribute(s) detected: {primary}, falling back on default unique fields for type: {cls.__name__.lower()}"
575
+ )
576
+
577
+ @classmethod
578
+ def _insert_primary_fields(cls, primary: List[str]) -> None:
579
+ """
580
+ Insert primary fields into the unique fields list.
581
+
582
+ :param List[str] primary: The primary override fields
583
+ :rtype: None
584
+ """
585
+ # Check if primary already exists in the list
586
+ if primary not in cls._unique_fields:
587
+ cls._unique_fields.insert(1, primary)
588
+ else:
589
+ # Move primary to index 1 if it exists
590
+ cls._unique_fields.insert(1, cls._unique_fields.pop(cls._unique_fields.index(primary)))
591
+
530
592
  @classmethod
531
593
  def _get_endpoints(cls) -> ConfigDict:
532
594
  """
@@ -665,17 +727,45 @@ class RegScaleModel(BaseModel, ABC):
665
727
  self.cache_object(instance)
666
728
  return False, instance
667
729
  else:
668
- created_instance = self.create(bulk=bulk)
669
- self.cache_object(created_instance)
670
- return True, created_instance
730
+ try:
731
+ created_instance = self.create(bulk=bulk)
732
+ self.cache_object(created_instance)
733
+ return True, created_instance
734
+ except APIInsertionError as e:
735
+ # Check if this is a duplicate error (race condition in threading)
736
+ error_str = str(e).lower()
737
+ if "already exists" in error_str or "mapping already exists" in error_str:
738
+ logger.debug(
739
+ f"Race condition detected for {self.__class__.__name__}, retrying find_by_unique: {e}"
740
+ )
741
+ # Clear the cache to force a fresh lookup
742
+ self.clear_cache()
743
+ # Try to find the instance again - another thread may have created it
744
+ instance = self.find_by_unique()
745
+ if instance:
746
+ self.cache_object(instance)
747
+ logger.debug(
748
+ f"Successfully found existing {self.__class__.__name__} after duplicate creation error, ID: {instance.id}"
749
+ )
750
+ return False, instance
751
+ else:
752
+ # If we still can't find it, log error but don't stop the process
753
+ logger.error(
754
+ f"Failed to find {self.__class__.__name__} after duplicate creation error: {e}"
755
+ )
756
+ # Return None to indicate creation failed but don't raise
757
+ return False, None
758
+ else:
759
+ # Not a duplicate error, re-raise
760
+ logger.error(f"Failed to create object: {self.__class__.__name__} creation error: {e}")
671
761
 
672
- def get_or_create(self: T, bulk: bool = False) -> T:
762
+ def get_or_create(self: T, bulk: bool = False) -> Optional[T]:
673
763
  """
674
764
  Get or create an instance.
675
765
 
676
766
  :param bool bulk: Whether to perform a bulk create operation, defaults to False
677
- :return: The instance
678
- :rtype: T
767
+ :return: The instance or None if creation failed due to race condition
768
+ :rtype: Optional[T]
679
769
  """
680
770
  _, instance = self.get_or_create_with_status(bulk=bulk)
681
771
  return instance
@@ -727,44 +817,100 @@ class RegScaleModel(BaseModel, ABC):
727
817
  instance = cached_object or self.find_by_unique()
728
818
 
729
819
  if instance:
730
- # An existing instance was found (either in cache or database)
731
- logger.debug(f"Found {'cached' if cached_object else 'existing'} instance of {self.__class__.__name__}")
732
- # Update the current object's ID with the found instance's ID
733
- self.id = instance.id
734
- # If the object has a 'dateCreated' attribute, update it
735
- if hasattr(self, "dateCreated"):
736
- self.dateCreated = instance.dateCreated # noqa
737
-
738
- # Update the _original_data attribute with the instance data
739
- self._original_data = instance.dict(exclude_unset=True)
740
-
741
- # Check if the current object has any changes compared to the found instance
742
- if self.has_changed():
743
- logger.debug(f"Instance of {self.__class__.__name__} has changed, updating")
744
- # Save the changes, potentially in bulk
745
- updated_instance = self.save(bulk=bulk_update)
746
- # Update the cache with the new instance
747
- self.cache_object(updated_instance)
748
- # Return the updated instance, optionally with a flag indicating it wasn't newly created
749
- return False, updated_instance
750
-
751
- # If no changes, return the existing instance
752
- return False, instance
820
+ return self._handle_existing_instance(instance, cached_object, bulk_update)
753
821
 
754
822
  # No existing instance was found, so create a new one
755
- # apply defaults if they are provided
756
- if defaults:
757
- for key, value in defaults.items():
758
- # Handle callable values by executing them
759
- if callable(value):
760
- value = value()
761
- setattr(self, key, value)
762
- logger.debug(f"No existing instance found for {self.__class__.__name__}, creating new")
763
- created_instance = self.create(bulk=bulk_create)
764
- # Cache the newly created instance
765
- self.cache_object(created_instance)
766
- # Return the created instance, optionally with a flag indicating it was newly created
767
- return True, created_instance
823
+ return self._handle_new_instance(defaults, bulk_create)
824
+
825
+ def _handle_existing_instance(
826
+ self: T, instance: T, cached_object: Optional[T], bulk_update: bool
827
+ ) -> Tuple[bool, T]:
828
+ """
829
+ Handle processing of an existing instance found in cache or database.
830
+
831
+ :param T instance: The found instance
832
+ :param Optional[T] cached_object: The cached object if found in cache
833
+ :param bool bulk_update: Whether to perform a bulk update
834
+ :return: Tuple of (was_created, instance)
835
+ :rtype: Tuple[bool, T]
836
+ """
837
+ # An existing instance was found (either in cache or database)
838
+ logger.debug(f"Found {'cached' if cached_object else 'existing'} instance of {self.__class__.__name__}")
839
+
840
+ # Update current object with instance data
841
+ self._sync_with_existing_instance(instance)
842
+
843
+ # Check if the current object has any changes compared to the found instance
844
+ if self.has_changed():
845
+ return self._update_existing_instance(bulk_update)
846
+
847
+ # If no changes, return the existing instance
848
+ return False, instance
849
+
850
+ def _sync_with_existing_instance(self: T, instance: T) -> None:
851
+ """
852
+ Synchronize current object with existing instance data.
853
+
854
+ :param T instance: The existing instance to sync with
855
+ :rtype: None
856
+ """
857
+ # Update the current object's ID with the found instance's ID
858
+ self.id = instance.id
859
+ # If the object has a 'dateCreated' attribute, update it
860
+ if hasattr(self, "dateCreated"):
861
+ self.dateCreated = instance.dateCreated # noqa
862
+
863
+ # Update the _original_data attribute with the instance data
864
+ self._original_data = instance.dict(exclude_unset=True)
865
+
866
+ def _update_existing_instance(self: T, bulk_update: bool) -> Tuple[bool, T]:
867
+ """
868
+ Update an existing instance that has changes.
869
+
870
+ :param bool bulk_update: Whether to perform a bulk update
871
+ :return: Tuple of (was_created, updated_instance)
872
+ :rtype: Tuple[bool, T]
873
+ """
874
+ logger.debug(f"Instance of {self.__class__.__name__} has changed, updating")
875
+ # Save the changes, potentially in bulk
876
+ updated_instance = self.save(bulk=bulk_update)
877
+ # Update the cache with the new instance
878
+ self.cache_object(updated_instance)
879
+ # Return the updated instance, optionally with a flag indicating it wasn't newly created
880
+ return False, updated_instance
881
+
882
+ def _handle_new_instance(self: T, defaults: Optional[Dict[str, Any]], bulk_create: bool) -> Tuple[bool, T]:
883
+ """
884
+ Handle creation of a new instance when none exists.
885
+
886
+ :param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply
887
+ :param bool bulk_create: Whether to perform a bulk create
888
+ :return: Tuple of (was_created, created_instance)
889
+ :rtype: Tuple[bool, T]
890
+ """
891
+ # apply defaults if they are provided
892
+ self._apply_defaults(defaults)
893
+
894
+ logger.debug(f"No existing instance found for {self.__class__.__name__}, creating new")
895
+ created_instance = self.create(bulk=bulk_create)
896
+ # Cache the newly created instance
897
+ self.cache_object(created_instance)
898
+ # Return the created instance, optionally with a flag indicating it was newly created
899
+ return True, created_instance
900
+
901
+ def _apply_defaults(self: T, defaults: Optional[Dict[str, Any]]) -> None:
902
+ """
903
+ Apply default values to the instance.
904
+
905
+ :param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply
906
+ :rtype: None
907
+ """
908
+ if defaults:
909
+ for key, value in defaults.items():
910
+ # Handle callable values by executing them
911
+ if callable(value):
912
+ value = value()
913
+ setattr(self, key, value)
768
914
 
769
915
  @classmethod
770
916
  def _handle_list_response(
@@ -1269,7 +1415,15 @@ class RegScaleModel(BaseModel, ABC):
1269
1415
  endpoint = self.get_endpoint("insert")
1270
1416
  response = self._get_api_handler().post(endpoint=endpoint, data=self.dict(), headers=self._get_headers())
1271
1417
  if response and response.ok:
1272
- obj = self.__class__(**response.json())
1418
+ response_data = response.json()
1419
+
1420
+ # Handle special case for ComponentMapping which may have nested response structure
1421
+ if self.__class__.__name__ == "ComponentMapping" and "componentMapping" in response_data:
1422
+ component_mapping_data = response_data["componentMapping"]
1423
+ obj = self.__class__(**component_mapping_data)
1424
+ else:
1425
+ obj = self.__class__(**response_data)
1426
+
1273
1427
  self.cache_object(obj)
1274
1428
  return obj
1275
1429
  else:
@@ -1468,6 +1622,17 @@ class RegScaleModel(BaseModel, ABC):
1468
1622
  logger.warning(f"{cls.__name__}: No matching record found for ID: {cls.__name__} {object_id}")
1469
1623
  return None
1470
1624
 
1625
+ @classmethod
1626
+ def get(cls, id: Union[str, int]) -> Optional[T]:
1627
+ """
1628
+ Get a RegScale object by ID. shortcut for get_object.
1629
+
1630
+ :param Union[str, int] id: The ID of the object
1631
+ :return: The object or None if not found
1632
+ :rtype: Optional[T]
1633
+ """
1634
+ return cls.get_object(object_id=id)
1635
+
1471
1636
  @classmethod
1472
1637
  def get_objects_and_attachments_by_parent(
1473
1638
  cls, parent_id: int, parent_module: str
@@ -1480,7 +1645,7 @@ class RegScaleModel(BaseModel, ABC):
1480
1645
  :return: A tuple of a list of objects and a list of attachments
1481
1646
  :rtype: Tuple[List[T], dict[int, List["File"]]]
1482
1647
  """
1483
- from regscale.models import File
1648
+ from regscale.models.regscale_models import File
1484
1649
 
1485
1650
  # get the existing issues for the parent record that are already in RegScale
1486
1651
  logger.info("Fetching full %s list from RegScale %s #%i.", cls.__name__, parent_module, parent_id)
@@ -107,6 +107,7 @@ class SecurityPlan(RegScaleModel):
107
107
  fedrampDateSubmitted: Optional[str] = ""
108
108
  fedrampDateAuthorized: Optional[str] = ""
109
109
  fedrampId: Optional[str] = ""
110
+ complianceSettings: Optional[str] = None
110
111
  complianceSettingsId: Optional[int] = 1
111
112
  tenantsId: int = 1
112
113
 
@@ -119,11 +120,11 @@ class SecurityPlan(RegScaleModel):
119
120
  :return: The ComplianceSettings ID if the RegScale version is compatible, None otherwise
120
121
  :rtype: Optional[int]
121
122
  """
122
- from packaging.version import Version
123
+ from regscale.utils.version import RegscaleVersion
123
124
 
124
125
  regscale_version = cls._get_api_handler().regscale_version
125
126
 
126
- if len(regscale_version) >= 10 or Version(regscale_version) >= Version("6.13.0.0"):
127
+ if len(regscale_version) >= 10 or RegscaleVersion.compare_versions(regscale_version, "6.13.0.0"):
127
128
  return v
128
129
  else:
129
130
  return None
regscale/regscale.py CHANGED
@@ -142,6 +142,11 @@ nist = import_command_with_timing(PUBLIC, "nist")
142
142
  oscal = import_command_with_timing(PUBLIC, "oscal")
143
143
  criticality_updater = import_command_with_timing(PUBLIC, "criticality_updater")
144
144
 
145
+ ############################################################
146
+ # Utils
147
+ ############################################################
148
+ cci_importer = import_command_with_timing("regscale.integrations.public.cci_importer", "cci_importer")
149
+
145
150
  ############################################################
146
151
  # Commercial Integrations
147
152
  ############################################################
@@ -816,6 +821,8 @@ cli.add_command(emass) # add eMASS support
816
821
  cli.add_command(fedramp) # add FedRAMP support
817
822
  cli.add_command(nist) # add Nist_Catalog support
818
823
  cli.add_command(oscal) # add OSCAL support
824
+ cli.add_command(criticality_updater) # add Criticality Updater support
825
+ cli.add_command(cci_importer) # add CCI Importer support
819
826
 
820
827
  # start function for the CLI
821
828
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: regscale-cli
3
- Version: 6.23.0.0
3
+ Version: 6.24.0.0
4
4
  Summary: Command Line Interface (CLI) for bulk processing/loading data into RegScale
5
5
  Home-page: https://github.com/RegScale/regscale-cli
6
6
  Author: Travis Howerton
@@ -68,7 +68,7 @@ Requires-Dist: setuptools
68
68
  Requires-Dist: simple-salesforce
69
69
  Requires-Dist: smart-open ~=7.0
70
70
  Requires-Dist: static
71
- Requires-Dist: synqly >=0.4.56
71
+ Requires-Dist: synqly ==0.4.56
72
72
  Requires-Dist: tools
73
73
  Requires-Dist: typing-extensions ~=4.12.2
74
74
  Requires-Dist: wheel
@@ -150,7 +150,7 @@ Requires-Dist: setuptools >=69.0.0 ; extra == 'airflow'
150
150
  Requires-Dist: simple-salesforce ; extra == 'airflow'
151
151
  Requires-Dist: smart-open ~=7.0 ; extra == 'airflow'
152
152
  Requires-Dist: static ; extra == 'airflow'
153
- Requires-Dist: synqly >=0.4.56 ; extra == 'airflow'
153
+ Requires-Dist: synqly ==0.4.56 ; extra == 'airflow'
154
154
  Requires-Dist: tools ; extra == 'airflow'
155
155
  Requires-Dist: typing-extensions ~=4.12.2 ; extra == 'airflow'
156
156
  Requires-Dist: wheel ; extra == 'airflow'
@@ -235,7 +235,7 @@ Requires-Dist: setuptools >=69.0.0 ; extra == 'airflow-azure'
235
235
  Requires-Dist: simple-salesforce ; extra == 'airflow-azure'
236
236
  Requires-Dist: smart-open ~=7.0 ; extra == 'airflow-azure'
237
237
  Requires-Dist: static ; extra == 'airflow-azure'
238
- Requires-Dist: synqly >=0.4.56 ; extra == 'airflow-azure'
238
+ Requires-Dist: synqly ==0.4.56 ; extra == 'airflow-azure'
239
239
  Requires-Dist: tools ; extra == 'airflow-azure'
240
240
  Requires-Dist: typing-extensions ~=4.12.2 ; extra == 'airflow-azure'
241
241
  Requires-Dist: wheel ; extra == 'airflow-azure'
@@ -320,7 +320,7 @@ Requires-Dist: setuptools >=69.0.0 ; extra == 'airflow-sqlserver'
320
320
  Requires-Dist: simple-salesforce ; extra == 'airflow-sqlserver'
321
321
  Requires-Dist: smart-open ~=7.0 ; extra == 'airflow-sqlserver'
322
322
  Requires-Dist: static ; extra == 'airflow-sqlserver'
323
- Requires-Dist: synqly >=0.4.56 ; extra == 'airflow-sqlserver'
323
+ Requires-Dist: synqly ==0.4.56 ; extra == 'airflow-sqlserver'
324
324
  Requires-Dist: tools ; extra == 'airflow-sqlserver'
325
325
  Requires-Dist: typing-extensions ~=4.12.2 ; extra == 'airflow-sqlserver'
326
326
  Requires-Dist: wheel ; extra == 'airflow-sqlserver'
@@ -410,7 +410,7 @@ Requires-Dist: setuptools >=69.0.0 ; extra == 'all'
410
410
  Requires-Dist: simple-salesforce ; extra == 'all'
411
411
  Requires-Dist: smart-open ~=7.0 ; extra == 'all'
412
412
  Requires-Dist: static ; extra == 'all'
413
- Requires-Dist: synqly >=0.4.56 ; extra == 'all'
413
+ Requires-Dist: synqly ==0.4.56 ; extra == 'all'
414
414
  Requires-Dist: tools ; extra == 'all'
415
415
  Requires-Dist: typing-extensions ~=4.12.2 ; extra == 'all'
416
416
  Requires-Dist: werkzeug >=2.3.8 ; extra == 'all'
@@ -472,7 +472,7 @@ Requires-Dist: setuptools ; extra == 'ansible'
472
472
  Requires-Dist: simple-salesforce ; extra == 'ansible'
473
473
  Requires-Dist: smart-open ~=7.0 ; extra == 'ansible'
474
474
  Requires-Dist: static ; extra == 'ansible'
475
- Requires-Dist: synqly >=0.4.56 ; extra == 'ansible'
475
+ Requires-Dist: synqly ==0.4.56 ; extra == 'ansible'
476
476
  Requires-Dist: tools ; extra == 'ansible'
477
477
  Requires-Dist: typing-extensions ~=4.12.2 ; extra == 'ansible'
478
478
  Requires-Dist: wheel ; extra == 'ansible'
@@ -555,7 +555,7 @@ Requires-Dist: sphinx ; extra == 'dev'
555
555
  Requires-Dist: sphinx-autodoc-typehints ; extra == 'dev'
556
556
  Requires-Dist: sphinx-click ; extra == 'dev'
557
557
  Requires-Dist: static ; extra == 'dev'
558
- Requires-Dist: synqly >=0.4.56 ; extra == 'dev'
558
+ Requires-Dist: synqly ==0.4.56 ; extra == 'dev'
559
559
  Requires-Dist: tools ; extra == 'dev'
560
560
  Requires-Dist: types-python-dateutil ; extra == 'dev'
561
561
  Requires-Dist: types-pyyaml ; extra == 'dev'
@@ -624,7 +624,7 @@ Requires-Dist: setuptools ; extra == 'server'
624
624
  Requires-Dist: simple-salesforce ; extra == 'server'
625
625
  Requires-Dist: smart-open ~=7.0 ; extra == 'server'
626
626
  Requires-Dist: static ; extra == 'server'
627
- Requires-Dist: synqly >=0.4.56 ; extra == 'server'
627
+ Requires-Dist: synqly ==0.4.56 ; extra == 'server'
628
628
  Requires-Dist: tools ; extra == 'server'
629
629
  Requires-Dist: typing-extensions ~=4.12.2 ; extra == 'server'
630
630
  Requires-Dist: werkzeug >=2.3.8 ; extra == 'server'