civics-cdf-validator 1.60.dev2__tar.gz → 1.60.dev3__tar.gz

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.
Files changed (22) hide show
  1. {civics_cdf_validator-1.60.dev2/civics_cdf_validator.egg-info → civics_cdf_validator-1.60.dev3}/PKG-INFO +1 -1
  2. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3/civics_cdf_validator.egg-info}/PKG-INFO +1 -1
  3. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/rules.py +62 -0
  4. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/version.py +1 -1
  5. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/CONTRIBUTING.md +0 -0
  6. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/LICENSE-2.0.txt +0 -0
  7. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/MANIFEST.in +0 -0
  8. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/README.md +0 -0
  9. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/__init__.py +0 -0
  10. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/base.py +0 -0
  11. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/SOURCES.txt +0 -0
  12. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/dependency_links.txt +0 -0
  13. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/entry_points.txt +0 -0
  14. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/requires.txt +0 -0
  15. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/top_level.txt +0 -0
  16. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/gpunit_rules.py +0 -0
  17. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/loggers.py +0 -0
  18. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/office_utils.py +0 -0
  19. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/setup.cfg +0 -0
  20. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/setup.py +0 -0
  21. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/stats.py +0 -0
  22. {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev3}/validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: civics_cdf_validator
3
- Version: 1.60.dev2
3
+ Version: 1.60.dev3
4
4
  Summary: Checks if an election feed follows best practices
5
5
  Home-page: https://github.com/google/civics_cdf_validator
6
6
  Author: Google Civics
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: civics_cdf_validator
3
- Version: 1.60.dev2
3
+ Version: 1.60.dev3
4
4
  Summary: Checks if an election feed follows best practices
5
5
  Home-page: https://github.com/google/civics_cdf_validator
6
6
  Author: Google Civics
@@ -132,6 +132,11 @@ _UNTYPED_URI_PLATFORMS = frozenset([
132
132
  ])
133
133
  _ALL_URI_PLATFORMS = _TYPED_URI_PLATFORMS | _UNTYPED_URI_PLATFORMS
134
134
 
135
+ _WINNER_POST_ELECTION_STATUSES = frozenset([
136
+ "winner",
137
+ "projected-winner",
138
+ ])
139
+
135
140
 
136
141
  def _get_office_roles(element, is_post_office_split_feed=False):
137
142
  if is_post_office_split_feed:
@@ -3547,6 +3552,62 @@ class ImproperCandidateContest(base.TreeRule):
3547
3552
  raise loggers.ElectionWarning(warning_log)
3548
3553
 
3549
3554
 
3555
+ class WinnerCountLimit(base.BaseRule):
3556
+ """Number of winners must be less than or equal to NumberElected."""
3557
+
3558
+ def setup(self):
3559
+ self.candidate_post_election_status_by_object_id = (
3560
+ self._get_candidate_post_election_status_by_object_id()
3561
+ )
3562
+
3563
+ def _get_candidate_post_election_status_by_object_id(self):
3564
+ candidate_post_election_status_by_object_id = {}
3565
+ candidates = self.get_elements_by_class(
3566
+ self.election_tree, "CandidateCollection//Candidate"
3567
+ )
3568
+ for candidate in candidates:
3569
+ object_id = candidate.get("objectId")
3570
+ post_election_status = candidate.find("PostElectionStatus")
3571
+ if object_id and element_has_text(post_election_status):
3572
+ candidate_post_election_status_by_object_id[object_id] = (
3573
+ post_election_status.text.strip()
3574
+ )
3575
+ return candidate_post_election_status_by_object_id
3576
+
3577
+ def elements(self):
3578
+ return ["CandidateContest"]
3579
+
3580
+ def check(self, element):
3581
+ number_elected_element = element.find("NumberElected")
3582
+ number_elected = (
3583
+ int(number_elected_element.text)
3584
+ if element_has_text(number_elected_element)
3585
+ else 1
3586
+ )
3587
+
3588
+ candidate_ids_elements = element.findall("BallotSelection//CandidateIds")
3589
+ candidate_ids = set()
3590
+ for candidate_ids_element in candidate_ids_elements:
3591
+ if element_has_text(candidate_ids_element):
3592
+ candidate_ids.update(candidate_ids_element.text.split())
3593
+
3594
+ winner_count = 0
3595
+ for candidate_id in candidate_ids:
3596
+ status = self.candidate_post_election_status_by_object_id.get(
3597
+ candidate_id
3598
+ )
3599
+ if status in _WINNER_POST_ELECTION_STATUSES:
3600
+ winner_count += 1
3601
+
3602
+ if winner_count > number_elected:
3603
+ raise loggers.ElectionError.from_message(
3604
+ f"Contest {element.get('objectId')} has {winner_count} candidates"
3605
+ " with PostElectionStatus of 'winner' or 'projected-winner',"
3606
+ f" which exceeds NumberElected: {number_elected}.",
3607
+ [element],
3608
+ )
3609
+
3610
+
3550
3611
  class MissingFieldsError(base.MissingFieldRule):
3551
3612
  """Check for missing fields for given entity types and field names.
3552
3613
 
@@ -4940,6 +5001,7 @@ ELECTION_RULES = COMMON_RULES + (
4940
5001
  ValidateInfoUriAnnotation,
4941
5002
  VoteCountTypesCoherency,
4942
5003
  VoteCountValidSeatsDeltaTypes,
5004
+ WinnerCountLimit,
4943
5005
  # go/keep-sorted end
4944
5006
  )
4945
5007
 
@@ -5,4 +5,4 @@ No dependencies should be added to this module.
5
5
  See https://packaging.python.org/guides/single-sourcing-package-version/
6
6
  """
7
7
 
8
- __version__ = '1.60.dev2'
8
+ __version__ = '1.60.dev3'