Elegant epoch timestamp handling with timezone-aware operations, fluent API, and smart defaults for Python developers who hate datetime complexity.
Project description
Bear Epoch Time
A lightweight Python library for working with epoch timestamps. EpochTimestamp inherits from int, so it slots into any code that expects an integer — but layers on a fluent, precision-aware API for arithmetic, timezone conversion, formatting, and component access without wrestling datetime.
Key Features:
EpochTimestampinherits fromint— usable anywhere anintis- Precision-aware: works in seconds, milliseconds (default), microseconds, or nanoseconds
- UTC-first with explicit timezone conversions
- Fluent, chainable API for arithmetic and component access
- Instant-based comparisons — the same moment is equal across precisions
- Serialization built in: compact bytes, pickle, and optional Pydantic v2 support
- Full comparison support with
datetime,date,time,int, andfloat - High-resolution
Timer/async_timerhelpers with auto display-unit picking
Requirements: Python 3.12+
Installation
pip install bear-epoch-time
# or with uv
uv pip install bear-epoch-time
Quick Start
from bear_epoch_time import EpochTimestamp, Precision, now
# Current UTC timestamp (default precision: milliseconds)
ts = EpochTimestamp.now()
print(ts) # 1734567890123
print(ts.to_seconds) # 1734567890
print(ts.date_str()) # "12-18-2025"
# Module-level shortcuts for the common cases
ts_ms = now() # current ms
ts_ns = now(p=Precision.NANOSECONDS) # current ns
Precision
Bear Epoch Time supports four precisions via the Precision enum. Internally, every conversion routes through nanoseconds so round-trips are lossless across precisions:
from bear_epoch_time import Precision
Precision.SECONDS # 1 unit = 1s
Precision.MILLISECONDS # 1 unit = 1ms ← default
Precision.MICROSECONDS # 1 unit = 1μs
Precision.NANOSECONDS # 1 unit = 1ns
The default precision used across the library is exported as DEFAULT_PRECISION. Most factory methods accept a precision= keyword to override it per-call:
from bear_epoch_time import EpochTimestamp, Precision, DEFAULT_PRECISION
print(DEFAULT_PRECISION) # Precision.MILLISECONDS
ts_s = EpochTimestamp.now(p=Precision.SECONDS)
ts_us = EpochTimestamp.now(p=Precision.MICROSECONDS)
Precision values can also be passed as strings ("s", "ms", "us", "ns", or their full names).
Creating Timestamps
From the current time
from bear_epoch_time import EpochTimestamp, Precision
# Class methods
ts_ms = EpochTimestamp.now() # ms (default)
ts_s = EpochTimestamp.now(p=Precision.SECONDS)
ts_ns = EpochTimestamp.now(p="ns") # strings work too
From other types
from datetime import datetime, date
from bear_epoch_time import EpochTimestamp, Precision
# From datetime (timezone-aware preferred; naive datetimes are treated as UTC)
dt = datetime(2025, 12, 25, 10, 30, 0)
ts = EpochTimestamp.from_datetime(dt)
ts = EpochTimestamp.from_datetime(dt, precision=Precision.NANOSECONDS)
# From a date (midnight in the configured class timezone)
ts = EpochTimestamp.from_date(date(2025, 12, 25))
# From seconds since epoch
ts = EpochTimestamp.from_seconds(1734567890)
# From an ISO 8601 string
ts = EpochTimestamp.from_iso_string("2025-12-25T10:30:00+00:00")
# From a custom format string
ts = EpochTimestamp.from_dt_string("12-25-2025 10:30 AM", fmt="%m-%d-%Y %I:%M %p")
Precision-targeted shortcuts
When you know which precision you want, there are dedicated constructors:
from bear_epoch_time import EpochTimestamp, Precision
# Create at the named precision; if v is None, current time is used.
EpochTimestamp.create_as_seconds() # now in seconds
EpochTimestamp.create_as_milliseconds(1734567890, inp=Precision.SECONDS)
EpochTimestamp.create_as_microseconds()
EpochTimestamp.create_as_nanoseconds()
# Also available as module-level helpers
from bear_epoch_time import to_seconds, to_milliseconds, to_microseconds, to_nanoseconds
ts = to_seconds(1.5, inp=Precision.SECONDS) # 1.5s -> stored as 1 (seconds precision)
Converting between precisions
from bear_epoch_time import EpochTimestamp, Precision
ts = EpochTimestamp.now(p=Precision.MILLISECONDS)
# Build a new instance at a different precision
ts_ns = EpochTimestamp.create(v=int(ts), inp=Precision.MILLISECONDS, outp=Precision.NANOSECONDS)
# Or just the numeric conversion, returned as (value, precision, ns_value)
value, p, ns = EpochTimestamp.to_precision(v=1500, inp=Precision.MILLISECONDS, outp=Precision.SECONDS)
# value == 1, p == Precision.SECONDS, ns == 1_500_000_000
value, p, ns = EpochTimestamp.to_precision(
v=1500, inp=Precision.MILLISECONDS, outp=Precision.SECONDS, as_float=True
)
# value == 1.5
Time Arithmetic
Adding and Subtracting
from datetime import timedelta
from bear_epoch_time import EpochTimestamp
now = EpochTimestamp.now()
# Add time components (down to nanoseconds)
future = now.add(days=7, hours=3, minutes=30)
fine = now.add(microseconds=500, nanoseconds=250)
# Subtract
past = now.subtract(days=1, hours=12)
# Use a timedelta
two_weeks_later = now.add(delta=timedelta(weeks=2))
# Negative values work for subtraction-via-add
yesterday = now.add(days=-1)
Differences
from bear_epoch_time import EpochTimestamp, Precision
ts1 = EpochTimestamp.now()
ts2 = ts1.add(days=5, hours=3)
# diff() returns an int in the requested precision (defaults to ms)
diff_ms = ts1.diff(ts2)
diff_s = ts1.diff(ts2, precision=Precision.SECONDS)
# Or get a timedelta
delta = ts1.diff(ts2, as_timedelta=True)
# time_since() returns a float in human-friendly units
days = ts1.time_since(ts2, unit="d")
hours = ts1.time_since(ts2, unit="h")
minutes = ts1.time_since(ts2, unit="m")
# since() returns the absolute difference as another EpochTimestamp
delta_ts = ts2.since(ts1, precision=Precision.MILLISECONDS)
Day Boundaries
from zoneinfo import ZoneInfo
from bear_epoch_time import EpochTimestamp
now = EpochTimestamp.now()
# Start/end of day in UTC
start = now.start_of_day()
end = now.end_of_day()
# In a specific timezone
pacific = ZoneInfo("America/Los_Angeles")
start_pacific = now.start_of_day(tz=pacific)
end_pacific = now.end_of_day(tz=pacific)
String Formatting
from bear_epoch_time import EpochTimestamp
ts = EpochTimestamp.now()
# Default formatted strings
ts.to_string() # "12-18-2025 03:30 PM PST"
ts.date_str() # "12-18-2025"
ts.time_str() # "03:30 PM"
# Custom strftime format
ts.to_string(fmt="%A, %B %d, %Y") # "Thursday, December 18, 2025"
# Ordinal day (custom %Do code)
ts.to_string(fmt="%B %Do, %Y") # "December 18th, 2025"
# ISO format
ts.to_iso # "2025-12-18T23:30:00+00:00"
ts.get_iso_string(sep=" ") # "2025-12-18 23:30:00+00:00"
# Template formatting with $variables
ts.format("$month_name $day, $year") # "December 18, 2025"
ts.format("$day_name at $time") # "Thursday at 03:30 PM"
Template variables: $epoch, $seconds, $milliseconds, $iso, $date, $time, $datetime, $year, $month, $day, $hour, $minute, $week, $isoweekday, $day_of_week, $day_of_year, $month_name, $day_name
Accessing Components
from bear_epoch_time import EpochTimestamp
ts = EpochTimestamp.now()
# Date components
ts.year # 2025
ts.month # 12
ts.day # 18
ts.week # ISO week number (1-53)
# Time components
ts.hour # 15
ts.minute # 30
ts.second # 45
ts.millisecond # 0-999
ts.microsecond # 0-999999
# Day information
ts.day_of_week # 0-6 (Monday=0)
ts.iso_weekday # 1-7 (Monday=1)
ts.day_of_year # 1-366
ts.day_name # "Thursday"
ts.month_name # "December"
# Conversions (all cached on first access)
ts.to_datetime # datetime (UTC)
ts.to_seconds # int (epoch seconds)
ts.to_milliseconds # int (epoch ms)
ts.microseconds # int (epoch μs)
ts.ns_value # int (epoch ns — the internal currency)
ts.date # date
ts.time # time
ts.to_iso # ISO 8601 string
ts.to_duration # "675M 28d 2h 12m 28s" (human-readable)
For fractional output, call the method forms:
ts.seconds(as_float=True) # 1734567890.123456
ts.ms(as_float=True)
ts.to_microseconds(as_float=True)
Replacing Components
from bear_epoch_time import EpochTimestamp
ts = EpochTimestamp.now()
new_year = ts.replace(year=2026)
noon = ts.replace(hour=12, minute=0, second=0)
first_of_mo = ts.replace(day=1)
Finding Future Times
from bear_epoch_time import EpochTimestamp
now = EpochTimestamp.now()
# Next occurrence of a weekday (case-insensitive)
next_monday = now.next_weekday("Monday")
next_friday = now.next_weekday("friday")
# Next occurrence of a specific time
next_9am = now.next_time_of_day(hour=9)
next_meeting = now.next_time_of_day(hour=14, minute=30)
Comparisons
EpochTimestamp compares cleanly against EpochTimestamp, datetime, date, time, int, and float:
from datetime import datetime, date
from bear_epoch_time import EpochTimestamp
ts = EpochTimestamp.now()
ts < datetime.now()
ts == date.today() # compares the date portion only
ts > 1734567890000 # raw int comparison
bool(ts) # False only for the zero placeholder
Comparisons between two EpochTimestamp instances are instant-based — they
compare the underlying nanosecond value, so the same moment is equal regardless of
the precision each was created at:
from bear_epoch_time import EpochTimestamp, Precision
a = EpochTimestamp.from_seconds(1, precision=Precision.SECONDS) # stored as 1
b = EpochTimestamp.from_seconds(1, precision=Precision.NANOSECONDS) # stored as 1_000_000_000
a == b # True — same instant
hash(a) == hash(b) # True — hashing is also instant-based
A bare int still compares against the raw stored value (the int-subclass behavior),
so EpochTimestamp(1500, Precision.MILLISECONDS) == 1500.
Serialization
Every timestamp round-trips losslessly through its (ns_value, precision, True) tuple —
the nanosecond value preserves the exact instant, and the precision preserves how it
should be viewed. That single tuple form backs pickling, compact byte packing, and
Pydantic support.
from bear_epoch_time import EpochTimestamp, Precision
ts = EpochTimestamp.now(p=Precision.MICROSECONDS)
# Canonical tuple form
ts.to_tuple() # (1779860420000000, MICROSECONDS, True)
# Compact 10-byte struct packing (uint64 ns, uint8 precision, bool flag)
blob = ts.serialize() # b'...'
EpochTimestamp.deserialize(blob) == ts # True
# Pickle works out of the box (via __reduce__)
import pickle
pickle.loads(pickle.dumps(ts)) == ts # True
Pydantic
EpochTimestamp provides a Pydantic v2 core schema, so it works as a model field with
no extra config. It accepts an existing EpochTimestamp, a bare int, or the serialized
tuple, and dumps to the (ns_value, precision, True) form:
from pydantic import BaseModel, Field
from bear_epoch_time import EpochTimestamp
class Event(BaseModel):
created: EpochTimestamp = Field(default_factory=EpochTimestamp.now)
event = Event(created=EpochTimestamp.now())
dumped = event.model_dump_json()
restored = Event.model_validate_json(dumped)
restored.created == event.created # True — instant and precision preserved
Pydantic is an optional dependency; the schema hook only activates when Pydantic calls it.
Placeholder Values
Zero acts as a "no timestamp set" sentinel:
from bear_epoch_time import EpochTimestamp
placeholder = EpochTimestamp(0)
if placeholder.is_default:
print("No timestamp set")
if not placeholder:
print("Same idea via bool")
Class Configuration
Customize global defaults that every EpochTimestamp instance picks up:
from zoneinfo import ZoneInfo
from bear_epoch_time import EpochTimestamp
EpochTimestamp.set_timezone(ZoneInfo("America/New_York"))
EpochTimestamp.set_date_format("%Y-%m-%d")
EpochTimestamp.set_time_format("%H:%M:%S")
EpochTimestamp.set_full_format("%Y-%m-%d %H:%M:%S %Z")
# repr style: "int" (default), "object", or "datetime"
EpochTimestamp.set_repr_style("datetime")
Module-Level Conveniences
For the most common one-liners, the top-level package exposes a few helpers:
from bear_epoch_time import now, day, month, Precision
now() # EpochTimestamp at default precision
now(p=Precision.SECONDS) # ...at a different precision
day() # current weekday as int (0=Monday .. 6=Sunday)
day(as_str=True) # "wednesday"
month() # current month as int
month(as_str=True) # "May"
TimeTools Helper
For instance-bound, timezone-aware operations:
from bear_epoch_time import TimeTools
tools = TimeTools() # defaults to the local timezone
start, end = tools.get_day_range() # today's bounds as EpochTimestamps
tools.is_same_day(ts1, ts2)
tools.is_multi_day("12-18-2025 11:00 PM", "12-19-2025 01:00 AM")
# Quick conversions
tools.datetime_to_timestamp(datetime_obj)
tools.string_to_timestamp("12-18-2025 03:30 PM")
tools.timestamp_to_string(ts)
tools.timestamp_to_datetime(ts)
TimeConverter Utilities
Parse and format duration strings:
from bear_epoch_time import TimeConverter
TimeConverter.parse_to_seconds("2d 3h 15m") # 183300.0
TimeConverter.parse_to_seconds("1M 5d") # ~35 days in seconds
TimeConverter.format_seconds(183300) # "2d 3h 15m"
TimeConverter.format_seconds(90061.5, show_subseconds=True) # "1d 1h 1m 1s 500ms"
The free-function equivalents (parse_to_seconds, parse_to_milliseconds, format_seconds, format_milliseconds, format_since, time_since, from_timedelta, to_timedelta, delta_to_ms) are also available at the top level.
Timer Utilities
Context managers and decorators for high-resolution timing, backed by perf_counter_ns():
from bear_epoch_time import timer, create_timer, Precision
# Basic context manager — defaults to recording in milliseconds
with timer(name="my_operation", console=print) as t:
do_work()
print(f"Took {t.seconds:.2f}s")
# Pick the recording precision
with timer(name="hot_path", console=print, precision=Precision.NANOSECONDS) as t:
do_hot_thing()
# t.nanoseconds, t.microseconds, t.milliseconds, t.seconds all available
# Decorator factory
@create_timer(console=print, precision=Precision.MICROSECONDS)
def my_function():
...
Auto display precision
display_precision="auto" keeps your recording precision high while picking the most human-readable display unit at format time. Great for log lines where the same timer might measure microseconds or multi-second jobs:
from bear_epoch_time import timer, Precision
with timer(name="any", console=print, precision=Precision.NANOSECONDS, display_precision="auto") as t:
do_thing()
# Outputs the most readable unit for the actual duration:
# <any> Elapsed time: 2.125000 microsecond (fast)
# <any> Elapsed time: 505.044667 millisecond (medium)
# <any> Elapsed time: 1.250000 second (slow)
Or pin the display unit explicitly:
with timer(precision="ns", display_precision="ms", console=print):
...
Async timers
from bear_epoch_time import async_timer, create_async_timer
async with async_timer(name="async_op", console=print) as t:
await some_async_work()
@create_async_timer(console=print)
async def my_async_function():
...
Limitations
EpochTimestamp is designed for UTC timestamps in the recent past and future. It is not intended for:
- Historical dates before the Unix epoch (1970-01-01)
- The kind of high-precision interval timing that wall-clock-based epochs can't guarantee — use the
Timerhelpers (which useperf_counter_ns()) for that - Dates far enough in the future that leap-second handling becomes significant
This is a utility library for working with epoch timestamps in a more human-friendly way.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bear_epoch_time-1.4.0-py3-none-any.whl.
File metadata
- Download URL: bear_epoch_time-1.4.0-py3-none-any.whl
- Upload date:
- Size: 39.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
289d34011e66bff8dfc5d78b02ac6301f2c584f183c87b20aac2407f03a0c05f
|
|
| MD5 |
057c133530db5186d20e0dfdfa89b6d4
|
|
| BLAKE2b-256 |
64cb23f82cba10d842ff387e19f7cd24983d48039cf3126aa37c2281b503c35d
|
Provenance
The following attestation bundles were made for bear_epoch_time-1.4.0-py3-none-any.whl:
Publisher:
build-wheels.yml on sicksubroutine/bear-epoch-time
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bear_epoch_time-1.4.0-py3-none-any.whl -
Subject digest:
289d34011e66bff8dfc5d78b02ac6301f2c584f183c87b20aac2407f03a0c05f - Sigstore transparency entry: 1810429528
- Sigstore integration time:
-
Permalink:
sicksubroutine/bear-epoch-time@ad1a4bbbe4475ef030f662e27ad41943db042b91 -
Branch / Tag:
refs/tags/v1.4.0 - Owner: https://github.com/sicksubroutine
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build-wheels.yml@ad1a4bbbe4475ef030f662e27ad41943db042b91 -
Trigger Event:
push
-
Statement type: