persiantools 4.1.1__tar.gz → 4.1.2__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 (23) hide show
  1. {persiantools-4.1.1/persiantools.egg-info → persiantools-4.1.2}/PKG-INFO +1 -1
  2. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools/__init__.py +1 -1
  3. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools/jdatetime.py +197 -3
  4. {persiantools-4.1.1 → persiantools-4.1.2/persiantools.egg-info}/PKG-INFO +1 -1
  5. {persiantools-4.1.1 → persiantools-4.1.2}/tests/test_jalalidate.py +1 -1
  6. {persiantools-4.1.1 → persiantools-4.1.2}/tests/test_jalalidatetime.py +48 -3
  7. {persiantools-4.1.1 → persiantools-4.1.2}/LICENSE +0 -0
  8. {persiantools-4.1.1 → persiantools-4.1.2}/MANIFEST.in +0 -0
  9. {persiantools-4.1.1 → persiantools-4.1.2}/README.md +0 -0
  10. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools/characters.py +0 -0
  11. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools/digits.py +0 -0
  12. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools/utils.py +0 -0
  13. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools.egg-info/SOURCES.txt +0 -0
  14. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools.egg-info/dependency_links.txt +0 -0
  15. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools.egg-info/not-zip-safe +0 -0
  16. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools.egg-info/requires.txt +0 -0
  17. {persiantools-4.1.1 → persiantools-4.1.2}/persiantools.egg-info/top_level.txt +0 -0
  18. {persiantools-4.1.1 → persiantools-4.1.2}/pyproject.toml +0 -0
  19. {persiantools-4.1.1 → persiantools-4.1.2}/setup.cfg +0 -0
  20. {persiantools-4.1.1 → persiantools-4.1.2}/setup.py +0 -0
  21. {persiantools-4.1.1 → persiantools-4.1.2}/tests/test_characters.py +0 -0
  22. {persiantools-4.1.1 → persiantools-4.1.2}/tests/test_digits.py +0 -0
  23. {persiantools-4.1.1 → persiantools-4.1.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: persiantools
3
- Version: 4.1.1
3
+ Version: 4.1.2
4
4
  Summary: Jalali date and datetime with other tools
5
5
  Home-page: https://github.com/majiidd/persiantools
6
6
  Author: Majid Hajiloo
@@ -7,7 +7,7 @@
7
7
 
8
8
  __title__ = "persiantools"
9
9
  __url__ = "https://github.com/majiidd/persiantools"
10
- __version__ = "4.1.1"
10
+ __version__ = "4.1.2"
11
11
  __build__ = __version__
12
12
  __author__ = "Majid Hajiloo"
13
13
  __author_email__ = "majid.hajiloo@gmail.com"
@@ -129,6 +129,12 @@ _MONTH_COUNT = [
129
129
  [29, 30, 336], # esfand
130
130
  ]
131
131
 
132
+ _FRACTION_CORRECTION = [100000, 10000, 1000, 100, 10]
133
+
134
+
135
+ def _is_ascii_digit(c):
136
+ return c in "0123456789"
137
+
132
138
 
133
139
  class JalaliDate:
134
140
  """
@@ -558,12 +564,20 @@ class JalaliDate:
558
564
  if not isinstance(date_string, str):
559
565
  raise TypeError("fromisoformat: argument must be str")
560
566
 
561
- return cls(*cls._parse_isoformat_date(digits.fa_to_en(date_string)))
567
+ if len(date_string) not in (7, 8, 10):
568
+ raise ValueError(f"Invalid isoformat string: {date_string!r}")
569
+
570
+ try:
571
+ return cls(*cls._parse_isoformat_date(digits.fa_to_en(date_string)))
572
+ except Exception:
573
+ raise ValueError(f"Invalid isoformat string: {date_string!r}")
562
574
 
563
575
  @classmethod
564
576
  def _parse_isoformat_date(cls, dtstr):
565
577
  # It is assumed that this function will only be called with a
566
578
  # string of length exactly 10, and (though this is not used) ASCII-only
579
+ assert len(dtstr) in (7, 8, 10)
580
+
567
581
  year = int(dtstr[0:4])
568
582
  if dtstr[4] != "-":
569
583
  raise ValueError("Invalid date separator: %s" % dtstr[4])
@@ -952,7 +966,7 @@ class JalaliDateTime(JalaliDate):
952
966
 
953
967
  @classmethod
954
968
  def utcfromtimestamp(cls, t):
955
- return cls(dt.utcfromtimestamp(t))
969
+ return cls(dt.fromtimestamp(t, tz=timezone.utc))
956
970
 
957
971
  def date(self):
958
972
  return JalaliDate(self.year, self.month, self.day).to_gregorian()
@@ -1025,7 +1039,187 @@ class JalaliDateTime(JalaliDate):
1025
1039
 
1026
1040
  @classmethod
1027
1041
  def utcnow(cls):
1028
- return cls(dt.utcnow())
1042
+ return cls(dt.now(tz=timezone.utc))
1043
+
1044
+ @classmethod
1045
+ def fromisoformat(cls, date_string):
1046
+ """Construct a datetime from a string in one of the ISO 8601 formats."""
1047
+ if not isinstance(date_string, str):
1048
+ raise TypeError("fromisoformat: argument must be str")
1049
+
1050
+ if len(date_string) < 7:
1051
+ raise ValueError(f"Invalid isoformat string: {date_string!r}")
1052
+
1053
+ # Split this at the separator
1054
+ try:
1055
+ separator_location = cls._find_isoformat_datetime_separator(date_string)
1056
+ dstr = date_string[0:separator_location]
1057
+ tstr = date_string[(separator_location + 1) :]
1058
+
1059
+ date_components = cls._parse_isoformat_date(dstr)
1060
+ except ValueError:
1061
+ raise ValueError(f"Invalid isoformat string: {date_string!r}") from None
1062
+
1063
+ if tstr:
1064
+ try:
1065
+ time_components = cls._parse_isoformat_time(tstr)
1066
+ except ValueError:
1067
+ raise ValueError(f"Invalid isoformat string: {date_string!r}") from None
1068
+ else:
1069
+ time_components = [0, 0, 0, 0, None]
1070
+
1071
+ return cls(*(date_components + time_components))
1072
+
1073
+ @classmethod
1074
+ def _find_isoformat_datetime_separator(cls, dtstr):
1075
+ # See the comment in _datetimemodule.c:_find_isoformat_datetime_separator
1076
+ len_dtstr = len(dtstr)
1077
+ if len_dtstr == 7:
1078
+ return 7
1079
+
1080
+ assert len_dtstr > 7
1081
+ date_separator = "-"
1082
+ week_indicator = "W"
1083
+
1084
+ if dtstr[4] == date_separator:
1085
+ if dtstr[5] == week_indicator:
1086
+ if len_dtstr < 8:
1087
+ raise ValueError("Invalid ISO string")
1088
+ if len_dtstr > 8 and dtstr[8] == date_separator:
1089
+ if len_dtstr == 9:
1090
+ raise ValueError("Invalid ISO string")
1091
+ if len_dtstr > 10 and _is_ascii_digit(dtstr[10]):
1092
+ # This is as far as we need to resolve the ambiguity for
1093
+ # the moment - if we have YYYY-Www-##, the separator is
1094
+ # either a hyphen at 8 or a number at 10.
1095
+ #
1096
+ # We'll assume it's a hyphen at 8 because it's way more
1097
+ # likely that someone will use a hyphen as a separator than
1098
+ # a number, but at this point it's really best effort
1099
+ # because this is an extension of the spec anyway.
1100
+ # TODO(pganssle): Document this
1101
+ return 8
1102
+ return 10
1103
+ else:
1104
+ # YYYY-Www (8)
1105
+ return 8
1106
+ else:
1107
+ # YYYY-MM-DD (10)
1108
+ return 10
1109
+ else:
1110
+ if dtstr[4] == week_indicator:
1111
+ # YYYYWww (7) or YYYYWwwd (8)
1112
+ idx = 7
1113
+ while idx < len_dtstr:
1114
+ if not _is_ascii_digit(dtstr[idx]):
1115
+ break
1116
+ idx += 1
1117
+
1118
+ if idx < 9:
1119
+ return idx
1120
+
1121
+ if idx % 2 == 0:
1122
+ # If the index of the last number is even, it's YYYYWwwd
1123
+ return 7
1124
+ else:
1125
+ return 8
1126
+ else:
1127
+ # YYYYMMDD (8)
1128
+ return 8
1129
+
1130
+ @classmethod
1131
+ def _parse_isoformat_time(cls, tstr):
1132
+ # Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]
1133
+ len_str = len(tstr)
1134
+ if len_str < 2:
1135
+ raise ValueError("Isoformat time too short")
1136
+
1137
+ # This is equivalent to re.search('[+-Z]', tstr), but faster
1138
+ tz_pos = tstr.find("-") + 1 or tstr.find("+") + 1 or tstr.find("Z") + 1
1139
+ timestr = tstr[: tz_pos - 1] if tz_pos > 0 else tstr
1140
+
1141
+ time_comps = cls._parse_hh_mm_ss_ff(timestr)
1142
+
1143
+ tzi = None
1144
+ if tz_pos == len_str and tstr[-1] == "Z":
1145
+ tzi = timezone.utc
1146
+ elif tz_pos > 0:
1147
+ tzstr = tstr[tz_pos:]
1148
+
1149
+ # Valid time zone strings are:
1150
+ # HH len: 2
1151
+ # HHMM len: 4
1152
+ # HH:MM len: 5
1153
+ # HHMMSS len: 6
1154
+ # HHMMSS.f+ len: 7+
1155
+ # HH:MM:SS len: 8
1156
+ # HH:MM:SS.f+ len: 10+
1157
+
1158
+ if len(tzstr) in (0, 1, 3):
1159
+ raise ValueError("Malformed time zone string")
1160
+
1161
+ tz_comps = cls._parse_hh_mm_ss_ff(tzstr)
1162
+
1163
+ if all(x == 0 for x in tz_comps):
1164
+ tzi = timezone.utc
1165
+ else:
1166
+ tzsign = -1 if tstr[tz_pos - 1] == "-" else 1
1167
+
1168
+ td = timedelta(hours=tz_comps[0], minutes=tz_comps[1], seconds=tz_comps[2], microseconds=tz_comps[3])
1169
+
1170
+ tzi = timezone(tzsign * td)
1171
+
1172
+ time_comps.append(tzi)
1173
+
1174
+ return time_comps
1175
+
1176
+ @classmethod
1177
+ def _parse_hh_mm_ss_ff(cls, tstr):
1178
+ # Parses things of the form HH[:?MM[:?SS[{.,}fff[fff]]]]
1179
+ len_str = len(tstr)
1180
+
1181
+ time_comps = [0, 0, 0, 0]
1182
+ pos = 0
1183
+ for comp in range(0, 3):
1184
+ if (len_str - pos) < 2:
1185
+ raise ValueError("Incomplete time component")
1186
+
1187
+ time_comps[comp] = int(tstr[pos : pos + 2])
1188
+
1189
+ pos += 2
1190
+ next_char = tstr[pos : pos + 1]
1191
+
1192
+ if comp == 0:
1193
+ has_sep = next_char == ":"
1194
+
1195
+ if not next_char or comp >= 2:
1196
+ break
1197
+
1198
+ if has_sep and next_char != ":":
1199
+ raise ValueError("Invalid time separator: %c" % next_char)
1200
+
1201
+ pos += has_sep
1202
+
1203
+ if pos < len_str:
1204
+ if tstr[pos] not in ".,":
1205
+ raise ValueError("Invalid microsecond component")
1206
+ else:
1207
+ pos += 1
1208
+
1209
+ len_remainder = len_str - pos
1210
+
1211
+ if len_remainder >= 6:
1212
+ to_parse = 6
1213
+ else:
1214
+ to_parse = len_remainder
1215
+
1216
+ time_comps[3] = int(tstr[pos : (pos + to_parse)])
1217
+ if to_parse < 6:
1218
+ time_comps[3] *= _FRACTION_CORRECTION[to_parse - 1]
1219
+ if len_remainder > to_parse and not all(map(_is_ascii_digit, tstr[(pos + to_parse) :])):
1220
+ raise ValueError("Non-digit values in unparsed fraction")
1221
+
1222
+ return time_comps
1029
1223
 
1030
1224
  @staticmethod
1031
1225
  def _check_tzinfo_arg(tz):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: persiantools
3
- Version: 4.1.1
3
+ Version: 4.1.2
4
4
  Summary: Jalali date and datetime with other tools
5
5
  Home-page: https://github.com/majiidd/persiantools
6
6
  Author: Majid Hajiloo
@@ -278,7 +278,7 @@ class TestJalaliDate(TestCase):
278
278
  with pytest.raises(TypeError):
279
279
  JalaliDate.fromisoformat(13670214)
280
280
 
281
- with pytest.raises(ValueError, match="Invalid date separator: /"):
281
+ with pytest.raises(ValueError):
282
282
  JalaliDate.fromisoformat("1367/02/14")
283
283
 
284
284
  with pytest.raises(ValueError):
@@ -9,7 +9,7 @@ from unittest import TestCase
9
9
  import pytest
10
10
  import pytz
11
11
 
12
- from persiantools.jdatetime import JalaliDate, JalaliDateTime
12
+ from persiantools.jdatetime import JalaliDate, JalaliDateTime, _is_ascii_digit
13
13
 
14
14
 
15
15
  class TestJalaliDateTime(TestCase):
@@ -64,7 +64,7 @@ class TestJalaliDateTime(TestCase):
64
64
  )
65
65
  self.assertEqual(
66
66
  JalaliDateTime.utcfromtimestamp(578723400),
67
- JalaliDateTime(1367, 2, 14, 4, 30, 0, 0),
67
+ JalaliDateTime(1367, 2, 14, 4, 30, 0, 0, tzinfo=timezone.utc),
68
68
  )
69
69
 
70
70
  with pytest.raises(TypeError):
@@ -376,7 +376,7 @@ class TestJalaliDateTime(TestCase):
376
376
  JalaliDateTime.strptime(date_string, fmt, locale="invalid")
377
377
 
378
378
  def test_utcnow(self):
379
- now_utc = datetime.utcnow()
379
+ now_utc = datetime.now(timezone.utc)
380
380
  jalali_now = JalaliDateTime.utcnow()
381
381
  gregorian_now = jalali_now.to_gregorian()
382
382
 
@@ -592,3 +592,48 @@ class TestJalaliDateTime(TestCase):
592
592
  def test_strftime_edge_case_midnight(self):
593
593
  jdate = JalaliDateTime(1400, 1, 1, 0, 0, 0)
594
594
  self.assertEqual(jdate.strftime("%Y-%m-%d %H:%M:%S"), "1400-01-01 00:00:00")
595
+
596
+ def test_fromisoformat_valid_date_and_time(self):
597
+ jdt = JalaliDateTime.fromisoformat("1403-08-09T02:21:45.123456+04:30")
598
+ self.assertEqual(jdt.year, 1403)
599
+ self.assertEqual(jdt.month, 8)
600
+ self.assertEqual(jdt.day, 9)
601
+ self.assertEqual(jdt.hour, 2)
602
+ self.assertEqual(jdt.minute, 21)
603
+ self.assertEqual(jdt.second, 45)
604
+ self.assertEqual(jdt.microsecond, 123456)
605
+ self.assertEqual(jdt.tzinfo, timezone(timedelta(hours=4, minutes=30)))
606
+
607
+ def test_fromisoformat_with_timezone(self):
608
+ jdt = JalaliDateTime.fromisoformat("1403-08-09T02:21:45+04:30")
609
+ self.assertEqual(jdt.tzinfo, timezone(timedelta(hours=4, minutes=30)))
610
+
611
+ def test_fromisoformat_invalid_string(self):
612
+ with self.assertRaises(ValueError):
613
+ JalaliDateTime.fromisoformat("invalid-date-time")
614
+
615
+ def test_find_isoformat_datetime_separator(self):
616
+ separator = JalaliDateTime._find_isoformat_datetime_separator("1403-08-09T02:21:45")
617
+ self.assertEqual(separator, 10)
618
+
619
+ def test_parse_isoformat_time_with_microseconds(self):
620
+ time_components = JalaliDateTime._parse_isoformat_time("02:21:45.123456")
621
+ self.assertEqual(time_components, [2, 21, 45, 123456, None])
622
+
623
+ def test_parse_isoformat_time_with_timezone(self):
624
+ time_components = JalaliDateTime._parse_isoformat_time("02:21:45+04:30")
625
+ self.assertEqual(time_components, [2, 21, 45, 0, timezone(timedelta(hours=4, minutes=30))])
626
+
627
+ def test_parse_hh_mm_ss_ff_with_microseconds(self):
628
+ time_components = JalaliDateTime._parse_hh_mm_ss_ff("02:21:45.123456")
629
+ self.assertEqual(time_components, [2, 21, 45, 123456])
630
+
631
+ def test_is_ascii_digit(self):
632
+ self.assertTrue(_is_ascii_digit("5"))
633
+ self.assertFalse(_is_ascii_digit("a"))
634
+
635
+ def test_isoformat_round_trip(self):
636
+ original = JalaliDateTime(1403, 8, 9, 2, 21, 45, 123456, tzinfo=timezone.utc)
637
+ iso_format = original.isoformat()
638
+ parsed = JalaliDateTime.fromisoformat(iso_format)
639
+ self.assertEqual(original, parsed)
File without changes
File without changes
File without changes
File without changes
File without changes