offpeak 0.1.2__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {offpeak-0.1.2 → offpeak-0.2.0}/.github/workflows/ci.yml +1 -1
  2. offpeak-0.2.0/.github/workflows/nightly.yml +74 -0
  3. {offpeak-0.1.2 → offpeak-0.2.0}/PKG-INFO +30 -1
  4. {offpeak-0.1.2 → offpeak-0.2.0}/README.md +29 -0
  5. {offpeak-0.1.2 → offpeak-0.2.0}/pyproject.toml +1 -1
  6. {offpeak-0.1.2 → offpeak-0.2.0}/src/offpeak/__init__.py +7 -1
  7. offpeak-0.2.0/src/offpeak/__main__.py +62 -0
  8. {offpeak-0.1.2 → offpeak-0.2.0}/src/offpeak/client.py +11 -10
  9. {offpeak-0.1.2 → offpeak-0.2.0}/src/offpeak/deadline.py +8 -2
  10. {offpeak-0.1.2 → offpeak-0.2.0}/src/offpeak/job.py +16 -1
  11. {offpeak-0.1.2 → offpeak-0.2.0}/src/offpeak/prices.py +19 -0
  12. offpeak-0.2.0/src/offpeak/quote.py +248 -0
  13. offpeak-0.2.0/tests/conftest.py +19 -0
  14. {offpeak-0.1.2 → offpeak-0.2.0}/tests/test_deadline.py +17 -0
  15. {offpeak-0.1.2 → offpeak-0.2.0}/tests/test_job_receipt.py +7 -0
  16. offpeak-0.2.0/tests/test_night_report.py +178 -0
  17. offpeak-0.2.0/tests/test_quote.py +204 -0
  18. {offpeak-0.1.2 → offpeak-0.2.0}/tests/test_run.py +45 -0
  19. offpeak-0.2.0/tools/night_report.py +283 -0
  20. {offpeak-0.1.2 → offpeak-0.2.0}/.github/workflows/publish.yml +0 -0
  21. {offpeak-0.1.2 → offpeak-0.2.0}/.gitignore +0 -0
  22. {offpeak-0.1.2 → offpeak-0.2.0}/CONTRIBUTING.md +0 -0
  23. {offpeak-0.1.2 → offpeak-0.2.0}/LICENSE +0 -0
  24. {offpeak-0.1.2 → offpeak-0.2.0}/SPEC.md +0 -0
  25. {offpeak-0.1.2 → offpeak-0.2.0}/src/offpeak/venues/__init__.py +0 -0
  26. {offpeak-0.1.2 → offpeak-0.2.0}/src/offpeak/venues/anthropic_batch.py +0 -0
  27. {offpeak-0.1.2 → offpeak-0.2.0}/src/offpeak/venues/base.py +0 -0
  28. {offpeak-0.1.2 → offpeak-0.2.0}/src/offpeak/venues/openai_batch.py +0 -0
@@ -20,6 +20,6 @@ jobs:
20
20
  - name: Install
21
21
  run: pip install -e ".[dev]"
22
22
  - name: Lint
23
- run: ruff check src tests
23
+ run: ruff check src tests tools
24
24
  - name: Test
25
25
  run: pytest
@@ -0,0 +1,74 @@
1
+ name: Night board
2
+
3
+ # Quotes at dusk, marks at dawn. Output is committed to the `board-data`
4
+ # branch, not to main: main is protected (PR + four required checks), so a
5
+ # nightly bot push there would be rejected every night.
6
+
7
+ on:
8
+ schedule:
9
+ - cron: "0 19 * * *" # quote — the night ahead, carbon forecast
10
+ - cron: "30 6 * * *" # mark — the night just finished, carbon actuals
11
+ workflow_dispatch:
12
+ inputs:
13
+ mode:
14
+ description: "Which pass to run"
15
+ required: true
16
+ default: quote
17
+ type: choice
18
+ options: [quote, mark]
19
+
20
+ permissions:
21
+ contents: write
22
+
23
+ concurrency:
24
+ group: night-board
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ board:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - name: Check out the generator (main)
32
+ uses: actions/checkout@v7
33
+
34
+ - uses: actions/setup-python@v7
35
+ with:
36
+ python-version: "3.12"
37
+
38
+ - name: Check out the ledger (board-data)
39
+ uses: actions/checkout@v7
40
+ with:
41
+ ref: board-data
42
+ path: board
43
+
44
+ - name: Pick the pass
45
+ id: pick
46
+ run: |
47
+ if [ -n "${{ inputs.mode }}" ]; then
48
+ mode="${{ inputs.mode }}"
49
+ elif [ "${{ github.event.schedule }}" = "0 19 * * *" ]; then
50
+ mode=quote
51
+ else
52
+ mode=mark
53
+ fi
54
+ echo "mode=$mode" >> "$GITHUB_OUTPUT"
55
+ echo "running the $mode pass"
56
+
57
+ - name: Generate
58
+ run: |
59
+ python tools/night_report.py \
60
+ --mode "${{ steps.pick.outputs.mode }}" \
61
+ --outdir board/nightly
62
+
63
+ - name: Commit to board-data
64
+ working-directory: board
65
+ run: |
66
+ git config user.name "github-actions[bot]"
67
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
68
+ git add nightly
69
+ if git diff --staged --quiet; then
70
+ echo "nothing changed — not committing an empty night"
71
+ exit 0
72
+ fi
73
+ git commit -m "board: ${{ steps.pick.outputs.mode }} $(date -u +%Y-%m-%d)"
74
+ git push
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: offpeak
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Deadline-priced inference: give AI jobs a deadline and run them on the cheapest venue — provider batch tiers (−50%) today. Same model, same tokens, a different hour.
5
5
  Project-URL: Homepage, https://github.com/offpeak-ai/offpeak
6
6
  Project-URL: Repository, https://github.com/offpeak-ai/offpeak
@@ -72,6 +72,7 @@ prices snapshot 2026-08 — override via offpeak.prices
72
72
 
73
73
  ## What it does
74
74
 
75
+ - **Know the price before you spend it.** `quote(jobs, deadline=...)` prices a run against the published sheets with no API calls and no key — list versus batch, per venue, plus what the wait is worth.
75
76
  - **One argument, not a workflow.** `run(jobs, deadline=...)` handles batching, submission, polling, collection, and result matching across providers.
76
77
  - **Deadlines are guarded, not hoped for.** If a batch hasn't landed by the time the remaining window shrinks to a risk buffer, `offpeak` cancels and re-runs the stragglers synchronously at list price. You state the deadline; it gets met.
77
78
  - **Every run settles a receipt.** List cost, paid cost, captured spread — arithmetic against public price sheets, not estimates.
@@ -88,6 +89,32 @@ pip install "offpeak[openai]"
88
89
 
89
90
  Venues use the standard environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`), or pass a configured client: `OpenAIBatch(client=my_client)`.
90
91
 
92
+ ## The free quote
93
+
94
+ What is the wait worth? Ask before you spend anything — `quote()` makes no API calls and needs no key.
95
+
96
+ ```bash
97
+ python -m offpeak quote --model gpt-5.6-luna --input-tokens 800 --output-tokens 200 --jobs 5000
98
+ ```
99
+
100
+ ```
101
+ OFFPEAK QUOTE ─────────────────────────────────
102
+ jobs 5000 across 1 venue(s)
103
+ deadline 2026-08-21 21:11 PDT (24.0h out)
104
+ tokens 4,000,000 in · 1,000,000 out
105
+
106
+ openai:batch 5000 job(s) list $1.00 batch $0.50 save $0.50 (50.0%)
107
+
108
+ list $1.00 (run now, synchronously)
109
+ batch $0.50 (run by the deadline)
110
+ save $0.50 (50.0%)
111
+ basis input explicit; output explicit
112
+ prices snapshot 2026-08 — estimate only, not a bill
113
+ ───────────────────────────────────────────────
114
+ ```
115
+
116
+ From Python, `offpeak.quote(jobs, deadline="06:00")` takes the same jobs you would pass to `run()`. Token counts come from the job where it knows them (`metadata={"input_tokens": ..., "output_tokens": ...}`, or `max_tokens` as an output ceiling) and are a labeled chars/4 estimate where it does not — every figure reports its provenance in `basis`, and a quote with no output signal is marked a **floor**, not an estimate.
117
+
91
118
  ## Deadlines
92
119
 
93
120
  Deadlines are how software says "this can wait" — the full semantics live in [SPEC.md](SPEC.md).
@@ -131,6 +158,8 @@ Unknown models settle with `cost = None` rather than a guess.
131
158
 
132
159
  `offpeak` is the open client and spec for a simple claim: **intelligence has a time value**. A large share of AI work — embeddings, evals, backfills, report generation, overnight agents — has no human waiting on it, and the venues already price that patience at −50%. This library is the missing workflow.
133
160
 
161
+ The **[night board](https://github.com/offpeak-ai/offpeak/blob/board-data/nightly/BOARD.md)** marks the same claim against open grid data every night — power and carbon peak/off-peak spreads, alongside the 2.0x token spread the batch tiers already publish.
162
+
134
163
  The roadmap follows the same interface upward: more venues (Google batch, spot capacity, off-peak windows on your own GPUs), queue-latency forecasting instead of a fixed risk buffer, portfolio placement across venues, energy- and carbon-aware scheduling with per-job receipts. The venue interface (`offpeak.Venue`) is deliberately the extension point — a venue is anywhere deferred work can run.
135
164
 
136
165
  A hosted desk that does the forecasting, cross-venue portfolio scheduling, and SLA insurance at fleet scale — payloads never leaving your perimeter — is being built by the same team. The SDK and the deadline spec stay open, Apache-2.0.
@@ -35,6 +35,7 @@ prices snapshot 2026-08 — override via offpeak.prices
35
35
 
36
36
  ## What it does
37
37
 
38
+ - **Know the price before you spend it.** `quote(jobs, deadline=...)` prices a run against the published sheets with no API calls and no key — list versus batch, per venue, plus what the wait is worth.
38
39
  - **One argument, not a workflow.** `run(jobs, deadline=...)` handles batching, submission, polling, collection, and result matching across providers.
39
40
  - **Deadlines are guarded, not hoped for.** If a batch hasn't landed by the time the remaining window shrinks to a risk buffer, `offpeak` cancels and re-runs the stragglers synchronously at list price. You state the deadline; it gets met.
40
41
  - **Every run settles a receipt.** List cost, paid cost, captured spread — arithmetic against public price sheets, not estimates.
@@ -51,6 +52,32 @@ pip install "offpeak[openai]"
51
52
 
52
53
  Venues use the standard environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`), or pass a configured client: `OpenAIBatch(client=my_client)`.
53
54
 
55
+ ## The free quote
56
+
57
+ What is the wait worth? Ask before you spend anything — `quote()` makes no API calls and needs no key.
58
+
59
+ ```bash
60
+ python -m offpeak quote --model gpt-5.6-luna --input-tokens 800 --output-tokens 200 --jobs 5000
61
+ ```
62
+
63
+ ```
64
+ OFFPEAK QUOTE ─────────────────────────────────
65
+ jobs 5000 across 1 venue(s)
66
+ deadline 2026-08-21 21:11 PDT (24.0h out)
67
+ tokens 4,000,000 in · 1,000,000 out
68
+
69
+ openai:batch 5000 job(s) list $1.00 batch $0.50 save $0.50 (50.0%)
70
+
71
+ list $1.00 (run now, synchronously)
72
+ batch $0.50 (run by the deadline)
73
+ save $0.50 (50.0%)
74
+ basis input explicit; output explicit
75
+ prices snapshot 2026-08 — estimate only, not a bill
76
+ ───────────────────────────────────────────────
77
+ ```
78
+
79
+ From Python, `offpeak.quote(jobs, deadline="06:00")` takes the same jobs you would pass to `run()`. Token counts come from the job where it knows them (`metadata={"input_tokens": ..., "output_tokens": ...}`, or `max_tokens` as an output ceiling) and are a labeled chars/4 estimate where it does not — every figure reports its provenance in `basis`, and a quote with no output signal is marked a **floor**, not an estimate.
80
+
54
81
  ## Deadlines
55
82
 
56
83
  Deadlines are how software says "this can wait" — the full semantics live in [SPEC.md](SPEC.md).
@@ -94,6 +121,8 @@ Unknown models settle with `cost = None` rather than a guess.
94
121
 
95
122
  `offpeak` is the open client and spec for a simple claim: **intelligence has a time value**. A large share of AI work — embeddings, evals, backfills, report generation, overnight agents — has no human waiting on it, and the venues already price that patience at −50%. This library is the missing workflow.
96
123
 
124
+ The **[night board](https://github.com/offpeak-ai/offpeak/blob/board-data/nightly/BOARD.md)** marks the same claim against open grid data every night — power and carbon peak/off-peak spreads, alongside the 2.0x token spread the batch tiers already publish.
125
+
97
126
  The roadmap follows the same interface upward: more venues (Google batch, spot capacity, off-peak windows on your own GPUs), queue-latency forecasting instead of a fixed risk buffer, portfolio placement across venues, energy- and carbon-aware scheduling with per-job receipts. The venue interface (`offpeak.Venue`) is deliberately the extension point — a venue is anywhere deferred work can run.
98
127
 
99
128
  A hosted desk that does the forecasting, cross-venue portfolio scheduling, and SLA insurance at fleet scale — payloads never leaving your perimeter — is being built by the same team. The SDK and the deadline spec stay open, Apache-2.0.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "offpeak"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  description = "Deadline-priced inference: give AI jobs a deadline and run them on the cheapest venue — provider batch tiers (−50%) today. Same model, same tokens, a different hour."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -14,24 +14,30 @@ from . import prices
14
14
  from .client import Settlement, default_venues, receipt, run
15
15
  from .deadline import parse_deadline, seconds_until
16
16
  from .job import Job, Receipt, Result, Status, job
17
+ from .prices import format_usd
18
+ from .quote import Quote, VenueQuote, quote
17
19
  from .venues.base import BatchState, Venue
18
20
 
19
- __version__ = "0.1.2"
21
+ __version__ = "0.2.0"
20
22
 
21
23
  __all__ = [
22
24
  "job",
23
25
  "run",
26
+ "quote",
24
27
  "receipt",
25
28
  "Job",
26
29
  "Result",
27
30
  "Receipt",
28
31
  "Settlement",
32
+ "Quote",
33
+ "VenueQuote",
29
34
  "Status",
30
35
  "Venue",
31
36
  "BatchState",
32
37
  "parse_deadline",
33
38
  "seconds_until",
34
39
  "default_venues",
40
+ "format_usd",
35
41
  "prices",
36
42
  "__version__",
37
43
  ]
@@ -0,0 +1,62 @@
1
+ """``python -m offpeak`` — the quote desk, from a terminal.
2
+
3
+ python -m offpeak quote --model gpt-5.6-luna --input-tokens 800 \\
4
+ --output-tokens 200 --jobs 5000
5
+
6
+ Prices against the bundled sheet only. No API calls, no key required.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+
14
+ from . import __version__
15
+ from .job import Job
16
+ from .quote import quote
17
+
18
+
19
+ def _quote_cmd(a: argparse.Namespace) -> int:
20
+ jobs = [
21
+ Job(
22
+ model=a.model,
23
+ messages=[],
24
+ metadata={"input_tokens": a.input_tokens, "output_tokens": a.output_tokens},
25
+ )
26
+ for _ in range(a.jobs)
27
+ ]
28
+ try:
29
+ print(quote(jobs, a.deadline))
30
+ except ValueError as exc:
31
+ print(f"error: {exc}", file=sys.stderr)
32
+ return 2
33
+ return 0
34
+
35
+
36
+ def main(argv: list[str] | None = None) -> int:
37
+ ap = argparse.ArgumentParser(prog="offpeak", description=__doc__.splitlines()[0])
38
+ ap.add_argument("--version", action="version", version=f"offpeak {__version__}")
39
+ sub = ap.add_subparsers(dest="command", required=True)
40
+
41
+ q = sub.add_parser("quote", help="price a batch of jobs against a deadline")
42
+ q.add_argument("--model", required=True, help="e.g. gpt-5.6-luna, claude-haiku-4-5")
43
+ q.add_argument("--input-tokens", type=int, required=True, help="per job")
44
+ q.add_argument("--output-tokens", type=int, required=True, help="per job")
45
+ q.add_argument("--jobs", type=int, default=1, help="how many jobs (default 1)")
46
+ q.add_argument(
47
+ "--deadline",
48
+ default="24h",
49
+ help='when it must be done: "24h", "06:00", an ISO timestamp (default 24h)',
50
+ )
51
+ q.set_defaults(func=_quote_cmd)
52
+
53
+ a = ap.parse_args(argv)
54
+ if a.jobs < 1:
55
+ ap.error("--jobs must be at least 1")
56
+ if a.input_tokens < 0 or a.output_tokens < 0:
57
+ ap.error("token counts cannot be negative")
58
+ return a.func(a)
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main())
@@ -8,14 +8,13 @@ own-GPU off-peak windows, and carbon-aware scheduling on the same interface.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import math
12
11
  import time
13
12
  from dataclasses import dataclass, field
14
13
  from datetime import datetime
15
14
 
16
15
  from .deadline import parse_deadline, seconds_until
17
16
  from .job import Job, Receipt, Result, Status
18
- from .prices import PRICE_SHEET_DATE
17
+ from .prices import BATCH_DISCOUNT, PRICE_SHEET_DATE, format_usd
19
18
  from .venues.base import Venue
20
19
 
21
20
  __all__ = ["run", "receipt", "Settlement", "default_venues"]
@@ -205,14 +204,7 @@ def _run_sync(venue: Venue, j: Job) -> Result:
205
204
  return Result(job=j, error=str(exc))
206
205
 
207
206
 
208
- def _usd(amount: float) -> str:
209
- """Money for humans: 2dp once there are cents to show, more significant
210
- digits below that so a sub-cent run does not settle as a column of $0.00."""
211
- if amount == 0:
212
- return "0.00"
213
- if abs(amount) >= 0.005:
214
- return f"{amount:,.2f}"
215
- return f"{amount:,.{-math.floor(math.log10(abs(amount))) + 2}f}"
207
+ _usd = format_usd # kept as a private alias; the canonical home is prices
216
208
 
217
209
 
218
210
  @dataclass
@@ -228,6 +220,7 @@ class Settlement:
228
220
  output_tokens: int = 0
229
221
  list_usd: float = 0.0
230
222
  paid_usd: float = 0.0
223
+ left_on_table_usd: float = 0.0
231
224
  unpriced: int = 0
232
225
  by_venue: dict = field(default_factory=dict)
233
226
 
@@ -253,6 +246,11 @@ class Settlement:
253
246
  f"captured ${_usd(self.captured_usd)} ({self.captured_pct:.1f}%)",
254
247
  f"prices snapshot {PRICE_SHEET_DATE} — override via offpeak.prices",
255
248
  ]
249
+ if self.fell_back:
250
+ lines.append(
251
+ f"left ${_usd(self.left_on_table_usd)} on the table "
252
+ f"({self.fell_back} job(s) missed the batch tier)"
253
+ )
256
254
  if self.unpriced:
257
255
  lines.append(f"note {self.unpriced} job(s) had no price sheet entry")
258
256
  lines.append("─" * 47)
@@ -281,4 +279,7 @@ def receipt(results: list[Result]) -> Settlement:
281
279
  else:
282
280
  settlement.list_usd += r.list_usd
283
281
  settlement.paid_usd += r.paid_usd
282
+ if r.fell_back:
283
+ # The spread this job would have captured had the batch held.
284
+ settlement.left_on_table_usd += r.list_usd * (1 - BATCH_DISCOUNT)
284
285
  return settlement
@@ -8,7 +8,8 @@ Accepted forms:
8
8
  - ``"06:00"`` — the next occurrence of that wall-clock time (today if it is
9
9
  still ahead, otherwise tomorrow). This is the canonical overnight form.
10
10
  - ``"6h"``, ``"90m"``, ``"45s"``, ``"2d"`` — relative to now.
11
- - ISO 8601 strings — ``"2026-08-21T06:00:00-07:00"``.
11
+ - ISO 8601 strings — ``"2026-08-21T06:00:00-07:00"``, including the
12
+ ``Z`` (UTC) suffix on every supported Python.
12
13
 
13
14
  All deadlines resolve to an aware :class:`datetime.datetime`. See SPEC.md for
14
15
  the full semantics.
@@ -76,8 +77,13 @@ def _parse(value: object, now: datetime) -> datetime:
76
77
  if candidate <= now:
77
78
  candidate += timedelta(days=1)
78
79
  return candidate
80
+ text = value.strip()
81
+ # Python 3.11 taught fromisoformat to read a trailing "Z"; 3.10 did not,
82
+ # and Z is the ISO form most timestamps in the wild actually use.
83
+ if text.endswith(("Z", "z")):
84
+ text = f"{text[:-1]}+00:00"
79
85
  try:
80
- parsed = datetime.fromisoformat(value.strip())
86
+ parsed = datetime.fromisoformat(text)
81
87
  except ValueError:
82
88
  raise ValueError(f"unrecognized deadline: {value!r}") from None
83
89
  return parsed if parsed.tzinfo else parsed.astimezone()
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
7
7
  from datetime import datetime
8
8
  from enum import Enum
9
9
 
10
- from .prices import batch_cost_usd, list_cost_usd
10
+ from .prices import batch_cost_usd, format_usd, list_cost_usd
11
11
 
12
12
  __all__ = ["Job", "Result", "Receipt", "Status", "job"]
13
13
 
@@ -93,6 +93,21 @@ class Receipt:
93
93
  return None
94
94
  return self.list_usd - self.paid_usd
95
95
 
96
+ def __str__(self) -> str:
97
+ """One line, in money you can actually read.
98
+
99
+ The float properties above stay floats — this is the rendering, so a
100
+ sub-cent job reports what it cost instead of $0.00.
101
+ """
102
+ where = f"{self.venue} {self.model}"
103
+ if self.fell_back:
104
+ where += " (sync fallback)"
105
+ return (
106
+ f"{where}: {self.input_tokens:,} in · {self.output_tokens:,} out · "
107
+ f"list ${format_usd(self.list_usd)} · paid ${format_usd(self.paid_usd)} · "
108
+ f"captured ${format_usd(self.spread_usd)}"
109
+ )
110
+
96
111
 
97
112
  @dataclass
98
113
  class Result:
@@ -12,9 +12,12 @@ list, which is what :data:`BATCH_DISCOUNT` encodes.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import math
16
+
15
17
  __all__ = [
16
18
  "PRICE_SHEET_DATE",
17
19
  "BATCH_DISCOUNT",
20
+ "format_usd",
18
21
  "register_price",
19
22
  "get_price",
20
23
  "list_cost_usd",
@@ -77,3 +80,19 @@ def list_cost_usd(model: str, input_tokens: int, output_tokens: int) -> float |
77
80
  def batch_cost_usd(model: str, input_tokens: int, output_tokens: int) -> float | None:
78
81
  cost = list_cost_usd(model, input_tokens, output_tokens)
79
82
  return None if cost is None else cost * BATCH_DISCOUNT
83
+
84
+
85
+ def format_usd(amount: float | None) -> str:
86
+ """Money for humans: 2dp once there are cents to show, more significant
87
+ digits below that so a sub-cent job does not settle as a column of $0.00.
88
+
89
+ ``None`` (an unpriced model) renders as an em dash, never as zero — a price
90
+ we do not know is not a price of nothing.
91
+ """
92
+ if amount is None:
93
+ return "—"
94
+ if amount == 0:
95
+ return "0.00"
96
+ if abs(amount) >= 0.005:
97
+ return f"{amount:,.2f}"
98
+ return f"{amount:,.{-math.floor(math.log10(abs(amount))) + 2}f}"