Skip to main content

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:

  • EpochTimestamp inherits from int — usable anywhere an int is
  • 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, and float
  • High-resolution Timer / async_timer helpers 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 Timer helpers (which use perf_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bear_epoch_time-1.3.6-py3-none-any.whl (39.6 kB view details)

Uploaded Python 3

File details

Details for the file bear_epoch_time-1.3.6-py3-none-any.whl.

File metadata

  • Download URL: bear_epoch_time-1.3.6-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

Hashes for bear_epoch_time-1.3.6-py3-none-any.whl
Algorithm Hash digest
SHA256 f906c7c1b4ef5d12d52679b68215ee01aee4ddda650a997c23218a7c93cb1d6c
MD5 f2a1926a1a86e65d4639e5b46aa2c046
BLAKE2b-256 d9340df91d54f39e2a36d6e425fb3774ebd9942314dba1a670bcdfd905011331

See more details on using hashes here.

Provenance

The following attestation bundles were made for bear_epoch_time-1.3.6-py3-none-any.whl:

Publisher: build-wheels.yml on sicksubroutine/bear-epoch-time

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page