lythonic.periodic¶
Time and scheduling utilities.
Time and scheduling utilities.
This module provides tools for working with time intervals, frequencies, and periodic tasks:
Time Simulation¶
SimulatedTime allows testing time-dependent code by offsetting the clock:
from lythonic.periodic import stime
from datetime import timedelta
stime.set_offset(timedelta(days=1)) # Pretend it's tomorrow
print(stime.get_datetime())
stime.reset() # Back to real time
Frequencies and Intervals¶
Frequency: Human-friendly periods (weekly, monthly, quarterly, annually)FrequencyOffset: Frequency with day offset (e.g., "20th of each month")Interval: Precise duration with multiplier (e.g., "3M" for 3 months, "2W" for 2 weeks)
from lythonic.periodic import Frequency, Interval
from datetime import date
freq = Frequency("monthly")
print(freq.first_day(date(2025, 11, 15))) # 2025-11-01
print(freq.last_day(date(2025, 11, 15))) # 2025-11-30
interval = Interval.from_string("2W")
print(interval.timedelta()) # 14 days
Periodic Tasks¶
Run async tasks at specified intervals:
from lythonic.periodic import PeriodicTask, run_all
task = PeriodicTask(freq=60, logic=my_function) # Run every 60 seconds
await run_all(task)
Timing with Moment¶
Track elapsed time between checkpoints:
from lythonic.periodic import Moment
m = Moment.start()
# ... do work ...
m = m.capture("step1")
# ... more work ...
m = m.capture("done")
print(m.chain()) # [start] 0.5s-> [step1] 1.2s-> [done]
stime = SimulatedTime()
module-attribute
¶
SimulatedTime
¶
>>> st = SimulatedTime()
>>> st.get_datetime().tzinfo
datetime.timezone.utc
>>> cmp = lambda ss: abs((st.get_datetime()-utc_now()).total_seconds()-ss) < 1e-3
>>> cmp(0)
True
>>> st.set_offset(timedelta(days=1))
>>> cmp(86400)
True
>>> st.set_offset(timedelta(days=1).total_seconds())
>>> cmp(86400)
True
>>> st.set_now(utc_now() + timedelta(days=1))
>>> cmp(86400)
True
>>> st.set_now( (utc_now() - timedelta(days=1)).timestamp() )
>>> cmp(-86400)
True
>>> st.is_real_time()
False
>>> st.reset()
>>> st.is_real_time()
True
Source code in src/lythonic/periodic.py
Frequency
¶
People friendly interval
>>> Frequency("weekly").first_day(date(2025, 11, 21))
datetime.date(2025, 11, 17)
>>> Frequency("weekly").last_day(date(2025, 11, 21))
datetime.date(2025, 11, 23)
>>> Frequency("monthly").first_day(date(2025, 11, 21))
datetime.date(2025, 11, 1)
>>> Frequency("monthly").last_day(date(2025, 11, 21))
datetime.date(2025, 11, 30)
>>> Frequency("quarterly").first_day(date(2025, 11, 21))
datetime.date(2025, 10, 1)
>>> Frequency("quarterly").last_day(date(2025, 11, 21))
datetime.date(2025, 12, 31)
>>> Frequency("quarterly").first_day(date(2025, 2, 21))
datetime.date(2025, 1, 1)
>>> Frequency("quarterly").last_day(date(2025, 2, 21))
datetime.date(2025, 3, 31)
>>> Frequency("quarterly").last_day(date(2025, 5, 21))
datetime.date(2025, 6, 30)
>>> Frequency("annually").first_day(date(2025, 11, 21))
datetime.date(2025, 1, 1)
>>> Frequency("annually").last_day(date(2025, 11, 21))
datetime.date(2025, 12, 31)
Source code in src/lythonic/periodic.py
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | |
FrequencyOffset
¶
>>> FrequencyOffset(Frequency("weekly"), 0).boundaries(date(2025, 11, 21))
(datetime.date(2025, 11, 17), datetime.date(2025, 11, 23))
>>> FrequencyOffset("weekly", 1).boundaries(date(2025, 11, 21))
(datetime.date(2025, 11, 18), datetime.date(2025, 11, 24))
>>> FrequencyOffset(Frequency("weekly"), -1).boundaries(date(2025, 11, 21))
(datetime.date(2025, 11, 16), datetime.date(2025, 11, 22))
>>> on20thOfMonth = FrequencyOffset(Frequency("monthly"), 19)
>>> on20thOfMonth.boundaries(date(2025, 11, 21))
(datetime.date(2025, 11, 20), datetime.date(2025, 12, 19))
>>> on20thOfMonth.boundaries(on20thOfMonth.boundaries(date(2025, 11, 21))[1]+timedelta(days=1))
(datetime.date(2025, 12, 20), datetime.date(2026, 1, 19))
>>> on3rdBeforeEndOfMonth = FrequencyOffset("monthly", -3)
>>> b1 = on3rdBeforeEndOfMonth.boundaries(date(2025, 11, 21)); b1
(datetime.date(2025, 10, 29), datetime.date(2025, 11, 27))
>>> b2 = on3rdBeforeEndOfMonth.boundaries(b1[1]+timedelta(days=1)); b2
(datetime.date(2025, 11, 28), datetime.date(2025, 12, 28))
>>> b3 = on3rdBeforeEndOfMonth.boundaries(b2[1]+timedelta(days=1)); b3
(datetime.date(2025, 12, 29), datetime.date(2026, 1, 28))
>>> b4 = on3rdBeforeEndOfMonth.boundaries(b3[1]+timedelta(days=1)); b4
(datetime.date(2026, 1, 29), datetime.date(2026, 2, 25))
Source code in src/lythonic/periodic.py
boundaries(as_of)
¶
Return the boundaries around the given date (inclusive).
Source code in src/lythonic/periodic.py
IntervalUnit
¶
Bases: Enum
Approximate time interval units expressed in days.
>>> for n in "usmhDWMQY": print(f"{n} = {IntervalUnit.from_string(n).timedelta()}")
u = 0:00:00.000001
s = 0:00:01
m = 0:01:00
h = 1:00:00
D = 1 day, 0:00:00
W = 7 days, 0:00:00
M = 30 days, 10:30:43.200000
Q = 91 days, 7:32:09.600000
Y = 365 days, 6:08:38.400000
>>> [i.name for i in IntervalUnit]
['u', 's', 'm', 'h', 'D', 'W', 'M', 'Q', 'Y']
>>> IntervalUnit.minutes
IntervalUnit.m
>>> list(IntervalUnit.aliases())
['microseconds', 'seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'quarters', 'years']
Source code in src/lythonic/periodic.py
Interval
¶
Mapping years, month, and quarter to real numbers of approximate days. Weeks and days mapped to integer. "
>>> p = lambda i: f"{i} {i.timedelta()}"
>>> p(Interval(2, IntervalUnit.s))
'2s 0:00:02'
>>> p(Interval.from_string("2s"))
'2s 0:00:02'
>>> p(Interval.from_string("2seconds"))
'2s 0:00:02'
>>> p(Interval.from_string("2 seconds"))
'2s 0:00:02'
>>> p(Interval.from_string("2 second"))
Traceback (most recent call last):
...
ValueError: ('Invalid interval string', '2 second')
>>> p(Interval(1, IntervalUnit.D))
'1D 1 day, 0:00:00'
>>> float(Interval.from_string("1D"))
1.0
>>> float(Interval.from_string("3h"))
0.125
>>> Interval.from_string("1D") == Interval(1, IntervalUnit.D)
True
>>> Interval("1D") >= Interval.from_string("23 hours")
True
>>> Interval(Interval("1D")) >= Interval.from_string("24 hours")
True
>>> Interval.from_string("1D") >= Interval.from_string("25 hours")
False
Source code in src/lythonic/periodic.py
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 | |
Moment
¶
>>> m = Moment.start()
>>> m = m.capture("instant")
>>> tt.sleep(1)
>>> m = m.capture("a second")
>>> s = m.chain()
>>> s.startswith('[start] 0.0'), 's-> [instant] 1.' in s , s.endswith('s-> [a second]')
(True, True, True)
Source code in src/lythonic/periodic.py
PeriodicTask
¶
A task that runs at a specified frequency in seconds.
Use with run_all() to execute multiple tasks in an async event loop.
Source code in src/lythonic/periodic.py
run_all(*tasks, shutdown_event=None, collect_results=_collect_nothing)
async
¶
Run multiple periodic tasks in an async loop.
Tasks are scheduled based on their freq (in seconds). The loop tick interval
is the GCD of all task frequencies for efficiency. Set shutdown_event to
signal graceful termination.
Source code in src/lythonic/periodic.py
dt_to_bytes(dt)
¶
Convert datetime to bytes
>>> dt_to_bytes(datetime( 1900,1,1,0,0,0))
b'\xff\xf8&\xef\xb7C`\x00'
>>> dt_to_bytes(datetime( 2000,1,1,0,0,0))
b'\x00\x03]\x01;7\xe0\x00'
Source code in src/lythonic/periodic.py
dt_from_bytes(b)
¶
Convert bytes to datetime
>>> dt_from_bytes(b'\xff\xf8&\xef\xb7C`\x00')
datetime.datetime(1900, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
>>> dt_from_bytes(b'\x00\x03]\x01;7\xe0\x00')
datetime.datetime(2000, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)