civics-cdf-validator 1.49.dev8__tar.gz → 1.49.dev10__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.49.dev8/civics_cdf_validator.egg-info → civics_cdf_validator-1.49.dev10}/PKG-INFO +1 -1
  2. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/base.py +9 -0
  3. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10/civics_cdf_validator.egg-info}/PKG-INFO +1 -1
  4. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/rules.py +70 -39
  5. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/version.py +1 -1
  6. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/CONTRIBUTING.md +0 -0
  7. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/LICENSE-2.0.txt +0 -0
  8. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/MANIFEST.in +0 -0
  9. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/README.md +0 -0
  10. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/__init__.py +0 -0
  11. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/civics_cdf_validator.egg-info/SOURCES.txt +0 -0
  12. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/civics_cdf_validator.egg-info/dependency_links.txt +0 -0
  13. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/civics_cdf_validator.egg-info/entry_points.txt +0 -0
  14. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/civics_cdf_validator.egg-info/requires.txt +0 -0
  15. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/civics_cdf_validator.egg-info/top_level.txt +0 -0
  16. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/gpunit_rules.py +0 -0
  17. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/loggers.py +0 -0
  18. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/office_utils.py +0 -0
  19. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/setup.cfg +0 -0
  20. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/setup.py +0 -0
  21. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/stats.py +0 -0
  22. {civics_cdf_validator-1.49.dev8 → civics_cdf_validator-1.49.dev10}/validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: civics_cdf_validator
3
- Version: 1.49.dev8
3
+ Version: 1.49.dev10
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
@@ -17,7 +17,9 @@ limitations under the License.
17
17
  from __future__ import print_function
18
18
 
19
19
  import datetime
20
+ import functools
20
21
  import re
22
+
21
23
  from civics_cdf_validator import loggers
22
24
  from civics_cdf_validator import stats
23
25
  from lxml import etree
@@ -235,6 +237,7 @@ class DateRule(BaseRule):
235
237
  self.error_log.append(loggers.LogEntry(error_message, [self.end_elem]))
236
238
 
237
239
 
240
+ @functools.total_ordering
238
241
  class PartialDate():
239
242
  """Check for PartialDate."""
240
243
 
@@ -257,6 +260,12 @@ class PartialDate():
257
260
  else:
258
261
  return "Not defined"
259
262
 
263
+ def __lt__(self, other):
264
+ return self.is_older_than(other) > 0
265
+
266
+ def __eq__(self, other):
267
+ return self.is_older_than(other) == 0
268
+
260
269
  @classmethod
261
270
  def init_partial_date(cls, date_string):
262
271
  """Initializing partial date."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: civics_cdf_validator
3
- Version: 1.49.dev8
3
+ Version: 1.49.dev10
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
@@ -2683,7 +2683,7 @@ class ElectionDatesSpanContestDates(base.DateRule):
2683
2683
  if (
2684
2684
  election_end_date is not None
2685
2685
  and self.end_date is not None
2686
- and election_end_date.is_older_than(self.end_date) > 0
2686
+ and election_end_date < self.end_date
2687
2687
  # Only compare election end date to contests that are not canceled
2688
2688
  and (
2689
2689
  contest_date_status is None
@@ -2705,7 +2705,7 @@ class ElectionDatesSpanContestDates(base.DateRule):
2705
2705
  if (
2706
2706
  election_start_date is not None
2707
2707
  and self.start_date is not None
2708
- and self.start_date.is_older_than(election_start_date) > 0
2708
+ and self.start_date < election_start_date
2709
2709
  ):
2710
2710
  self.error_log.append(
2711
2711
  loggers.LogEntry(
@@ -2898,37 +2898,76 @@ class OfficeSelectionMethodMatch(base.BaseRule):
2898
2898
  )
2899
2899
 
2900
2900
 
2901
+ class OfficeHolderTenureTermDates(base.DateRule):
2902
+ """OfficeHolderTenure elements should contain valid term dates."""
2903
+
2904
+ def elements(self):
2905
+ return ["OfficeHolderTenure"]
2906
+
2907
+ def check(self, element):
2908
+ start_date = element.find("StartDate")
2909
+ end_date = element.find("EndDate")
2910
+ if element_has_text(start_date) and element_has_text(end_date):
2911
+ start_date_obj = base.PartialDate.init_partial_date(start_date.text)
2912
+ end_date_obj = base.PartialDate.init_partial_date(end_date.text)
2913
+ if end_date_obj < start_date_obj:
2914
+ raise loggers.ElectionError.from_message(
2915
+ "OfficeHolderTenure element has an EndDate that is before the"
2916
+ " StartDate.",
2917
+ [element],
2918
+ )
2919
+
2920
+
2901
2921
  class OfficeTermDates(base.DateRule):
2902
2922
  """Office elements should contain valid term dates.
2903
2923
 
2904
2924
  Offices with OfficeHolderPersonIds should have a Term declared. Given
2905
2925
  term should have a start date. If term also has an end date then end date
2906
- should come after start date.
2926
+ should come after start date. Post Office split feed office objects should
2927
+ not have a Term element.
2907
2928
  """
2908
2929
 
2930
+ def __init__(self, election_tree, schema_tree, **kwargs):
2931
+ self.is_post_office_split_feed = False
2932
+ officeholder_tenure_collection_element = self.get_elements_by_class(
2933
+ election_tree, "OfficeHolderTenureCollection"
2934
+ )
2935
+ if officeholder_tenure_collection_element:
2936
+ self.is_post_office_split_feed = True
2937
+
2909
2938
  def elements(self):
2910
2939
  return ["Office"]
2911
2940
 
2912
2941
  def check(self, element):
2913
2942
  self.reset_instance_vars()
2914
- off_per_id = element.find("OfficeHolderPersonIds")
2915
- if element_has_text(off_per_id):
2943
+ if self.is_post_office_split_feed:
2916
2944
  term = element.find("Term")
2917
- if term is None:
2918
- raise loggers.ElectionWarning.from_message(
2919
- "The Office is missing a Term.", [element]
2945
+ if term is not None:
2946
+ raise loggers.ElectionError.from_message(
2947
+ "Office should not contain Term data in post Office split feed.",
2948
+ [element],
2920
2949
  )
2950
+ if self.error_log:
2951
+ raise loggers.ElectionError(self.error_log)
2952
+ else:
2953
+ off_per_id = element.find("OfficeHolderPersonIds")
2954
+ if element_has_text(off_per_id):
2955
+ term = element.find("Term")
2956
+ if term is None:
2957
+ raise loggers.ElectionWarning.from_message(
2958
+ "The Office is missing a Term.", [element]
2959
+ )
2921
2960
 
2922
- self.gather_dates(term)
2923
- if self.start_date is None:
2924
- raise loggers.ElectionWarning.from_message(
2925
- "The Office is missing a Term > StartDate.", [element]
2926
- )
2927
- elif self.end_date is not None:
2928
- self.check_end_after_start()
2961
+ self.gather_dates(term)
2962
+ if self.start_date is None:
2963
+ raise loggers.ElectionWarning.from_message(
2964
+ "The Office is missing a Term > StartDate.", [element]
2965
+ )
2966
+ elif self.end_date is not None:
2967
+ self.check_end_after_start()
2929
2968
 
2930
- if self.error_log:
2931
- raise loggers.ElectionError(self.error_log)
2969
+ if self.error_log:
2970
+ raise loggers.ElectionError(self.error_log)
2932
2971
 
2933
2972
 
2934
2973
  class RemovePersonAndOfficeHolderId60DaysAfterEndDate(base.TreeRule):
@@ -2955,7 +2994,6 @@ class RemovePersonAndOfficeHolderId60DaysAfterEndDate(base.TreeRule):
2955
2994
  term = office.find(".//Term")
2956
2995
  if term is not None:
2957
2996
  date_validator = base.DateRule(None, None)
2958
- limit_check = 0
2959
2997
  date_validator.gather_dates(term)
2960
2998
  end_date_person = date_validator.end_date
2961
2999
  if end_date_person is not None:
@@ -2967,8 +3005,7 @@ class RemovePersonAndOfficeHolderId60DaysAfterEndDate(base.TreeRule):
2967
3005
  sixty_days_earlier.month,
2968
3006
  sixty_days_earlier.day,
2969
3007
  )
2970
- limit_check = partial_date_sixty_days.is_older_than(end_date_person)
2971
- if limit_check < 0:
3008
+ if end_date_person < partial_date_sixty_days:
2972
3009
  outdated_offices.append(office.get("objectId"))
2973
3010
  for person in persons:
2974
3011
  pid = person.get("objectId")
@@ -3518,11 +3555,11 @@ class SubsequentContestIdIsValidRelatedContest(base.DateRule):
3518
3555
  subsequent_contest = contest_by_id[subsequent_contest_id]
3519
3556
  # Check that the subsequent contest has a later end date
3520
3557
  if subsequent_contest is not None and contest is not None:
3521
- end_delta = base.PartialDate.is_older_than(
3522
- contest_end_date_by_id[subsequent_contest_id],
3523
- contest_end_date_by_id[contest_id],
3524
- )
3525
- if end_delta > 0:
3558
+ contest_end_date = contest_end_date_by_id[contest_id]
3559
+ subsequent_contest_end_date = contest_end_date_by_id[
3560
+ subsequent_contest_id
3561
+ ]
3562
+ if subsequent_contest_end_date < contest_end_date:
3526
3563
  error_log.append(
3527
3564
  loggers.LogEntry(
3528
3565
  f"Contest {contest_id} references a subsequent contest with"
@@ -3839,8 +3876,7 @@ class ContestEndDateOccursBeforeSubsequentContestStartDate(base.DateRule):
3839
3876
  _, contest_end = dates_by_contest_id[contest_id]
3840
3877
  subsequent_contest_start, _ = dates_by_contest_id[subsequent_contest_id]
3841
3878
  if contest_end is not None and subsequent_contest_start is not None:
3842
- date_delta = contest_end.is_older_than(subsequent_contest_start)
3843
- if date_delta < 0:
3879
+ if subsequent_contest_start < contest_end:
3844
3880
  self.error_log.append(
3845
3881
  loggers.LogEntry(
3846
3882
  "Contest {} with end date {} does not occur before"
@@ -4092,8 +4128,7 @@ class ElectionEventDatesAreSequential(base.DateRule):
4092
4128
  full_delivery_date = base.PartialDate.init_partial_date(
4093
4129
  element.find("FullDeliveryDate").text
4094
4130
  )
4095
- date_delta = self.start_date.is_older_than(full_delivery_date)
4096
- if date_delta > 0:
4131
+ if self.start_date < full_delivery_date:
4097
4132
  self.error_log.append(
4098
4133
  loggers.LogEntry(
4099
4134
  "StartDate is older than FullDeliveryDate",
@@ -4109,8 +4144,7 @@ class ElectionEventDatesAreSequential(base.DateRule):
4109
4144
  full_delivery_date = base.PartialDate.init_partial_date(
4110
4145
  element.find("FullDeliveryDate").text
4111
4146
  )
4112
- date_delta = full_delivery_date.is_older_than(initial_delivery_date)
4113
- if date_delta > 0:
4147
+ if full_delivery_date < initial_delivery_date:
4114
4148
  self.error_log.append(
4115
4149
  loggers.LogEntry(
4116
4150
  "FullDeliveryDate is older than InitialDeliveryDate",
@@ -4143,10 +4177,7 @@ class NoSourceDirPathBeforeInitialDeliveryDate(base.BaseRule):
4143
4177
  if element_has_text(initial_delivery)
4144
4178
  else None
4145
4179
  )
4146
- if (
4147
- initial_delivery_date
4148
- and initial_delivery_date.is_older_than(today_partial_date) > 0
4149
- ):
4180
+ if initial_delivery_date and initial_delivery_date < today_partial_date:
4150
4181
  return
4151
4182
  raise loggers.ElectionWarning.from_message(
4152
4183
  "SourceDirPath is defined but all initialDeliveryDate are in the"
@@ -4171,8 +4202,7 @@ class OfficeHolderSubFeedDatesAreSequential(base.DateRule):
4171
4202
  full_delivery_date = base.PartialDate.init_partial_date(
4172
4203
  element.find("FullDeliveryDate").text
4173
4204
  )
4174
- date_delta = full_delivery_date.is_older_than(initial_delivery_date)
4175
- if date_delta > 0:
4205
+ if full_delivery_date < initial_delivery_date:
4176
4206
  raise loggers.ElectionError.from_message(
4177
4207
  "FullDeliveryDate is older than InitialDeliveryDate",
4178
4208
  [element],
@@ -4194,14 +4224,14 @@ class FeedInactiveDateIsLatestDate(base.BaseRule):
4194
4224
  full_delivery_date = base.PartialDate.init_partial_date(
4195
4225
  full_delivery_date_element.text
4196
4226
  )
4197
- if feed_inactive_date.is_older_than(full_delivery_date) > 0:
4227
+ if feed_inactive_date < full_delivery_date:
4198
4228
  raise loggers.ElectionError.from_message(
4199
4229
  "FeedInactiveDate is older than FullDeliveryDate",
4200
4230
  [element],
4201
4231
  )
4202
4232
  for end_date_element in element.iter("EndDate"):
4203
4233
  end_date = base.PartialDate.init_partial_date(end_date_element.text)
4204
- if feed_inactive_date.is_older_than(end_date) > 0:
4234
+ if feed_inactive_date < end_date:
4205
4235
  raise loggers.ElectionError.from_message(
4206
4236
  "FeedInactiveDate is older than EndDate",
4207
4237
  [element],
@@ -4587,6 +4617,7 @@ ELECTION_RESULTS_RULES = ELECTION_RULES + (
4587
4617
  OFFICEHOLDER_RULES = COMMON_RULES + (
4588
4618
  # go/keep-sorted start
4589
4619
  DateOfBirthIsInPast,
4620
+ OfficeHolderTenureTermDates,
4590
4621
  OfficeSelectionMethodMatch,
4591
4622
  OfficeTermDates,
4592
4623
  PersonHasOffice,
@@ -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.49.dev8'
8
+ __version__ = '1.49.dev10'