Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/aniso8601/builders/__init__.py: 35%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

237 statements  

1# -*- coding: utf-8 -*- 

2 

3# Copyright (c) 2025, Brandon Nielsen 

4# All rights reserved. 

5# 

6# This software may be modified and distributed under the terms 

7# of the BSD license. See the LICENSE file for details. 

8 

9import calendar 

10from collections import namedtuple 

11 

12from aniso8601.exceptions import ( 

13 DayOutOfBoundsError, 

14 HoursOutOfBoundsError, 

15 ISOFormatError, 

16 LeapSecondError, 

17 MidnightBoundsError, 

18 MinutesOutOfBoundsError, 

19 MonthOutOfBoundsError, 

20 SecondsOutOfBoundsError, 

21 WeekOutOfBoundsError, 

22 YearOutOfBoundsError, 

23) 

24 

25DateTuple = namedtuple("Date", ["YYYY", "MM", "DD", "Www", "D", "DDD"]) 

26TimeTuple = namedtuple("Time", ["hh", "mm", "ss", "tz"]) 

27DatetimeTuple = namedtuple("Datetime", ["date", "time"]) 

28DurationTuple = namedtuple( 

29 "Duration", ["PnY", "PnM", "PnW", "PnD", "TnH", "TnM", "TnS"] 

30) 

31IntervalTuple = namedtuple("Interval", ["start", "end", "duration"]) 

32RepeatingIntervalTuple = namedtuple("RepeatingInterval", ["R", "Rnn", "interval"]) 

33TimezoneTuple = namedtuple("Timezone", ["negative", "Z", "hh", "mm", "name"]) 

34 

35Limit = namedtuple( 

36 "Limit", 

37 [ 

38 "casterrorstring", 

39 "min", 

40 "max", 

41 "rangeexception", 

42 "rangeerrorstring", 

43 "rangefunc", 

44 ], 

45) 

46 

47 

48def cast( 

49 value, 

50 castfunction, 

51 caughtexceptions=(ValueError,), 

52 thrownexception=ISOFormatError, 

53 thrownmessage=None, 

54): 

55 try: 

56 result = castfunction(value) 

57 except caughtexceptions: 

58 raise thrownexception(thrownmessage) 

59 

60 return result 

61 

62 

63def range_check(valuestr, limit): 

64 # Returns cast value if in range, raises defined exceptions on failure 

65 if valuestr is None: 

66 return None 

67 

68 if "." in valuestr: 

69 castfunc = float 

70 else: 

71 castfunc = int 

72 

73 value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring) 

74 

75 if limit.min is not None and value < limit.min: 

76 raise limit.rangeexception(limit.rangeerrorstring) 

77 

78 if limit.max is not None and value > limit.max: 

79 raise limit.rangeexception(limit.rangeerrorstring) 

80 

81 return value 

82 

83 

84class BaseTimeBuilder(object): 

85 # Limit tuple format cast function, cast error string, 

86 # lower limit, upper limit, limit error string 

87 DATE_YYYY_LIMIT = Limit( 

88 "Invalid year string.", 

89 0000, 

90 9999, 

91 YearOutOfBoundsError, 

92 "Year must be between 1..9999.", 

93 range_check, 

94 ) 

95 DATE_MM_LIMIT = Limit( 

96 "Invalid month string.", 

97 1, 

98 12, 

99 MonthOutOfBoundsError, 

100 "Month must be between 1..12.", 

101 range_check, 

102 ) 

103 DATE_DD_LIMIT = Limit( 

104 "Invalid day string.", 

105 1, 

106 31, 

107 DayOutOfBoundsError, 

108 "Day must be between 1..31.", 

109 range_check, 

110 ) 

111 DATE_WWW_LIMIT = Limit( 

112 "Invalid week string.", 

113 1, 

114 53, 

115 WeekOutOfBoundsError, 

116 "Week number must be between 1..53.", 

117 range_check, 

118 ) 

119 DATE_D_LIMIT = Limit( 

120 "Invalid weekday string.", 

121 1, 

122 7, 

123 DayOutOfBoundsError, 

124 "Weekday number must be between 1..7.", 

125 range_check, 

126 ) 

127 DATE_DDD_LIMIT = Limit( 

128 "Invalid ordinal day string.", 

129 1, 

130 366, 

131 DayOutOfBoundsError, 

132 "Ordinal day must be between 1..366.", 

133 range_check, 

134 ) 

135 TIME_HH_LIMIT = Limit( 

136 "Invalid hour string.", 

137 0, 

138 24, 

139 HoursOutOfBoundsError, 

140 "Hour must be between 0..24 with 24 representing midnight.", 

141 range_check, 

142 ) 

143 TIME_MM_LIMIT = Limit( 

144 "Invalid minute string.", 

145 0, 

146 59, 

147 MinutesOutOfBoundsError, 

148 "Minute must be between 0..59.", 

149 range_check, 

150 ) 

151 TIME_SS_LIMIT = Limit( 

152 "Invalid second string.", 

153 0, 

154 60, 

155 SecondsOutOfBoundsError, 

156 "Second must be between 0..60 with 60 representing a leap second.", 

157 range_check, 

158 ) 

159 TZ_HH_LIMIT = Limit( 

160 "Invalid timezone hour string.", 

161 0, 

162 23, 

163 HoursOutOfBoundsError, 

164 "Hour must be between 0..23.", 

165 range_check, 

166 ) 

167 TZ_MM_LIMIT = Limit( 

168 "Invalid timezone minute string.", 

169 0, 

170 59, 

171 MinutesOutOfBoundsError, 

172 "Minute must be between 0..59.", 

173 range_check, 

174 ) 

175 DURATION_PNY_LIMIT = Limit( 

176 "Invalid year duration string.", 

177 0, 

178 None, 

179 ISOFormatError, 

180 "Duration years component must be positive.", 

181 range_check, 

182 ) 

183 DURATION_PNM_LIMIT = Limit( 

184 "Invalid month duration string.", 

185 0, 

186 None, 

187 ISOFormatError, 

188 "Duration months component must be positive.", 

189 range_check, 

190 ) 

191 DURATION_PNW_LIMIT = Limit( 

192 "Invalid week duration string.", 

193 0, 

194 None, 

195 ISOFormatError, 

196 "Duration weeks component must be positive.", 

197 range_check, 

198 ) 

199 DURATION_PND_LIMIT = Limit( 

200 "Invalid day duration string.", 

201 0, 

202 None, 

203 ISOFormatError, 

204 "Duration days component must be positive.", 

205 range_check, 

206 ) 

207 DURATION_TNH_LIMIT = Limit( 

208 "Invalid hour duration string.", 

209 0, 

210 None, 

211 ISOFormatError, 

212 "Duration hours component must be positive.", 

213 range_check, 

214 ) 

215 DURATION_TNM_LIMIT = Limit( 

216 "Invalid minute duration string.", 

217 0, 

218 None, 

219 ISOFormatError, 

220 "Duration minutes component must be positive.", 

221 range_check, 

222 ) 

223 DURATION_TNS_LIMIT = Limit( 

224 "Invalid second duration string.", 

225 0, 

226 None, 

227 ISOFormatError, 

228 "Duration seconds component must be positive.", 

229 range_check, 

230 ) 

231 INTERVAL_RNN_LIMIT = Limit( 

232 "Invalid duration repetition string.", 

233 0, 

234 None, 

235 ISOFormatError, 

236 "Duration repetition count must be positive.", 

237 range_check, 

238 ) 

239 

240 DATE_RANGE_DICT = { 

241 "YYYY": DATE_YYYY_LIMIT, 

242 "MM": DATE_MM_LIMIT, 

243 "DD": DATE_DD_LIMIT, 

244 "Www": DATE_WWW_LIMIT, 

245 "D": DATE_D_LIMIT, 

246 "DDD": DATE_DDD_LIMIT, 

247 } 

248 

249 TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT} 

250 

251 DURATION_RANGE_DICT = { 

252 "PnY": DURATION_PNY_LIMIT, 

253 "PnM": DURATION_PNM_LIMIT, 

254 "PnW": DURATION_PNW_LIMIT, 

255 "PnD": DURATION_PND_LIMIT, 

256 "TnH": DURATION_TNH_LIMIT, 

257 "TnM": DURATION_TNM_LIMIT, 

258 "TnS": DURATION_TNS_LIMIT, 

259 } 

260 

261 REPEATING_INTERVAL_RANGE_DICT = {"Rnn": INTERVAL_RNN_LIMIT} 

262 

263 TIMEZONE_RANGE_DICT = {"hh": TZ_HH_LIMIT, "mm": TZ_MM_LIMIT} 

264 

265 LEAP_SECONDS_SUPPORTED = False 

266 

267 @classmethod 

268 def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None): 

269 raise NotImplementedError 

270 

271 @classmethod 

272 def build_time(cls, hh=None, mm=None, ss=None, tz=None): 

273 raise NotImplementedError 

274 

275 @classmethod 

276 def build_datetime(cls, date, time): 

277 raise NotImplementedError 

278 

279 @classmethod 

280 def build_duration( 

281 cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None 

282 ): 

283 raise NotImplementedError 

284 

285 @classmethod 

286 def build_interval(cls, start=None, end=None, duration=None): 

287 # start, end, and duration are all tuples 

288 raise NotImplementedError 

289 

290 @classmethod 

291 def build_repeating_interval(cls, R=None, Rnn=None, interval=None): 

292 # interval is a tuple 

293 raise NotImplementedError 

294 

295 @classmethod 

296 def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""): 

297 raise NotImplementedError 

298 

299 @classmethod 

300 def range_check_date( 

301 cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None, rangedict=None 

302 ): 

303 if rangedict is None: 

304 rangedict = cls.DATE_RANGE_DICT 

305 

306 if "YYYY" in rangedict: 

307 YYYY = rangedict["YYYY"].rangefunc(YYYY, rangedict["YYYY"]) 

308 

309 if "MM" in rangedict: 

310 MM = rangedict["MM"].rangefunc(MM, rangedict["MM"]) 

311 

312 if "DD" in rangedict: 

313 DD = rangedict["DD"].rangefunc(DD, rangedict["DD"]) 

314 

315 if "Www" in rangedict: 

316 Www = rangedict["Www"].rangefunc(Www, rangedict["Www"]) 

317 

318 if "D" in rangedict: 

319 D = rangedict["D"].rangefunc(D, rangedict["D"]) 

320 

321 if "DDD" in rangedict: 

322 DDD = rangedict["DDD"].rangefunc(DDD, rangedict["DDD"]) 

323 

324 if DD is not None: 

325 # Check calendar 

326 if DD > calendar.monthrange(YYYY, MM)[1]: 

327 raise DayOutOfBoundsError( 

328 "{0} is out of range for {1}-{2}".format(DD, YYYY, MM) 

329 ) 

330 

331 if DDD is not None: 

332 if calendar.isleap(YYYY) is False and DDD == 366: 

333 raise DayOutOfBoundsError( 

334 "{0} is only valid for leap year.".format(DDD) 

335 ) 

336 

337 return (YYYY, MM, DD, Www, D, DDD) 

338 

339 @classmethod 

340 def range_check_time(cls, hh=None, mm=None, ss=None, tz=None, rangedict=None): 

341 # Used for midnight and leap second handling 

342 midnight = False # Handle hh = '24' specially 

343 

344 if rangedict is None: 

345 rangedict = cls.TIME_RANGE_DICT 

346 

347 if "hh" in rangedict: 

348 try: 

349 hh = rangedict["hh"].rangefunc(hh, rangedict["hh"]) 

350 except HoursOutOfBoundsError as e: 

351 if float(hh) > 24 and float(hh) < 25: 

352 raise MidnightBoundsError("Hour 24 may only represent midnight.") 

353 

354 raise e 

355 

356 if "mm" in rangedict: 

357 mm = rangedict["mm"].rangefunc(mm, rangedict["mm"]) 

358 

359 if "ss" in rangedict: 

360 ss = rangedict["ss"].rangefunc(ss, rangedict["ss"]) 

361 

362 if hh is not None and hh == 24: 

363 midnight = True 

364 

365 # Handle midnight range 

366 if midnight is True and ( 

367 (mm is not None and mm != 0) or (ss is not None and ss != 0) 

368 ): 

369 raise MidnightBoundsError("Hour 24 may only represent midnight.") 

370 

371 if cls.LEAP_SECONDS_SUPPORTED is True: 

372 if hh != 23 and mm != 59 and ss == 60: 

373 raise cls.TIME_SS_LIMIT.rangeexception( 

374 cls.TIME_SS_LIMIT.rangeerrorstring 

375 ) 

376 else: 

377 if hh == 23 and mm == 59 and ss == 60: 

378 # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is 

379 raise LeapSecondError("Leap seconds are not supported.") 

380 

381 if ss == 60: 

382 raise cls.TIME_SS_LIMIT.rangeexception( 

383 cls.TIME_SS_LIMIT.rangeerrorstring 

384 ) 

385 

386 return (hh, mm, ss, tz) 

387 

388 @classmethod 

389 def range_check_duration( 

390 cls, 

391 PnY=None, 

392 PnM=None, 

393 PnW=None, 

394 PnD=None, 

395 TnH=None, 

396 TnM=None, 

397 TnS=None, 

398 rangedict=None, 

399 ): 

400 if rangedict is None: 

401 rangedict = cls.DURATION_RANGE_DICT 

402 

403 if "PnY" in rangedict: 

404 PnY = rangedict["PnY"].rangefunc(PnY, rangedict["PnY"]) 

405 

406 if "PnM" in rangedict: 

407 PnM = rangedict["PnM"].rangefunc(PnM, rangedict["PnM"]) 

408 

409 if "PnW" in rangedict: 

410 PnW = rangedict["PnW"].rangefunc(PnW, rangedict["PnW"]) 

411 

412 if "PnD" in rangedict: 

413 PnD = rangedict["PnD"].rangefunc(PnD, rangedict["PnD"]) 

414 

415 if "TnH" in rangedict: 

416 TnH = rangedict["TnH"].rangefunc(TnH, rangedict["TnH"]) 

417 

418 if "TnM" in rangedict: 

419 TnM = rangedict["TnM"].rangefunc(TnM, rangedict["TnM"]) 

420 

421 if "TnS" in rangedict: 

422 TnS = rangedict["TnS"].rangefunc(TnS, rangedict["TnS"]) 

423 

424 return (PnY, PnM, PnW, PnD, TnH, TnM, TnS) 

425 

426 @classmethod 

427 def range_check_repeating_interval( 

428 cls, R=None, Rnn=None, interval=None, rangedict=None 

429 ): 

430 if rangedict is None: 

431 rangedict = cls.REPEATING_INTERVAL_RANGE_DICT 

432 

433 if "Rnn" in rangedict: 

434 Rnn = rangedict["Rnn"].rangefunc(Rnn, rangedict["Rnn"]) 

435 

436 return (R, Rnn, interval) 

437 

438 @classmethod 

439 def range_check_timezone( 

440 cls, negative=None, Z=None, hh=None, mm=None, name="", rangedict=None 

441 ): 

442 if rangedict is None: 

443 rangedict = cls.TIMEZONE_RANGE_DICT 

444 

445 if "hh" in rangedict: 

446 hh = rangedict["hh"].rangefunc(hh, rangedict["hh"]) 

447 

448 if "mm" in rangedict: 

449 mm = rangedict["mm"].rangefunc(mm, rangedict["mm"]) 

450 

451 return (negative, Z, hh, mm, name) 

452 

453 @classmethod 

454 def _build_object(cls, parsetuple): 

455 # Given a TupleBuilder tuple, build the correct object 

456 if isinstance(parsetuple, DateTuple): 

457 return cls.build_date( 

458 YYYY=parsetuple.YYYY, 

459 MM=parsetuple.MM, 

460 DD=parsetuple.DD, 

461 Www=parsetuple.Www, 

462 D=parsetuple.D, 

463 DDD=parsetuple.DDD, 

464 ) 

465 

466 if isinstance(parsetuple, TimeTuple): 

467 return cls.build_time( 

468 hh=parsetuple.hh, mm=parsetuple.mm, ss=parsetuple.ss, tz=parsetuple.tz 

469 ) 

470 

471 if isinstance(parsetuple, DatetimeTuple): 

472 return cls.build_datetime(parsetuple.date, parsetuple.time) 

473 

474 if isinstance(parsetuple, DurationTuple): 

475 return cls.build_duration( 

476 PnY=parsetuple.PnY, 

477 PnM=parsetuple.PnM, 

478 PnW=parsetuple.PnW, 

479 PnD=parsetuple.PnD, 

480 TnH=parsetuple.TnH, 

481 TnM=parsetuple.TnM, 

482 TnS=parsetuple.TnS, 

483 ) 

484 

485 if isinstance(parsetuple, IntervalTuple): 

486 return cls.build_interval( 

487 start=parsetuple.start, end=parsetuple.end, duration=parsetuple.duration 

488 ) 

489 

490 if isinstance(parsetuple, RepeatingIntervalTuple): 

491 return cls.build_repeating_interval( 

492 R=parsetuple.R, Rnn=parsetuple.Rnn, interval=parsetuple.interval 

493 ) 

494 

495 return cls.build_timezone( 

496 negative=parsetuple.negative, 

497 Z=parsetuple.Z, 

498 hh=parsetuple.hh, 

499 mm=parsetuple.mm, 

500 name=parsetuple.name, 

501 ) 

502 

503 @classmethod 

504 def _is_interval_end_concise(cls, endtuple): 

505 if isinstance(endtuple, TimeTuple): 

506 return True 

507 

508 if isinstance(endtuple, DatetimeTuple): 

509 enddatetuple = endtuple.date 

510 else: 

511 enddatetuple = endtuple 

512 

513 if enddatetuple.YYYY is None: 

514 return True 

515 

516 return False 

517 

518 @classmethod 

519 def _combine_concise_interval_tuples(cls, starttuple, conciseendtuple): 

520 starttimetuple = None 

521 startdatetuple = None 

522 

523 endtimetuple = None 

524 enddatetuple = None 

525 

526 if isinstance(starttuple, DateTuple): 

527 startdatetuple = starttuple 

528 else: 

529 # Start is a datetime 

530 starttimetuple = starttuple.time 

531 startdatetuple = starttuple.date 

532 

533 if isinstance(conciseendtuple, DateTuple): 

534 enddatetuple = conciseendtuple 

535 elif isinstance(conciseendtuple, DatetimeTuple): 

536 enddatetuple = conciseendtuple.date 

537 endtimetuple = conciseendtuple.time 

538 else: 

539 # Time 

540 endtimetuple = conciseendtuple 

541 

542 if enddatetuple is not None: 

543 if enddatetuple.YYYY is None and enddatetuple.MM is None: 

544 newenddatetuple = DateTuple( 

545 YYYY=startdatetuple.YYYY, 

546 MM=startdatetuple.MM, 

547 DD=enddatetuple.DD, 

548 Www=enddatetuple.Www, 

549 D=enddatetuple.D, 

550 DDD=enddatetuple.DDD, 

551 ) 

552 else: 

553 newenddatetuple = DateTuple( 

554 YYYY=startdatetuple.YYYY, 

555 MM=enddatetuple.MM, 

556 DD=enddatetuple.DD, 

557 Www=enddatetuple.Www, 

558 D=enddatetuple.D, 

559 DDD=enddatetuple.DDD, 

560 ) 

561 

562 if endtimetuple is None: 

563 return newenddatetuple 

564 

565 if (starttimetuple is not None and starttimetuple.tz is not None) and ( 

566 endtimetuple is not None and endtimetuple.tz != starttimetuple.tz 

567 ): 

568 # Copy the timezone across 

569 endtimetuple = TimeTuple( 

570 hh=endtimetuple.hh, 

571 mm=endtimetuple.mm, 

572 ss=endtimetuple.ss, 

573 tz=starttimetuple.tz, 

574 ) 

575 

576 if enddatetuple is not None and endtimetuple is not None: 

577 return TupleBuilder.build_datetime(newenddatetuple, endtimetuple) 

578 

579 return TupleBuilder.build_datetime(startdatetuple, endtimetuple) 

580 

581 

582class TupleBuilder(BaseTimeBuilder): 

583 # Builder used to return the arguments as a tuple, cleans up some parse methods 

584 @classmethod 

585 def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None): 

586 

587 return DateTuple(YYYY, MM, DD, Www, D, DDD) 

588 

589 @classmethod 

590 def build_time(cls, hh=None, mm=None, ss=None, tz=None): 

591 return TimeTuple(hh, mm, ss, tz) 

592 

593 @classmethod 

594 def build_datetime(cls, date, time): 

595 return DatetimeTuple(date, time) 

596 

597 @classmethod 

598 def build_duration( 

599 cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None 

600 ): 

601 

602 return DurationTuple(PnY, PnM, PnW, PnD, TnH, TnM, TnS) 

603 

604 @classmethod 

605 def build_interval(cls, start=None, end=None, duration=None): 

606 return IntervalTuple(start, end, duration) 

607 

608 @classmethod 

609 def build_repeating_interval(cls, R=None, Rnn=None, interval=None): 

610 return RepeatingIntervalTuple(R, Rnn, interval) 

611 

612 @classmethod 

613 def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""): 

614 return TimezoneTuple(negative, Z, hh, mm, name)