repomatic.humanize module

Conversions between raw machine quantities and their human forms.

Byte counts, modification times and service timestamps reach the user through more than one surface (the image-optimization summary, the repomatic cache tables, every dependency report), and each surface should spell them the same way. One home for those conversions keeps the wording consistent and keeps the formatters out of the modules that merely happen to be the first consumer; parse_iso_datetime() is the machine-to-datetime half the renderers start from.

A leaf module with no project-internal imports, so any module can import it without risking a cycle.

repomatic.humanize.MONTHS_PER_YEAR = 12

Months in a year.

repomatic.humanize.SECONDS_PER_MINUTE = 60

Seconds in a minute.

repomatic.humanize.SECONDS_PER_HOUR = 3600

Seconds in an hour.

repomatic.humanize.SECONDS_PER_DAY = 86400

Seconds in a day: what turns an mtime delta into whole days.

repomatic.humanize.SECONDS_PER_WEEK = 604800

Seconds in a week.

repomatic.humanize.SECONDS_PER_MONTH = 2635200.0

Seconds in a mean month: past it, format_relative() counts no weeks.

repomatic.humanize.SECONDS_PER_YEAR = 31536000

Seconds in a common year.

repomatic.humanize.parse_iso_datetime(value)[source]

Parse an ISO 8601 / RFC 3339 timestamp into a timezone-aware datetime.

The package-wide parser for any timestamp an external service writes: repomatic.deps.report reads PyPI upload times, repomatic.deps.lockfile lock timestamps, repomatic.cloudflare.api token expiries and repomatic.github.job_timings job clocks through it, so every consumer tolerates the same shapes. A JSON or TOML lookup that found no value hands over None, which reads as no timestamp.

fromisoformat() reads a Z suffix and a nanosecond fraction since Python 3.11, and truncates the digits datetime cannot hold. An instant with no offset is read as UTC, the one zone the services above write in.

Parameters:

value (str | None) – An ISO 8601 / RFC 3339 instant, or empty.

Return type:

datetime | None

Returns:

A timezone-aware datetime, or None when value is empty or not a valid instant.

repomatic.humanize.today_utc()[source]

Today’s date, in UTC.

The one read of the clock behind every date this package stamps: a release heading, a scan record, a cooldown reference. UTC, so a runner and a laptop in another timezone agree on the day. Callers that write the date out use today_utc().isoformat(), which is the YYYY-MM-DD form.

Return type:

date

repomatic.humanize.utc_midnight(day)[source]

The UTC instant day starts on.

What a date-granular datasource hands format_countdown() and format_elapsed(), which take instants so no caller can silently drop a precision it holds. A date holds none to drop: midnight is the whole of what it says.

Return type:

datetime

repomatic.humanize.shift_months(instant, months)[source]

Move instant by months calendar months.

The day is clamped to the target month, so one month before March 31 is February 28, or 29 in a leap year. The time of day is kept.

Parameters:
  • instant (datetime) – The instant to move.

  • months (int) – Calendar months to add, negative to subtract.

Return type:

datetime

Returns:

The moved instant.

repomatic.humanize.format_relative(instant, reference)[source]

Phrase the gap from reference to instant: in 5 hours, 2 days ago.

The buckets and their wording follow the humanize() of arrow 1.4. Under ten seconds the gap is just now. Then come seconds, a minute under two, minutes, an hour under two, hours, a day under two, and days under a week. Past a week, whole calendar months come first, up to a year, and a remainder past 14 days counts as one more month: 15 days already read as a month. When no month counts, the gap reads a week, or 2 weeks once it reaches 14 days. Past a year, it reads a year under two, then years. A count is truncated, never rounded, and starts at two: a single unit has its own phrase.

Parameters:
  • instant (datetime) – The instant phrased.

  • reference (datetime) – The instant the gap is measured from, usually now.

Return type:

str

Returns:

in :gap:` for an *instant* after *reference*, `{gap} ago for one before it, or just now.

repomatic.humanize.format_countdown(deadline, reference)[source]

Render deadline as a date, with a countdown to it.

The countdown measures the two instants, not their calendar dates. A deadline later today then reads in 5 hours, where truncating to dates rendered just now: a reader takes that for something already elapsed, the opposite of what the line reports.

Both parameters are instants for that reason, and a date is rejected by the type checker. Every deadline this renders is computed at instant granularity (uv records a package upload-time to the second, and a cooldown cutoff is now - span), so a caller holding that precision must not drop it. A date-granular datasource passes the UTC midnight its date starts on through utc_midnight(), on both arguments: an instant measured against a midnight reference is off by up to a day.

Parameters:
  • deadline (datetime) – The instant counted down to.

  • reference (datetime) – The current instant, for the relative offset.

Return type:

str

Returns:

A string like 2026-06-25 (in 4 days) or 2026-06-25 (in 5 hours), or the bare date once the deadline has passed.

repomatic.humanize.format_elapsed(instant, reference)[source]

Render instant as a date, with how long ago it happened.

The mirror of format_countdown(), for a date a reader looks back at rather than waits for: a package upload, a release, a recorded reading. The same precision rules apply, since the two read the same gap.

Each drops the phrase in the direction it cannot speak for. A countdown says nothing about a deadline already passed, and this says nothing about an instant still ahead, where in 3 days would contradict the column it sits in. Both then render the bare date.

Parameters:
  • instant (datetime) – The instant being looked back at.

  • reference (datetime) – The current instant, for the relative offset.

Return type:

str

Returns:

A string like 2026-06-24 (2 days ago) or 2026-06-24 (6 hours ago), or the bare date while instant is still ahead.

repomatic.humanize.format_file_size(size_bytes)[source]

Format a byte count as a human-readable string.

A thin binding of click_extra.format_size() to the JEDEC unit style: binary powers with the customary KB/MB symbols.

Return type:

str

repomatic.humanize.format_age(mtime)[source]

Format a file mtime as a human-readable age string.

Rounds down to whole days, since the cache tables it feeds exist to answer “is this stale?”, not to time anything precisely.

Parameters:

mtime (float) – POSIX timestamp, as returned by Path.stat().st_mtime.

Return type:

str

Returns:

"today", "1 day", or "{n} days".