civics-cdf-validator 1.61.dev5__tar.gz → 1.61.dev7__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.61.dev5/civics_cdf_validator.egg-info → civics_cdf_validator-1.61.dev7}/PKG-INFO +1 -1
  2. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7/civics_cdf_validator.egg-info}/PKG-INFO +1 -1
  3. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/rules.py +72 -0
  4. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/version.py +1 -1
  5. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/CONTRIBUTING.md +0 -0
  6. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/LICENSE-2.0.txt +0 -0
  7. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/MANIFEST.in +0 -0
  8. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/README.md +0 -0
  9. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/__init__.py +0 -0
  10. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/base.py +0 -0
  11. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/civics_cdf_validator.egg-info/SOURCES.txt +0 -0
  12. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/civics_cdf_validator.egg-info/dependency_links.txt +0 -0
  13. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/civics_cdf_validator.egg-info/entry_points.txt +0 -0
  14. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/civics_cdf_validator.egg-info/requires.txt +0 -0
  15. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/civics_cdf_validator.egg-info/top_level.txt +0 -0
  16. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/gpunit_rules.py +0 -0
  17. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/loggers.py +0 -0
  18. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/office_utils.py +0 -0
  19. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/setup.cfg +0 -0
  20. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/setup.py +0 -0
  21. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/stats.py +0 -0
  22. {civics_cdf_validator-1.61.dev5 → civics_cdf_validator-1.61.dev7}/validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: civics_cdf_validator
3
- Version: 1.61.dev5
3
+ Version: 1.61.dev7
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.61.dev5
3
+ Version: 1.61.dev7
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
@@ -25,6 +25,7 @@ from civics_cdf_validator import base
25
25
  from civics_cdf_validator import gpunit_rules
26
26
  from civics_cdf_validator import loggers
27
27
  from civics_cdf_validator import office_utils
28
+ import dateutil
28
29
  from frozendict import frozendict
29
30
  import language_tags
30
31
  from lxml import etree
@@ -1631,6 +1632,50 @@ class DuplicateContestNames(base.BaseRule):
1631
1632
  raise loggers.ElectionError(error_log)
1632
1633
 
1633
1634
 
1635
+ class DuplicateBallotTitleSeatPair(base.BaseRule):
1636
+ """Checks that each English (BallotTitle, Seat) combination is unique.
1637
+
1638
+ Adds Warning/Error if duplicate English (BallotTitle, Seat) combinations are
1639
+ found.
1640
+ """
1641
+
1642
+ def elements(self):
1643
+ return ["ContestCollection"]
1644
+
1645
+ def check(self, element):
1646
+ error_log = []
1647
+ contest_elements = element.findall("Contest")
1648
+ contests_by_title_and_seat = collections.defaultdict(list)
1649
+
1650
+ for contest in contest_elements:
1651
+ ballot_title_element = get_language_to_text_map(
1652
+ contest.find("BallotTitle")
1653
+ )
1654
+ english_title = ballot_title_element.get("en")
1655
+ seat_element = contest.find("Seat")
1656
+
1657
+ ballot_title = english_title[0].strip() if english_title else ""
1658
+ seat = seat_element.text.strip() if element_has_text(seat_element) else ""
1659
+
1660
+ if not ballot_title:
1661
+ continue
1662
+
1663
+ ballot_title_and_seat = (ballot_title, seat)
1664
+ contests_by_title_and_seat[ballot_title_and_seat].append(contest)
1665
+
1666
+ for ballot_title_and_seat, contests in contests_by_title_and_seat.items():
1667
+ if len(contests) > 1:
1668
+ ballot_title, seat = ballot_title_and_seat
1669
+ error_message = (
1670
+ "Multiple Contests found with the same BallotTitle"
1671
+ f" ('{ballot_title}') and Seat ('{seat}')."
1672
+ )
1673
+ error_log.append(loggers.LogEntry(error_message, contests))
1674
+
1675
+ if error_log:
1676
+ raise loggers.ElectionError(error_log)
1677
+
1678
+
1634
1679
  class UniqueStableID(base.TreeRule):
1635
1680
  """Check that every stableID is unique.
1636
1681
 
@@ -4786,6 +4831,31 @@ class FeedHasValidCountryCode(base.BaseRule):
4786
4831
  )
4787
4832
 
4788
4833
 
4834
+ class FeedInactiveDateNotOlderThanOneYear(base.BaseRule):
4835
+ """Feeds should not have a FeedInactiveDate older than 1 year."""
4836
+
4837
+ def elements(self):
4838
+ return ["Feed"]
4839
+
4840
+ def check(self, element):
4841
+ feed_inactive_date_element = element.find("FeedInactiveDate")
4842
+ if not element_has_text(feed_inactive_date_element):
4843
+ return
4844
+ feed_inactive_date = datetime.date.fromisoformat(
4845
+ feed_inactive_date_element.text
4846
+ )
4847
+ one_year_ago = datetime.date.today() - dateutil.relativedelta.relativedelta(
4848
+ years=1
4849
+ )
4850
+ if feed_inactive_date < one_year_ago:
4851
+ feed_id_element = element.find("FeedId")
4852
+ raise loggers.ElectionError.from_message(
4853
+ f"FeedInactiveDate '{feed_inactive_date_element.text}' is older than"
4854
+ f" 1 year for feed '{feed_id_element.text}'.",
4855
+ [element],
4856
+ )
4857
+
4858
+
4789
4859
  class FeedInactiveDateSetForNonEvergreenFeed(base.BaseRule):
4790
4860
  """All non-evergreen feeds should have a FeedInactiveDate set."""
4791
4861
 
@@ -5538,6 +5608,7 @@ ELECTION_RULES = COMMON_RULES + (
5538
5608
  ContestStartDateContainsCorrespondingEndDate,
5539
5609
  CorrectCandidateSelectionCount,
5540
5610
  DateStatusMatches,
5611
+ DuplicateBallotTitleSeatPair,
5541
5612
  DuplicateContestNames,
5542
5613
  DuplicatedPartyAbbreviation,
5543
5614
  DuplicatedPartyName,
@@ -5618,6 +5689,7 @@ METADATA_RULES = (
5618
5689
  FeedHasValidCountryCode,
5619
5690
  FeedIdsAreUnique,
5620
5691
  FeedInactiveDateIsLatestDate,
5692
+ FeedInactiveDateNotOlderThanOneYear,
5621
5693
  FeedInactiveDateSetForNonEvergreenFeed,
5622
5694
  FeedTypeHasValidFeedLongevity,
5623
5695
  OfficeholderSubFeedDatesAreSequential,
@@ -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.61.dev5'
8
+ __version__ = '1.61.dev7'